• linux篇【6】:进程等待


    目录

    一.fork函数初识

    1.概念

    2.父子进程共享fork之前和fork之后的所有代码,只不过子进程只能执行fork之后的

    3.fork之后,操作系统做了什么?

    进程具有独立性,代码和数据必须独立的

    写时拷贝

    fork常规用法

    fork调用失败的原因

    4.Fork后子进程保留了父进程的什么?

    5.fork和exec系统调用

    二.进程终止

    1.常见进程退出

    2.关于进程终止的正确认识

    (1)进程退出码

     echo $? 查看进程退出码

    (2)关于终止的常见做法——exit()

    (3)exit和_exit

    (4)关于终止,内核做了什么?

    三.进程等待

    1.为什么要进行进程等待

    ①为了解决僵尸进程内存泄漏问题

    ②为了获取子进程的退出状态

    2.wait与waitpid

    waitpid:

    第二个参数status:

    (1)低16个比特位的次低8比特位(次低8比特位) 是退出码

    (2)低8个比特位是终止信号

    3.父进程非阻塞等待(WNOHANG)

    (1)父进程基于非阻塞的轮询等待的 例子:

    (2)父进程基于非阻塞的轮询等待,父进程也有任务的例子


    一.fork函数初识

    1.概念

    在linux fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
    #include
    pid_t fork(void);
    返回值:子进程中返回 0 ,父进程返回子进程 id ,出错返回 -1
    进程调用fork,当控制转移到内核中的fork代码后,内核做:
            ①分配新的内存块和内核数据结构给子进程
            ②将父进程部分数据结构内容拷贝至子进程
            ③添加子进程到系统进程列表当中(链进运行队列)
            ④fork返回,开始调度器调度
    当一个进程调用 fork 之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序

    2.父子进程共享fork之前和fork之后的所有代码,只不过子进程只能执行fork之后的

    fork之前父进程独立执行,fork之后, 父子两个执行流分别执行。

    那么fork之后,是否只有fork之后的代码是被父子进程共享的? ?
    fork之后,父子共享所有的代码,但fork之前的代码也是父子共享的,只不过子进程只能执行fork之后的
    子进程执行的后续代码! =共享的所有代码,只不过子进程只能从这里开始执行! !

    为什么呢:

    CPU中有一个程序计数器叫eip,用途是eip叫做,保存当前正在执行指令的下一条指令!
    eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行啦! !

    如果我想让子进程拿到fork之前的代码,可以让子进程把CPU中的eip改成main函数入口就可以执行fork之前的代码。

    3.fork之后,操作系统做了什么?

    进程=内核的进程数据结构+进程的代码和数据
    创建子进程的内核数据结构(struct task_ struct + struct mm_ struct +页表) +代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立!
     

    进程具有独立性,代码和数据必须独立的

    数据通过写时拷贝保证独立性
    代码因为是只读的,不可修改

    写时拷贝

    通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。

    fork常规用法

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

    fork调用失败的原因

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

    4.Fork后子进程保留了父进程的什么?

    A.环境变量

    B.父进程的文件锁,pending alarms和pending signals

    C.当前工作目录

    D.进程号

    fork函数功能是通过复制父进程,创建一个新的子进程。

    • A选项正确:环境变量默认会继承于父进程,与父进程相同
    • B选项错误:信号相关信息各自独立,并不会复制
    • C选项正确:工作路径也相同
    • D选项错误:每个进程都有自己独立的标识符

    根据理解分析,正确选项为A和C选项

    5.fork和exec系统调用

    • fork生成的进程是当前进程的一个相同副本(fork调用通过复制父进程创建子进程,子进程与父进程运行的代码和数据完全一样)
    • fork系统调用与clone系统调用的工作原理基本相同(fork创建子进程就是在内核中通过调用clone实现)
    • exec是程序替换函数,本身并不创建进程
    • clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现,而exec函数中本身并不创建进程,而是程序替换,因此工作机理并不相同

    二.进程终止

    1.常见进程退出

    ________________________________
    常见进程退出:                                         |         
    1.代码跑完,结果正确                            | 
    2.代码跑完,结果不正确                           | 
    3.代码没跑完,程序异常了                     | 
    ——————————————————

    2.关于进程终止的正确认识

    C/C++的时候,main函数为什么 return 0; ?
    a.return 0,给谁return
    b.为何是0?其他值可以吗?


    return 0表示进程代码跑完,结果正确
    return 非零:结果不正确

    在main函数中return代表进程结束。其他非main 函数return 代表函数调用结束

    (1)进程退出码

    代码跑完,结果正确就没什么好说的就exit(0)/return 0返回码是0;如果代码跑完,结果不正确,那我们最想知道的是失败的原因!
    所以:非零标识不同的原因! 比如exit(13)
    return X的X叫做进程退出码,表征进程退出的信息,是让父进程读取的! !

     echo $? 查看进程退出码

     echo $? :在bash中,最近一次执行完毕时,对应进程的退出码

    解释这里:第一次 echo $? 打印了进程退出码 123 ,第二次 echo $? 打印的是上一次 echo $?的进程退出码,因为上一次 echo $? 执行成功了,所以进程退出码是0,。

    一般而言,失败的非零值我该如何设置呢? ?以及默认表达的含义?
    可以自定义,也可以用系统定义的sterror。
    错误码退出码可以对应不同的错误原因,方便定位问题!

    (2)关于终止的常见做法——exit()

    1. 在main函数中return代表进程结束。其他非main 函数return 代表函数调用结束
    2.在自己的代码任意地点中main函数/非main函数,调用exit()都叫进程退出,exit(X)中的X是退出码

    (3)exit和_exit

    exit终止进程刷新缓冲区
    _exit,是系统调用,直接中止进程,不会有任何刷新操作

    终止进程推荐exit或main函数中的return。

    会打印: hello bit,即刷新缓冲区

    如果是_exit(0),就不会打印任何东西,即不刷新缓冲区

    (4)关于终止,内核做了什么?

    进程 = 内核结构 + 进程代码 和 数据
    进程代码 和 数据一定会释放,但是 task/struct && mm_ struct:操作系统可能并不会释放该进程的内核数据结构
    因为再次创建对象:1.开辟空间 2.初始化 都要花时间。
    linux会维护一张废弃的数据结构链表叫 obj,若释放进程,对应的进程的数据结构会被维护到这个链表中,这个链表没有释放空间,只是被设成无效,需要时就拿,节省开辟时间(这样的链表也称 内核的数据结构缓冲池,操作系统叫:slab分派器)

    三.进程等待

    1.为什么要进行进程等待

    ①为了解决僵尸进程内存泄漏问题

    子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力。

    ②为了获取子进程的退出状态

    父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
    或者是否正常退出。

    2.wait与waitpid

    wait()的方案可以解决回收子进程z状态,让子进程进入X十,wait是等待任意一个退出的子进程

      wait演示:

    waitpid:

    pid_ t (int) 的返回值 :
    >0:等待子进程成功,返回值就是子进程的pid
    <0:等待失败  
    =0:等待成功,但子进程没有退出     

    第一个参数 pid:
    >0:是几,就代表等待哪一个子进程退出,比如pid=1234, 指定等待
    -1:等待任意进程退出(通常是最后退出的那个进程)

    第三个参数 option:

    0:标识阻塞等待(就是父进程等待子进程死,子进程死后就回收它)

    当options被设置为WNOHANG则函数非阻塞(Wait No Hang 夯住了),且当没有子进程退出时,waitpid返回0

    第二个参数status:

    int* status:是一个整数,是输出型参数:父进程调用waitpid,可以通过status拿到子进程的退出码(具体过程:子进程退出后变为Z状态—子进程代码释放,只是维护着子进程的进程控制块 task_struct;此时子进程的进程控制块 task_struct中 有两个整形退出码int exit_ code和退出信号 exit_ signal 会被填充,然后父进程会拿到这两个值放入status中,所以输出型参数要先定义然后传参时取地址 waitpid(id, &status, 0),或者传nullptr 是不需要退出码 )

    只需要关心改整数的低16个比特位!

    (1)低16个比特位的次低8比特位(次低8比特位) 是退出码

    证明:让子进程先睡眠5秒,然后退出,退出码设为0。子进程睡眠5秒期间父进程用 wait/waitpid 设成阻塞态,已知返回值:

    pid_ t (int) 的返回值 :
    >0:等待子进程成功,返回值就是子进程的pid
    <0:等待失败        
    =0:等待成功,但子进程没有退出 

    利用ret接收返回值,当接收成功时,打印ret(这里的ret就是子进程的pid),

    并打印(status>>8) &0xFF ,status>>8是 次低8比特位开始,与上0xFF(8个1),就是退出码。我们会发现status中的退出码确实记录了子进程的退出码

    (2)低8个比特位是终止信号

    (kill -l 可查退出信号,异常就是因为收到信号,status&0x7F 可打印终止信号)

     status&0x7F:status的低8位与上111 1111 ,就可得到终止信号

    没有0号信号:

    另一个接收

    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)

     

     waitpid():
    阻塞等待和非阻塞等待

    当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待,本质:就是当前进
    程自己变成阻塞状态,等条件就绪的时候,在被唤醒!(这里的条件不就绪可能是任意的软硬件条件!)

    3.父进程非阻塞等待(WNOHANG)

    waitpid(-1,&status, WNOHANG);
    WNOHANG 父进程为非阻塞等待 (Wait No Hang 夯住了)

    返回值:=0,等待成功,但子进程没有退出 ;等待成功返回子进程pid,等待失败返回-1。

    (1)父进程基于非阻塞的轮询等待的 例子:

    waitpid(-1,&status, WNOHANG); 在while循环内每次 waitpid执行一次,就检测一次子进程,如果子进程退出,就等待成功返回子进程pid;如果子进程没有退出,因为是非阻塞等待就等待成功返回0;

     

    (2)父进程基于非阻塞的轮询等待,父进程也有任务的例子

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. typedef void (*handler_t)();
    9. //方法集
    10. std::vector<handler_t> handlers;
    11. void fun1()
    12. {
    13. printf("hello, 我是方法1\n");
    14. }
    15. void fun2()
    16. {
    17. printf("hello, 我是方法2\n");
    18. }
    19. void Load()
    20. {
    21. //加载方法
    22. handlers.push_back(fun1);
    23. handlers.push_back(fun2);
    24. }
    25. int main()
    26. {
    27. pid_t id = fork();
    28. if(id == 0)
    29. {
    30. //子进程
    31. while(1)
    32. {
    33. printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
    34. sleep(3);
    35. }
    36. exit(104);
    37. }
    38. else if(id >0)
    39. {
    40. //父进程
    41. // 基于非阻塞的轮询等待方案
    42. int status = 0;
    43. while(1)
    44. {
    45. pid_t ret = waitpid(-1, &status, WNOHANG);
    46. if(ret > 0)
    47. {
    48. printf("等待成功, %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
    49. break;
    50. }
    51. else if(ret == 0)
    52. {
    53. //等待成功了,但是子进程没有退出
    54. printf("子进程好了没,奥, 还没,那么我父进程就做其他事情啦...\n");
    55. if(handlers.empty())
    56. Load(); 添加任务
    57. for(auto f : handlers)
    58. {
    59. f(); //回调处理对应的任务,即执行任务
    60. }
    61. sleep(1);
    62. }
    63. else{
    64. //出错了,暂时不处理
    65. }
    66. }
    67. }

     

  • 相关阅读:
    macOS 13 Ventura后,打开软件显示“XXapp已损坏,无法打开”如何解决?
    动力节点springboot笔记
    python *和**的用法
    EMMC打印cqhci: timeout for tag 10提示分析与解决
    LastPass 开发者系统被黑客窃取源代码
    【ARC与MRC的相互兼容 Objective-C语言】
    [PAT-Advanced] A1054. The Dominant Color (20)
    mac中给brew配置环境变量
    Allegro Design Entry HDL(OrCAD Capture HDL)视图管理菜单详细介绍
    互联网产品前后端分离测试(Eolink 分享)
  • 原文地址:https://blog.csdn.net/zhang_si_hang/article/details/126589679