一、函数wait、waitpid
一个进程在终止时会关闭所有文件描述符,释放在用户空间释放的内存,但它的PCB还保留着,内核在其中保存一些信息:如果是正常终止时则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个,这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除这个进程,我们知道一个进程的退出状态可以在shell用特殊变量$?查看,因为shell是它的父进程,当它终止时shell调用wait或waitpid得到它的退出状态同时彻底清除这个进程。
1. wait函数原型:一次只能回收一个子进程
pid_t wait(int *status);
- 当进程终止时,操作系统隐式回收机制会:1. 关闭所有的文件描述符 2. 释放用户空间分配的内存。内核PCB仍存在,其中保存该进程的退出状态。(正常终止--------退出值;异常终止-------终止信号
2. 函数waitpid原型:
作用:同wait,但可指定pid进程清理,可以不阻塞( 一次只能回收一个子进程)
pid_t waitpid(pid_t pid, int *staloc, int options);
参数pid:
- pid == -1:回收任一子进程
- pid > 0 :回收指定pid的进程
- pid == 0 :回收与父进程同一个进程组的任一个子进程
- pid < -1 :回收指定进程组内的任意子进程
参数 options:
- 设置为WNOHANG:函数不阻塞;
- 设置为0:函数阻塞。
3. 测试代码
#include <stdio.h>
#include <unistd.h>
#include<sys/wait.h>
int main(int argc, const char* argv[])
{
pid_t pid = fork();
if (pid > 0) // 父进程
{
printf("parent process, pid = %d, ppid = %d\n", getpid(), getppid());
int status;
pid_t wpid = wait(&status);
if (WIFEXITED(status))
printf("exit value: %d", WEXITSTATUS(status));
if (WIFSIGNALED(status))
printf("exit by signal: %d\n", WTERMSIG(status)); //是否被信号杀死
printf(" die child pid = %d\n", wpid);
}
else if(pid == 0)
{
sleep(1);
printf("child process, pid = %d, ppid = %d\n", getpid(), getppid());
}
return 9;
}
输出结果:
二、孤儿进程、僵尸进程
- 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
- 僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
1. unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
2. 孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤 儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
1. 僵尸进程测试
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main()
{
pid_t pid;
while (1)
{
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am a child process.\nI am exiting.\n");
exit(0); //子进程退出,成为僵尸进程
}
else
{
sleep(20); //父进程休眠20s继续创建子进程
continue;
}
}
return 0;
}
输出结果:
僵尸进程解决办法
1) . 通过信号机制
子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。测试程序如下所示:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
static void sig_child(int signo)
{
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) //处理僵尸进程
printf("child %d terminated.\n", pid);
}
int main()
{
pid_t pid;
signal(SIGCHLD, sig_child); //创建捕捉子进程退出信号
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0)
{
printf("I am child process,pid id %d.I am exiting.\n", getpid());
exit(0);
}
printf("I am father process.I will sleep two seconds\n"); //等待子进程先退出
sleep(2);
system("ps -o pid,ppid,state,tty,command"); //输出进程信息
printf("father process is exiting.\n");
return 0;
}
输出结果:
2)fork两次
《Unix 环境高级编程》8.6节说的非常详细。原理是将子进程成为孤儿进程,从而其的父进程变为init进程,通过init进程可以处理僵尸进程。测试程序如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid == 0) //第一个子进程
{
printf("I am the first child process. pid:%d\tppid:%d\n", getpid(), getppid());
pid = fork();
if (pid < 0)
{
perror("fork error:");
exit(1);
}
else if (pid > 0) //第一个子进程退出
{
printf("first procee is exited.\n");
exit(0);
}
//第二个子进程
//睡眠3s保证第一个子进程退出,这样第二个子进程的父亲就是init进程里
sleep(3);
printf("I am the second child process. pid: %d\tppid:%d\n", getpid(), getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) //父进程处理第一个子进程退出
{
perror("waitepid error:");
exit(1);
}
exit(0);
return 0;
}
输出结果:
二、exec函数族
1. 简介
- 进程程序替换原理
fork创建子进程执行的是和父进程相同的程序(也有可能是某个分支),通常fork出的子进程是为了完成父进程所分配的任务,所以子进程通常会调用一种exec函数(六种中的任何一种)来执行另一个任务。当进程调用exec函数时,当前用户空间的代码和数据会被新程序所替换,该进程就会从新程序的启动历程开始执行。在这个过程中没有创建新进程,所以调用exec并没有改变进程的id。
- 替换图解(图解)
(1). execl函数原型:
int execl(const char *path, const char *arg, ...);
分析:
- path: 要执行的程序的绝对路径
- 变参arg: 要执行的程序的需要的参数
- 第一arg:占位
- 后边的arg: 命令的参数
- 参数写完之后: NULL
- 一般执行自己写的程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
printf("entering main process---\n");
if(execl("ls","ls","-l",NULL)<0)
perror("excl error");
return 0;
}
输出结果:
(2). execv函数原型:
int execv(const char *path, char *const argv[]);
分析:
- path = /bin/ps
- char* args[] = {"ps", "aux", NULL};
- execv("/bin/ps", args);
(3). execlp函数原型
int execlp(const char *file, const char *arg, ...);
分析:
- file: 执行的命令的名字
- 第一arg:占位
- 后边的arg: 命令的参数
- 参数写完之后: NULL
- 执行系统自带的程序
-
execlp执行自定义的程序: file参数绝对路径
(4). execvp函原型:
int execvp(const char *file, char *const argv[]);
(5). execle函数原型:
int execle(const char *path, const char *arg, ..., char *const envp[]);
分析:
- path: 执行的程序的绝对路径 /home/itcast/a.out
- arg: 执行的的程序的参数
- envp: 用户自己指定的搜索目录, 替代PATH
- char* env[] = {"/home/itcast", "/bin", NULL};
int execve(const char *path, char *const argv[], char *const envp[]);
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 参数列表 | 否 | 是 |
execlp | 参数列表 | 是 | 是 |
execle | 参数列表 | 否 | 是 |
execv | 参数数组 | 否 | 是 |
execvp | 参数数组 | 是 | 是 |
execve | 参数数组 | 否 | 否 |
7. 测试代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
for (int i = 0; i < 8; ++i)
printf(" parent i = %d\n", i);
pid_t pid = fork();
if (pid == 0)
{
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(1);
}
for (int i = 0; i < 3; ++i)
printf("----------- i = %d\n", i);
return 0;
}
参考资料
1. 操作系统重点知识汇总