1. 进程创建
fork()函数创建一个进程:
- 父进程返回子进程的pid
- 子进程返回0
创建进程后,我们希望子进程做的事有两种:
- 子进程帮父进程完成同样的工作
- 子进程干其他任务
进程 = 内核的相关管理数据结构(tack_struct + mm_struct + 页表) + 代码和数据
由于进程具有独立性,父子进程的内核数据结构各自独有一份;代码是可读的,父子共享;数据根据写时拷贝,分配给子进程
fork创建子进程其实是:
- 分配新的内存块和内核数据结构给子进程
- 将父进程的内核数据结构拷贝给子进程
- 将子进程添加到系统进程列表当中
- fork函数返回
一个进程,在内存中,先有它的内核数据结构,再有它的数据和代码
2. 进程终止
进程终止是在做什么?
- 释放掉代码和数据占据的内存空间
- 释放掉内核数据结构
进程终止的三种情况:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码没跑完,中途出现异常
main 函数中,return 的值实际上是进程的退出码,交给父进程;父进程根据退出码判断进程结果正不正确,为0表示正确,!0表示不正确;我们可以自己设置,也可以直接使用系统的
使用 echo $? 查看最近一次进程的退出码
父进程为什么要得到子进程的退出码?
父进程创建子进程是为了帮自己完成任务,子进程任务结束,父进程就得知道任务的结果是怎样的;要知道子进程退出的情况(成功或失败,失败的原因是什么)
当然,进程也有可能中途出现异常,提前退出;比较常见的情况是,用vs编译代码时,程序崩溃了,原因是OS发现你做了不该做的事,OS杀掉了进程
进程出异常,本质是进程收到了OS发出的信号;进程异常退出的时候,可以查看退出信号,就能知道异常的原因是什么
- 进程终止时,只要查看两个变量,退出码和退出信号就能知道进程终止的原因
- 判断一个进程退出是否正常,先看退出信号,再看退出码
进程终止时,会设置自身的退出码和退出信息,等待父进程读取
如何终止进程?
- return:main函数 return 表示进程退出,函数 return 表示函数退出
- exit函数(库函数):在程序的任意位置使用 exit 函数,都表示进程退出
- _exit函数(系统接口):与exit函数同理
exit 与 _exit 不同的是,前者会冲刷缓冲区,后者不会;也告诉了我们,缓冲区在系统层之上,因为 exit 底层也是调用 _exit
3. 进程等待
任何子进程,在退出前,都要被父进程等待;子进程退出后进入Z状态,如果父进程不管不顾,会有内存泄漏
父进程为什么要等待?
- 父进程通过等待,解决子进程的僵尸问题,回收系统资源,防止内存泄漏
- 获取子进程的退出码和退出信号,知道子进程为什么退出
父进程如何等待?
子进程的退出码和退出信号,最总还要我们进行位操作才能拿到,太麻烦了,有没有简单的方式拿到子进程的退出码和退出信号?
使用宏WITEXITED(status)
来判断进程是正常退出还是异常退出;如果为真,则正常退出,使用宏WEXITSTATUS(status)
获取退出码;如果为假,则异常退出,退出信号我们自己获取
如果子进程一直没有退出,父进程就一直在阻塞等待,第一,我们***如何理解这种阻塞等待?***第二,能不能让父进程在等待的期间干其他事,提高效率?
-
子进程本身是软件,父进程本质是在等待某种软件条件就绪,会将
task_struct
插入到子进程的等待队列中 -
将
pid_t waitpid(pid_t, int* status, int options)
的第三个参数设置为WNOHANG
,表示非阻塞等待rid > 0
:等待成功rid == 0
:子进程还在运行,需要下一次来等待rid < 0
:等待失败
4. 进程程序替换
现象:
- 进程执行了
ls -la
操作 execl()
函数之后的代码不再执行
出现以上现象的原因是:exec*
系列的函数将当前程序替换成指定的程序了
4.1 进程程序替换的原理
进程的程序替换的本质是将该进程的代码和数据替换为指定程序的代码和数据,在替换期间没有创建进程
从被替换者的角度看,就是这个程序加载到内存当中了;我们之前说程序被运行之前需要加载到内存当中,是怎么加载的呢?其实是用加载函数完成的,而execl*
系列的函数就是Linux中的加载函数
4.2 exec*
系列函数
int execl(const char* path, const char* arg, ...);
int execv(const char* path, char* const argv[]);
int execlp(const char* file, const char* arg, ...);
int execvp(const char* file, char* const argv[]);
int execle(const char* path, const char* arg, ..., char* const envp[]);
int execvpe(const char* file, char* const argv[], char* const envp[]);
之前我们说,我们希望创建子进程干两种事情:
- 让子进程执行父进程代码的一部分
- 让子进程执行一个全新的程序
如果让子进程执行一个新的程序,按照之前的知识,父子进程共用同一份代码,数据按照写时拷贝的方式相互独立,但此时新程序有自己的代码,不可能跟父进程共用代码;因此,程序替换会将子进程的代码和数据都写时拷贝一份,再去替换子进程的代码和数据
知道了如何使用exec*
系列函数替换进程程序,就能创建子进程,然后替换,让子进程完成其他程序的工作;这里的程序可以是系统内的,也可以是我们自己的程序
我们也可以自定义命令行参数和环境变量传给新程序,也可以使用bash
中的命令行参数和环境变量
由此我们能得知,bash
是如何给子进程传递自身建立的命令行参数表和环境变量表,其原理都是一样的