当前位置: 首页 > news >正文

Linux进程控制(下):进程等待和进程替换

文章目录

    • 一、进程等待
      • 是什么?
      • 为什么?(进程等待必要性)
      • 怎么做?(进程等待的方法)
        • 获取子进程的status
        • wait函数
        • waitpid
        • 具体代码实现
          • 进程的阻塞等待方式
          • 进程的非阻塞等待方式
      • 问题
    • 二、进程替换
      • 替换原理
      • 七个替换函数
      • 函数解释
      • 命令理解


一、进程等待

是什么?

通过系统调用wait/waitpid,来进行对子进程进行1状态检测与回收的功能

为什么?(进程等待必要性)

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

注意

  • 僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题——这是必须解决的
  • 通过进程等待,获取子进程的特殊情况(知道我们布置给子进程的任务,完成的怎么样了)——取决于我们的需求,不是必要的

怎么做?(进程等待的方法)

获取子进程的status

进程等待所使用的两个函数wait/waitpid,都有一个status参数:

#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int*status);
pid_ t waitpid(pid_t pid, int *status, int options);

该参数是一个输出型参数,由操作系统进行填充。

如果不关心子进程的退出状态信息,就对status参数传入NULL。反之,操作系统会通过该参数将子进程的退出信息反馈给父进程。

status是一个整型变量,但不能简单的当作整型来看待,可以当作位图来看待:
一个整数有4个字节,每个字节有8个比特位,status共有32个比特位,status的不同比特位代表的信息不同,如图(只研究status低16比特
位):
在这里插入图片描述

所以,我们可以通过位操作,配合status得到进程的退出码和退出信号。

exit_code = (status >> 8) & 0xFF; //退出码
exit_signal = status & 0x7F;      //退出信号

为此,系统提供了两个宏来获取退出码和退出信号:

  • WIDEXITED(status):若子进程正常终止退出,则为真。(用于查看进程是否是正常退出
  • WEXITSTATUS(status):若非零,则为子进程的退出码(用于查看进程的退出码

值的注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

wait函数

这个函数可以说是waitpid的子集,它的功能没有waitpid强大,且它的功能waitpid也都能够实现。因此,我们学会了waitpid,wait也就自然会用了,但推荐使用waitpid。

#include<sys/types.h>
#include<sys/wait.h>pid_t wait(int* status);
  • 作用:等待任意子进程
  • 参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
  • 返回值:等待成功则返回等待进程的pid,等待失败则返回-1
waitpid
#include<sys/types.h>
#include<sys/wait.h>pid_t waitpid(pid_t pid, int* status, int options);
  • 作用:等待指定子进程或任意子进程
  • 参数:
    1. pid:
      • pid = 0,等待任意一个子进程,与wait等效
      • pid > 0,等待进程ID与pid相等的指定子进程
    2. status:输出型参数,获取子进程的退出状态,不关心可设置为NULL。
    3. options:选择父进程是否需要阻塞等待子进程退出
      • WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
      • 0:默认阻塞等待子进程退出
  • 返回值:
    1. 等待成功返回被等待进程的pid
    2. 如果设置了参数option为WNOHANG,而调用中waitpid发现没有已退出的子进程可以等待,则返回0
    3. 如果调用中出错,返回-1
具体代码实现
进程的阻塞等待方式

将options设置为0,进程就会以阻塞的状态进行等待:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>int main()
{int id = fork();//代码创建子进程if(id < 0){printf("fork error\n");}else if(id == 0)//child{printf("I am a child process PID:%d, PPID:%d\n", getpid(), getppid());sleep(5);exit(1);//子进程退出}//fatherint status = 0;//状态int ret = waitpid(id, &status, 0);//参数3为0,默认阻塞等待,等待5Sif(ret > 0)//等待成功{if(WIFEXITED(status)) {printf("子进程正常退出了 ret:%d,  exit code:%d,  exit sig:%d\n", ret, WEXITSTATUS(status), status&0x7F);}else {printf("进程异常了\n");}}else if(ret == 0){printf("子进程还在运行!\n");}else //等待失败{printf("wait false\n");}return 0;
}

结果:
在这里插入图片描述

进程的非阻塞等待方式

如果等待的时间略长,就让进程在那傻傻的等未免有点浪费了,我们可以将options设置为WNOHANG,让进程在等待的闲暇做点其他事:

#include <stdio.h> 
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>int main()
{int pid = fork();if(pid < 0){printf("%s fork error\n",__FUNCTION__);return 1;}else if( pid == 0 )//child{ printf("child is run, pid is : %d\n",getpid());sleep(5);exit(1);} int status = 0;int ret = 0;do{ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待if( ret == 0 ){printf("Child is running.");printf("I can do other thing\n");//这里父进程可以做其他事情}sleep(1);}while(ret == 0);if( WIFEXITED(status) && ret == pid ){printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else{printf("wait child failed, return.\n");return 1;}return 0;
}

结果:
在这里插入图片描述
这里每一次循环都会检查子进程是否完成,若未完成,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程终止后读取子进程的退出信息。因此被称作非阻塞轮询

问题

父进程要拿子进程的状态数据,任意数据,为什么必须要用wait等系统调用呢》

因为进程具有独立性,子进程与父进程相互独立,都有各自的PCB,是没有权利去其他PCB去获取数据的

在进程的PCB中,存在两个成员:
在这里插入图片描述
退出码和信号码,它们便是进程的退出信息


二、进程替换

替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),然而子进程往往需要调用一种exec函数一执行另一个程序。

当进程调用一种exec函数时,该进程的用户空间代码和数据会完全被新的程序替换,从新程序的起始开始执行。

调用exec并不创建新进程,所以调用exec前后该进程的ID并未发生改变。

在这里插入图片描述

CPU如何得知新程序的入口地址呢?

Linux中形成的可执行程序是有格式的,ELF是可执行程序的表头,而可执行程序的入口地址就在表中。

子进程进行进程程序替换后,会影响父进程的代码和数据吗?

子进程刚被创建时,父子进程代码和数据共享,当子进程需要被程序替换时,也就是需要修改子进程的数据和代码了,这时就会发生写时拷贝,这样父子进程的代码和数据不再共享,因此子进程被程序替换后,父进程代码和数据不受影响。

注意:代码也可以写时拷贝

环境变量是什么时候给进程的?环境变量会被替换吗?

环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承了。
环境变量是具有全局属性的,当当是只替换程序,环境变量信息是不会被替换的。

程序替换成功后,exec后面的代码会被执行吗?替换失败呢?

程序替换成功后,exec后面的代码不会被执行!
替换失败了,才可能会执行后续的代码。

七个替换函数

进程程序替换函数共有七个,其中六个都是在调用系统调用函数execve,对其进行封装,实现不同需求的六大函数。
在这里插入图片描述
它们统称为exec函数。


int execl(const char *path, const char *arg, ...);

  • 第一个参数是要执行程序的路径
  • 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

例如,要替换的是ls程序:

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);

用可变参数的原因其实很好理解,找到这个程序后,我们要知道如何执行这个程序,需要涵盖哪些选项?这是不确定的。
💡:命令行怎么写,我们就怎么传


int execlp(const char *file, const char *arg, ...);

  • 第一个参数是要执行程序的名字
  • 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

例如,要替换的是ls程序:

execlp("ls", "ls", "-a", "-i", "-l", NULL);

这里看到两个一样的参数,会不会冗余?
其实它们的含义的作用是不同的:前者作用为找到该程序,后者作用是如何运行这个程序。
那为什么一个凭字符串指针就可以帮助我们找到这个程序呢?
这是环境变量的功劳,PATH中有ls的路径。


如果我们的exec*能够替换系统命令,那能不能替换我们自己的程序呢?
当然可以,但是需要注意环境变量的问题。

如果想给子进程传递环境变量,该怎么传递呢?

  1. 新增环境变量:在父进程中直接使用putenv
  2. 彻底替换:下文函数

int execle(const char *path, const char *arg, ...,char *const envp[]);

  • 第一个参数是要执行程序的路径
  • 第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾
  • 第三个参数是你自己设置的环境变量,覆盖子进程原有的环境变量。

例如,你设置了MYVAL环境变量,在mycmd程序内部就可以使用该环境变量。

char* myenvp[] = { "MYVAL=2021", NULL };
execle("./mycmd", "mycmd", NULL, myenvp);

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

int execvp(const char *file, char *const argv[]);

int execvpe(const char *file, char *const argv[], char *const envp[]);

剩余这三个函数与前三个函数作用是对应相同的,唯一区别是将可变参数更改为了字符指针数组,我们需要预先设置好该数组,然后作为参数传入。
一样的,命令行怎么写,我们数组就怎么设置
注意:数组需要以NULL结尾


函数解释

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

命令理解

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

在这里插入图片描述
在这里插入图片描述
exec函数族:
在这里插入图片描述

http://www.dtcms.com/a/391522.html

相关文章:

  • 如何检查数据库是否处于恢复模式
  • AI一周资讯 250913-250919
  • Livox-mid-360录制的.lvx2文件转化为.bag文件(TBC)
  • 【 svn】自动重试: cleanup + update
  • 有哪些Java学习书籍推荐?
  • 机动车登记证 OCR 识别:让车辆业务办理驶入 “快车道“
  • 在QT中使用FFmpeg实现录屏功能
  • 使用redisson实现延迟队列
  • 算法面试(1)-----两阶段检测器(如Faster R-CNN)和单阶段检测器(如YOLO、SSD)的区别与优劣?
  • 10cm钢板矫平机:一条“钢铁传送带”上的隐形战场
  • 数据结构与算法3:链式最基本的表示和实现——单链表
  • redisson延迟队列最佳实践
  • Netty ByteToMessageDecoder解码机制全解析
  • scrapy项目-爬取某招聘网站信息
  • 解决ubuntu下搜狗输入法在浏览器不可用的问题
  • 设计模式- 命令模式详解
  • 谈一谈Java成员变量,局部变量和静态变量的创建和回收时机
  • OSCP - Proving Grounds - Leyla
  • 9 月 19 日 IT 界热点大赏:科技浪潮下的创新与变革
  • 自动化脚本的零失误之路
  • Redis(三)Redis集群的三种模式
  • 网络环路:成因、影响与防环机制深度解析
  • 力扣刷题笔记(1)--面试150数组部分
  • 分割模型Maskformer
  • C# TCP的方式 实现上传文件
  • 高压消解罐:难溶物质消解的首选工具
  • JavaScript 字符串截取最后一位的几种方法
  • MobileNetV3训练自定义数据集并通过C++进行推理模型部署
  • nvshmem源码学习(一)ibgda视角的整体流程
  • Redis群集的三种模式