今晚和一位500强的leader喝喝小酒吃吃烤鱼,生活乐无边。这位兄弟伙才毕业2年,已经做到管理层了,机遇和能力不可谓不好。喝酒之余,聊到Linux内核的两个问题——fork()、exec()的原理。
兄弟伙:fork()的原理是什么呢?
我:其实一句话就概括了——copy on write。
兄弟伙:copy on wirte我懂,书上介绍的一抓一大把,但是没几本书是能说明白的。我想从你这里得到通俗的解释。
我:我在《口述程序员如何意淫进程》的三篇文章里详细介绍过进程是什么样子的。你应该得到启发的。
兄弟伙:我明白进程是什么样子的了。但是fork()与exec()的原理还不甚明了。
我:从你的角度,你觉得进程需要具备哪些东西呢?
兄弟伙:至少具备四个东西。
1、task_struct结构体。这玩意儿好比是进程的身份证。(线程则没有)
2、进程还必须要有一段可执行代码。
3、进程必须具备它独立的内存空间(线程则没有)。
4、进程必须具备独立的内核堆栈。
我:是的。我顺便补充一下。之所以必须具备内核堆栈,是因为代码从内核态进入用户态时(从0级切换到3级),必须保护内核态“现场”,使其能够恢复。
兄弟伙:那fork()与这4点是什么关系呢?
我:理解这一点,必须分2种情况。
1、调用fork()之后立即调用exec()执行新的程序,生成一个全新的进程。
2、调用fork()之后不调用exec(),仅仅是为将当前进程生成多个,以提升软件并发能力——典型的是Web Server,如Apache,nginx等。
兄弟伙:对于第一点,有什么需要关注的吗?
我:我们先聊第二点吧。
兄弟伙:好。
我:调用fork()之后,操作系统会复制一个全新的task_struct结构体,这个结构体除了id号不一样外,其余的都完全一样——这意味着,两个进程的内存空间也是映射到相同的地址。
兄弟伙:这种情况应该是最简单,也最完美的情况。
我:是的。这种情况下,一般fork的进程数只要与CPU数量一致,整个server的性能就不会太差——至少不会因为context switch而变差。而且,具备一个优点——如果每个进程都使用了IO多路复用,比如最典型的epoll,每一个进程都会因为fork而具备独立的数据结构,这相对与多线程模型来说,实在太简单了。
兄弟伙:啊。你不是说“两个进程的内存空间也是映射到相同的地址”吗?这岂不是互相矛盾?
我:这个问题提得很好。这并不矛盾。在fork时,两个进程是共享想同的内存的。但是,当其中一个进程试图去修改其中一个数据结构时(写时复制),Linux内核就会产生“缺页中断”为该数据结构分配全新的空间。
兄弟伙:为什么Unix采用写时复制会大幅度提升内存管理性能呢?
我:如果不采用写时复制,那么调用fork时,就会为进程分配全新的、独立的内存空间地址,而事实上,其中很大一部分内容可能与父进程是相同的——也就是说,大部分内存其实被重复浪费了。而采用写时复制之后,只有当真的需要分配独立的内存空间的时候,才会发生缺页中断,分配全新的内存空间,这个copy on write是基于page的,而不是基于进程的。
兄弟伙:明白了。通过代码分析下fork()吧。
我:首先,你需要明白,fork()其实做了些什么。我选一些早期的Linux内核代码给你看看吧。fork()其实做了2步。1、找到空闲的进程号。2、从父进程拷贝进程信息。
1、
2、
find_empty_process()这个函数你一看就明白了,实在太简单。我们分析下copy_process这个函数吧。
在copy_process这个函数里,有一行比较牛逼的语句。这一句相对比较难一点,需要重点说明下。
p = (struct task_struct *) get_free_page();
这里的新task_struct为什么会指向一个free page呢?
明白了吧?
task_struct结构体是按page分配的,多余的部分作为该进程的内核堆栈,从底向task_struct延伸。
之后就是对task_struct的属性进行设置了,包括“智能”与CPU相关部分属性。
通过这部分源代码的分析,你应该明白了吧——最初的“口述程序员如何意淫进程”这样的吹牛B的话看似随意,其实是理解Linux内核的基础与根本,如果真把那些文章当成吹牛逼了,这里的源代码分析对你来说就是天书了——如何才能轻松看懂源代码分析?
答案是——多看几遍吹牛逼的对话,直到你明白这其中的深意。