• 【Linux进程篇】Linux中的等待机制与替换策略


    W...Y的主页 😊

    代码仓库分享💕 

    目录

    ​编辑

    进程等待

    进程等待必要性

    进程等待的方法

    wait方法

    waitpid方法

    获取子进程status

     阻塞与非阻塞

    进程程序替换

    替换原理

    替换函数


    进程等待

    进程等待必要性

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

    上篇博客中,我们讲到进程退出时有两个非常重要的信息:退出信号和退出码。当我们进程退出时退出信息会被放入进程的PCB中进行保存,等待父进程的回收。

    因为进程拥有独立性,所以我们想通过参数或返回值将信息转交给父进程那是不可能的,所以我们才要进行回收资源。

    进程等待的方法

    wait方法

    返回值:
    成功返回被等待进程pid,失败返回-1。
    参数:
    输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

    下面是测试子进程变僵尸后wait是否进行回收资源:

     

    我们可以看出父进程可以将僵尸进程回收!!! 

    waitpid方法

    返回值:
    当正常返回的时候waitpid返回收集到的子进程的进程ID;
    如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
    如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
    参数:
    pid:
    Pid=-1,等待任一个子进程。与wait等效。
    Pid>0.等待其进程ID与pid相等的子进程。
    status:
    WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
    WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    options:
    WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. pid_t id = fork();
    9. if(id == 0)
    10. {
    11. // child
    12. int cnt = 5;
    13. while(cnt)
    14. {
    15. printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
    16. sleep(1);
    17. cnt--;
    18. }
    19. exit(1);
    20. }
    21. int status = 0;
    22. pid_t rid = waitpid(id, &status, 0); // 阻塞等待
    23. if(rid > 0)
    24. {
    25. printf("wait success, rid: %d, status: %d\n", rid, status);
    26. }
    27. }
    '
    运行

    如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
    如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
    如果不存在该子进程,则立即出错返回。 

    获取子进程status

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

    当我们的子进程exit(1)时,子进程的退出状态status为256。256代表什么呢?

    任何进程最终执行状态我们可以使用两个数字具体表明情况

     我们为了直观一点,可以使用位操作,将退出信号和退出码分别打印出来:

    printf("wait success, rid: %d, status: %d, exit signo: %d, exit code: %d\n", rid, status, status&0x7F, (status >> 8)&0xFF);

    我们也可以使用宏来获取退出信号与退出码:

    1. int main()
    2. {
    3. pid_t id = fork();
    4. if(id == 0)
    5. {
    6. // child
    7. int cnt = 5;
    8. while(cnt)
    9. {
    10. printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
    11. sleep(1);
    12. cnt--;
    13. }
    14. exit(1);
    15. }
    16. int status = 0;
    17. pid_t rid = waitpid(id, &status, 0); // 阻塞等待
    18. if(WIFEXITED(status))
    19. {
    20. printf("wait success, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));
    21. }
    22. }

     阻塞与非阻塞

    在waitpid函数中,我们发现在上面的代码中,我们一直默认第三个参数为0。其就是对应的阻塞状态。而WNOHANG这个宏在第三个参数中可以进行填入,代表非阻塞等待。

    阻塞等待时,父进程不能做任何事情,只能等待子进程变成僵尸进程后进行回收。而非阻塞等待可以轮转时进行行为。

    非阻塞等待代码:

    1. int main()
    2. {
    3. pid_t id = fork();
    4. if(id == 0)
    5. {
    6. // child
    7. int cnt = 5;
    8. while(cnt)
    9. {
    10. printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid());
    11. sleep(1);
    12. cnt--;
    13. }
    14. exit(1);
    15. }
    16. int status = 0;
    17. pid_t rid = waitpid(id, &status, WNOHANG);
    18. while(1)
    19. {
    20. if(rid > 0)
    21. {
    22. printf("wait success, rid: %d, status: %d, exit code: %d\n", rid, status, WEXITSTATUS(status));
    23. break;
    24. }
    25. else if(rid == 0)
    26. {
    27. printf("father say: child is running, do other thing\n");
    28. }
    29. else
    30. {
    31. perror("waitpid");
    32. break;
    33. }
    34. }
    35. return 0;
    36. }

    进程程序替换

    替换原理

    用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。 

    当我们要进行替换时,新程序的数据和代码会将原来物理内存中的数据段和代码段进行替换。其实程序替换工作本质就是加载!!! 

    替换函数

    其实有六种以exec开头的函数,统称exec函数:

    #include `
    int execl(const char *path, const char *arg, ...);
    int execlp(const char *file, const char *arg, ...);
    int execle(const char *path, const char *arg, ...,char *const envp[]);
    int execv(const char *path, char *const argv[]);
    int execvp(const char *file, char *const argv[]);

    int execve(const char *path, char *const argv[], char *const envp[]);

    这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
    如果调用出错则返回-1
    所以exec函数只有出错的返回值而没有成功的返回值。 

    这些函数原型看起来很容易混,但只要掌握了规律就很好记。 

    l(list) : 表示参数采用列表
    v(vector) : 参数用数组
    p(path) : 有p自动搜索环境变量PATH
    e(env) : 表示自己维护环境变量

    我们强调一下结尾带e的exec*的函数,其就是表示自己维护环境变量。我们想要子进程全部继承父进程的全部环境变量直接可以。如果单纯再父进程的环境变量中添加一些环境变量可以使用putenv函数。但是我们子进程如果想要自己拥有一个全新的环境变量,我们可以使用exec*函数中后面带e的,他会将父进程继承的环境变量覆盖掉!!! 

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. int main()
    7. {
    8. char *const env[] ={
    9. (char*)"haha=hehe",
    10. (char*)"PATH=/",
    11. NULL
    12. };
    13. printf("I am a process, pid: %d\n", getpid());
    14. //putenv("MYVAL=bbbbbbbbbbbbbbbbbbbbbbbbbbbb");
    15. pid_t id = fork();
    16. if(id == 0)
    17. {
    18. //extern char**environ;
    19. sleep(1);
    20. //execle("./mytest", "mytest", NULL, environ); // 我们传递环境变量表了吗??no. 子进程默认就拿到了.他是怎么做到的?
    21. execle("./mytest", "mytest", NULL, env); // 我们传递环境变量表了吗??no. 子进程默认就拿到了.他是怎么做到的?
    22. //execl("/usr/bin/python3", "python3", "test.py", NULL);
    23. //execl("/usr/bin/bash", "bash", "test.sh", NULL);
    24. //execl("./mytest", "mytest", NULL); // 我们传递环境变量表了吗??no. 子进程默认就拿到了.他是怎么做到的?
    25. //char *const argv[] = {
    26. // (char*)"ls",
    27. // (char*)"-a",
    28. // (char*)"-l"
    29. //};
    30. //sleep(3);
    31. //printf("exec begin...\n");
    32. //execvp("ls", argv);
    33. //execv("/usr/bin/ls", argv);
    34. //execl("/usr/bin/ls", "ls", "-a", "-l", NULL); //NULL 不是 "NULL"
    35. //execlp("ls", "ls", "-a", "-l", NULL); //NULL 不是 "NULL"
    36. //execl("/usr/bin/top", "/usr/bin/top", NULL); //NULL 不是 "NULL"
    37. printf("exec end ...\n");
    38. exit(1);
    39. }
    40. pid_t rid = waitpid(id, NULL, 0);
    41. if(rid > 0)
    42. {
    43. printf("wait success\n");
    44. }
    45. exit(1);
    46. }

     

    exec调用举例如下:

    1. #include
    2. int main()
    3. {
    4. char *const argv[] = {"ps", "-ef", NULL};
    5. char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    6. execl("/bin/ps", "ps", "-ef", NULL);
    7. // 带p的,可以使用环境变量PATH,无需写全路径
    8. execlp("ps", "ps", "-ef", NULL);
    9. // 带e的,需要自己组装环境变量
    10. execle("ps", "ps", "-ef", NULL, envp);
    11. execv("/bin/ps", argv);
    12. // 带p的,可以使用环境变量PATH,无需写全路径
    13. execvp("ps", argv);
    14. // 带e的,需要自己组装环境变量
    15. execve("/bin/ps", argv, envp);
    16. exit(0);
    17. }

     事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。

    总结:

    细节1:程序替换一旦成功,exec*后续代码不再执行,因为被替换掉了。

    细节2:exec*只有失败有返回值,没有成功返回值。

    细节3:替换完成,不再创建新的程序。

    细节4:创建一个进程先创建PCB、地址空间、页表,再将程序加载到内存中。 

    这些函数功能上没有任何区别,区别就在于传递的参数不同! 


    以上就是全部内容,感谢大家观看!!!

  • 相关阅读:
    鸿蒙面试心得
    What is a TCP SYN Flood DDoS Attack?
    基于YOLOv5的车牌识别系统(YOLOv5+LPRNet)
    5-3传输层-TCP协议
    什么是Executors框架?
    Serverless 架构落地实践及案例解析
    java毕业设计企业员工管理系统源码+lw文档+mybatis+系统+mysql数据库+调试
    【Java】环境配置以及快速切换环境的技巧和方法
    微电网和直流电网中最优潮流(OPF)的凸优化(Matlab代码实现)
    Qt第三十一章:渐变QGradient
  • 原文地址:https://blog.csdn.net/m0_74755811/article/details/139453661