一.SIGCHLD
(1)当子进程退出的时候,内核会向父进程发送SIGCHLD信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止)
(2)子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它只保留最小的一些内核数据结构,以便父进程查询子进程的退出状态。
(3)父进程查询子进程的退出状态可以用wait/waitpid函数
二.wait
(1)头文件<sys/types.h>和<sys/wait.h>
函数功能:当我们用fork启动一个进程时,子进程就有了自己的生命,并将独立地运行。有时,我们需要知道某个子进程是否已经结束了,我们可以通过wait安排父进程在子进程结束之后。
函数原型:pid_t wait(int *status)
函数参数:status:该参数可以获得你等待子进程的信息
返回值:成功等待子进程函数返回等待子进程的ID
(2)wait系统调用会使父进程暂停执行,直到它的一个子进程结束为止。
(3)返回的是子进程的PID,它通常是结束的子进程
(4)状态信息允许父进程判定子进程的退出状态,即从子进程的main函数返回的值或子进程中exit语句的退出码。
(5)如果status不是一个空指针,状态信息将被写入它指向的位置
(6)这些宏在sys/wait.h头文件里定义
宏定义 | 描述 |
WIFEXITED(status) | 如果子进程正常结束,返回一个非零值 |
WEXITSTATUS(status) | 如果WIFEXITED非零,返回子进程退出码 |
WIFSIGNALED(status) | 子进程因为捕获信号而终止,返回非零值 |
WTERMSIG(status) | 如果WIFSIGNALED非零,返回信号代码 |
WIFSTOPPED(status) | 如果子进程被暂停,返回一个非零值 |
WSTOPSIG(status) | 如果WIFSTOPPED非零,返回一个信号代码 |
三.waitpid
(1)函数功能:用来等待某个特定进程的结束
(2)函数原型:pid_t waitpid(pid_t pid, int *status,int options)
(3)参数:status:如果不是空,会把状态信息写到它指向的位置
options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起
返回值:如果成功返回等待子进程的ID,失败返回-1
(4)函数说明:
对于waitpid的p i d参数的解释与其值有关:
[1]id == -1 等待任一子进程。于是在这一功能方面waitpid与wait等效。
[2]pid > 0 等待其进程I D与p i d相等的子进程。
[3]pid == 0 等待其组I D等于调用进程的组I D的任一子进程。换句话说是与调用者进程同在一个组的进程。
[4]pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
(5)wait与waipid的区别
[1]在一个子进程终止前, wait 使其调用者阻塞,而waitpid 有一选择项,可使调用者不阻塞。
[2]waitpid并不等待第一个终止的子进程—它有若干个选择项,可以控制它所等待的特定进程。
[3]实际上wait函数是waitpid函数的一个特例。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if (pid == -1)
ERR_EXIT("fork error");
if (pid == 0)
{
sleep(3);
printf("this is child\n");
//exit(100);
abort();
}
int ret;
printf("this is parent\n");
int status;
//ret = wait(&status);
//ret = waitpid(-1, &status, 0);
ret = waitpid(pid, &status, 0);
printf("ret = %d pid = %d\n", ret, pid);
if (WIFEXITED(status))
printf("child exited normal exit status=%d\n", WEXITSTATUS(status));
/*else
printf("child exited abnormal\n");
*/
else if (WIFSIGNALED(status))
printf("child exited abnormal signal number=%d\n", WTERMSIG(status));
else if (WIFSTOPPED(status))
printf("child stoped signal number=%d\n", WSTOPSIG(status));
return 0;
}
四.僵进程
(1)当一个子进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行或者父进程调用了wait才告终止。
(2)进程表中代表子进程的数据项是不会立刻释放的,虽然不再活跃了,可子进程还停留在系统里,因为它的退出码还需要保存起来以备父进程中后续的wait调用使用。它将称为一个“僵进程”。
五.如何避免僵进程
(1)调用wait或者waitpid函数查询子进程退出状态,此方法父进程会被挂起。
(2)如果不想让父进程挂起,可以在父进程中加入一条语句:signal(SIGCHLD,SIG_IGN);表示父进程忽略SIGCHLD信号,该信号是子进程退出的时候向父进程发送的。
六.system
(1)功能:system()函数调用“/bin/sh -c command”执行特定的命令,阻塞当前进程直到command命令执行完毕
(2)原型:int system(const char *command);
(3)返回值:
如果无法启动shell运行命令,system将返回127;出现不能执行system调用的其他错误时返回-1。如果system能够顺利执行,返回那个命令的退出码。
(4)system函数执行时,会调用fork、execve、waitpid等函数。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int my_system(const char *command);
int main(int argc, char *argv[])
{
//system("ls -l | wc -w");
my_system("ls -l | wc -w");
return 0;
}
int my_system(const char *command)
{
pid_t pid;
int status;
if (command == NULL)
return 1;
if ((pid = fork()) < 0)
status = -1;
else if (pid == 0)
{
execl("/bin/sh", "sh", "-c", command, NULL);
exit(127);
}
else
{
while (waitpid(pid, &status, 0) < 0)
{
if (errno == EINTR)
continue;
status = -1;
break;
}
}
return status;
}
1、什么是僵尸进程和孤儿进程:
在 Unix/Linux 系统中,正常情况下,子进程是通过父进程创建的,且两者的运行是相互独立的,父进程永远无法预测子进程到底什么时候结束。当一个进程调用 exit 命令结束自己的生命时,其实它并没有真正的被销毁,内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等,但是留下一个称为僵尸进程的数据结构,这个结构保留了一定的信息(包括进程号 the process ID,退出状态,运行时间),这些信息直到父进程通过 wait()/waitpid() 来取时才释放。这样设计的目的主要是保证只要父进程想知道子进程结束时的状态信息,就可以得到
僵尸进程:一个进程使用 fork 创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并由 init 进程对它们完成状态收集工作。
2、僵尸进程与孤儿进程的问题危害:
僵尸进程虽然不占有任何内存空间,但如果父进程不调用 wait() / waitpid() 的话,那么保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程,此即为僵尸进程的危害。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了 init 进程身上,init 进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为 init,而 init 进程会循环地 wait() 它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init 进程就会出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
如果子进程在 exit() 之后,父进程没有来得及处理,这时用 ps 命令就能看到子进程的状态是“Z”。如果父进程能及时处理,可能用 ps 命令就来不及看到子进程的僵尸状态,但这并不等于子进程不经过僵尸状态。 如果父进程在子进程结束之前退出,则子进程将由 init 接管。init 将会以父进程的身份对僵尸状态的子进程进行处理。
3、如果解决僵尸进程造成的问题:
(1)方案一:父进程通过 wait 和 waitpid 等函数等待子进程结束,但这会导致父进程挂起,所以这并不是一个好办法,父进程如果不能和子进程并发执行的话,那我们创建子进程的意义就没有。同时一个 wait 只能解决一个子进程,如果有多个子进程就要用到多个 wait
(2)方案二:通过信号机制:
子进程退出时,向父进程发送 SIGCHILD 信号,父进程处理 SIGCHILD 信号,在信号处理函数中调用 wait 进行处理僵尸进程。
(3)方案三:fork两次:
原理是将进程成为孤儿进程,从而其的父进程变为 init 进程,通过 init 进程处理僵尸进程。具体操作为:父进程一次 fork() 后产生一个子进程随后立即执行 wait(NULL) 来等待子进程结束,然后子进程 fork() 后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵死进程了
(4)方案四:kill 父进程:
严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大量僵死进程的那个元凶枪毙掉(也就是通过 kill 发送 SIGTERM 或者 SIGKILL 信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被 init 进程接管,init 进程会 wait() 这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程就能瞑目而去了。
原文链接:https://blog.csdn.net/a745233700/article/details/120715371