目录
进程创建
在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。


进程调⽤fork,创建子进程,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给⼦进程。
- 将⽗进程部分数据结构内容拷⻉⾄⼦进程。
- 添加⼦进程到系统进程列表当中。
- fork返回,开始调度器调度。
fork之前,父进程独立执行,fork之后,父子进程分别执行后续代码,也就是说代码是共享的。
而谁先执行完,由调度器决定。
fork的返回值:子进程返回0,父进程返回子进程的pid。
写时拷贝
通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅ 式各⾃⼀份副本。

因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证! 写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率。
fork常用场景
- ⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求, ⽣成⼦进程来处理请求。
- ⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际⽤⼾的进程数超过了限制
进程终止
进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出场景
- 代码运⾏完毕,结果正确
- 代码运⾏完毕,结果不正确
- 代码异常终⽌
有这三种场景,我们在平常肯定需要去查看我们进程运行结束的结果。
所以有了进程退出码,我们可以查看某个进程结束之后的退出码,即可查看进程是否运行符合预期。但是一旦异常了,退出码就没有意义!
进程结束了,会将自己的退出码存在task_struct中,最终这个退出码会返回给父进程,我们之前说过,父进程要知道子进程的退出信息!而退出码实际上都代表着子进程的退出结束信息。
退出码
退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。 代码 1 或 0 以外的任何代码都被视为不成功。
大小一个字节。
Linux Shell 中的主要退出码:

- 退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
- 退出码 1 我们也可以将其解释为"不被允许的操作"。例如在没有sudo权限的情况下使⽤ yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 a=1/0
- 130 ( SIGINT 或 ^C )和 143等终⽌信号是⾮常典型的,它们属于128+n 信号,其中 n 代表终⽌码。
- 可以使⽤strerror函数来获取退出码对应的描述。
我们需要理解一些:
main函数结束,进程结束,而其他函数结束,只是代表函数调用结束,然后返回main函数。
而一旦代码异常(直接退出),退出码也就无意义了。
我们尝试打印一下退出码信息:

我们可以看到0代表成功,这也就是为什么我们每次main函数结尾都要写上return 0的原因!
当进程结束我们可以查看它的退出码:

echo $? 这个命令它会打印最进的那个进程的退出码。
_exit函数

- 说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现 返回值是255。
注意:这是一个系统调用。

在这里的作用相当于return 0,但其实这两者的区别还是有的。
return是⼀种更常⻅的退出进程⽅法。执⾏returnn等同于执⾏exit(n),因为调⽤main的运⾏时函数会 将main的返回值当做exit的参数。

我们看到只打印了x字符串,却没打印y字符串,进程就直接退出了,这里也就显现了和return 的区别,不管任何地方使用了_exit函数,就代表进程结束,不会执行后续代码!
exit函数

图:

我们看到_exit和exit相同的结果,但其实还是有区别的:

我们之前也说过,当printf中没带\n时,缓冲区不会刷新,也就不会打印出来,只有进程结束才能看到打印情况,现在看这两种情况,我们可以得出:

区别:
按理来说只有OS能决定进程退出与否,而库函数却能退出函数为什么?
_exit是系统提供的,也就是说是系统调用,exit是C语言提供的,但它的底层调用了_exit函数,也就是说封装了系统调用。
所以我们可以得出我们谈论的缓冲区是在哪里呢?
它一定不在OS内部,而是库缓冲区,是C语言提供的缓冲区!
后续再仔细讲解缓冲区。
exit最后也会调⽤_exit,但在调⽤_exit之前,还做了其他⼯作:
- 执⾏⽤⼾通过atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写⼊
- 调⽤_exit

进程等待
进程等待必要性
- 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存 泄漏。
- 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的kill-9也⽆能为⼒,因为谁也 没有办法杀死⼀个已经死去的进程。
- 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是 不对,或者是否正常退出。
- ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息
进程等待方法
wait方法

用法:
status是输出型参数,当我们需要得到子进程的退出码信息时,就可以传参进去,如果不关心子进程退出状态,可以设置成NULL.
waitpid方法

- 如果⼦进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得⼦ 进程退出信息。
- 如果在任意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则进程可能阻塞。
- 如果不存在该⼦进程,则⽴即出错返回。
现在一步一步讲解这个函数。
参数pid ,我们可以将需要等待的某个子进程的pid传进去,表示等待这个子进程,传 -1 表示等待任意一个子进程,和wait等效。
参数status用法和wait函数一样。
参数 option 表示等待类型,阻塞等待和非阻塞等待的区别。
获取⼦进程status
- wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
- 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
- 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
status不能简单的当作整形来看待,可以当作位图来看待(32位比特位),这也就是为什么上面程序要调用WEXISTATUS函数才能获得真正的子进程的退出状态信息。

如果我们需要拿到status的退出码信息,我们需要进行位运算或者调用函数获得!
如:

需要得到子进程退出码,我们就要得到status那8个比特位表示的整数值,(按照上图)所以先右移动8位,再按位与上0xFF(8个比特位都是1,换算成16进制就是0xFF),这样就能得到子进程退出码!
当然也可以调用函数拿到子进程退出码。
异常例子:


注意:
如果子进程没有异常退出,低7个比特位,为0,一旦低7个比特位不为0,就是进程异常退出(代码没跑完,所以退出码无意义)。
阻塞和非阻塞等待
wait是默认阻塞等待,而waitpid可以利用传options参数来选择阻塞等待还是非阻塞等待。
区别:
阻塞等待就是父进程会阻塞在wait调用处,一直等待子进程退出,就像scanf一样,命令行一直阻塞在scanf这里。而非阻塞等待是父进程等待子进程的过程中,依然可以做自己的事情,也就是父子并发,非阻塞调用往往效率高一点!
例子:
1.调用waitpid时,非阻塞等待返回值和阻塞等待的返回值不同,非阻塞等待时,waitpid返回值大于0时,等待结束(返回值是子进程pid),返回值等于0,调用结束,但子进程没有退出,返回值小于0时,等待失败。
2.同时传options参数(WNOHANG),表示非阻塞等待,在等待期间,根据waitpid的返回值就可以获取每一时刻子进程的执行情况!所以注定父进程需要是个循环完成!
3.从返回值就能看出,阻塞等待是,waitpid返回值大于0(返回子进程pid),就意味着等待成功,子进程已经执行完毕(异常除外,异常也是返回子进程pid,当然也大于0,但是子进程不一定执行完毕),小于0,代表等待失败。



结果:

发现父子进程一起执行!
好了,我们下期见!