- 作者: 雪山肥鱼
- 时间:20210530 10:01
- 目的:多进程模式,subreaper,main函数、exit vs _exit, 动态链接库的构造函数和析构函数等
# 多进程解决的问题
## 进程的生命周期初探
### 举例
### 多进程服务的关键点
### main函数
## 关于进程的背景问题
## 进程之间的通信
### compositor 与 其他进程通信举例
## 进程之间的界限
## 多进程与多线程
# 进程生命周期
## fork
## init vs subreaper
## exec 举例
## fork vs vfork
# main() 函数 之前与退出究竟在干什么
# main进出的钩子函数与 Asan 初步
多进程解决生命问题
- 解决进程的生命周期问题(init, systemd
- 解决进程背景的问题(exec)
- 解决进程之间的通信问题(IPC)
- 设计进程与进程之间的界限
进程的生命周期初探
举例:
- init 进程启动了一个 httpd的server,很多人会访问。
- httpd 凌晨服务挂了,公司的IT人员不可能半夜去把httpd服务重启,况且,也不知道httpd什么时候会挂。
- 在多进程模型中,这种关键的服务死掉,谁应该被通知,被通知后采取什么行为,如何处理这种退出的情况是多进程服务的关键点。
多进程服务的关键点
另一类问题,父进程挂了后,子进程自己也挂,不久完事儿了?为什么要再找一个父进程呢?
这要看你程序员怎么设计了,不是kernel关心的,而是自己本身实现策略所关心的。
比如 deamon() 这个api,父进程创建出来子进程后,就自杀了,故意托孤,让子进程去找新的父进程。
如何处理进程的退出,即设计的系统如何去响应子进程不同的退出情况,是多进程生命周期里面关心的。
main函数
进入main 函数之前 和之后,都做了不少事情,后续会有阐述。
关于进程的背景问题
linux的进程很灵活,在运行期间会更改自己的程序背景。这就用到了 exec系列 函数
进程之间的通信
进程是资源封装的单位,进程之间是看不到彼此的资源的,但彼此之间又要通信。
举例 :桌面 compositor 系统
桌面是可以开很多的apps。
这些apps是不可能直接去写显示器的,是由compositor(合成器)直面LED显示屏
![](https://img.haomeiwen.com/i25953572/b360c68dbf95bf11.png)
很明显这里采用多进程模型。不可能采用多线程模型,资源都混在一起后,一旦其中一个线程出现bug,其他全都死了。
比如记事本显示在屏幕上,并不是记事本这个app 在显存上掏个洞,把自己画进去(你也画,我也画,则全部乱套)。
记事本等app肯定有管理窗口的一套buffers。每一个app 会告诉 compositor自己的位置坐标,通过进程间的通讯方式,将数据(画在buffers中的)告知给Compositor,由Compositor 画到 LED 屏幕上。(不可能直接画在屏幕上)
涉及到的通讯
- 记事本 告诉 compositor, 我的坐标是100x200, buffer的大小是400x300
- compositor收到消息后,帮记事本画了一个框框,并通知记事本画好了。(比如会采用socket 通讯)
- 记事本需要buffers 的内容 要传递给 compositor, 这里不可能用 socket去send 和 receive, 因为涉及到了内存拷贝。
- 通过共享内存的通信机制,让客户端的内存 和 服务器的内存共享, compositor直接拿到内存后,掉openGL,叠加画到LED的显示器上。
进程之间的界限
每个进程单职性,要看懂每个进程到底要做什么。
要合适的拆分进程模型,不要把所有的进程都揉合在一起,这一坨,那一坨,bug 会在 每个进程中乱窜。
多进程与多线程优劣
其实没有哪个更好,就像无法回答是男人好,还是女人好。是否采用多进程和多线程模型是根据你的业务场景和实际需求而定的。
- 多进程
资源相互依赖不那么紧密 - 多线程
资源依赖紧密,比如音乐播放器,需要多个线程工作,比如播放歌词,播放歌曲,显示歌词等,所需要的资源都在一个进程中。
进程生命周期
fork
![](https://img.haomeiwen.com/i25953572/7431b8b6dcb31e3d.png)
fork后所创建出来的p2 进程,如果挂了,p1进程可能会重启 p2,变成p3 进程。p2 与 p3 具有相同的程序背景
当然fork 也是有可能创建进程失败的,比如 pid 用完了,身份证用完了。
init vs. subreaper
![](https://img.haomeiwen.com/i25953572/5d8ecfd31bb92390.png)
- p2 死了 p3 直接挂在p1下
- p4 死了,p5 挂在了 subreaper下
systemd 是 init的符号链接。
systemd/init 在linux里有两层level
- system level 系统级
- user level 用户级 也有一个systemd。
if(!arg_system) //系统级
/*becom reaper of our children*/
if(prctl(PR_SET_CHILD_SUBREAPER, 1) < 0)
log_warning_errno(errno, "Failed to make us a subreaper: %m" );
服务的管理者,才会变为 subreaper
pidof a.out// 可以查看a.out 的pid 和 ppid
2945, 2944 //后面的是父进程
exec 系列 api
linux 的 进程和程序组合是非常自由的。调用exec,可以将进程的程序背景换掉。
![](https://img.haomeiwen.com/i25953572/eeddb976a51d56a6.png)
int main(int argc, char ** argv)
{
if(argc<2) {
printf("Usage: %s path\n", argv[0]);
return 1;
}
execlp("/bin/ls", "ls", argv[1], char(*)NULL);
}
}
windows 类似的是 CreateProcess API,遇到再来学习吧。
exec 不是创建进程, 而是改变进程的程序背景!
fork vs vfork
![](https://img.haomeiwen.com/i25953572/d3c3c6ed5de62943.png)
fork 是 对拷 =》 COW
vfork 共享一个内存资源 不执行COW
前面文章已有阐述
https://www.jianshu.com/p/d687574fd8a5
long do_fork(..) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current , p);
if(clone_flags & CLONE_VFORK) {
p->vfrok_done = &vfork;
int_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
if(clone_flags & CLONE_VFORK) {
if(!wait_for_vfork_done(p, &vfork)
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
补充:vfork 是 阻塞式的
父进程会阻塞,直到子进程vfork_done, 等到vfork 阻塞。
int data = 10;
int child_process()
{
printf("Child process %d, data %d\n", getpid(), data);
data = 20;
printf("Child prcess %d, data %d\n", getpid(), data);
//_exit(0);
while(1);
}
int main(int argc, char ** argv)
{
if(vfork() == 0) {
child_process();
}
else {
sleep(1);
printf("Parent process :%d, data %d\n", getpid(), data);
}
}
子进程不结束,父进程不往下走了。父进程没有调用 waitpid 呀。
kill 子进程,父进程则会继续输出。
mm_release 的时候 会处理 vfork_done.
![](https://img.haomeiwen.com/i25953572/42bc8238cc081d6f.png)
vfork_done:
- 子进程正常结束
- 子进程挂掉
- 子进程调用exec,子进程会释放共享的mm. 因为子进程不会用父进程任何代码段和内存了,自己重新 alloc一份 mm_struct. p1 的内存改动 和 p2 的内存完全没关系
如果创建子进程后一上来就exec, 用vfork 的效率 会比 fork 高。
main() 函数进去之前和退出之后
main 函数 即不是入口也不是出口
- main函数进去之前
- 执行了动态库的构造函数
- main函数退出之后
- 执行动态库的析构函数
- flush stdio
- 执行atexit() 上的函数
进入到main 函数之前,已经进入了clib 即 C库的流程。
从main函数出来后,也没结束,也要回到C库的流程。
利用这种机制,对多进程模型进行特殊处理。
比如在动态库的构造函数里做手脚, 做一些初始化的动作。比main 函数 更早执行
析构函数 则在main函数 返回之后 执行,比如clib的析构会做以下事件:
- flush IO,进程执行过程中 printf 的东西还只是蓄积在 clib 的buffer中,并没有真正的打印出来。flush io 可以打印出来。
printf("hello"); // 无 /n
- 动态库的析构函数,main函数返回之后执行
- 程序运行中 用 atexit(fn1) or on_exit(fn2) 注册一些钩子函数, 在main函数返回之后执行
程序的退出
- main 函数返回 n, clib 会把n 带给exit(n). 把exit(n) 理解为clib 调用。只不过最终会传给_exit(n)这个系统调用。main函数的返回值会作为系统调用的退出码,传给linux。
- 调用exit(n)
- _exit(n), 直接退出,更为底层,非正常返回,直接系统调用了。
- exit(n) 需要执行 flush io, 动态库析构,exit钩子,再调用_exit(n)
- control c 也不是exit(n), 而是_exit() 属于非正常退出,不会执行exit(n)的流程
void exit1print(void) {
printf("hello exit1\n");
}
void exit2print(void) {
printf("hello exit2\n");
}
int main(int argc, char ** argv)
{
atexit(exit1print);
atexit(exit2print);
printf("hello main enter");
//printf("hello main enter\n");
return 1;
//exit(1);
//_exit(1);
//while(1); //用control c 中断
}
尝试上述几种情况。
正常的顺序是:
- hello main enter
- hello exit2
- hello exit1
_exit, control-c 都没有打印。
但是加了回车 就可以出现啦。
钩子函数利用 与 Asan 初步
写这部分之前,要先完成对signal 的文章
网友评论