【Linux】进程创建、进程终止和进程等待
创始人
2024-05-04 17:25:52
0

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
在这里插入图片描述

目录

    • 👉进程创建👈
      • fork 函数的理解
      • 写时拷贝
      • fork 的常规用法
      • fork调用失败的原因
    • 👉进程终止👈
      • 进程退出码
      • 进程退出场景
      • 进程常见退出方法
        • 1. exit 函数
        • 2. _exit 系统调用
        • 3. exit 和 _exit 的区别
    • 👉进程等待👈
      • 进程等待的必要性
      • 进程等待的方法
        • 1. wait
        • 2. waitpid
      • 非阻塞等待
    • 👉总结👈

👉进程创建👈

fork 函数的理解

fork 函数能够从已存在进程中创建一个新进程,新进程为子进程,而原进程为父进程。进程调用 fork 函数后,操作系统会做一下的事情:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 将子进程添加到系统进程列表当中
  • fork 返回,开始调度器调度

在这里插入图片描述

当一个进程调用 fork 函数之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程都将可以开始它们自己的旅程。fork 之前由父进程独立执行,fork 之后父子进程分别执行。注意:fork 之后,谁先执行完全由调度器决定。

在这里插入图片描述

fork 函数的返回值:fork 失败返回 -1;fork 成功给父进程返回子进程的 ID,给子进程返回 0。那如何理解 fork 函数给父进程返回子进程的 ID,给子进程返回 0 呢?其实是因为父进程可能有多个子进程,而子进程只有一个父进程,所以不要给子进程返回父进程的 ID,但需要给父进程返回子进程的 ID,将子进程管理起来。

如何理解 fork 函数有两个返回值呢?又如何理解同一个变量 id,怎么可能保存两个不同的值,让 if - else if - else 分支结构同时执行呢?见下图解释!

在这里插入图片描述

写时拷贝

父子进程共享 fork 函数之后的代码。当父子进程都不进行数据写入时,数据也是共享的。当任意一方试图写入时,便以写时拷贝的方式各自一份副本。具体过程见下图:

在这里插入图片描述

fork 的常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码。例如:父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如:子进程从 fork 返回后,调用 exec 函数。

fork调用失败的原因

  • 系统中有太多的进程
  • 实际用户的进程数超过了限制

死循环创建进程代码

#include 
#include int main()
{int cnt = 0;while(1){int id = fork();if(id < 0){printf("fork fail! cnt:%d\n", cnt);break;}else if(id == 0){// childwhile(1) sleep(1);}// parent++cnt;}
}

在这里插入图片描述

👉进程终止👈

进程退出码

main 函数调用结束总会要 return 一个值,其实这个值是进程退出的时候对应的退出码,标定进程执行的结果是否正确。

验证进程退出码的存在

在这里插入图片描述

echo $? #输出最近一个进程执行完毕时的退出码

在这里插入图片描述

在这里插入图片描述

那如何设定 main 函数的返回值呢?如果不关心进出退出码, return 0 就行了。如果关心进程退出码,那么就需要返回特定的数据表面特定的执行结果。

退出码的意义:0 表示标识成功,非 0 标识失败,不同的数字标识不同的错误。

退出码对人是不友好的,而对计算机比较友好。一般而言,退出码都必须有对应退出码的文字描述。

strerror 函数能够将退出码转化成对应的退出码信息。

在这里插入图片描述

#include 
#include int main()
{for(int i = 0; i < 200; ++i){printf("%d:%s\n", i, strerror(i));}return 0;
}

在这里插入图片描述

Linux 系统一共提供了 134 个退出码信息。

进程退出场景

  • 代码运行完毕,结果正确(return 0)
  • 代码运行完毕,结果不正确(return 非0)
  • 代码异常终止(退出码无意义)

进程常见退出方法

正常终止(可以通过 echo $? 查看进程退出码)

  1. 从 main 函数 return 返回
  2. 任意位置调用 exit 函数
  3. _exit 系统调用

异常退出:Ctrl + c,信号终止进程

1. exit 函数

exit 函数的使用

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2. _exit 系统调用

_exit 系统调用的使用

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3. exit 和 _exit 的区别

exit 是库函数,而 _exit 是系统调用。exit 库函数相对于 _exit 系统调用更加上层,eixt 函数本质也是调用了 _exit 但是 exit 函数还做了别的处理。

通过代码来体会 exit 和 _exit 的区别

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
调用 exit 函数的现象是休眠两秒过后,数据被打印出来了;而调用 _exit 的现象是休眠两秒后,数据并没有被打印出来。

结论:exit 终止进程会主动刷新缓冲区,而 _exit 终止进程并不会刷新缓冲区。该缓存区是用户空间的缓冲区。如果该缓冲区是在内核空间的话,那么 exit 和 _exit 都会刷新该缓冲区。该用户级的缓冲区会在基础 IO 部分里讲解!

调用 exit 函数所做的事情:

  • 执行用户通过 atexit或on_exit定义的清理函数
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用 _exit

在这里插入图片描述

👉进程等待👈

进程等待的必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入。“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。子进程运行完成,结果对还是不对, 或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

进程等待的方法

1. wait

在这里插入图片描述

注:wait 函数的参数是输出型参数,用来获取子进程退出的状态。如果不关心子进程退出的状态,可以设置为 NULL。wait 函数的返回值:成功返回被等待进程的 ID,失败返回 -1。

在这里插入图片描述
在这里插入图片描述

现象:子进程执行完毕后的五秒钟,子进程处于僵尸状态。当父进程调用 wait 时,等待子进程成功,回收子进程的资源,那子进程就彻底结束了。

2. waitpid

pid_t waitpid(pid_t pid, int *status, int options);

waitpid 的参数 pid 是想要等待的进程,status 是输出型参数,记录子进程退出的信息,options 为 0 时为阻塞等待。当正常返回的时候,waitpid 返回收集到的子进程的进程 ID;如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在。

在这里插入图片描述
在这里插入图片描述

status 并不是退出码 10。因为 status 并不是整体使用,而是使用 status 的位信息。

我们知道,进程退出的场景有三种:代码运行完毕,结果正确、代码运行完毕,结果不正确以及代码异常终止。那么,status 就是用来表示以上三种情况的。

获取子进程状态 status

  • wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递 NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低16比特位)

在这里插入图片描述

注:当低七位为 0 时,表示进程正常退出。然后再看次低八位,看进程退出的结果是否正确。如果低七位不为 0,则次低八位无意义。

#include 
#include 
#include 
#include 
#include 
#include int main()
{pid_t id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){// childint cnt = 5;while(cnt){printf("我是子进程, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);sleep(1);}exit(10); // 子进程退出}int status = 0;pid_t ret = waitpid(id, &status, 0);if(ret > 0)  // 等待成功{printf("wait success:%d, signal number:%d, child exit code:%d\n", ret, (status & 0x7F), (status >> 8) & 0xFF);}return 0;
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

进程等待的本质是父进程从子进程的进程控制块 PCB 中拿取子进程的退出信息。

在这里插入图片描述

除了通过位运算来拿到子进程的退出信息,还可以通过宏来拿到子进程的退出信息。

  • WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
  • WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
#include 
#include 
#include 
#include 
#include 
#include int main()
{pid_t id = fork();assert(id != -1);if(id == 0){// childint cnt = 8;while(cnt){printf("child procees running, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);sleep(1);}exit(0);}// parentint status = 0;// 1. 让操作系统释放子进程的僵尸状态// 2. 获取子进程的退出结果// 在子进程没有退出的时候,父进程只能阻塞等待,什么事也不干int ret = waitpid(id, &status, 0);  // 0表阻塞等待if(ret > 0) // 等待成功1{// 是否正常退出if(WIFEXITED(status)){// 查看子进程的退出码printf("exit code:%d\n", WEXITSTATUS(status));}else{printf("child eixt not normal!\n");}//printf("wait success, signal number:%d, exit code:%d\n", status & 0x7F, (status >> 8) & 0xFF);}return 0;
}

在这里插入图片描述

非阻塞等待

子进程没有退出前,父进程一直在等待子进程退出,不执行其他代码,这种等待就是阻塞等待。而非阻塞等待是就算子进程没退出,父进程也不会一直等待子进程退出,期间父进程会去执行它的代码,过一段时间再来看子进程是否退出,这种等待就是非阻塞等待。父进程每次检查子进程是否退出都是一次非阻塞等待,多次非阻塞等待就是轮询的过程。

非阻塞等待代码

#include 
#include 
#include 
#include 
#include 
#include int main()
{pid_t id = fork();assert(id != -1);if(id == 0){// childint cnt = 8;while(cnt){printf("child procees running, pid:%d, ppid:%d, cnt:%d\n", getpid(), getppid(), cnt--);sleep(1);}exit(0);}// parentint status = 0;while(1){pid_t ret = waitpid(id, &status, WNOHANG); // 非阻塞等待if(ret == 0){// waitpid调用成功 && 子进程没有退出// 子进程没有退出,我们waitpid没有等待失败,仅仅是监测到子进程没有退出printf("waitpid success, but child process is running!\n");sleep(1);}else if(ret > 0){// waitpid调用成功 && 子进程退出来printf("wait success, signal number:%d, exit code:%d\n", status & 0x7F, (status >> 8) & 0xFF);break;}else{// waitpid调用失败printf("waitpid call failed!\n");break;}}return 0;
}

在这里插入图片描述

waitpid 等待失败的场景

在这里插入图片描述
在这里插入图片描述

非阻塞等待的好处

  • 不会占用父进程的所有精力,可以在轮询期间做别的时期。
#include 
#include 
#include 
#include 
#include 
#include 
#include #define NUM 10typedef void (*func_t)(); //函数指针func_t handlerTask[NUM];//样例任务
void task1()
{printf("handler task1\n");
}
void task2()
{printf("handler task2\n");
}
void task3()
{printf("handler task3\n");
}void loadTask()
{memset(handlerTask, 0, sizeof(handlerTask));handlerTask[0] = task1;handlerTask[1] = task2;handlerTask[2] = task3;
}int main()
{pid_t id = fork();assert(id != -1);if(id == 0){//childint cnt = 5;while(cnt){printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}exit(10);}loadTask();  // parentint status = 0;while(1){pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回if(ret == 0){// waitpid调用成功 && 子进程没退出//子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.printf("wait done, but child is running...., parent running other things\n");for(int i = 0; handlerTask[i] != NULL; i++){handlerTask[i](); //采用回调的方式,执行我们想让父进程在空闲的时候做的事情}}else if(ret > 0){// 1.waitpid调用成功 && 子进程退出了printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);break;}else{// waitpid调用失败printf("waitpid call failed\n");//    break;}sleep(1);}return 0;
}

在这里插入图片描述

👉总结👈

本篇博客主要讲解了进程创建、进程终止和进程等待。那么以上就是本篇博客的全部内容了,如果大家觉得有收获的话,可以点个三连支持一下!谢谢大家!💖💝❣️

上一篇:老王linux面试题汇总

下一篇:初步改造2

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...