目录
一、进程终止
1.1进程退出的场景
1.2进程常见的退出方法
1.3多进程的退出
1.4exit( )和_exit( )
二、进程等待
2.1进程等待的必要性
2.2进程等待的方式
2.3获取子进程的statue
2.4非阻塞轮询
2.5进程等待的底层原理
三、进程程序替换
3.1单进程程序替换
3.2多进程程序替换--验证各种程序替换接口
3.3 系统调用execve( )
一、进程终止
1.1进程退出的场景
- 程序运行正常结束,退出结果正确
- 程序运行正常结束,退出结果错误
- 程序异常退出
1.2进程常见的退出方法
- 从main返回
- 调用exit
- _exit
- ctrl + c
return 退出
-
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
-
在函数内return,只会返回该函数的返回值,在函数内exit就会直接退出进程
-
也就是说,在本进程内的任意函数位置调用exit都会使该进程直接退出
代码测试:
exit( )直接退出进程,后续代码不再执行
return返回该函数的返回值,继续执行函数调用处之后的代码
查看退出码对应的信息:
- 进程的退出码和退出信息是一一对应的关系
- 我们也可以自己设计出一套退出码体系
为什么需要退出码?
- 程序在退出时需要退出码,mian()函数的退出码本质上是表示程序运行结束后的结果是否正确,如果不正确,可以用不同的退出码值来表示不同的错误原因
- 程序运行结束时,父进程要关心子进程的运行情况,获得子进程的退出信息
- 如果程序运行的结果是正确的,退出码为0,就不需要再关心进程的运行情况原因;如果退出码为其他数字,说明程序运行的结果是错误的,父进程必须拿到该结果的原因返回给上层用户,知道错误原因决定是否重新运行
异常的本质?
- 异常的本质通俗来说是代码没有跑完,此时我们不再需要关心退出码的问题了,转而关心出现异常的原因
- 进程异常的发生本质上是进程收到了某种信号(我们曾经使用过的kill -9 就是一种信号)
- 通过kill -l 可以查看到异常退出的信息
- 实例
1.3多进程的退出
1.4exit( )和_exit( )
exit( )和_exit( )的区别:
- exit( )是库函数,_exit( )是一种系统调用
- 本质上exit( )是对_exit( )的封装
- 调用exit( )会执行缓冲区的刷新和关闭流等操作再退出进程,调用_exit( )不做任何其他操作,直接退出进程
-
exit最后也会调用_exit 但在调用_exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
代码测试:
exit( )进程退出后会刷新缓冲区
_exit( )进程退出后不会刷新缓冲区 ,没有打印信息
- 这样说明了printf输出的数据存入的缓冲区不在Linux内核里,而是由c语言层面提供的一个缓冲区
二、进程等待
2.1进程等待的必要性
- 进程等待是指:通过调用wait( )或者waitpid( )来实现对子进程进行状态检查和回收的功能
- 子进程在退出时,它的退出信息(退出码和接收到的信号)会被保存在该子进程的pcb数据结构对象内,如果父进程一直不对子进程的退出信息进行回收,该pcb数据结构对象就无法被释放,就会造成僵尸进程,进而引发内存泄露的问题
- 僵尸进程无法被直接杀死,进程等待可以杀死僵尸进程,从而解决内存泄露的问题--必须解决
- 同时,通过进程等待可以获取子进程的退出信息,让关心子进程的父进程知道子进程将任务完成得如何了--非必要
- 如果子进程一直不退出,父进程调用wait( )这个系统调用的时候,就一直不会返回,处于阻塞状态
2.2进程等待的方式
- wait( )等待
-
pid_t wait(int*status);
-
返回值:成功返回被等待进程pid,失败返回-1
-
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
代码测试:
wait( )等待的是任意一个进程
父进程没有回收子进程前,子进程退出形成僵尸进程
休眠十秒后父进程开始循环回收每一个子进程:
- waitpid( )
- pid_ t waitpid(pid_t pid, int *status, int options)
-
返回值:当正常返回的时候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
代码测试:
使用WEXITSTATUS()可以查看进程的退出码
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
代码测试:
父进程等待子进程退出,处于阻塞状态
- 如果不存在该子进程,则立即出错返回
2.3获取子进程的statue
子进程在退出的时候会有哪些退出信息?
- 程序正常运行结束,返回结果正确
- 程序正常运行结束,返回结果错误
- 异常退出
父进程要关心子进程的什么退出信息?
- 子进程是否出现异常
- 没有出现异常,程序正常运行结束,结果是否正确
- 结果不正确是什么原因--通过获得不同的错误码来表示不同的错误信息
statue参数是什么?
-
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充,如果传递NULL,表示不关心子进程的退出状态信息
-
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
-
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
-
0-7比特位表示的是异常的信号,因为异常信号是从1开始的,如果0-7为0就说明程序是正常运行结束的,如果为其他数字,那么就表示异常终止,此时前9-15比特位没有使用
-
8比特位是core dump标志
-
9-15比特位表示的是退出状态,如果为0就说明结果正确,不为0表示不同的出错信息(前提是0-7比特位必须都为0)
代码测试:
2.4非阻塞轮询
- 子进程还没退出,父进程调用wait( )或者waitpid( ),就会处于阻塞状态,不会返回,这种等待方式叫作阻塞等待,父进程在此期间不做任何事情,一直等待子进程的退出
- 除此之外,还有一种等待方式叫作非阻塞轮询等待,父进程在等待子进程退出的期间可以去完成自己的任务--while循环+非阻塞
- 非阻塞轮询等待要使用一个参数:WNOHANG:--若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,则返回该子进程的id
代码测试:
实现在父进程在等待子进程退出的时候完成相应的任务:
21 #define TASK_NUM 10
22
23 typedef void(*task_t)();
24 task_t tasks[TASK_NUM];
25
26 void task1()
27 {
28 printf("这是一个执行打印日志的任务, pid: %d\n", getpid());
29 }
30
31 void task2()
32 {
33 printf("这是一个执行检测网络健康状态的一个任务, pid: %d\n", getpid());
34 }
35
36 void task3()
37 {
38 printf("这是一个进行绘制图形界面的任务, pid: %d\n", getpid());
39 }
40
41 int AddTask(task_t t);
42
43 // 任务的管理代码
44 void InitTask()
45 {
46 for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
47 AddTask(task1);
48 AddTask(task2);
49 AddTask(task3);
50 }
51
52 int AddTask(task_t t)
53 {
54 int pos = 0;
55 for(; pos < TASK_NUM; pos++) {
56 if(!tasks[pos]) break;
57 }
58 if(pos == TASK_NUM) return -1;
59 tasks[pos] = t;
60 return 0;
61 }
62
63 void DelTask()
64 {}
65
66 void CheckTask()
67 {}
68
69 void UpdateTask()
70 {}
71
72 void ExecuteTask()
73 {
74 for(int i = 0; i < TASK_NUM; i++)
75 {
76 if(!tasks[i]) continue;
77 tasks[i]();
78 }
79 }
2.5进程等待的底层原理
-
父进程要拿到子进程的退出信息,拿到子进程的任意数据,为什么要使用系统调用呢?因为进程具有独立性!
-
父进程不能直接拿到子进程的数据!因为父进程浅显来说是运行用户自己写的代码,任何用户都不能直接访问操作系统内的数据,访问操作系统内的数据都必须通过系统调用!这就要求父进程要拿到子进程的退出信息必须使用wait或waitpid系统调用接口
-
子进程退出的时候,对应的代码和数据会被释放,但会把自己的sig code和exit code保存在pcb数据结构对象里,通过位运算将两者整合放到参数statue里后,pcb数据结构才会被释放销毁,然后通过wait或waitpid被调用者(父进程)拿到子进程的退出信息
三、进程程序替换
3.1单进程程序替换
代码测试:
- 程序运行结果并没有执行第二句printf,这是因为在进行程序替换的时候,调用exec后会把该进程的用户空间代码和数据完全被新程序替换,从新程序的启动,进程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
- 执行程序的第一个步骤要先找到程序,exec的第一个参数决定了如何找到这个程序
- 找到该程序后,就要判断如何执行该程序!需要涵盖哪些选项就传哪些选项
- 单进程的程序替换,操作系统会根据程序的路径在磁盘中找到该程序所对应的代码和数据,再加载到内存中,对原代码和数据进行覆盖
- 程序替换成功后序代码不再执行,exec只有失败的返回值,没有成功的返回值,替换失败继续执行后序的代码
- Linux中形成的可执行程序是有ELF格式的,ELF中存在一个表,可执行程序的入口就存在这个表头里,当一个程序被运行起来,他的代码和数据不一定立马被加载到内存中,但是表头里的可执行程序的入口一定要先被加载到内存中,程序执行从该入口开始执行
- 环境变量也是一份数据,环境变量在子进程创建的时候,就已经被子进程继承下来了,程序替换不会替换环境变量的信息
3.2多进程程序替换--验证各种程序替换接口
- 替换原理
- 替换方法
-
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[]);
-
l(list) : 表示参数采用列表
-
v(vector) : 参数用数组
-
p(path) : 有p自动搜索环境变量PATH
-
e(env) : 表示自己维护环境变量
代码示例:
execl前面已经演示过,这边就不再重复演示
execlp( ):l选项表示选项参数采用列表的形式,p选项表示操作系统在找该程序时会自动到环境变量PATH里去搜索 ,命令行参数表默认以NULL结尾,所以传选项参数时也要以NULL结尾
execle( ):e选项表示自己传入环境变量
- 如果不传环境变量,子进程默认也有一份从父进程继承下来的环境变量
- 如果传入环境变量,可以直接传char** environ
- 如果要改变环境变量,有两种方式:
- putenv给父进程的地址空间中导入新的环境变量
- 调用含e选项的函数接口,传入自己定义一份环境变量表,覆盖子进程原有的环境变量表
代码示例:
execl可以执行系统的命令,也可以执行自己的命令
创建一个otherExe.cpp文件来打印子进程的命令行参数表和环境变量表
#include <iostream>
using namespace std;
int main(int argc, char *argv[], char *env[])
{
cout << argv[0] << " begin running" << endl;
cout << "这是命令行参数: \n";
for(int i=0; argv[i]; i++)
{
cout << i << " : " << argv[i] << endl;
}
cout << "这是环境变量信息: \n";
for(int i = 0; env[i]; i++)
{
cout << i << " : " << env[i] << endl;
}
cout << argv[0] << " stop running" << endl;
return 0;
}
不传环境变量表:
- 可以看到,execl会把对应的选项参数当成命令行参数表传递给新替换的程序
传环境变量表并putenv:
覆盖替换子进程的环境变量表:
execv( ):v表示采用数组的形式(命令行参数表)传入选项
execvp( ):表示自动到PATH里搜索程序路径,并且采用数组的形式传入选项
execvpe( ):采用数组形式,从PATH中搜索路径,并可以自己传入环境变量(e选项和execle用法类似)
3.3 系统调用execve( )
-
只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册第2节,其它函数(库函数)在 man手册第3节,这些函数之间的关系如下图所示:
- 各个库函数之间的区别只是传参的不同,他们都是对系统函数execve( )的封装
-
一个C程序可以fork/exec另一个程序,并传给它一些参数,这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait来获取exit的返回值。