进程终止
进程终止时,操作系统要释放对应进程申请的相关内核数据结构和对应的代码和数据。其不本质就是释放进程申请的系统资源。
进程终止的常见方式:
1、代码运行完毕且结果正确。
2、代码运行完毕但结果不正确。
3、代码没运行完,进程异常退出。
main函数的返回值是进程的退出码,如果为0,表示运行结果是正确的,如果非零,标识的是运行结果不正确。进程退出码用来返回给上一级进程,用来评判该进程的执行结果,另外,如果进程退出码是非零的,不同的非零值可以标识不同的错误原因,便于定位错误原因。通常使用stderror(errno)以字符串形式来输出具体错误原因。
进程等待
子进程退出时,如果父进程不管子进程,那么子进程就处于僵尸状态,会导致系统资源层面的内存泄漏问题。另外,父进程创建子进程,是为了完成某种任务,那么父进程就可能要关心子进程将该任务完成的怎么样,因此需要等待子进程退出。
具体系统接口:
wait函数和waitpid函数的返回值:
wait函数等待子进程成功就会返回子进程的pid,如果失败,就会返回-1。 如果设置了status参数,那么对status进行设置,将进程的具体状态设置进去。
waitpid函数参数
pid_t pid:可以有四种状态,当pid = -1时,代表等待任意一个子进程退出,当pid > 0时,会等待对应子进程识别码为pid的子进程退出,当pid < -1时,等待进程组识别码为pid绝对值的任何子进程,当pid = 0时,等待进程组识别码为pid的子进程。
options: 0表示阻塞等待,如果没有等到子进程退出,就会一直阻塞等待,WNOHANG表示非阻塞等待。
status:wait和waitpid函数会把被等待进程的退出状态信息记录到status中。另外,如果进程异常退出,那么此时的进程退出码是没有意义的。
status的构成
status并不是按照整数来整体使用的,而是按照比特位的方式,将32个比特位进行划分,这里只关心低七位和次低八位。
最低的7个比特位用来表示进程收到的信号,次低八位用来表示进程退出码。
具体例子:
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<stdlib.h>
int code = 0;
int main()
{
pid_t id = fork();
if (id < 0)
{
//创建子进程失败
perror("fork");
exit(1);//不正常退出
}
else if (id == 0)
{
//child process
int cnt = 5;
while (cnt)
{
printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid());
sleep(1);
cnt--;
}
code = 15;
exit(15);
}
else
{
//parent process
printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid());
int status = 0;
pid_t ret = waitpid(id, &status, 0); //阻塞式的等待!
if (ret > 0)
{
//0x7F -> 00000..1111111低七位表示退出信号
//次低八位表示退出信号
printf("等待子进程成功,ret: %d, 子进程信号编号:%d,子进程退出码%d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
}
}
}
因为进程异常退出时,所得到的进程退出码是无意义的,所以一般只有在进程正常退出时,才去关心进程的退出码。Linux中提供了对应的宏,不用每次手动提取。
WIFEXITED:若为正常终止的子进程返回的状态,则为真(查看进程是否正常退出)
WEXITSTATUS:若WIFEXITED非零,提取子进程退出码(查看进程退出码)
具体例子:
#include<sys/wait.h>
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int status = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
exit(-1);
}
else if(id > 0)
{
//child process
}
else
{
//parent process
pid_t ret = waitpid(id, &status, 0);
if(WIFEXITED(status))//退出信号
{
//子进程正常退出
printf("子进程执行完毕,退出码:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程异常退出:%d\n", WIFEXITED(status));
}
}
return 0;
}
进程程序替换
fork()之后,父子进程各自执行进程代码的一部分,但如果子进程想执行一个全新的程序,就要通过进程程序替换,通过特定的接口,加载磁盘上的一个全新的程序(代码+数据),加载到调用进程的地址空间中。其本质是将新的磁盘上的程序上的程序加载到内存,并和当前进程的页表重新建立映射的过程。
进程替换并没有创建新的子进程,只是改变了对应的映射关系。
具体的exec接口系列:
具体例子:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int status = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
_exit(1);//系统接口不会刷新缓冲区
}
else if(id > 0)
{
//parent process
printf("我是父进程\n");
}
else
{
//child process
printf("我是子进程\n");
execlp("python", "python", "test.py", NULL);
fflush(stdout);
pid_t ret = waitpid(id, &status, 0);
if(WIFEXITED(status))//退出信号
{
//子进程正常退出
printf("子进程执行完毕,退出码:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程异常退出:%d\n", WIFEXITED(status));
}
printf("我是子进程");
}
return 0;
}
细节:execl系列的接口是程序替换,调用该函数成功后,会将当前进程的所有代码和数据都进行替换,包括已经执行的和没有执行的。所以一旦调用成功,后续所有代码都不会被执行。
为何需要创建子进程的原因:
结合进程替换,如果不创建子进程,那么在进程替换时只能替换父进程。创建子进程之后,替换的就是子进程,而不影响父进程。因为父进程一般聚焦于读取数据,解析数据,指派执行代码的功能。