1.进程创建
1.1.fork函数
在linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。#include <unistd.h> pid_t fork(void); 返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中fork返回,开始调度器调度当一个进程调用 fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程。代码如下图一所示,运行结果如下图二所示,这里看到了三行输出,第一行是执行的fork函数之前父进程的打印代码,第二行是执行的fork函数之后父进程的打印代码,第三行是执行的fork函数之后子进程的打印代码。可以得出结论, fork 之前父进程独立执行, fork 之后,父子两个执行流分别执行,如下图三所示。注意, fork 之后,谁先执行完全由调度器决定。问题:对于父子进程的代码部分,是否只有fork之后的代码是被父子进程共享的?
答:我们前面提到进程具有独立性,也就是说进程的代码和数据必须独立,数据的独立我们上一个博客讲过是依靠写时拷贝实现的,代码部分因为是只读的,所以可以默认代码是独立的。一般情况下,fork之后父子共享所有的代码,子进程执行的后续代码不等于共享的所有代码,只不过子进程只能从这里开始执行。
CPU的寄存器中有一个eip寄存器,该寄存器一般被称为程序计数器或pc指针,其功能是保存当前正在执行指令的下一条指令。fork之后,eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行。
问题:fork之后,操作系统做了什么?
答:进程=内核的进程数据结构+进程的代码和数据,fork之后操作系统创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表) + 代码继承父进程以共享的方式,数据以写时拷贝的方式,操作系统通过这些操作保证了不同进程之间的独立性。
1.2.写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:注:
1.父子进程的页表都是只读的,当子进程修改内容之后,数据段所对应的父子进程的页表部分会将只读属性去掉,对子进程数据段对应页表部分进行修改。
2.写时拷贝是由操作系统的内存管理模块完成的。问题:为什么要写时拷贝?创建子进程的时候就把数据分开不行吗?原因一:父进程的数据,子进程不一定全用,即便使用也不一定全部写入,因此创建子进程的时候就把数据分开会有浪费空间的嫌疑。原因二:最理想的情况,会被父子修改的数据提前进行分离拷贝,不需要修改的共享即可,但是从技术角度实现复杂,因为代码不跑很难知道要修改哪些变量。原因三:如果fork的时候就无脑拷贝数据给子进程,会增加fork的成本(内存和时间)根据以上三个原因,所以最终采用写时拷贝,写时拷贝只会拷贝父子修改的,其实就是拷贝数据的最小成本,并且写时拷贝本质是一种延迟拷贝策略,只有真正使用的时候才开空间进行拷贝,变相的提高了内存的使用率。
1.3.fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.4.fork调用失败的原因
系统中有太多的进程。实际用户的进程数超过了限制。子进程创建失败的例子:代码如下图一所示,利用for循环不停的让父进程fork创建子进程,对于父进程来说,如果id小于0说明子进程创建失败退出循环,如果id大于0则说明子进程创建成功循环继续。对于子进程来说,id等于0子进程创建成功,使用sleep函数和exit函数使得子进程持续两秒就退出,因此子进程不会循环fork。如下图二所示,在输出结果中有子进程创建失败的情况。
2.进程终止
2.1.进程终止的正确认识
问题:在c/c++中,main函数可以认为是入口函数,函数最后都要return或return 0,那么return和return 0是给谁return?一定要return数值0吗,其他值可以吗?
答:常见进程退出有三种情况,第一种代码跑完结果正确,第二种代码跑完结果不正确,第三种代码没跑完程序异常了。main函数的return返回值一般叫做进程退出码,如果return返回值为0代表代码跑完结果正确,如果return返回值为非0代表代码跑完结果不正确,使用不同的非0值代表不同的错误原因。
进程退出码表征了进程退出的信息,前面我们讲过进程退出要进入僵尸状态,僵尸状态的进程必须要被父进程或操作系统回收,读取其退出信息之后进程才会进入x状态释放,因此进程退出码很重要,其是会被父进程或操作系统读取的。因此return返回是给父进程或操作系统return的。
代码如下图一所示,运行该代码,该代码对应进程的父进程是bash进程,此时连续两次使用echo $?命令,如 下图二所示,可以看到第一次运行结果打印123,第二次运行结果打印0。echo $?的功能是打印bash进程中最近一次子进程执行完毕时对应进程的退出码,因此第一次echo $?打印的是下图一代码的退出码,第二次echo $?打印的是第一次echo $?命令的退出码。
系统中的命令如果执行失败,那么该命令的退出码也是非0的,如下图所示。
问题:一般而言,失败的非零值应该如何设置呢?各种非零值默认表达的含义是什么?
答:我们之前学过strerror函数,其功能是将一个错误码转为错误码描述,使用下图一所示的代码,打印1-100错误码对应的错误码描述,打印结果如下图二所示。
从打印结果可以看到0代表成功,1代表权限不允许等等。要注意这里打印的是c语言规定自己的错误码标准,其他语言或系统不一定遵守(Linux操作系统就没有遵守)。
2.2.进程终止的常见做法
方法一:
在main函数中return,进程退出。
注:只有在main函数中return才是进程退出,在其他函数中return不代表进程退出,非main函数return代表函数调用结束。
方法二:
在代码的任意地点中调用exit函数,进程退出。exit函数后面括号中的内容就是退出码,对应main函数return的值。
注:
1.main函数和非main函数调用exit都可以让进程退出。
2.使用exit需要包含<stdlib.h>头文件。
代码如下图一所示,运行该代码,然后使用echo $?命令,打印的结果为111,如下图二所示。退出码为111说明该进程代码没有执行完,在函数fun中就将该进程终止了。
exit和_exit:
_exit和exit功能基本相同,二者的关系是调用和被调用的关系,exit的实现中调用了_exit。
_exit和exit唯一的区别是:exit中止进程并且刷新缓冲区,_exit只中止进程没有任何刷新操作。
exit测试代码如下图一所示,运行结果如下图二所示,运行结果为先停顿一秒然后显示hello word。_exit测试代码如下图三所示,运行结果如下图四所示,运行结果仅为停顿一秒。因此可以看出exit函数刷新了缓冲区而_exit函数没有刷新缓冲区。
注:
1._exit是系统函数,exit是c语言的函数。
2.使用_exit需要包含<unistd>头文件。
2.3.Linux内核对于进程终止的操作
进程=内核结构(task_struct、mm_struct)+进程代码和数据
当一个进程终止时,进程进入z状态,父进程或操作系统读取其退出码等信息,然后将该进程设置为x状态,等待释放其内核结构以及代码和数据。
实际上在进程释放的时候,操作系统一定会将进程的代码和数据部分释放掉,进程的内核结构部分(task_struct、mm_struct)不一定会被释放。创建对象首先要开辟空间,然后要进行初始化,这两步都要花时间,因此重新创建一个进程内核结构部分要花费时间,在Linux中会维护一个废弃的数据结构链表,将要释放的内核结构挂在链表中,当需要创建内核结构对象时,在该链表中拿出来一个然后进行初始化工作即可,节省了开辟空间的开销。这里提到的废弃的数据结构链表就是内核数据结构缓冲池或slab分派器。
3.进程等待
3.1.进程等待的原因
问题:为什么要进程等待?
原因一:解决僵尸进程的内存泄漏问题子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题, 进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程,这样就会造成内存泄漏。原因二:获取子进程的退出状态父进程派给子进程的任务完成的如何我们需要知道。子进程运行完成,结果对还是不对,或者是否正常退出,父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息(退出码等)。
3.2.进程等待的常见做法
方法一:
父进程中调用wait函数,获取子进程退出信息,并将其子进程由僵尸状态转为释放状态。wait函数如果等待成功则返回等待子进程的pid,如果等待失败则返回小于0的值。
代码如下图一所示,运行代码的同时使用while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; echo "-----------------------------------------------------------------"; sleep 1; done命令作为监控脚本,监控进程状态,代码运行情况如下图二所示。
父进程运行后等待40秒,在这40秒内子进程本应一直处于S运行状态,但在这40秒内我们使用kill -9 命令将子进程杀掉,子进程由S运行状态变为Z僵尸状态,40秒之后父进程调用wait函数获取子进程退出信息并将子进程由z僵尸状态转为释放状态进行释放,此时只剩下父进程,父进程10秒后打印等待成功和wait函数返回的等待的子进程pid,最后父进程退出。
注:
1.wait函数是一个系统调用接口,需要包含<sys/types.h>和<sys/wait.h>头文件。
方法二: