fork源码分析

来源:互联网 发布:微商城网站源码 编辑:程序博客网 时间:2024/04/27 22:38

1.kernel/system_call.s


207 .align 2
208 _sys_fork:
209     call _find_empty_process
210     testl %eax,%eax
211      js 1f
212     push %gs
213     pushl %esi
214     pushl %edi
215     pushl %ebp
216     pushl %eax
217     call _copy_process
218     addl $20,%esp
219 1:  ret
先看一下_find_empty_process调用:
kernel/fork.c
135 int find_empty_process(void)
136 {
137     int i;
138
139     repeat:
140        if ((++last_pid)<0) last_pid=1;
141         for(i=0 ; i<NR_TASKS ; i++)
142             if (task[i] && task[i]->pid == last_pid) goto repeat;
143     for(i=1 ; i<NR_TASKS ; i++)
144         if (!task[i])
145             return i;
146     return -EAGAIN;
147 }
可以看到last_pid记录了最近使用的一个pid的下一个位置,140-142行的for循环是用来维护last_pid的,143-145是找一个目前的task没有用过的最小的pid,并返回它它在task中的位置。
我们继续:
212-216行进行参数压栈操作:
然后调用copy_process

 63 /*
 64  *  Ok, this is the main fork-routine. It copies the system process
 65  * information (task[nr]) and sets up the necessary registers. It
 66  * also copies the data segment in it's entirety.
 67  */
 68 int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
 69         long ebx,long ecx,long edx,
 70         long fs,long es,long ds,
 71         long eip,long cs,long eflags,long esp,long ss)
 72 {
 73     struct task_struct *p;
 74     int i;
 75     struct file *f;
 76
 77     p = (struct task_struct *) get_free_page();
获取一个新的内存页用于保存task_struct数据结构和作为内核栈使用,而且我们可以知道get_free_page得到的是一个页的起始地址,因此task_struct保存在一个页的起始位置处。

 78     if (!p)
 79         return -EAGAIN;

没有内存页可以分配,直接返回。

继续
 80     task[nr] = p;
 81     *p = *current;  /* NOTE! this doesn't copy the supervisor stack */
 82     p->state = TASK_UNINTERRUPTIBLE;
 83     p->pid = last_pid;
 84     p->father = current->pid;
 85     p->counter = p->priority;
 86     p->signal = 0;
 87     p->alarm = 0;
 88     p->leader = 0;      /* process leadership doesn't inherit */
 89     p->utime = p->stime = 0;
 90     p->cutime = p->cstime = 0;
 91     p->start_time = jiffies;
 92     p->tss.back_link = 0;
 93     p->tss.esp0 = PAGE_SIZE + (long) p;
这里可以看到把栈指针设置到了task_struct所在页的顶端。

 94     p->tss.ss0 = 0x10;
特权级0级,全局描述符表,索引2,也就是数据段。

 95     p->tss.eip = eip;
eip直接继承,所以返回后会执行与父进程相同的代码(调用fork的下一条指令)

 96     p->tss.eflags = eflags;
 97     p->tss.eax = 0;                   //所以子进程返回0?
 98     p->tss.ecx = ecx;
 99     p->tss.edx = edx;

100     p->tss.ebx = ebx;
101     p->tss.esp = esp;
102     p->tss.ebp = ebp;
103     p->tss.esi = esi;
104     p->tss.edi = edi;
105     p->tss.es = es & 0xffff;
106     p->tss.cs = cs & 0xffff;
107     p->tss.ss = ss & 0xffff;
108     p->tss.ds = ds & 0xffff;
109     p->tss.fs = fs & 0xffff;
110     p->tss.gs = gs & 0xffff;
以上是拷贝寄存器的操作。

111     p->tss.ldt = _LDT(nr);
看一下
include/linux/sched.h:156:#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
include/linux/sched.h:154:#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
include/linux/sched.h:153:#define FIRST_TSS_ENTRY 4
可见这里是为p->tss.ldt设置了在GDT中相应的索引(以字节为单位)。

112     p->tss.trace_bitmap = 0x80000000;

113     if (last_task_used_math == current)
114         __asm__("clts ; fnsave %0"::"m" (p->tss.i387));

115     if (copy_mem(nr,p)) {
116         task[nr] = NULL;
117         free_page((long) p);
118         return -EAGAIN;
119     }
我们看一下copy_mem的实现:
kernel/fork.c:39:int copy_mem(int nr,struct task_struct * p)
 39 int copy_mem(int nr,struct task_struct * p)
 40 {
 41     unsigned long old_data_base,new_data_base,data_limit;
 42     unsigned long old_code_base,new_code_base,code_limit;
 43
 44     code_limit=get_limit(0x0f);
 45     data_limit=get_limit(0x17);
我们之前分析过这个函数,它主要是根据这里的参数来获取相应段的限长。

 46     old_code_base = get_base(current->ldt[1]);
 47     old_data_base = get_base(current->ldt[2]);
获得代码段和数据段的基址

 48     if (old_data_base != old_code_base)
 49         panic("We don't support separate I&D");
这里数据段与代码段是重叠的

 50     if (data_limit < code_limit)
 51         panic("Bad data_limit");

 52     new_data_base = new_code_base = nr * 0x4000000;
新的数据段的基址nr×64M,这里需要注意一下,可见对于该版本的内核并不是每个进程的虚拟地址空间都是从0开始的

 53     p->start_code = new_code_base;
设置start_code
 54     set_base(p->ldt[1],new_code_base);
 55     set_base(p->ldt[2],new_data_base);
设置ldt中数据段和代码段的基址。看一下set_base:
188 #define _set_base(addr,base) \
189 __asm__("movw %%dx,%0\n\t" \
190     "rorl $16,%%edx\n\t" \
191     "movb %%dl,%1\n\t" \
192     "movb %%dh,%2" \
193     ::"m" (*((addr)+2)), \
194       "m" (*((addr)+4)), \
195       "m" (*((addr)+7)), \
196       "d" (base) \
197     :"dx")
可以看到这里就是把基址设置到了ldt中。

 56     if (copy_page_tables(old_data_base,new_data_base,data_limit)) {
 57         free_page_tables(new_data_base,data_limit);
 58         return -ENOMEM;
 59     }
可以看到这里是拷贝页表的操作,我们来看一下具体操作:
150 int copy_page_tables(unsigned long from,unsigned long to,long size)
151 {
152     unsigned long * from_page_table;
153     unsigned long * to_page_table;
154     unsigned long this_page;
155     unsigned long * from_dir, * to_dir;
156     unsigned long nr;
157
158     if ((from&0x3fffff) || (to&0x3fffff))
159         panic("copy_page_tables called with wrong alignment");
160     from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
获取原进程的页目录项
161     to_dir = (unsigned long *) ((to>>20) & 0xffc);
获取目标进程的页目录项

162     size = ((unsigned) (size+0x3fffff)) >> 22;
以4M为单位(一个页目录对应4M内存)

163     for( ; size-->0 ; from_dir++,to_dir++) {
164         if (1 & *to_dir)
165             panic("copy_page_tables: already exist");
166         if (!(1 & *from_dir))
167             continue;
168         from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
从页目录中取得源进程的页表地址。

169         if (!(to_page_table = (unsigned long *) get_free_page()))
170             return -1;  /* Out of memory, see freeing */
分配一页内存用于存放目标进程的页表地址。

171         *to_dir = ((unsigned long) to_page_table) | 7;
设置目标进程的页目录项,这里7代表存在内存中,用户可读写。

172         nr = (from==0)?0xA0:1024;
173         for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
174             this_page = *from_page_table;
175             if (!(1 & this_page))
176                 continue;
177             this_page &= ~2;
178             *to_page_table = this_page;
这里需要注意了,copy_on_write的实现与这里有重大关系。177行把页表项的R/W位清零,也就是设置只读模式。178行把它设置给目标进程的的页表中。那么当子进程被调度后运行过程中如果进行写操作,就会触发写保护异常,相应的处理函数就会为它分配新的内存页。

179             if (this_page > LOW_MEM) {
这里的判断是因为内核空间不采用写时复制,正如前面的文章中提到过的,任务0 fork出init进程后,init进程其实与任务0使用的是相同的内存空间,所以不允许任务0通过函数调用来创建新的进程,而采用内联函数的形式。
180                 *from_page_table = this_page;
可以看出源页表也被设置为只读模式
181                 this_page -= LOW_MEM;
182                 this_page >>= 12;
183                 mem_map[this_page]++;
184             }
185         }
186     }
187     invalidate();     //刷新缓存
188     return 0;
189 }
 60     return 0;
 61 }

============
120     for (i=0; i<NR_OPEN;i++)
121         if (f=p->filp[i])
122             f->f_count++;
123     if (current->pwd)
124         current->pwd->i_count++;
125     if (current->root)
126         current->root->i_count++;
127     if (current->executable)
128         current->executable->i_count++;

129     set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
130     set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
把新建进程的ldt和tss写入到gdt表中(主要是写入了ldt和tss的地址,以及一些权限相关的内容)
131     p->state = TASK_RUNNING;    /* do this last, just in case */
132     return last_pid;
133 }

跟踪一下其中一些方法
 65 #define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x89")
 66 #define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),addr,"0x82")

 52 #define _set_tssldt_desc(n,addr,type) \
 53 __asm__ ("movw $104,%1\n\t" \
 54     "movw %%ax,%2\n\t" \
 55     "rorl $16,%%eax\n\t" \
 56     "movb %%al,%3\n\t" \
 57     "movb $" type ",%4\n\t" \
 58     "movb $0x00,%5\n\t" \
 59     "movb %%ah,%6\n\t" \
 60     "rorl $16,%%eax" \
 61     ::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
 62      "m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
 63     )
看懂这些还需要一些补充
153 #define FIRST_TSS_ENTRY 4
154 #define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
155 #define _TSS(n) ((((unsigned long) n)<<4)+(FIRST_TSS_ENTRY<<3))
156 #define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))
157 #define ltr(n) __asm__("ltr %%ax"::"a" (_TSS(n)))
158 #define lldt(n) __asm__("lldt %%ax"::"a" (_LDT(n)))
可以看到,129, 130 两行主要就是把一些信息设置到gdt的相应位置。

为了更好的看清这个结构,下面给出描述符表的结构。如下:

65行语句对应如下:

Base
0
1000 1001
Base
Base p->tss
$104


对比上面的段描述符结构可知,这里S为0表示是系统段,TYPE为1001表示32位TSS(可用),DPL为0,说明特权级最高。P为1,代表在内存中。
这里为什么设置为系统段呢?而且特权级最高?
为系统段很好理解,因为它既不是代码段也不是数据段或者说它二者兼有。特权级最高是为了确保安全,使得只有内核才能调度其运行。

继续看一下ldt的设置(66行):
Base
0
1000 0010
Base
p->ldt
$104

同样这里S为0说明是系统段描述符(为1说明是数据段或代码段描述符)。type为2表示这是一个LDT。然后特权级为0,P为1表示在内存中。

现在已经设置好tss和ldt的基地址,那么我们通过ldtr中的索引就可以找到这个ldt,进一步找到基地址,再根据段寄存器就能找到对应的段的基地址。然后与偏移相加就得到了线性地址。


由于创建新进程时是完全通过复制父进程的代码段和数据段的方式来完成的。因此在创建新进程时,为了确保子进程的用户态堆栈没有父进程的多余信息,要求父进程在创建子进程时不要调用函数。因此fork()不能以函数形式被调用。V0.11中通过定义内嵌宏代码,调用linux的系统调用中断0x80首先fork()调用。

宏定义如下:

#define _syscall0(type,name) \

type name(void) \

{ \

long __res; \

__asm__ volatile ( "int $0x80" \      // 调用系统中断0x80

:"=a"

(__res) \                               // 返回值??eax(__res)

:""

(__NR_

##name)); \                         // 输入为系统中断调用号__NR_name

 if (__res >= 0) \                // 如果返回值>=0,则直接返回该值。

 return (type) __res; errno = -__res; \    // 否则置出错号,并返回-1

 return -1;

 }

static inline _syscall0(int, fork)

当调用fork创建子进程时上面的内联宏被调用,系统的中断调用号为_NR_fork被存入eax中,同时调用系统调用中断0x80,将用户态的ss, esp, eflags, cs, eip压栈

进程堆栈转入内核栈:

0x80调用system_call:

_system_call:

cmpl $nr_system_calls-1,eax #调用号若超出范围就在eax中置-1并退出

ja bad_sys_call

push ds保存原寄存器的值

push es

push fs

pushl edx #若有参数,则edx,ecx,ebx中装有函数的参数

pushl ecx

pushl ebx

movl $0x10,edx

mov dx,ds将ds,es指向内核数据段(全局描述附表GDT中数据段描述符)

mov dx,es

movl $0x17,edx  将fs指向局部数据段描述符(局部描述表LDT中数据段描述符)

mov dx,fs

call _sys_call_table(,eax,4)调用系统调用处理函数数组中的第eax个函数

pushl eax

 

调用call _sys_call_table(_NR_fork,4)内核栈状态:

 

_sys_call_table[eax*4]指向函数_sys_fork

 

_sys_fork:

call _find_empty_process   #获得新进程的进程号pid=last_pid,返回进程在进程数组task[]中的任务号nr存入eax,nr最大为63,因为linux 0.11最多同时支持64个进程同时运行,

testl eax,eax

js 1f

push gs

pushl esi

pushl edi

pushl ebp

pushl eax

call _copy_process

addl $20,esp

ret

相信此时调用_copy_process时内核堆栈的使用情况你应该了解了,这里我就不在画出来了。

_copy_process实现功能

  1.       在内存中为新进程申请一页内存作为新进程的内核堆栈,并将新进程的任务数据结构存入栈底部。
  2.       对从父进程复制来的任务数据结构做一定的修改。
  3.       在线性地址空间中设置新进程的基地址和限长,并将父进程的数据段和代码段(其实是同一段)复制到子     进程的数据段和代码段。
  4.       如果父进程打开某文件,则将次文件的引用次数加一,将父进程的pwd,root, executable引用次数加一,因为子进程也引用了这些i节点。
  5.       在全局段描述符表GDT中加入子进程的TSSLDT段描述符,返回子进程的pid,回到_sys_fork

 

addl $20,esp _copy_process压栈的所有内容全部退栈。此时eax中存的是子进程的pid

从_sys_fork返回到_system_call,将子进程号压栈


这里我们不在考虑进程处于非就绪态和时间片用完的情况。

将内核栈中内容依次pop出栈存到相应寄存器中,再调用iret使得堆栈指针重新指到进程用户态堆栈,回到进程用户态继续执行程序,子进程会在进程调度函数schedule()执行时被调用


2.总结


现在我们来总结一下fork的整个处理流程。从C语言中的函数开始,它在glibc库中会被转换为int0x80加调用号的形式,触发中断。该中断在系统初始化过程中注册,它的处理函数是system_call,这个函数在system_call.s文件中,在这里面它首先压栈一些参数,然后会根据调用号调用sys_call_table的相应表项,sys_call_table定义在include/linux/sys.h中,它是一个函数指针数组。现在对应的就是sys_fork函数,它仍然是在system_call.s中定义的。我们来看它的处理过程,首先它会调用find_empty_process函数来从task数组中查找一个还没有使用的task,

找到之后把对应的索引返回(保存到eax中),随后它又保存了其他一些寄存器,并且把返回的任务索引压入栈中,依次作为参数来调用copy_process函数。下面我们来看copy_process的处理过程。首先,它会分配一个内存页,把task_struct结构体保存到这个内存页的最开始位置(同时需要注册进task数组),并把内核栈指针设为该页的顶端。紧接着为这个新的task_struct复制为之前的task_struct,然后需要修改一些域,这些域都是不能直接从父进程继承的。这些域包括进程id设置为我们在find_empty_process过程中找到的id,pid设置为调用fork的进程id,leader不能继承父类的,时钟和信号也不继承,设置eip,设置eax为0,设置内核栈指针等等。然后比较重要的现在需要为它设置页表了。我们再详细总结一下设置页表的过程。
为了设置页表,我们需要知道一个进程的代码段和数据段的起始地址以及占用了多大的空间。由于二者是重叠的,我们设置一个就可以了。获取的过程是通过查看进程ldt表项来获取基址和限长。由于linux0.11中进程的起始地址为64M×nr,所以前面的基址其实并没有太大作用,知道了起始地址,现在就可以知道它在页目录中的索引了,我们设置它的页目录项,现在可以设置它的页表了,首先获取父进程的页表,并对页表进行拷贝,拷贝的过程中,我们把子进程的页面属性设置为只读(父进程也被设为只读了,内核空间除外)。这样就完成了页表的设置,由于是只读的,当子进程执行写操作时会触发写保护,在写保护处理中会拷贝页面。现在页表就已经设置好了。接下来该为它设置gdt中的项目了。还需要提一点,之前我们根据进程号nr计算出代码段和数据段起始地址,并把它设置到了task_struct的ldt中。现在我们再次根据nr计算出ldt和tss在gdt中的位置,并向其中注册ldt和tss,通过这个过程我们就把task_struct中的tss和ldt的地址注册到gdt中对应的表项处。到此,所有的工作都已经完成了,设置其状态为可运行状态(刚开始是被设置为不可中断状态),返回新建进程号。
1 0