目录
- 前言
- 1.options 参数
- 2. 非阻塞轮询
- 3. 模拟非阻塞轮询
- 4. 非阻塞轮询 + 执行其它任务
前言
继上一篇文章 详谈进程等待 讲到 waitpid 系统调用,在该系统调用接口中还有一个 options 参数,本篇文章介绍 watipid 系统调用中的options 参数 以及 什么是非阻塞轮询,非阻塞轮询的同时是如何执行其它任务的。
1.options 参数
pid_t waitpid(pid_t pid, int *status, int options);
当父进程 waitpid 等待子进程时,如果子进程退出了,那么父进程读取其 PCB 中的信号码和退出码返回即可;如果子进程迟迟不退出,那父进程就得一直等下去,进而导致父进程阻塞调用,也即父进程处于阻塞状态。
而 options 选项就是设置父进程等待的方式:
- 默认0,阻塞等待方式(子进程处于 R 状态,操作系统把父进程状态由 R -> S,然后把父进程投递到子进程 PCB 中维护的等待队列,当子进程退出了,操作系统再将父进程唤醒。换言之,也即父进程一直等待子进程的本质就是阻塞与子进程的等待队列中)
- 非阻塞轮询 WNOHANG。
2. 非阻塞轮询
将 options 设置为 WNOHANG,就是非阻塞轮询,又即等待的时候不要夯住(调用时系统不返回,进程阻塞,系统无响应等,就称为该系统 或 该接口 被夯住了,也即阻塞的专业词语)。
所以什么是非阻塞呢??
故事线:outlier 大学即将迎来一学期一度的期末考试,你作为学渣,平时不上课,课后不作为,马上要考C语言了,你慌的一批。
于是马上打电话给好朋友张三(张三是一名学霸,课后笔记大师):“张三啊,马上要考试了,带上你的笔记过来辅导一下你爹”。
张三:“儿子,你等会,我在复习,过一会就好了”,说完就挂掉电话。
于是你也就等下去了,等了十来分钟,你又打电话问张三好了没,张三回复了再等会。于是你又再等下去。
十分钟后,再打一次。。。。
二十分钟,再打一次,这一次张三说,我看到你了,你今天穿白T 对吧,你二话不说,啪的一下挂掉了电话。因为你穿的是蓝T。
循环往复,最终你们终于踏上去图书馆的道路。
上述的张三就是 操作系统;你就是 用户;打电话的过程就是调用系统调用;打电话的本质就是检查操作系统(张三)的状态;每次打电话得到回复之后立马又挂掉就是 系统调用立马返回;每一次打电话检测时操作系统(张三)没好,用户(你)不会一直占线等待,而是立马返回,这叫做非阻塞!而一直打电话问张三,就是轮询!
非阻塞轮询 = 非阻塞 + 循环
下学期,outlier再次迎来数据结构期末考试,你依旧找到张三,“儿子啊,老地方啊”。张三还是让你等着,他还没准备好。
但这次,你跟张三说:“上次一直打你电话,我也不知道你啥时候能好,这次我就不挂了,你好了,下楼见到我了,再挂吧”
而这种占线等待回复,就叫做 阻塞调用!操作系统一直在执行着某种任何,用户一直在检查操作系统任务的完成情况,检测时,即便操作系统任务没完成,系统调用也不返回,即阻塞调用。
下学年,操作系统考试如期而至。你也听闻操作系统的难处,因此你不敢怠慢,吸取前两次的经历,你觉得不能干等着浪费时间。
因此当张三还没下楼找你时,你就在自己宿舍看书复习,时不时的在打电话问一下张三好了没。
这一次,你学聪明了,一边等待张三,一边做着自己的事情,这就是非阻塞轮询的同时,可以执行其它任务。而纯阻塞调用,是无法执行其它任务的,只能干等着。非阻塞轮询 + 执行其它任务,才是最常见的。
所以 waitpid 返回值 ret > 0 时,即代表等待成功; ret < 0 即等待失败;而大量存在的情况是 ret = 0 的情况,即非阻塞轮询检查状态时,目标子进程还没有就绪(即没有退出),而子进程没有退出,并不代表调用的失败或成功,只不过是等待的条件还没有就绪而已!
3. 模拟非阻塞轮询
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else
{
int status = 0;
while(1) // 轮询
{
pid_t ret = waitpid(id, &status, WNOHANG); // 非阻塞
if(ret > 0)
{
if(WIFEXITED(status))
printf("process is normal, exit_code: %d\n", WEXITSTATUS(status));
else
printf("the process terminated abnormally! \n");
break;
}
else if(ret == 0)
{
printf("the child process has not exited and continues to poll non-blocking\n");
sleep(1);
}
else
{
0 printf("wait failed!\n");
break;
}
}
sleep(1);
}
return 0;
}
4. 非阻塞轮询 + 执行其它任务
当 waitpid 等待子进程时,检测到子进程状态还没退出,于是立马返回,这之后是如何执行自己的其它任务的呢??执行任务的同时有没有可能错过子进程退出呢??
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define TASK_NUM 10
typedef void(*task_t)(); // 函数指针
task_t tasks[TASK_NUM]; // 函数指针数组
void task1() { printf("task 1, pid: %d\n", getpid()); }
void task2() { printf("task 2, pid: %d\n", getpid()); }
void task3() { printf("task 3, pid: %d\n", getpid()); }
int AddTask(task_t t);
// 任务的管理代码
void InitTask()
{
for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
AddTask(task1);
AddTask(task2);
AddTask(task3);
}
int AddTask(task_t t)
{
int pos = 0;
for(; pos < TASK_NUM; pos++)
if(!tasks[pos])
{
// 模拟添加任务
break;
}
if(pos == TASK_NUM) return -1; // 任务队列满,返回-1
tasks[pos] = t; // 添加指定任务的函数指针
return 0;
}
void DelTask() { }
void CheckTask() { }
void UpdateTask() { }
void ExecuteTask() // 执行任务
{
for(int i = 0; i < TASK_NUM; i++)
{
if(!tasks[i]) continue;
tasks[i](); // 回调函数
}
}
int main()
{
InitTask();
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else
{
int status = 0;
while(1)
{
pid_t ret = waitpid(id, &status, WNOHANG); // 非阻塞
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("process is normal, exit_code: %d\n", WEXITSTATUS(status));
}
else
{
printf("the process terminated abnormally! \n");
}
break;
}
else if(ret == 0)
{
ExecuteTask();
usleep(500000);
}
else
{
printf("wait failed!\n");
break;
}
}
}
return 0;
}
以上就是创建单进程的模拟非阻塞轮询的同时,执行自己的其它任务,如果是创建多进程,只需要将 waitpid 中的 pid 参数设置为 -1(等待任意一个子进程),在等待成功之后,不是立马 break,而是维护一个计数器,等待成功一个子进程,就让计数器 -1,直到 计数器 == 0 然后 break 即可。
而至于会不会有可能父进程在执行其它任务的时候,错过了子进程这个问题,所谓错过,无非就是子进程退出的时候,父进程不会立刻马上立刻对其进行回收,但是当父进程执行完自己的任务回来之后,也会对其进行回收。而一般情况下,非阻塞轮询进行等待子进程,才是父进程此时的主线任务,执行其它任务,只不过是顺手的事,不让父进程干等着罢了,因此这类任务一般都是很轻量级的,所以一般也不必担心会不会执行了其它任务,子进程就没有人回收的问题。并且当子进程多一点时,子进程退出时,父进程晚一点来回收,反而可以集中回收,提高效率。
而至于创建出来的诸多子进程中,哪个进程先被调度,我们并无法知晓。但是,我们必须要知道,最后退出的进程一定是父进程!
为什么?----- 因为所有的子进程是父进程创建出来的,所以父进程有等待所有子进程的职责!父进程需要等待所有子进程退出之后,来回收子进程。这也是进程等待重要的一个原因,在编码得到保障时,父进程最后一个退出,是可以保证释放所有子进程的资源的。
关于非阻塞轮询的内容,小篇介绍到这里,后续文章还会介绍 进程替换。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!