Linux——进程终止/等待/替换
前言
本章主要对进程终止,进程等待,进程替换的详细认识,根据实验去理解其中的原理,干货满满!
1.进程终止
概念:进程终止就是释放进程申请的内核数据结构和对应的代码和数据
进程退出的三种状态
- 代码运行完毕 结果正确
- 代码运行完毕 结果错误
- 代码中止异常
我们在学习C语言的时候,写main函数,一般都会写return 0;main函数的返回值,通常就代表程序的执行情况,0代表成功,非0代表代码运行完毕,结果错误,不同的值代表不同的错误
当父进程创建子进程是为了让子进程完成某种任务,当子进程结束,肯定要将执行结果返回给父进程,让父进程知道是什么情况,我们知道子进程结束会保留task_struct,等待父进程获取结果信息,此时子进程就是僵尸进程,执行结果,也就是返回值就会存放到task_struct中,被父进程获取
1.1退出码
进程结束返回的一个状态 通常是一个整数值
echo $?
打印最近一个进程退出时的退出码
errno:当库函数调用失败或者系统调用失败 他们通常会将一个特定的错误代码赋值给errno
strerror是C语言的一个库函数,该函数接收一个errno的错误代码作为参数, strerror负责将这些错误代码转换为具体的错误描述信息,一共有134条
我们可以看到0表示成功 1表示操作不被允许 2表示没有这个目录或文件 3表示没有这个进程
相信后面的大家也可以读懂 这样我们就可以根据退出码知道我们的错误是什么了
此时在当前目录并没有c.txt文件,ls是C语言写的一个程序,当执行完程序,发现文件不存在,此时的退出码是2,不就是上面的2号找不到文件吗
1.2进程常见的退出方法
1.2.1从main返回
我们知道main函数是程序的入口,当main函数结束,也return了,进程也就结束了
其他的函数只表示调用函数完成了 返回退出码 并不代码进程结束
1.2.2exit
exit手册内容 exit是直接结束进程,引起进程终止 它需要一个参数,就是状态(进程退出码)
1.2.3_exit
_exit手册内容 用于终止进程的系统调用 通过实验我们发现和exit一样都可以终止进程
1.2.4exit和_exit区别
补充:我们知道只有OS才可以杀掉进程 库和系统调用是上下层的关系 库调用系统调用
相同点:都可以终止进程
不同点:
- exit是标准C库函数 _exit是系统调用
- exit会刷新缓冲区 _exit不会刷新缓冲区
实验:通过下面的实验我们也可以验证exit不会刷新缓冲区
2.进程等待
在理解进程等待前,我们先来想一下进程为什么要进行等待呢?
- 回收子进程资源(处理僵尸进程)
- 获取子进程退出信息
我们知道当子进程退出时,task_struct不会被释放,需要父进程回收资源,当父进程一直不管时,就可能会造成僵尸进程,可能会出现内存泄露
2.1wait
手册内容:wait的参数status是一个输出型参数 在后续的waitpid我们会详细了解
wait会等待任意一个子进程 回收成功会返回回收的pid
接下来我们来使用一下wait
创建一个子进程 让他跑五次 当子进程结束时,让程序暂停10s,这个时候父进程还没有回收资源,所以子进程此时就是僵尸进程,根据子进程状态我们可以看到是Z,父进程回收子进程资源,解决了僵尸进程,返回了子进程的pid,子进程资源被全部释放,剩余父进程,程序再休眠10s,最后父进程进程也结束
2.2waitpid
pid_t waitpid(pid_t pid, int *status, int options); waitpid共有三个参数
接下来会一个一个拆开分析:
2.2.1pid_t pid
pid_id的值有四种
等待指定pid或者等待任意子进程
如果等待失败 会出现什么样的情况呢?
2.2.2int *status
输出型参数 存储进程退出时的状态信息 不关心状态信息设置NULL
我们来进行测试一下:发现出现了一些问题
status不能当做整型看待,可以当做位图来看待,前16位不考虑,次8位是退出状态,那么此时应该前7位是0,然后是1,后面还有8个0,因为是2进制,所以应该是2的8次方,也就是256!
当除数为0时就会使程序出现异常
其实exit code 和 exit signal都存放在子进程的task_struct中,当子进程是僵尸进程,等待父进程通过操作系统调用回收子进程资源,获取子进程的退出信息
我们在上述的实验是通过位操作来提取信息的,真正的OS是使用宏,其实就是封装了一下位操作
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是
否是正常退出,程序是否异常,=0为真)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的
退出码)

2.2.3int options
补充:
- 阻塞调用:当一个程序发起操作时,程序会暂停操作,直到这个操作完成并返回结果,在执行操作时,程序不能做任何事,也就是子进程在执行任务时,父进程一直在wait阻塞
- 非阻塞调用:当一个程序发起操作,不会等操作完成,而是立即返回,执行后续的操作,程序可以定期检查这个操作是否已经完成,这个操作叫做非阻塞轮询
我们从手册里可以看到options是有几个选项的
- 默认是0:阻塞调用,如果子进程还没有退出父进程就会一直阻塞在那里
- WNOHANG:非阻塞调用
waitpid的返回值可以是 -1 0 >0
-1表示失败
0表示 调用结束 但是子进程没有退出
>0 子进程结束
非阻塞轮询实验,将waitpid的第三个参数设置为WNOHANG,非阻塞调用,这样父进程就不会一直等待子进程完成任务,而是立即返回,然后定期去询问子进程完成任务了吗?
非阻塞调用实验:我们可以通过函数指针回调函数来操作
通过实验我们可以看到在子进程执行任务时,父进程也在执行其他任务
3.进程替换 exec
进程替换就是OS根据指定的程序文件路径和参数,将新程序的代码和数据加载到当前进程的地址空间中,覆盖原来的进程内容,从而实现进程替换
程序替换错误返回-1 没有成功返回值
当我们知道程序替换会将原先的程序进行覆盖,我们原先的程序就没有了,所以我们一般会创建子进程去执行程序替换,这样父进程的代码数据也不会丢失了!!
3.1进程替换的原理
我们先来使用一下execl,我们可以发现第一句printf执行了,然后执行程序替换,然后就没有输出了,我们来了解一下程序替换的原理!其实在上面就已近谈到了,就是将新程序的代码和数据加载到当前进程的地址空间中,覆盖替换原来的进程内容,从而实现进程替换
在程序替换的过程中,并没有创建新的进程
3.2程序替换接口函数
我们在上面的手册中可以看到6个接口函数,接下来我们学习四个 后续的大家肯定就都懂
还有一个是系统命令 execve 我们在上面看到的6个接口函数其实都是需要去调用execve
在上层进行封装 为了应对各种各样的场景 最后会统一转化 调用系统调用execve
3.2.1execl
int execl(const char *path, const char *arg, ...);
execl的l可以看做是一个list,第一个参数就是路径+程序名,第二个参数就是命令,在命令行怎么写,这里我们就怎么写,...是可变参数列表的意思,因为我们不知道命令有几个,最后要以NULL结尾,表明参数已传完!!
3.2.2 execlp
int execlp(const char *file, const char *arg, ...);
execlp的l看做list,p看做PATH,第一个参数就是要执行的文件名,execp会自动在环境变量PATH中查找命令,第二个参数就是命令,同上!
3.2.3execv
int execv(const char *path, char *const argv[]);
execv的v可以看做是一个vector,第一个参数是要执行的路径+文件名,第二个参数是一个指针数组,也就是命令行参数表
3.2.4execvp
int execvp(const char *file, char *const argv[]);
execvp的v可以理解为vector,p理解为PATH,第一个参数就是要执行的文件,第二个是指针数组,就是指命令行参数表,相信大家看到这里一看就看懂了!!
3.2.5 execvpe
int execvpe(const char *file, char *const argv[], char *const envp[]);
execvpe的v可以看做vector,e看做environment,第一个参数就是要执行的文件,第二个就是命令行参数表,第三个也是一个指针数组,是环境变量表,这里的环境变量表会进行覆盖替换父进程的环境变量表,当然也有方法在原始的环境变量表的基础上进行添加!!
补充
putenv 添加环境变量的参数
envrion 访问当前环境的整个环境列表
解决方案 :
- 不使用需要传带env的参数 直接进行putenv
- 使用传env的参数 putenv envrion