Linux内核源代码分析——fork()原理&多进程网络模型

时间:2022-05-29 03:46:06

 今晚和一位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、

          

[html]  view plain copy
  1. int find_empty_process(void)  
  2. {  
  3.     int i;  
  4.   
  5.     repeat:  
  6.         if ((++last_pid)<0last_pid=1;  
  7.         for(i=0 ; i<NR_TASKS ; i++)  
  8.             if (task[i] && task[i]->pid == last_pid) goto repeat;  
  9.     for(i=1 ; i<NR_TASKS ; i++)  
  10.         if (!task[i])  
  11.             return i;  
  12.     return -EAGAIN;  
  13. }  


            2、

[html]  view plain copy
  1. int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,  
  2.         long ebx,long ecx,long edx,  
  3.         long fs,long es,long ds,  
  4.         long eip,long cs,long eflags,long esp,long ss)  
  5. {  
  6.     struct task_struct *p;  
  7.     int i;  
  8.     struct file *f;  
  9.   
  10.     p = (struct task_struct *) get_free_page();  
  11.     if (!p)  
  12.         return -EAGAIN;  
  13.     task[nr] = p;  
  14.     *p = *current;  /* NOTE! this doesn't copy the supervisor stack */  
  15.     p->state = TASK_UNINTERRUPTIBLE;  
  16.     p->pid = last_pid;  
  17.     p->father = current->pid;  
  18.     p->counter = p->priority;  
  19.     p->signal = 0;  
  20.     p->alarm = 0;  
  21.     p->leader = 0;       /* process leadership doesn't inherit */  
  22.     p->utime = p->stime = 0;  
  23.     p->cutime = p->cstime = 0;  
  24.     p->start_time = jiffies;  
  25.     p->tss.back_link = 0;  
  26.     p->tss.esp0 = PAGE_SIZE + (long) p;  
  27.     p->tss.ss0 = 0x10;  
  28.     p->tss.eip = eip;  
  29.     p->tss.eflags = eflags;  
  30.     p->tss.eax = 0;  
  31.     p->tss.ecx = ecx;  
  32.     p->tss.edx = edx;  
  33.     p->tss.ebx = ebx;  
  34.     p->tss.esp = esp;  
  35.     p->tss.ebp = ebp;  
  36.     p->tss.esi = esi;  
  37.     p->tss.edi = edi;  
  38.     p->tss.es = es & 0xffff;  
  39.     p->tss.cs = cs & 0xffff;  
  40.     p->tss.ss = ss & 0xffff;  
  41.     p->tss.ds = ds & 0xffff;  
  42.     p->tss.fs = fs & 0xffff;  
  43.     p->tss.gs = gs & 0xffff;  
  44.     p->tss.ldt = _LDT(nr);  
  45.     p->tss.trace_bitmap = 0x80000000;  
  46.     if (last_task_used_math == current)  
  47.         __asm__("clts ; fnsave %0"::"m" (p->tss.i387));  
  48.     if (copy_mem(nr,p)) {  
  49.         task[nr] = NULL;  
  50.         free_page((long) p);  
  51.         return -EAGAIN;  
  52.     }  
  53.     for (i=0; i<NR_OPEN;i++)  
  54.         if (f=p->filp[i])  
  55.             f->f_count++;  
  56.     if (current->pwd)  
  57.         current->pwd->i_count++;  
  58.     if (current->root)  
  59.         current->root->i_count++;  
  60.     if (current->executable)  
  61.         current->executable->i_count++;  
  62.     set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));  
  63.     set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));  
  64.     p->state = TASK_RUNNING; /* do this last, just in case */  
  65.     return last_pid;  
  66. }  

        find_empty_process()这个函数你一看就明白了,实在太简单。我们分析下copy_process这个函数吧。

        在copy_process这个函数里,有一行比较牛逼的语句。这一句相对比较难一点,需要重点说明下。

        p = (struct task_struct *) get_free_page();

        这里的新task_struct为什么会指向一个free page呢?

        Linux内核源代码分析——fork()原理&多进程网络模型

         明白了吧?

         task_struct结构体是按page分配的,多余的部分作为该进程的内核堆栈,从底向task_struct延伸。

         之后就是对task_struct的属性进行设置了,包括“智能”与CPU相关部分属性。

         通过这部分源代码的分析,你应该明白了吧——最初的“口述程序员如何意淫进程”这样的吹牛B的话看似随意,其实是理解Linux内核的基础与根本,如果真把那些文章当成吹牛逼了,这里的源代码分析对你来说就是天书了——如何才能轻松看懂源代码分析?

          答案是——多看几遍吹牛逼的对话,直到你明白这其中的深意。