Linux:进程控制
目录
进程的创建
fork函数
for的两个返回值
写时拷贝
fork失败的原因
进程退出
进程退出码
进程终止
终止正常进程
exit()和_exit()
进程退出OS做了什么?
进程等待
什么是进程等待
为什么要进程等待
如何进行进程等待
wait函数:
waitpid函数
status 输出型参数
status的详解
options
进程的创建
fork函数
#include <unistd.h>
pid_t fork(void);
// 返回值:子进程中返回0,父进程中返回子进程id,出错返回-1
fork是Linux中一个很重要的函数,主要是在已经创建的进程中基础创建一个新的进程,新的进程为子进程,原来的进程被叫做父进程。fork函数在进程的认识中提起到,并且也做了初步认识,现在对它做一些简单的复习以及拓展即可。
当进程调用fork的时候,控制会转移到内核中的fork代码,此时内核会进行下面的一些操作:
- 分配新的内存块和内存数据给子进程。
- 将父进程的部分数据结构内容拷贝给子进程。
- 添加子进程到系统的进程列表中(PCB)。
- 令fork的值返回,开始进行调度器的调度。
也就是说,当一个进程调用了fork
之后,就会有两个二进制的代码进行相同的进程,而且都会运行到一样的地方,每一个进程都可以有自己独立的过程,在系统中有很多个方面都可以对维护进程的独立性做出保证。
下面举一个简单的例子来使用温习fork
函数:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 10
int main()
{
int i;
for(i=0;i<N;i++)
{
pid_t id = fork();
if(id==0)
{
printf("[%d],my pid:%d\n",i,getpid());
sleep(1);
exit(0);
}
}
sleep(100);
return 0;
}
结果如下:
[0],my pid:1920
[2],my pid:1922
[3],my pid:1923
[5],my pid:1925
[6],my pid:1926
[1],my pid:1921
[7],my pid:1927
[4],my pid:1924
[9],my pid:1929
[8],my pid:1928
fork的两个返回值
当一个进程调用 fork 之后,在不写入的情况下,用户的代码和数据是父子进程共享的。就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:
#include<stdio.h> // perror
#include<unistd.h> // getpid, getppid, fork
int main()
{
// ...
pid_t ret = fork(); // 返回时发生了写时拷贝
if (ret == 0)
{
// child process
while (1)
{
printf("child process, pid:%u, ppid:%u\n", getpid(), getppid());
sleep(1);
}
}
else if (ret > 0)
{
// father process
while (1)
{
printf("father process, pid:%u, ppid:%u\n", getpid(), getppid());
sleep(1);
}
}
else
{
// failure
perror("fork");
}
return 0;
}
fork 之前父进程独立执行,fork 之后父子进程分别执行。注意:fork 之后谁先执行完全由调度器决定。
画图理解 fork 函数:
为什么 fork 有两个返回值,从而使父子进程进入不同的业务逻辑。为什么 fork 的返回值会返回两次呢?
fork 函数中的 return 语句是被父子进程共享的,所以都会被父子进程执行。当 frok 返回时,会往变量 ret 中写入数据(如:pid_t ret = fork(); ),发生了写时拷贝,导致 ret 有两份,分别被父子进程私有。(代码共享,数据各自私有)
返回值 ret 变量名相同,为什么会有两个不同的值呢?
变量名相同,有两个不同的值,本质是因为被映射到了不同的物理地址处。父进程返回1,子进程返回0,因为父进程可以有很多个子进程,子进程就只有一个父进程。父进程要记住每一个子进程的pid,但子进程只需要记住自己的父亲是谁就可以了,只需要记住一个。
写时拷贝
上篇已经细细讲过了,现在只需要回温一下,写时拷贝是一种延时操作的策略,为什么要有写时拷贝呢?写时拷贝的好处是什么?
- 为了保证父子进程的独立性!(数据各自私有一份)
- 不是所有的数据,都有必要被拷贝一份(比如只读的数据)。写时拷贝可以节约资源。
- fork 时,如果把所有的数据都拷贝一份,是需要花费时间的,降低了效率。写时拷贝可以提高 fork 执行的效率。
- fork 创建子进程本身就是向操作系统要资源,如果把所有的数据都拷贝一份,要更多的资源,更容易导致 fork 失败。写时拷贝可以减少 fork 失败的概率。
fork失败的原因
fork 的常规:
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。
例如:父进程等待客户端请求,生成子进程来处理请求。
2.一个进程要执行一个不同的程序。
例如:子进程从 fork 返回后,调用 exec 函数。
fork 调用失败的原因:
1.系统中有太多的进程,系统资源不足。
2.实际用户的进程数超过了限制。
进程退出
在接触C语言的第一个hello worlld 程序中,main
函数的最后结果返回的是一个return 0
,那么这个语句究竟是什么意思呢?
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
为什么 main 函数中总是会返回 0 ( return 0; )呢?
- main 函数中的这个返回值叫做:进程退出码,用来表示进程退出时,其执行结果是否正确。
- 返回的 0 是给操作系统看的,来确认进程的执行结果是否正确。(0 通常表示成功)
用户可以通过命令 echo $? 查看最近一次执行的程序的「进程退出码」,比如:
运行写好的代码变成的可执行程序的时候,程序最终会退出,常见的进程退出的原因主要有三个:
- 代码运行结束了,结果正确
- 代码运行结束了,结果不正确
- 代码压根没结束,运行异常而终止了
如何知道进程退出了?
对于所有进程的管理者操作系统来说,它需要知道关于进程的一系列信息,比如进程有没有退出,进程最后运行的结果如何,如果出错了是为什么出错的,而对于父进程来说,它创建的子进程也应该要有一定的返回值,通过不管何种形式的返回值,必须要让父进程知道,自己创建的这个子进程有没有完成自己当初交代给它的任务,如果完成了要返回完成,如果没有完成要知道没有完成的原因是什么,因此就引出了进程退出的概念。对于各种操作系统来说,都有关于进程退出的一定设置,比如有用数字来代表不同的原因,比如输出0代表成功,现在进程已经运行成功了,可以正常退出了,也有1,2,3...代表多种原因。那就是进程退出码。
进程退出码
父进程创建子进程的目的是为了让子进程给我们完成任务,父进程需要通过子进程的退出码知道子进程把任务完成的怎么样。比如在生活中,网页打不开时,用户需要通过返回的一串错误代码得知网页出错的原因,退出码可以人为的定义,也可以使用系统的错误码列表(错误码 (int) 与错误码描述 (string) 之间的映射表)
根据不同的现象,进程会返回不同的退出码,那错误码又是什么呢?
- 退出码:通常是说一个进程退出的时候,它的退出结果是什么
- 错误码:通常是衡量一个库函数或者是一个系统调用一个函数的调用情况
但都是在说,当调用失败的时候,用来衡量函数或者是进程的出错的详细原因是什么。
比如:C 语言库中提供一个接口,可以把错误码转换成对应的错误码描述,程序如下:
#include <stdio.h>
#include <string.h> // strerror
int main()
{
for (int i = 0; i < 10; i++)
{
printf("%d -- %s\n", i, strerror(i)); // char *strerror(int errnum);
}
return 0;
}
运行结果:
进程异常的问题
这里要介绍一个概念:进程退出异常,本质上是进程收到了对应的信号,自己终止了。例如在Linux中有kill命令,这当中的许多选项就代表这个意思,比如有段错误导致终止,也有浮点数计算错误导致终止等等…所以说,父进程如何知道子进程有没有出现异常?只需要看有没有收到对应的信号就可以了,通过看退出码和错误码就可以观察到这样的现象。
进程终止
终止正常进程
进程退出一般有正常终止,比如说从main函数返回,或者是调用系统调用或其他函数等;也有异常调用,比如说使用Ctrl+C来进行信号终止。
return、exit、_exit
- 只有 main 函数中的 return 表示的是终止进程,非 main 函数中的 return 不是终止进程,而是结束函数。
- 在任何函数中调用 exit 函数,都表示直接终止该进程。
exit()和_exit()
#include <stdlib.h>
void exit(int status); // 终止正常进程
// 参数 status: 定义了进程的终止状态,父进程通过 wait 函数来获取该值
库函数
#include <unistd.h>
void _exit(int status); // 终止正在调用的进程
系统函数接口
那么它们有什么区别呢?请看下面实现:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("i am a process\n");
sleep(1);
exit(0);
}
这是一段C
语言的代码,打印一句信息后休眠一秒,然后退出进程,这是没有问题的。如果将代码改成这样:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("i am a process");
sleep(1);
exit(0);
}
在用户层面会看到,会休眠一秒后,再将内容打印在屏幕上,也是没有什么问题的,因为数据被存储在缓冲区中,而缓冲区刷新可以使用fflush或者是进程结束强制刷新到界面上,但是如果将代码中的退出调用改为_exit():
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("i am a process");
sleep(1);
_exit(0);
}
此时进程并没有将信息显示到屏幕上,而是直接结束了,这是由于_exit
函数的原因,它是一个系统级别的调用,而它并没有刷新缓冲区的能力。
其实从底层上看,exit函数的内部就是借助了_exit
这个系统级别的调用函数,exit
只是对它做了一定程度的封装。
进程退出OS做了什么?
1.“释放” 曾经为了管理该进程,在内核中维护的所有数据结构对象。
这里说的 “释放” 不是真的把数据结构对象销毁、把占用的内核空间还给操作系统,而是把不用的相同类型对象(像进程控制块)归为一类,存到 “数据结构池” 里。内核空间维护一个内存池,能减少用户频繁申请和释放空间的操作,提升内存使用效率,不过从内存池申请空间时,还得先进行类型强转再初始化。有了 “数据结构池” 后,比如创建新进程要新的进程控制块(PCB),不用再从内存池申请空间并进行类型强转和初始化,直接从 “数据结构池” 拿一个不用的进程控制块覆盖初始化就行,这样又减少了频繁申请和释放空间的过程,进一步提高了内存使用效率。这种内存分配的机制在 Linux 里叫 slab 分配器。
2.释放程序代码和数据占用的内存空间。
这里的释放不是把代码和数据清空,而是把占用的那部分内存设置成「未使用」就可以了。
3.取消曾经该进程的链接关系。
进程等待
什么是进程等待
进程等待是指通过wait/waitpid
的方式,让父进程对子进程进行资源回收的等待过程。
为什么要进程等待
- 进程等待可以解决子进程的僵尸问题带来的内存泄漏问题!
- 父进程创建子进程的目的是要让子进程完成父进程交给子进程的任务,而父进程一般而言是需要知道子进程到底把任务完成的怎么样,因此进行进程等待的另外一个作用就是要获取子进程退出的信息,也就是退出码和错误码,值得注意的是,父进程并不是一定要知道子进程的完成情况,可能在一些情况下,父进程知道子进程一定会完成这个任务,或者说父进程并不在意子进程把任务完成的怎么样,但是作为操作系统依旧应该要有提供这样信息的能力。
总结:父进程通过进程等待的方式:回收子进程资源,防止内存泄漏;获取子进程的退出信息。
如何进行进程等待
系统调用 wait,waitpid - 等待任意一个子进程改变状态,子进程终止时,函数才会返回。(其实就是等待进程由 R/S(运行/睡眠) 状态变成 Z(僵尸) 状态,然后父进程读取子进程的状态,操作系统回收子进程)。
wait函数:
成功返回的是被等待进程的pid
,失败返回的是-1
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
/*
* wait() 系统调用:暂停正在调用进程的执行,直到它的一个子进程终止。
* 调用 wait(&status) 等价于 waitpid(-1, &status, 0);
*/
对于参数是输出型参数,这个输出型参数可以获取的是子进程的退出状态,如果不关心可以设置为NULL
,下面使用代码来进行验证wait
函数的功能:
- 父进程可以回收子进程的僵尸状态
- 子进程如果不退出,父进程就必须
wait
上进行阻塞等待,直到子进程僵尸,wait
进行回收
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void work()
{
int cut = 5;
while(cut)
{
printf("child process-> pid:%d ,ppid:%d ,cut:%d\n",getpid(),getppid(),cut);
cut--;
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
work();
printf("child process exit\n");
exit(0);
}
else
{
// 父进程
sleep(8);
pid_t rid = wait(NULL);
}
sleep(100);
return 0;
}
实验结果如下所示:
从中可以看出第一条结论,当子进程执行结束后,父进程没有及时将子进程的内容代码和数据进行回收,此时子进程会进入僵尸状态,而当父进程执行到wait
函数后,父进程将子进程的代码和数据进行了回收,此时子进程就不再是僵尸状态了。
下面验证第二条结论:如果子进程不退出,父进程就必须wait
上进行阻塞等待,直到子进程僵尸,wait
进行回收:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void work()
{
int cut = 5;
while(cut)
{
printf("child process-> pid:%d ,ppid:%d ,cut:%d\n",getpid(),getppid(),cut);
cut--;
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
work();
printf("child process exit\n");
exit(0);
}
else
{
// 父进程
//sleep(8);
pid_t rid = wait(NULL);
printf("wait success\n");
}
sleep(100);
return 0;
}
运行结果:
父进程进行多个子进程阻塞式等待:
#include <stdio.h>
#include <stdlib.h> // exit
#include <sys/types.h> // getpid, getppid
#include <sys/wait.h> // wait
#include <unistd.h> // fork, sleep, getpid, getppid
int main()
{
for (int i = 0; i < 5; i++) // 创建5个子进程
{
pid_t cpid = fork();
if (cpid == 0)
{
// child process
int count = 5;
while (count)
{
// 子进程运行5s
printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
sleep(1);
}
printf("child quit!\n");
exit(0); // 终止子进程
}
else if (cpid < 0)
{
// fork failure
perror("fork");
return 1;
}
}
sleep(7); // 休眠7s
// 父进程进行进程等待
for (int i = 0; i < 5; i++)
{
printf("father is waiting...\n");
pid_t ret = wait(NULL); // 等待任意一个子进程终止,不关心子进程退出状态
printf("father waits for success, ret: %d\n", ret); // 输出终止子进程的id
sleep(2);
}
printf("father quit!\n"); // 父进程退出
return 0;
}
运行结果:
可以看到子进程退出后,因为父进程在休眠,没有进行进程等待,子进程全部变成了僵尸进程,随着父进程进行进程等待,5 个僵尸进程被操作系统一一回收。
总结:上面三份代码数据证明了这个结论,一般而言,谁先运行不知道,但是一般来说都是父进程最后进行的退出,所以父进程会在wait
上进行阻塞等待,一直到子进程变为僵尸,wait
进行回收后,父进程返回。
waitpid函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
/*
* waitpid() 系统调用:暂停正在调用进程的执行,直到 pid 参数指定的子进程改变状态。
* 默认情况下,waitpid() 仅等待终止的子进程,但此行为可以通过 options 参数进行修改,如下所述。
*/
waitpid
通常是用来进行获取退出信息的函数,其中可以看出,它的函数参数有三个,分别代表着进程的pid
,输出型参数,和一个选项参数,返回值的情况是,如果正常返回则收集子进程的pid
,如果设置了选项后会返回0
,如果在调用出错会返回-1。
pid:
- pid = -1,等待任意一个子进程,与 wait 等效。
- pid > 0,等待其进程 ID 与 pid 相等的子进程,即传入进程 ID,等待指定的子进程。
思考下,fork 函数在父进程中返回子进程的 ID,是为什么呢?为了方便父进程等待指定的子进程。
status:输出型参数(即在函数内通过解引用拿到想要的内容)
- NULL:表示不关心子进程的退出状态信息。
- 宏函数 WIFEXITED(status):如果子进程正常终止,则返回 true。(查看进程是否是正常退出)。
- 宏函数 WEXITSTATUS(status):若 WIFEXITED 非零,说明子进程正常终止,返回子进程的退出码。(查看进程的退出码)。
options:
- 如果设为 0,默认是阻塞式等待,与 wait 等效。
- 如果设为 WNOHANG:是非阻塞等待。w no hang。
设为WNOHANG:
- 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。(说明这一次等待失败了,需要再次等待)。
- 若正常结束,则返回该子进程的 ID。(说明等待成功了)。
注意:wait(&status) 等价于 waitpid(-1, &status, 0)。
返回值:
- 成功时,返回状态已更改的子进程 ID,
- 如果参数 options 指定了
WNOHANG
(非阻塞等待),并且存在一个或多个由参数 pid 指定的子进程,尚未更改状态,则返回 0,轮询检测。 - 出错时,返回 -1。
status 输出型参数
status
在函数参数中是以指针的情况出现的,也就是说,是将一个int
类型的数据传递到函数内部,函数内部将数据进行更换后就可以将数据进行输出了,下面对这个输出型参数进行实验:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void work()
{
int cut = 5;
while(cut)
{
printf("child process-> pid:%d ,ppid:%d ,cut:%d\n",getpid(),getppid(),cut);
cut--;
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
work();
printf("child process exit\n");
exit(1);
}
else
{
// 父进程
//sleep(8);
int status = 0;
pid_t rid = waitpid(id,&status,0);
printf("wait success,status=%d\n",status);
}
//sleep(100);
return 0;
}
输出结果为:
status为什么是256呢?status到底扮演着什么样的角色?
status的详解
- wait和waitpid中都有一个status参数,这是一个输出型参数并且是由操作系统进行自动补充。
- 如果传递的是空指针,说明不关心子进程的进程状态退出信息。
- 如果传递的是变量的地址,操作系统就会根据参数,将子进程的退出信息反馈给父进程。
- status并不是一个数,要把status当成一个位图来理解,下面是status的具体实现细节。
status的类型是一个int类型的数据,而int类型的数据是四个字节,占据的是32个比特位,这里研究的是低地址的16个比特位。如果子进程是被正常退出的,那么在status的位图中的后八个比特位会存储的是进程的退出状态,而如果是被信号所杀,比如说调用kill命令强行进行终止,那么就会在低七个比特位中显示出终止的信号,而第八个比特位中存储的是一个标志
这样显示出的位图status是不方便查看的,那么借助位运算,可以把前八个比特位和后八个比特位分别分开来进行计算:
低八位:status & 0x7F
高八位:(status >> 8) & 0xFF
因此使用下面的测试代码进行测试:
1.kill命令强制进行终止:
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t id = fork();
if (id == -1)
perror("fork"), exit(1);
if (id == 0)
{
sleep(20);
exit(10);
}
else
{
int status = 0;
int ret = wait(&status);
if (ret > 0 && (status & 0X7F) == 0)
{
// 正常退出
printf("child exit code:%d\n", (status >> 8) & 0XFF);
}
else if (ret > 0)
{
// 异常退出
printf("sig code : %d\n", status & 0X7F);
}
}
}
2.正常退出:
#include <stdio.h>
#include <stdlib.h> // exit
#include <sys/types.h> // wait, getpid, getppid
#include <sys/wait.h> // wait
#include <unistd.h> // fork, sleep, getpid, getppid
int main()
{
pid_t cpid = fork();
if (cpid == 0) // child process
{
int count = 5;
while (count) // 子进程运行5s
{
printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
sleep(1);
}
printf("child quit...!\n");
exit(123); // 终止子进程,退出码为123
}
else if (cpid > 0) // father process
{
int status = 0; // 进程退出状态
pid_t ret = waitpid(-1, &status, 0); // 等待子进程终止
int exit_code = (status >> 8) & 0xff; // 计算子进程的退出码
// 输出子进程id、退出码
printf("father waits for success, ret: %d, exit code: %d\n", ret, exit_code);
// 通过子进程退出码判断子进程把事情办的怎么样
if (exit_code == 0)
printf("子进程把事情办成了!\n");
else
printf("子进程没有把事情办成!\n");
}
else
{
// fork failure
}
return 0;
}
父进程通过 waitpid 函数的 status 参数拿到了子进程的退出码:
每次都要这样判断子进程是否正常终止((status & 0x7f) == 0),以及计算退出码((status >> 8) & 0xff),太麻烦了,有没有什么更便捷的方法呢?
系统中定义了一堆的宏(函数),可以用来判断退出码、退出状态。父进程中 waitpid 函数调用结束后,把它的第二个参数 status 传递给宏函数:
宏函数 WIFEXITED(status):如果子进程正常终止,则返回 true。(查看进程是否是正常退出) w if exited
宏函数 WEXITSTATUS(status):若 WIFEXITED 非零,说明子进程正常终止,返回子进程的退出码。(查看进程的退出码)w exit status
实际中,一般都是使用宏函数来检测子进程的退出状态和获取子进程的退出码。
改进后的一个完整的进程等待:
int main()
{
pid_t cpid = fork();
if (cpid == 0) // child process
{
// do something
exit(123); // 终止子进程
}
else if (cpid > 0) // father process
{
int status = 0; // 进程退出状态
pid_t ret = waitpid(-1, &status, 0); // 等待子进程终止
// 判断父进程是否等待成功
if (ret > 0)
{
printf("father waits for success, ret: %d\n", ret);
// 判断子进程是否正常终止
if (WIFEXITED(status)) // 子进程正常终止
{
printf("child process exits normally\n");
printf("exit_code: %d\n", WEXITSTATUS(status)); // 输出退出码
}
else // 子进程异常终止
{
printf("child process exits abnormally\n");
printf("pid: %d, sig: %d\n", ret, status & 0x7F); // 输出终止信号
}
}
else
{
// wait failure
}
else
{
// fork failure
}
return 0;
}
父进程如何知道子进程的退出信息?
答案依旧是存在于进程的PCB
中,当子进程要退出的时候,会修改状态Z
,并且将子进程的退出信号和退出码写到它自己的PCB
中,这样父进程就可以接受到信息了。
options
两种方法:
- 如果设为 0,默认是阻塞式等待,与 wait 等效。
- 如果设为 WNOHANG:是非阻塞等待。
两种情况:
- 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。(说明这一次等待失败了,需要再次等待,此时父进程可以去干别的事情)
- 若正常结束,则返回该子进程的 ID。(说明等待成功了)
waitpid 的两种等待方式:阻塞 & 非阻塞
- 阻塞等待(给 options 参数传 0)
- 非阻塞等待(给 options 参数传 WNOHANG)
解释:
张三做菜缺酱油,叫李四去买,相当于张三调了一个买酱油的函数,若李四还没回来,则函数就没结束,而李四在买酱油期间,张三一直被卡住,不继续做菜。这就是「阻塞等待」。
注意:我们目前的大多数接口,都是阻塞函数(调用 --> 执行 --> 返回 --> 结束),因为都是单执行流,同时实现起来也比较简单。
阻塞等待:调用方需要一直等着,不能做其他事情,直到函数返回。
张三打电话问李四作业写完没,李四说没有,过了一会儿,张三又打电话问李四作业写完没,李四说没有……,张三多次打电话问李四作业写完没,直到李四作业写完,张三才会停止打电话。
上述例子的本质是,张三打电话不会把张三一直卡住,张三可以忙忙自己的事情,通过间隔多次打电话,检测李四的状态。张三每一次打电话,称之为「非阻塞等待」。多次打电话的过程,称之为「非阻塞轮询检测方案」。
为什么自然界一般选择非阻塞呢 —— 因为更加高效一些,不会一直卡在那里不做事。
非阻塞等待:调用方不需要一直等着,可以边轮询检测边做自己的事情。
以非阻塞举例:想让父进程中的 waitpid 函数是非阻塞调用(即父进程边运行边调用),需要将函数的第三个参数设为 WNOHANG。
这里的失败,有两种情况:
- 并非真的等待失败,而是子进程此时的状态没有达到预期。
- 真的等待失败了。
父进程中 waitpid 函数如果是非阻塞调用,返回值有三种情况:
- 等待失败:此次等待失败,需要再次检测。
- 等待失败:真的失败。
- 等待成功:已经返回。
代码实现:
#include<stdio.h>
#include<stdlib.h> // exit
#include<sys/types.h> // wait, getpid
#include<sys/wait.h> // wait
#include<unistd.h> // fork, sleep, getpid
int main()
{
pid_t cpid = fork(); // 创建子进程
if (cpid == 0) // child process
{
int count = 3;
while (count) // 子进程运行3s
{
printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid());
sleep(1);
}
printf("child quit...\n");
exit(123); // 终止子进程
}
else if (cpid > 0) // father process
{
int status = 0; // 进程退出信息
while (1) {
pid_t ret = waitpid(cpid, &status, WNOHANG); // 进程等待
if (ret == 0) // 此次等待失败,需要再次等待
{
sleep(1);
printf("wait next...\n");
printf("father do something...\n"); // do something
}
else if (ret > 0) // 等待成功,输出子进程id和退出码
{
printf("wait for success, ret: %d, exit_code: %d\n", ret, WEXITSTATUS(status));
break;
}
else // 等待失败
{
printf("waiting for the failure!\n");
break;
}
}
}
else
{
// fork failure
}
return 0;
}
运行结果:
【小结】
如何理解进程等待:即父进程在等待子进程终止,而子进程在跑自己的代码。
如何理解进程在 “ 阻塞 / 等待 ”:阻塞的本质就是进程被卡住了,没有被 CPU 执行。
操作系统将当前进程放入等待队列,并把进程状态设置为非 R(运行) 状态,暂时不会被 CPU 执行,当需要的时候,会唤醒等待(即把进程从等待队列移出,放回运行队列,并把进程状态设置为 R(运行) 状态,让 CPU 去调度)。比如:我们电脑上运行的软件太多,发现某个软件卡了,其实是当前运行队列中的进程太多,系统资源不足,把一些进程放入等待队列中了。
因此现在对于进程是动态的又多了一层新的理解,所谓进程的动态过程,就是进程不断的被链到不同的队列中,在被需要的时候被不停的调度,不断的从运行队列放到等待队列,再或者从等待队列调度到运行队列等等,整个过程是一个动态的过程。