目录
进程等待
进程等待是什么?
任何子进程,在退出的情况下,一般必须要被父进程进行等待。在退出的时候,如果父进程不管不顾,退出进程,则子进程就变成僵尸状态(Z状态),kill -9 都无法杀死,因为谁也没有办法杀死一个已经死去的进程,最终会造成内存泄漏
子进程的僵尸只能由父进程解决,父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息(因为父进程需要知道自己派给子进程运行结果是否正确。子进程是否正常退出等)
为什么?
1.父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的)
2.获取子进程的退出信息,要知道子进程是因为什么原因退出的(可选的功能)
正是因为这两点,我们需要进程等待
怎么办?
wait方法
当子进程等待成功的时候,会返回等待成功的子进程pid,失败则返回-1。而传入的参数status为输出型参数,如果需要回去子进程的退出状态,则需要传入status;若不关心,则可以填写NULL。
【示例1】
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("[%d]->I am child process! pid = %d, ppid = %d\n", cnt, getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
int status = 0;
pid_t ret = wait(&status);
if(ret > 0)
{
printf("wait %d success! status = %d\n", ret, status);
}
return 0;
}
cpp
[wuxu@Nanyi lesson17]$ gcc -o test wait1.c -std=c99
[wuxu@Nanyi lesson17]$ ./test
[5]->I am child process! pid = 4593, ppid = 4592
[4]->I am child process! pid = 4593, ppid = 4592
[3]->I am child process! pid = 4593, ppid = 4592
[2]->I am child process! pid = 4593, ppid = 4592
[1]->I am child process! pid = 4593, ppid = 4592
wait 4593 success! status = 0
从上面代码可以发现,当子进程在执行时,父进程并不会执行wait之后的if判断语句,而是阻塞在pid_t ret = wait(&status);
处等待子进程退出。故wait是阻塞等待的。
获取子进程status
❍ wait 和 waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
❍ 如果传递NULL,表示不关心子进程的退出状态信息
❍ 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
❍ status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16位的比特位)
①正常终止时,第8到第15位保存进程的退出状态 ②异常终止时,第8到第15位保存进程的退出状态(但异常终止的退出状态不一定能正确标识进程的退出状态,这里的退出状态是无效的,直接忽略),第7位为coredump标志位,这里先忽略这一位,将于后面的文章中介绍,第0到7位存储的是当前进程收到的信号。
status
参数中可能包含的信息:
-
退出码 :如果子进程是正常退出,
status
将包含子进程的退出码。 -
信号编号 :如果子进程是因信号而终止,
status
将包含导致子进程终止的信号编号。 -
暂停状态 :如果子进程被暂停(例如,由信号
SIGSTOP
、SIGTSTP
或SIGTTIN
暂停),status
将包含导致子进程暂停的信号编号。 -
退出状态 :如果子进程是正常退出,
status
的低8位(WIFEXITED(status)
)将设置为1,并且WEXITSTATUS(status)
将包含退出码。 -
信号状态 :如果子进程是因信号而终止,
status
的低8位(WIFSIGNALED(status)
)将设置为1,并且WTERMSIG(status)
将包含信号编号。 -
暂停状态 :如果子进程被暂停,
status
的低8位(WIFSTOPPED(status)
)将设置为1,并且WSTOPSIG(status)
将包含信号编号。
如果我们想获取子进程的退出码和退出信号,则上面代码应修改成下面这样
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("[%d]->I am child process! pid = %d, ppid = %d\n", cnt, getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
int status = 0;
pid_t ret = wait(&status);
if(ret > 0)
{
printf("wait %d success! exitcode = %d sig = %d\n", ret, ((status >> 8) & 0xFF), (status & 0x7F));
}
return 0;
}
cpp
[wuxu@Nanyi lesson17]$ gcc -o test wait1.c -std=c99
[wuxu@Nanyi lesson17]$ ./test
[5]->I am child process! pid = 4928, ppid = 4927
[4]->I am child process! pid = 4928, ppid = 4927
[3]->I am child process! pid = 4928, ppid = 4927
[2]->I am child process! pid = 4928, ppid = 4927
[1]->I am child process! pid = 4928, ppid = 4927
wait 4928 success! exitcode = 0 sign = 0
多进程的等待问题
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
int i = 0;
for(; i < 5; i++)
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("[%d]->I am child process! pid = %d, ppid = %d\n", cnt, getpid(), getppid());
cnt--;
sleep(1);
}
exit(0);
}
}
i = 0;
for(; i < 5; i++)
{
int status = 0;
pid_t ret = wait(&status);
if(ret > 0)
{
printf("wait %d success! exitcode = %d sign = %d\n", ret, ((status >> 8) & 0xFF), (status & 0x7F));
}
}
return 0;
}
waitpid方法
waitpid与wait方法相同,如果子进程等待成功,则返回该子进程的pid,如果返回失败则返回1;并且一样可以传入status获取子进程的退出信息
waitpid()
函数比wait()
函数多两个参数:pid
和options
。这两个参数允许用户更加精确地控制进程的等待行为
pid
参数
-
含义 :
pid
参数是一个整数,它指定等待哪个子进程的结束。如果pid
为-1
,则waitpid()
会等待任意一个子进程的结束。如果pid
为0,则waitpid()
会等待调用fork()
创建的所有子进程的结束。如果pid
大于0,则waitpid()
会等待指定PID的子进程的结束。 -
返回值 :如果
pid
为-1
,waitpid()
返回最后一个子进程的PID;如果pid
为其他值,则返回指定PID的子进程的PID。
options
参数
-
含义:
options
参数是一个整数,它定义了waitpid()
的行为。这个参数可以是WNOHANG
、WUNTRACED
或它们的组合。 -
WNOHANG
:如果指定的子进程尚未结束,waitpid()
不会阻塞调用进程,而是立即返回。 -
WUNTRACED
:如果指定的子进程暂停执行(如被信号暂停),waitpid()
也会返回,并且不会返回WNOHANG
。 -
WNOHANG
和WUNTRACED
不能同时使用。
status
参数
-
如果子进程是正常终止的:
WIFEXITED(status)
宏可以用来检查子进程是否正常退出。如果是,WEXITSTATUS(status)
宏可以提取子进程的退出状态(即exit()
或_exit()
的参数)。
-
如果子进程是因信号而终止的:
-
WIFSIGNALED(status)
宏可以用来检查子进程是否因为捕获到的信号而终止。如果是,WTERMSIG(status)
宏可以提取导致子进程终止的信号的编号。 -
WCOREDUMP(status)
宏可以用来检查子进程是否产生了核心转储文件。
-
-
如果子进程是暂停状态 (由于接收到
SIGSTOP
,SIGTSTP
,SIGTTIN
, 或SIGTTOU
信号):WIFSTOPPED(status)
宏可以用来检查子进程是否已经停止。如果是,WSTOPSIG(status)
宏可以提取导致子进程暂停的信号的编号。
-
如果子进程是从暂停状态继续运行的:
WIFCONTINUED(status)
宏可以用来检查子进程是否继续执行。
cpp
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t cpid, w;
int status;
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* 子进程 */
printf("子进程: 我将退出并返回状态 %d\n", 42);
_exit(42); // 子进程退出,返回42
} else { /* 父进程 */
do {
w = waitpid(cpid, &status, WUNTRACED | WCONTINUED);
if (w == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("子进程正常终止,返回状态=%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("子进程因信号 %d 终止\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("子进程被信号 %d 暂停\n", WSTOPSIG(status));
} else if (WIFCONTINUED(status)) {
printf("子进程继续执行\n");
}
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
exit(EXIT_SUCCESS);
}
}
【示例】
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("[%d]->child process, pid = %d\n", cnt, getpid());
cnt--;
sleep(1);
}
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret == id)
{
printf("wait %d success! exitcode = %d, sig =%d\n", ret ,((status >> 8) & 0xFF), (status & 0x7F));
}
return 0;
}
cpp
[wuxu@Nanyi lesson17]$ gcc -o test wait3.c -std=c99
[wuxu@Nanyi lesson17]$ ./test
[5]->child process, pid = 5313
[4]->child process, pid = 5313
[3]->child process, pid = 5313
[2]->child process, pid = 5313
[1]->child process, pid = 5313
wait 5313 success! exitcode = 0, sign =0
什么是阻塞等待?什么是非阻塞等待?
【例子】阻塞等待
张三在大学是个学渣,上课时间天天出去玩,以致于明天就要期末考试了,于是找到学霸李四,要求李四帮忙辅导一下。于是张三打电话告诉李四"李四,你帮我辅导一下明天操作系统期末考试呗,顺便请你吃个饭,就当报答你了",李四一听,说"可以,但是你要先等我一小时,我先把计算机组成原理看完",于是张三就来到李四家楼下,等待着李四。张三什么事也不做,就在干巴巴的等着,每过一分钟就给李四打电话,问李四好了没,直到李四说好了,下楼了,他才停止
这种循环等待,其他事也不做,就在干巴巴等着等反馈,这就是阻塞式等待,上面的wait和waitpid代码均是阻塞等待,父进程在wait/waitpid函数上阻塞等待着子进程,而不做其他事,等子进程执行结束才执行下面的if判断语句。
阻塞式等待可以及时回收子进程,但父进程花费大量时间阻塞等待子进程,只要CPU时间片轮到父进程,父进程就一直循环等待子进程,这样效率明显不高
【例子】非阻塞等待
男女朋友约好一起去逛街,女朋友对男友说,你先等我1小时,我画个妆。于是男友开始等待女友,在等待的过程中,男友打打游戏,每打完一把游戏问女朋友好了没。像这种在等待时,顺便做一下其它任务,这种称为非阻塞等待。
像这种再等待时,顺带做一下其他任务,这种称为非阻塞等待。非阻塞等待执行的其他任务不应花费过多时间,以免影响父进程对子进程的等待与回收(因为父进程等待子进程才是主要任务,执行其他代码只是顺带执行,不是重点)。
cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void PrintLog()
{
printf("begin PrintLog...\n");
}
void Download()
{
printf("begin Download...\n");
}
void MysqlDataSync()
{
printf("begin MySQLDataSync...\n");
}
typedef void(*func_t)();
#define N 3
func_t tasks[N] = {NULL};
void LoadTask()
{
tasks[0] = PrintLog;
tasks[1] = Download;
tasks[2] = MysqlDataSync;
}
void HandlerTask()
{
for(int i = 0; i < N; i++)
{
tasks[i](); // 回调方式
}
}
// fahter
void DoOtherThing()
{
HandlerTask();
}
void ChildRun()
{
//int *p = NULL;
int cnt = 5;
while(cnt)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
//*p = 100;
}
}
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if(id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(123);
}
LoadTask();
// father
while(1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG); // non block
if(rid == 0)
{
sleep(1);
printf("child is running, father check next time!\n");
DoOtherThing();
}
else if(rid > 0)
{
if(WIFEXITED(status))
{
printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
}
else
{
printf("child quit unnormal!\n");
}
break;
}
else
{
printf("waitpid failed!\n");
break;
}
}
return 0;
}
wait/waitpid获取子进程信息原理
当子进程处于僵尸状态时,系统至少要保留子进程的PCB,PCB中保存着子进程的退出码、收到的信号等退出信息。
当父进程在CPU上执行waitpid时,则进入内核态。处于内核态的执行语句有操作系统的权限,因此它可以读取子进程的退出信息,并将该退出信息填写入status。