
文章目录
前言
进程创建
fork
进程创建主要是通过fork()函数完成的,内核主要做以下几个工作:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
如下图所示:

写时拷贝
父子进程代码是共享的,父子不进行数据的写入时,数据也是共享的,但是当任意一方试图写入时,便以写时拷贝的方式各自一份副本,如下图所示:

正是因为写时拷贝,所以父子进程才具有独立性。
进程终止
进程终止,会有以下三种情况:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码都没跑完,进程出现异常
退出码
之前我们写C或C++程序 时,都会在main函数的最后写上"return 0",这里0其实是告诉系统进程正确结束。我们可以用"echo $?"来查看上一个退出进程的退出码。如下所示:

退出码可以告诉我们最后一次执行的命令的状态,在命令结束以后,我们可以通过退出码来判断命令是执行成功的还是错误的。
错误码
除此之外,每次调用库函数也会有一个错误码,用于判断库函数是否调用失败,错误码可以通过errno查看,我们来看一个示例代码:
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE *fp = fopen("./logtxt","r");
if(fp == NULL)
{
printf("%d: %s\n",errno,strerror(errno));
}
return 0;
}
运行以上代码,结果如下:

也可以将错误码进行返回。
当代码在运行期间,没有收到信号,并且退出码为0时,就代表代码正确跑完了;当代码在运行期间,没有收到信号,但是退出码非0时,就代表代码跑完了,但是结果不正确;当代码在运行期间,收到信号时,就代表代码没有跑完,进程出现了异常;
综上所述,进程执行的结果状态,可以用两个数字来表示:一个是信号,一个是退出码。这两个数字是不需要用户来维护的,当一个进程提出的时候,操作系统会将进程退出的详细信息写入到进程的task_struct结构体中,所以,进程退出,需要僵尸状态维持自己的退出状态。
退出
这里进程退出只考虑前两种情况。让进程退出,一方面main函数return的方式;另一方面,可以通过exit函数和_exit。
exit
exit函数简介如下图所示:

在任意地方都可以调用exit函数。
_exit
_exit函数用法与exit函数类似,只不过exit函数终止进程,会强制刷新缓冲区,但是_exit不会强制刷新缓冲区。同时,_exit是一种系统调用,exit函数底层调用的就是_exit。如下图所示:

进程等待
必要性
当子进程退出,父进程如果不对子进程进行处理,就会造成“僵尸进程”的问题,进而造成内存的泄漏。因此,父进程要通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。
wait
以下是一段进程等待的示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("I am a child process,pid:%d\n",getpid());
sleep(1);
}
exit(1);
}
else if(id > 0)
{
pid_t rid = wait(NULL);
if(rid == id)
{
printf("pid:%d,wait success!\n",getpid());
}
}
return 0;
}
运行结果如下:

如果父进程wait子进程,但是子进程没有退出,则父进程会阻塞在wait函数中,当子进程退出时,父进程就会回收子进程。当回收成功时,返回子进程的pid,否则返回-1。
waitpid
waitpid函数的声明如下:
pid_t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
当将pid设置为-1,options设置为0时,与wait函数等价。使用示例如下所示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("I am a child process,pid:%d\n",getpid());
sleep(1);
}
exit(1);
}
else if(id > 0)
{
sleep(10);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
int exit_code = ((status >> 8) & 0xFF);
int exit_sig = status & 0x7F;
printf("pid:%d,wait success!status:%d,status_code:%d,exit_sig:%d\n",getpid(),status,exit_code,exit_sig);
}
sleep(5);
}
return 0;
}
运行结果如下:

status获得的是子进程的退出信息,本质是要获得子进程提出的两个数字,stauts共有32个比特位,如下图所示:

其中高八位为对应的退出码,低七位为对应的信号码,可以通过右移和按位与操作获取status的前八位和后八位,得到退出码和对应的信号码。当使用"kill -9 [进程号]"杀掉子进程时,得到的信号码为9。
但是通过位操作获取对应的退出码和信号码不是很方便,因此还提供了相关宏来检测相关的信号。示例代码如下所示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id < 0)
{
exit(1);
}
else if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(10);
}
else{
int status = 0;
pid_t rid = waitpid(id, &status, 0 );
if(rid > 0)
{
if(WIFEXITED(status))
{
printf("wait success,退出的子进程是:%d,exit_code:%d\n",rid,WEXITSTATUS(status));
}
else{
printf("异常退出");
}
}
else{
printf("ret:%d\n",rid);
perror("waitpid");
}
}
return 0;
}
运行结果如下:

waitpid是一种系统调用,可以根据进程的PCB得到相关的信息。
当要等待的进程不是自己的子进程时,会返回-1。示例代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id < 0)
{
exit(1);
}
else if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(10);
}
else{
int status = 0;
pid_t rid = waitpid(id + 1, &status, 0 );
if(rid > 0)
{
printf("wait success,退出的子进程是:%d,exit_code:%d,exit_sig:%d\n",rid,(status >> 8) & 0xFF,status & 0x7F);
}
else{
printf("ret:%d\n",rid);
perror("waitpid");
}
}
return 0;
}
运行结果如下:

之前父进程等待的方式都是阻塞等待,但还有非阻塞轮询的等待方式,非阻塞等待可以使用WHOHANG宏。使用如下所示:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("I am a parent process,pid:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id < 0)
{
exit(1);
}
else if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("I am a child process,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(10);
}
else{
while(1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG); //非阻塞检测 && 回收
if(rid > 0)
{
printf("wait success,退出的子进程是:%d,exit_code:%d\n",rid,WEXITSTATUS(status));
}
else if(rid == 0)
{
printf("子进程正在运行,父进程需要等待\n");
usleep(100000);
}
else{
perror("waitpid");
break;
}
}
}
return 0;
}
父进程要等待多长时间,是由子进程来决定的,阻塞等待在等待期间,什么都没干,非阻塞等待在等待期间,可以继续执行相关代码,将等待时间利用了起来。
通过waitpid函数而C++中的vector容器,我们就可以创建多个进程,示例代码如下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <vector>
#include <iostream>
enum{
OK,
USAGE_ERR
};
void Task()
{
int cnt = 5;
while(cnt--)
{
printf("我是一个子进程,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
sleep(1);
}
}
void CreateChildProcess(int num,std::vector<pid_t> *subs)
{
for(int i = 0;i < num;i++)
{
pid_t id = fork();
if(id == 0)
{
//child
Task();
exit(0);
}
//父进程
subs->push_back(id);
}
}
void WaitAllChild(const std::vector<pid_t> &subs)
{
for(const auto &pid : subs)
{
int status = 0;
pid_t rid = waitpid(pid, &status, 0);
if(rid > 0)
{
printf("子进程:%d Exit,exit code: %d\n",rid,WEXITSTATUS(status));
}
}
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cout<< "Usage: " << argv[0] << " process_num" << std::endl;
exit(USAGE_ERR);
}
int num = std::stoi(argv[1]);
std::vector<pid_t> subs;
//创建多进程
CreateChildProcess(num,&subs);
//父进程
WaitAllChild(subs);
return OK;
}
运行结果和查看相关进程如下:


进程程序替换
进程程序替换可以用来执行其它的程序,一个简单使用如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
//执行另一个程序
execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
运行结果如下:

这种进程在执行过程中去执行另一个程序的代码和数据,这种过程就叫做进程替换。如下图所示:

当调用exec等函数时,磁盘中会出现一个新的可执行程序,会将原程序的数据段和代码段用新程序的数据和代码覆盖掉,这就是程序替换的一个基本原理。程序替换期间,只改变了物理内存的部分,进程的pid不变,因此,程序替换过程中没有创建新的进程,程序替换的本质是将代码和数据,拷贝到内存中。
之前我们也介绍过,程序运行之前,必须先加载到内存中,本质是一个IO操作,只有操作系统才能够将数据从磁盘中加载到内存,所以,操作系统必须能提供对应的系统调用,来完成加载的这个过程,程序替换的过程就是加载。
我们发现,程序替换之后,原来程序的后续代码也不执行了,这是因为代码被替换了。因此,exec系列函数,成功的时候,没有返回值。因此我们一般会创建个子进程,使子进程进行程序替换,如下所示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//执行另一个程序
execl("/usr/bin/ls","ls","-a","-l",NULL);//程序替换函数
exit(0);
}
wait(NULL);
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
运行结果如下:

默认的数据段和代码段都是只读区域,当子进程要加载数据段和代码段时,会发生写时拷贝,在其它区域加载数据段和代码段,并让子进程的页表指向对应的区域,使父子彻底分离。
execl
该函数声明如下:

该函数第一个参数是要执行的程序的路径,第二个参数是相关的栈帧,表明如何执行,命令行怎么写,这个参数就会怎么传,最后以NULL结尾即可。程序替换的函数,部分参数确实可以省略,但是,一般不要省略。
当替换成功时,没有返回值,也不会执行后续代码;但当出错时会返回-1,接着执行后续代码。
execlp
execlp的相关参数如下:

与execl不同的是,这个函数第一个参数为file,只需要告诉程序名即可,该函数会自动到环境变量PATH所表明的路径下查找。使用示例如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//执行另一个程序
execlp("ls","ls","-a","-l","-n",NULL);//程序替换函数
exit(0);
}
wait(NULL);
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
运行结果如下:

execv
execv函数声明如下:

execv没有可变参数,而是采用了一个指针数组,之前使用execl和execlp是将参数一个一个传入的,而这个函数可以通过传入一个指针数组参数表来传入相关的参数。使用示例如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//执行另一个程序
char *const argv[]={
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
execv("/usr/bin/ls",argv);//程序替换函数
exit(0);
}
wait(NULL);
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
运行结果如下:

execvp
execvp函数的声明如下:

execvp函数第一个参数为file,只需要告诉程序名即可,该函数会自动到环境变量PATH所表明的路径下查找。使用示例如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//执行另一个程序
char *const argv[]={
(char*)"ls",
(char*)"-a",
(char*)"-l",
NULL
};
execvp(argv[0],argv);//程序替换函数
exit(0);
}
wait(NULL);
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
运行结果如下:

execvpe
execvpe函数的声明如下:

与之前的函数相比,这个函数多了一个环境变量相关的参数。这个参数允许在程序替换时传入全新的环境变量。使用示例如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//执行另一个程序
char *const argv[]={
(char*)"./othercmd",
(char*)"-a",
(char*)"-b",
NULL
};
char* myenv[] = {
"PATH=/home/zhangsan/linux/exec",
NULL
};
//extern char** environ;
execvpe("./othercmd",argv,myenv);//程序替换函数
exit(3);
}
wait(NULL);
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
othercmd是C++编译运行后生成的可执行程序,以上代码运行结果如下:

在这个程序中,我们就通过程序替换的方式来执行了其它可执行程序,bash进程也会通过程序替换的方式,将相关的环境变量传给子进程并执行,命令行参数和环境变量表,都是父进程通过exec系列的函数传递的。
若想给子进程传已有的环境变量,可以传入environ,该函数第三个参数可以选择传给子进程的环境变量,若传入自定义环境变量,会覆盖掉原有的环境变量。
若想在已有的环境变量上新增环境变量,使用putenv函数即可,如下所示:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
printf("我是一个进程:%d\n",getpid());
pid_t id = fork();
if(id == 0)
{
//执行另一个程序
char *const argv[]={
(char*)"./othercmd",
(char*)"-a",
(char*)"-b",
NULL
};
char* myenv[] = {
"PATH=/home/zhangsan/linux/exec",
NULL
};
extern char** environ;
putenv((char*)"haha=aaaaaaaaaaaaaaaaaaa");
execvpe("./othercmd",argv,environ);//程序替换函数
exit(3);
}
wait(NULL);
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
printf("代码运行中....\n");
return 0;
}
execve
execve是exec系列函数底层调用的函数,其它函数都是库函数,execve函数是一个系统调用。

1203

被折叠的 条评论
为什么被折叠?



