进程控制。

文章详细阐述了程序运行后的几种情况,包括正常和异常退出,重点讲解了退出码的作用和如何通过wait、waitpid获取子进程的退出信息。此外,还介绍了进程程序替换的概念,如exec系列函数的用法,以及它们在进程上下文中的作用。
摘要由CSDN通过智能技术生成

程序运行完后可能的情况

进程等待

进程程序替换

程序运行完后可能的情况

正常情况

  • 正常执行完了,结果正确
  • 结果不正确

异常情况

  • 崩溃:进程因为某些原因(越界等),导致进程收到了来自操作系统的信号
  1. 正常情况

我们之前谈论过main函数的返回值就是退出码,我们判断进程运行完之后结果是否正确就可以通过退出码来知道结果

我们来写段代码来看看退出码
在这里插入图片描述
在这里插入图片描述
我们知道,进程退出码并不会帮我们打印出来,我们可以通过指令echo $?来打印进程退出码
在这里插入图片描述
可以看到我们运行了许多次打印的都是0,因为我们第一次使用指令打印的是我们运行的可执行程序的退出码,只后每一次打印都是前一次的指令的退出码,因为指令也是一个可执行程序,这是我们之前讲过的
在这里插入图片描述
在这里插入图片描述
我们可以看到,这些退出码对应的含义,我们是认识见过一些的,比如说前几个大家想必很熟悉,经常见,系统里这些退出码对应的字符串可能不一样,但是基本都有这些字符串

我们知道,return在main函数中才可以让进程退出,在函数内的return是不行的,我们可以使用函数exit来退出
在这里插入图片描述

我们可以看到exit让进程退出了,并且exit的参数就是退出码
在这里插入图片描述
在这里插入图片描述
exit函数在其他函数内部被调用也可以让进程退出

说明在代码的任何地方调用exit都可以让进程退出
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
可以看到_exit函数不仅用法和exit一样,结果也是一样,那么它们没有区别吗?有的
在这里插入图片描述
在这里插入图片描述

我们使用printf打印,然后等待3秒,在用exit函数退出,这其中因为缓冲区,我们等待的3秒内不会看到字符串被打印出来,在退出的时候才会打印
在这里插入图片描述
在这里插入图片描述
可以看到我们只是将exit换成了_exit其他没有变化,而我们这次并没有打印出字符串

所以我们可以得出结论:_exit函数不会在退出的时候不会刷新缓冲区,而exit函数会刷新缓冲区

  • exit函数会执行用户定义的清理函数
  • 冲刷缓冲区,关闭流等
  • exit只是将进程退出并返回退出码

exit是封装了_exit的函数

进程等待

为什么要进程等待?

  1. 避免内存泄漏
  2. 获取子进程执行的结果(如果需要就获取)

进程等待:通过系统调用,获取子进程退出码或退出信号的方式,顺便释放内存

怎么进程等待?- - wait/waitpid

那么waitwaitpid是怎么获取到子进程的退出信息的呢?

这两个函数都是系统调用接口,他们可以通过pid找到子进程,然后去子进程的pcb中获取退出信息

wait

在这里插入图片描述

wait就是等待子进程变成僵尸进程

在这里插入图片描述

在这里插入图片描述
子进程运行了5秒,这时候都是S状态,之后子进程退出变成Z状态,等了10秒,子进程被回收,只剩下了父进程,然后再等5秒,进程退出,ret就是子进程的pid

waitpid

在这里插入图片描述
通过man手册查看一下waitpid,我们来解读一下:

  1. 第一个参数pid就是要你调用的时候传进程的pid过来,传过来的pid大于0就表示等待指定的进程,如果等于-1就表示等待任意一个子进程,与wait等效

  2. 第二个参数表示状态,是输出型参数,如果传的是NULL,表示不关心子进程的退出状态信息,waitpid会使用位图的方法来通过这个参数输出两个值,一个是子进程的退出状态,一个是终止信号

    statusint*类型的,但是实际上,我们只会使用它的低16比特位,这和我们之前学的权限可以用8进制数字去设置是差不多的

    在使用的16个比特位之间,高8个比特位会用来表示子进程的退出码,低7位会用来表示终止终止信号,中间有一个比特位是core dump标志,0或1表示是否打开核心转储

    • 终止信号为0,表示正常退出,我们再来看退出码
    • 不为0,表示进程不是正常退出,我们不用再管退出码了,退出码就没有意义了

    我们来截取一下status里的两个参数:

    Linux下有一些宏可以获取退出码和终止信号:

    1. WIFEXITED(status):获取子进程的终止信号,为真表示正常退出
    2. WEXITSTATUS(status):提取子进程的退出码
    退出码

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

    status >> 8表示将后16个比特位中的高地址的8位移到最后,变成最后8位,然后再按位与上0xFF,表示获取到最后8位- - 退出码
    status按位与上0x7F表示获取最后的7位 - - 终止信号

    终止信号

    在这里插入图片描述
    我们在这里加一句错误的代码
    在这里插入图片描述
    可以看到,子进程还没有执行完立马就退出了,父进程通过waitpid函数收到了子进程的终止信号,这时候子进程的退出码就没有意义了

    在这里插入图片描述
    我们来看一下,我们将代码改成了死循环,然后通过指令去结束子进程,看看可不可以被父进程获取到终止信号
    在这里插入图片描述
    可以看到,父进程仍然可以获取到子进程的终止信号

  3. 第三个参数

    WNOHANG:让父进程非阻塞等待子进程,设置非阻塞选项,子进程没有退出,父进程就可以脱离阻塞状态,不会一直等待着,可以运行其他代码

    我们运行刚刚的几段代码可以发现,父进程好像一直都没有退出,在等待子进程退出,那么父进程这段时间在干什么呢?

    父进程在使用waipid函数之后,只能一直等待着子进程退出,然后才会执行之后的代码,这种情况叫做阻塞等待

    在使用waitpid函数的时候,我们可以让父进程不要一直等,可以多次检测子进程是否退出了,这叫做非阻塞轮询,这时候有三种情况:

    • 子进程还没有结束,父进程可以运行其他代码
    • waitpid出错
    • 子进程退出,父进程等待结束

    那么我们怎么样才能让父进程不要一直等待,可以去做运行其他代码呢?

    在这里插入图片描述
    在这里插入图片描述
    可以看到,父进程没有一直在等待子进程,父进程经过每次轮询之后,子进程没有退出,就运行自己的代码

  4. 返回值
    如果等待成功,返回子进程的pid,如果我们第三个参数设置的是WNOHANG,子进程还没有退出就会返回0,出错就返回-1

进程程序替换

用fork创建子进程之后执行的是和父进程相同的程序(有可能执行不同的代码分支)

但是如果我们不想让子进程执行父进程的代码,我们可以使用exec函数

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的pid并未改变

#include <unistd.h>
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) : 表示自己维护环境变量

execl

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

我们可以理解记忆,execl等于exec加上l,l就是list,就是把字符串一个个传过去

第一个参数传的是可执行程序(路径),后面的参数表示如何使用可执行程序(使用方法),**最后一个参数要传NULL**告知execl参数传完了

不管是子进程还是父进程使用了execl函数替换了程序,都不会影响另一个,也就是说,进行了程序替换,就会自动发生写时拷贝,将代码和数据都写时拷贝(代码也会发生写时拷贝)

我们写段代码来看看怎么用的
在这里插入图片描述
在这里插入图片描述
可以看到,子进程打印了自己的pid,然后也执行了ls -a -l的命令,也就是说,execl之前的程序不会被替换,会照常运行,execl之后的程序会被替换(虽然我们这里没有写),父进程是照常运行,并没有被子进程影响

我们看看如果替换失败了,会怎样,返回值是不是-1呢?
在这里插入图片描述
在这里插入图片描述

我们可以看到,替换失败了之后,返回确实是-1,然后会继续执行后面的代码

execv

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

理解记忆:exec加上v,v就是vector,c++里的容器,就是一个要传数组
在这里插入图片描述

在这里插入图片描述

也就是说将我们要在后面要传的放进一个指针数组里

execlp

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

理解记忆:l就是list,p就是PATH环境变量,file就是程序名,不用带路径,系统会自动在PATH中找

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

execvp

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

理解记忆:file就是程序名,v就是vector,传个数组,p就是PATH
在这里插入图片描述

execle

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

理解记忆:path就是路径,e就是自定义环境变量,l是list
在这里插入图片描述

自定义环境变量,会覆盖PATH,变成自己传入的环境变量

在这里插入图片描述

我们使用函数putenv将我们的环境变量加入到我们这个进程的环境变量中,再传进入函数execle中,这样就不会覆盖PATH

execve

int execve(const char *path, char *const argv[], char *const envp[]);
这个系统调用不和上面六个放在一起,这个是真正的系统调用,上面六个是对它的封装

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值