进程调用fork,当控制转移到内核中的fork代码后,内核做 :
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定 !
因为fork之前只有父进程,fork之后有两个进程!
那么fork之后是否只有fork之后的代码是被父子进程共享的??
我们之前认识到进程具有独立性:代码和数据必须是独立的!我们针对数据给了写时拷贝的方案,代码是只读的,所以只能共享。
一般情况,fork之后父子共享所有的代码!!
子进程执行的后续代码 != 共享的所有代码,只不过子进程只能从这里开始执行!!
为什么?
CPU有一个寄存器叫eip:程序计数器,保存当前正在执行指令的下一条指令!在某些教材也叫pc指针。eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码出开始执行啦!!
但是不代表子进程不能从头开始,子进程可以修改eip的值从main函数开始,就能从头开始执行代码!
进程 = 内核的进程数据结构 + 进程的代码和数据
通过创建子进程的内核数据结构(struct task_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进程共享或者独立!!
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图
父进程创建子进程之后,父子都有虚拟地址空间和页表,只不过子进程的各种隐射关系全部继承自父进程,所以父子的代码是共享的!
但是页表里面还会包含读写属性的设置,写时拷贝的底层实现其实就是在创建子进程之后,把父子进程的页表隐射关系全部设置为只读的。
所以读取的时候就支持读,当你写入时(当然也会判断写入的内存区域是代码还是数据)。比如说子进程想写入一份数据,父进程没有写入,所以父进程不管,依旧指向自己的代码和数据。而子进程代码是共享的,所以和父进程指向的代码区域是一样的!因为子进程想写入数据,所以操作系统会把数据拷贝到另一个区域,在此区域修改!有一个细节页表数据的只读属性被去掉了变成了正常属性,最后操作系统再重新修复父子进程页表的映射关系!
写时拷贝本身就是由OS的内存管理模块完成的!所以我们平常感知不到!
我们知道进程总要保持它的独立性,那么创建子进程的时候就把数据分开不行吗?
所以最终采用写时拷贝:只会拷贝父子修改的,变相的,就是拷贝数据的最小成本,但是拷贝的成本依旧存在。但是写时拷贝是一种延迟拷贝策略!只有真正使用的时候才给你!你想要,但是不立马使用的空间,先不给你,那么也就意味着可以先给别人!
C/C++的时候,main是程序的入口函数,且通常最后我们都要写一句return 0.
常见进程退出:
所以进程代码跑完,结果是否正确:
0:表示success,非0:表示失败。最想知道的是失败的原因!所以:非0标识不同的原因!return x其实是进程退出码!表征进程退出的信息,让父进程读取的,以便对处于僵尸态的子进程进行处理!
证明:
echo $?
这条命令表示在bash中最近一次执行完毕时对应进程的退出码!
假设我们自己的程序里返回123,第一次就是123,第二次是0的原因是echo本身这个程序执行成功了!
一般而言,失败的非零值我该如何设置呢?以及默认表达的含义?
非零值可以自定义,错误码/退出码可以对应不同的错误原因,方便定位问题!
_exit()
,它和exit()很像,其实就是exit()调用了_exit()。exit和_exit的区别:
exit:一秒之后数据就被刷新出来了,因为我们打印的时候没有加\n,数据还在缓冲区。程序退出后数据才被刷新出来。
_exit
:我们唯一做的就是把exit换成_exit.我们发现程序退出数据并没有被
刷新出来。
结论:
exit终止进程,刷新缓冲区
_exit直接终止进程,不会有任何刷新操作
进程 = 内核结构 + 进程代码 和 数据
当一个进程退出时会先进入僵尸态,然后父进程会去等待它,回收子进程信息,说白了就是读取子进程的退出码!然后再将子进程设置为x状态,就可以是否子进程的内核结构,释放加载到内存的进程代码和数据。
代码和数据必定会释放掉,内核结构(task_struct和mm_struct)操作系统可能不会释放。
我们知道创建对象要先开辟空间,再初始化。废弃的内核结构的数据已经被释放了,但是内核结构开辟的空间还在,当你再次创建进程时,会把相应的task_struct和mm_struct重新初始化,就节省了重新开辟空间的消耗!
内核的数据结构缓冲池,slab分派器。
父进程调用wait就可以了,一个简单的样例:
我们在40s内杀掉子进程,此时子进程就处于z状态,40s之后我们一旦等待成功了,子进程的z状态就没了。
下面我们再写一段监控命令行脚本:
while : ; do ps ajx | head -1 && ps ajx |grep mypro |grep -v 'grep\|worker\|master\|cache'; sleep 1; echo "################################################################"; done
运行结果:
现在我们知道了,我们可以通过wait()的方案解决回收子进程z状态,让子进程进入x
因为x太快了所以我们什么也看不到!
#include
#include
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int optinos);
pid_t:
>0:等待子进程成功,返回值就是子进程的pid
<0:等待失败
pid:
>0:是几就等待几号子进程,指定等待
-1:等待任意进程
options:
0:阻塞等待
*int status是一个输出型参数,wait/waitpid()是系统调用!!!通过调用该函数从函数内部拿出特定的数据,这里的内部是指从task_struct中拿出子进程退出的退出码!!
int *status这个整数其实是被当作位图在用,我们只关心它的低16位。我们通过这个整数的次低8位就能拿到子进程的退出码。1-7比特位可以拿到程序异常退出的终止信号。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
所以查看退出码还可以这样写:
if(WIFEXITEL(status))
{
printf("子进程是正常退出的,退出码:%d\n",WEXITSTATUS(status));
}
为什么不能定义一个全局变量code,子进程退出的时候就把code设置好特定的值,然后父进程回收的时候直接拿code的数据呢?
绝对不可以,因为会发生写时拷贝。
一个子进程既有退出码:0,又有退出信号:11.
那我先看谁?
常见进程退出:
退出码对应的是前两种情况,退出信号是第3种情况!
程序正常跑完,只关心退出码。一旦进程出现异常,只关心退出信号,退出码没有任何意义!
如果子进程就是不退出(如死循环),怎么办呢?我的父进程只能阻塞等待。
当我调用某些函数的时候,因为条件不就行,需要我们阻塞等待,本质:就是当前进程自己变成阻塞状态,等条件就绪的时候再被唤醒!
我们今天等待的资源就不是硬件了,而是软件。一个进程在等另一个进程!
退出码对应的是前两种情况,退出信号是第3种情况!
程序正常跑完,只关心退出码。一旦进程出现异常,只关心退出信号,退出码没有任何意义!