目录
一、进程的创建
1.1 fork
1.2 fork的应用场景
1.3 fork函数调用失败的原因
二、进程的终止
2.1 进程终止的场景
2.2 进程的退出码
2.2.1 echo $?
2.2.2 退出码的作用
2.3 进程的退出的方式
2.3.1 在main函数中使用return语句
2.3.2 exit函数
2.3.3 _exit函数
三、进程等待
3.1 wait
3.2 waitpid
3.3 深入分析进程等待(waitpid第三个参数的使用)
3.4 WIFEXITED/WEXITSTATUS
四、进程替换
4.1 execl
4.1.1 通过execl函数来引入进程替换
4.1.2 execl函数的参数
4.2 execv
4.3 execlp
4.4 execvp
4.5 execle
4.5.1 putenv
4.6 execvpe/execve
我们在之前的进程概念中说过进程是如何被创建的,在这里顺带复习一下
在C语言中我们使用fork函数来创建进程:
#include
pid_t fork(void);//返回值:子进程中返回0,父进程返回子进程id,出错返回-1
调用fork函数之后,系统内核开始依次执行:
● 分配新的内存块和内核数据结构给子进程
● 将父进程部分数据结构内容拷贝至子进程
● 添加子进程到系统进程列表当中
● fork函数返回,开始调度器调度
具体详细分析,请看:【Linux】进程概念(下)
fork函数通常被用来:
● 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
● 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
● 系统中有太多的进程
● 实际用户的进程数超过了限制
下面我们来谈谈进程的终止
进程终止通常有两种场景:
● 进程正常结束(正常结束有结果正确和结果不正确(关系到退出码))
● 进程异常结束(即进程崩溃)
进程异常结束涉及到信号,我们在后期会具体谈到,在这里先说明一下进程崩溃的本质:进程因为某些原因,导致进程收到了来自操作系统的信号(例如kill -9指令)
每个进程正常结束后,都会返回一个退出码,我们可以根据退出码来查看进程运行的最终结果是否符合预期
例如在C语言一直在写的main函数,在最后的return语句中返回的就是进程的退出码,下面我们写一段代码:
#include
int count(int n)
{
int sum = 0;
int i = 0;
for (; i < n; i++)
{
sum += i;
}
return sum;
}
int main()
{
if (count(100) == 5050)
return 0;//计算正确
else return 1;//计算错误
}
我们可以预测该段代码的最终的退出码为1
在Linux中我们可以使用echo $?指令来查看最近一个进程的退出码
我们运行一下上面的代码:
可以看到退出码为1
下面我们修改一下代码:
#include
int count(int n)
{
int sum = 0;
int i = 0;
for (; i <= n; i++)
{
sum += i;
}
return sum;
}
int main()
{
if (count(100) == 5050)
return 0;//计算正确
else return 1;//计算错误
}
这下我们可以预期修改后的代码的退出码为0
退出码可以让我们知道进程是为什么退出的,在C语言中可以使用库函数strerror(包含在头文件string.h中)来查看不同退出码的具体含义:
退出码在不同版本的编译器下个数有所不同
当然,除了使用系统定义的退出码,我们还可以自定义退出码的含义(不超过255个),具体定义方式也很简单,在全局变量中定义一个指针数组来表明每一个退出码的含义:
const char* err_string[] = {
"success",
//...
}
在上面的例子中可以看到我们在main函数中return加上退出码可以退出进程
return语句只有在main函数中使用才有退出功能,但exit函数(包含在头文件stdlib.h中)在哪里使用都有该功能
例如:
#include
#include
int count(int n)
{
int sum = 0;
int i = 0;
for (; i <= n; i++)
{
sum += i;
}
exit(123);
return sum;
}
int main()
{
if (count(100) == 5050)
return 0;//计算正确
else return 1;//计算错误
}
_exit函数的功能和exit函数差不多,但是_exit函数是一个系统函数接口,其与exit函数的区别是:exit在结束进程前会刷新缓冲区等一系列操作,而_exit函数并不会:
举例说明一下:
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("Hello");
exit(0);
}
else if (id > 0)
{
printf("Hello");
_exit(0);
}
return 0;
}
下面我们观察一下运行结果:
可以看到原本应该打印出两个Hello字符串的结果只有一个Hello字符串,这是因为父进程中使用了_exit函数,结束时并不会将输出缓冲区的字符刷新出来
实际上exit函数内部调用了_exit函数,所以功能更齐全一些,在使用时我们首选exit函数
我们在之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。所以,父进程派给子进程的任务完成的如何(子进程运行结果对还是不对, 或者是否正常退出),我们需要知道。
这就需要某些函数来帮父进程收集子进程的结束的状态:
我们可以使用wait函数来收集父进程创建的任何一个子进程结束的状态:
该函数只有一个形参,我们一般不去管它,直接传入NULL
该函数的返回值:成功返回被等待进程pid,失败返回-1
下面我们来实操一下:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int time = 3;
while (time)
{
sleep(1);
printf("我是子进程,距离结束还有%ds,pid:%d,ppid:%d\n", time--, getpid(), getppid());
}
exit(0);
}
else if (id > 0)
{
sleep(5);
wait(NULL);
printf("我是父进程,子进程已经被回收\n");
}
return 0;
}
在上述代码运行时,我们用下面的指令检测进程状态:
while :; do ps axj | head -1 && ps -axj | grep test | grep -v grep ; echo "--------------------------";sleep 1;done
运行结果:
我们可以看到在程序开始运行时,父子进程都处于S+的状态,当子进程结束后,父进程还在S状态没有立即回收子进程,此时子进程进入Z(僵尸状态),等待父进程sleep函数运行结束,调用wait函数回收子进程后子进程被销毁,父进程等待1s后退出进程
wait函数只能回收最先结束的子进程,而且没有参数来告诉我们子进程是否正常结束,为了补缺这些不足,我们下面介绍waitpid函数:
我们可以看到该函数有三个形参,
pid: pid=-1,等待任一个子进程。与wait等效。 pid>0.等待其进程ID与pid相等的子进程。
status: 该参数是一个输出型参数,传入一个int类型变量的地址,在调用该函数后,该变量的前16个比特位会带有子进程退出时的状态信息(后16位比特位舍弃不用)
status的实际前16位比特位记录方式分为正常终止和异常终止两种:
可以看到
● 在子进程正常结束时,status参数的前16位中的后8位比特位记录的是子进程的退出状态(退出码),前8位比特位为0值;
● 在子进程异常结束时status参数的前16位中的前7位比特位记录的是子进程的终止信号(我们在后面会说到),第8位比特位记录core dump标志(后期我们也会仔细讨论),后8位比特位舍弃不用
options: 将该参数设为0,如果检测到子进程还没退出,父进程进入阻塞状态等待子进程退出为止;将其设为WNOHANG,如果检测到子进程还没退出,父进程不进入阻塞,进行执行下面的代码
我们下面先暂时先不管options参数,暂且将其设为0来演示一下waitpid函数的使用:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int time = 3;
while (time)
{
sleep(1);
printf("我是子进程,距离结束还有%ds,pid:%d,ppid:%d\n", time--, getpid(), getppid());
}
exit(99);
}
else if (id > 0)
{
sleep(5);
int status = 0;
pid_t ret_pid = waitpid(id, &status, 0);
printf("我是父进程,子进程已经被回收,ret_pid:%d,child exit code:%d,child exit signal:%d\n", ret_pid, (status >> 8) & 0xFF, status & 0x7F);
//(status>>8)&0xFF先将status右移8个bit位再&上0xFF取得status变量的前16位中的后8位比特位的数据,status&0x7F也是同理取其前7位比特位的数据
sleep(1);
}
return 0;
}
运行结果:
我们可以看到在子进程正常结束,父进程调用waitpid函数回收子进程,拿到了子进程的pid、退出码(0)、进程终止信号(由于是正常退出,其为0)
下面我们写一段错误代码,让子进程崩溃异常退出来看看:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int time = 3;
while (time)
{
sleep(1);
printf("我是子进程,距离结束还有%ds,pid:%d,ppid:%d\n", time--, getpid(), getppid());
int a = 1;
a /= 0;//子进程运行到这里会崩溃
}
exit(0);
}
else if (id > 0)
{
sleep(5);
int status = 0;
pid_t ret_pid = waitpid(id, &status, 0);
printf("我是父进程,子进程已经被回收,ret_pid:%d,child exit code:%d,child exit signal:%d\n", ret_pid, (status >> 8) & 0xFF, status & 0x7F);
//(status>>8)&0xFF先将status右移8个bit位再&上0xFF取得status变量的前16位中的后8位比特位的数据,status&0x7F也是同理取其前7位比特位的数据
sleep(1);
}
return 0;
}
运行结果:
可以看到子进程异常退出被父进程回收之后,其终止信号为8,而错误码在这里为0但毫无意义
我们现在来看一下下面代码的运行结果:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int time = 3;
while (time)
{
sleep(1);
printf("我是子进程,距离结束还有%ds,pid:%d,ppid:%d\n", time--,getpid(),getppid());
}
exit(99);
}
else if (id > 0)
{
int status = 0;
pid_t ret_pid = waitpid(id, &status, 0);
printf("我是父进程,子进程已经被回收,ret_pid:%d,child exit code:%d,child exit signal:%d\n", ret_pid, (status >> 8) & 0xFF, status & 0x7F);
//(status>>8)&0xFF先将status右移8个bit位再&上0xFF取得status变量的前16位中的后8位比特位的数据,status&0x7F也是同理取其前7位比特位的数据
}
return 0;
}
这里我们有一个疑问,父子进程在cup上几乎是同时被运行的,那子进程在还没有退出时,这时父进程在干嘛呢?
在父进程调用waitpid函数时,如果要被回收的子进程还没有退出,这时父进程会退出cup运行队列进入阻塞状态。子进程的task_struct中有一个指针是指向其父进程的task_struc的,在子进程退出时,系统会使用这个指针将其指向的父进程的task_struc调入cup运行队列,当父进程再被cpu调度时会再次执行waitpid函数回收已经退出的字进程
那我们不想让父进程调用waitpid函数后进入阻塞状态呢(在子进程还没退出时,父进程不进入阻塞状态等待子进程结束,而是做一些任务)?
这样子我们将option参数设为WNOHANG即可:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int time = 3;
while (time)
{
printf("我是子进程,距离结束还有%ds,pid:%d,ppid:%d\n", time--, getpid(), getppid());
sleep(1);
}
exit(99);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
printf("我是父进程,子进程已经被回收,ret_pid:%d,child exit code:%d,child exit signal:%d\n", ret_pid, (status >> 8) & 0xFF, status & 0x7F);
//(status>>8)&0xFF先将status右移8个bit位再&上0xFF取得status变量的前16位中的后8位比特位的数据,status&0x7F也是同理取其前7位比特位的数据
break;
}
}
}
return 0;
}
运行结果:
我们可以看到将option参数设置为WNOHANG后,父进程调用waitpid函数时,即使子进程没有退出,父进程不会进入阻塞状态,而是进行向下执行代码
WIFEXITED和WEXITSTATUS是分别是两个宏,经常被用来提取waitpid函数中的status数据:
WIFEXITED(status): 进程正常退出,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 提取子进程的退出码。(查看进程的退出码)
下面是代码演示:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
int time = 3;
while (time)
{
printf("我是子进程,距离结束还有%ds,pid:%d,ppid:%d\n", time--, getpid(), getppid());
sleep(1);
}
exit(99);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
我们创建子进程目的无非就两个:
● 让子进程执行父进程代码的一部分
● 让子进程执行全新的代码程序
我们之前写的代码,子进程都在执行父进程代码的一部分(通过ifelse语句进行进程的分流,但子进程执行的代码就是父进程原有的)
下面我们来谈谈怎么让一个子进程执行全新的代码程序,这就要涉及到进程替换了:
我们来介绍程序替换的第一个函数execl(包含在头文件unistd.h中):
在该函数的形参里我们可以看到有...
这表示可变参数列表,在C语言中可以传入任意个数的参数并用NULL结尾
下面我们写出这样一段代码:
#include
#include
int main()
{
printf("begin.......\n");
printf("begin.......\n");
printf("begin.......\n");
execl("/bin/ls", "ls", "-a", "-l", NULL);
printf("end.........\n");
printf("end.........\n");
printf("end.........\n");
return 0;
}
这段代码的意思为,当程序调用execl函数时,这段代码会被替换为bin目录下的ls指令的代码,且ls指令后跟有-a-l的选项
我们来看看运行结果:
我们可以看到当执行到excel函数时,系统调用了ls程序的代码,执行了该进程代码以外的代码
奇怪的是execl函数后面的printf语句怎么没有继续被打印出来?
这是因为,execl函数会用调用的代码替换所有原来的代码,替换完之后原先的代码就不存在了,这时未执行的printf函数也就没有任何效果了:
那既然会进程替换函数会替换所有的原代码,那不是成事不足败事有余?
并不是,我们可以创建一个子进程,在子进程中进行进程替换,子进程在发生代码替换时会进行写时拷贝,这时子进程可以运行想要运行的新的代码,并且父进程并不会受到影响:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
execl("/bin/ls", "ls", "-a", "-l", NULL);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
说到这里,我们已经可以理解进程替换到底是怎么一回事了,下面我们来好好说说execl函数的参数:
该函数是用来进行进程替换的,所以第一个参数我们要告诉该函要替换的代码在上面地方(即要替换进程的路径)
后面的参数是我们要怎么执行替换的进程(我们在命令行怎么敲命令就怎么传参数),例如:
在结尾不要忘了加上NULL哦
下面我们来介绍第二个进程替换函数execv(包含在头文件unistd.h中):
该函与execl唯一不同的就是我们要传入指令和选项时是以指针数组的形式一次性传进去:
例如:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
char* const argv[] = { "ls","-n","-a","-l",NULL };
execv("/bin/ls", argv);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
第三个进程替换函数execlp(包含在头文件unistd.h中):
该函数与execl的不同在于,第一个参数传想要替换的程序名即可,传入程序名后execp函数会自动正在PATH环境变量的路径下查找该程序
举例:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
execlp("ls", "ls","-a","-l",NULL);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
该函数包含在头文件unistd.h中:
通过前三个函数我们不用说也能看出该函数的传参规律:
第一个参数传想要替换的程序名即可,第二个参数将要传入指令和选项时以指针数组的形式一次性传进去
例如:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
char* const argv[] = { "ls","-n","-a","-l",NULL };
execvp("ls", argv);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
execle函数需要我们花些时间研究(包含在头文件unistd.h中):
我们可以看到该函数的参数多了一个envp,这意味着我们可以自己导入环境变量!
下面我们写一个c++程序,再利用进程替换函数看能不能在一个c语言程序中调起这个c++程序:
test3.cc:
#include
using namespace std;
int main()
{
cout << "我是另一个c++程序" << endl;
return 0;
}
test2.c:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
execl("./test3", "test3", NULL);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
编译后运行:
成功了!
我们知道子进程是可以继承父进程的环境变量的,现在我们来看看调用c++程序运行时其中有什么环境变量:
test3.cc:
#include
using namespace std;
int main(int argc, char* argv[], char* envp[])
{
cout << "我是另一个c++程序" << endl;
for (int i = 0; envp[i]; ++i)
{
cout << envp[i] << endl;
}
return 0;
}
我们可以发现被调起的c++程序继承了其父进程中的所有环境变量
现在我们使用execle这个函数来调c++程序试试看:
test2:在下面的代码中加上了自定义的环境变量MYENV:
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
char* const myenv[] = { "MYENV=YouCanSeeMe", NULL };
execle("./test3", "test3", NULL, myenv);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
运行结果:
我们可以看到通过execle函数调起的进程只会继承最后一个函数接口中传入的环境变量,像上述代码的情况就不会继承父进程的环境变量了
如果我们使用execle函数想要调用的进程含有父进程的环境变量可以使用environ变量(具体讲解在【Linux】环境变量):
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
extern char** environ;//使用environ前先声明
execle("./test3", "test3", NULL, environ);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
这样子进程就带有父进程中所有环境变量了
那如果想要在父进程环境变量之上再加上一些自定义环境变量交一起给子进程呢?
可以使用putenv函数:
该函数包含在文件stdlib.h中,在形参传入想要添加的环境变量即可:
#include
#include
#include
#include
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程\n");
extern char** environ;//使用environ前先声明
putenv("MYENV=YouCanSeeMe");
execle("./test3", "test3", NULL, environ);
}
else if (id > 0)
{
int status = 0;
while (1)
{
pid_t ret_pid = waitpid(id, &status, WNOHANG);
if (ret_pid < 0)
{
printf("waitpid error\n");
exit(1);
}
else if (ret_pid == 0)
{
printf("子进程还没退出,我再做做其他事情...\n");
sleep(1);
continue;
}
else
{
if (WIFEXITED(status))
printf("success ,ret_pid:%d,child exit code:%d\n", ret_pid, WEXITSTATUS(status));
else
printf("child process error , ret_pid: % d, child exit signal : % d\n", ret_pid, status & 0x7F);
break;
}
}
}
return 0;
}
这样子被调用的c++进程中除了父进程原本的环境变量外还多了一个MYENV
execvpe/execve(包含在头文件中)这两个进程替换函数就不用多说了,根据上面5个函数的规律,很容易推导出它们的用法:
其中execve函数是所有进程替换函数的基础,其他的函数都是对execve函数的封装
本期内容结束~