书接上回,上回书咱们说到,fork 触发系统调用中断,最终调用到了 sys_fork 函数,借这个过程介绍了一次系统调用的流程。
那今天我们回到正题,开始讲 fork 函数的原理,实际上就是 sys_fork 函数干了啥。
还是个汇编代码,但我们要关注的地方不多。
1
2
3
4
5
6
7
8
9
10
11
12
| _sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process
addl $20,%esp
1: ret
|
其实就是调用了两个函数。我们先从方法名直接翻译一下,猜猜意思。
先是 find_empty_process,就是找到空闲的进程槽位。
然后 copy_process,就是复制进程。
那妥了,这个方法的意思非常简单,因为存储进程的数据结构是一个 task[64] 数组,这个是在之前 第18回 | 大名鼎鼎的进程调度就是从这里开始的 sched_init 函数的时候设置的。
就是先在这个数组中找一个空闲的位置,准备存一个新的进程的结构 task_struct,这个结构之前在 一个新进程的诞生(三)如果让你来设计进程调度 也简单说过了。
1
2
3
4
5
6
7
| struct task_struct {
long state;
long counter;
long priority;
...
struct tss_struct tss;
}
|
这个结构各个字段具体赋什么值呢?
通过 copy_process 这个名字我们知道,就是复制原来的进程,也就是当前进程。
当前只有一个进程,就是数组中位置 0 处的 init_task.init,也就是零号进程,那自然就复制它咯。
好了,以上只是我们的猜测,有了猜测再看代码会非常轻松,我们一个个函数看。
先来 find_empty_process。
1
2
3
4
5
6
7
8
9
10
11
12
13
| long last_pid = 0;
int find_empty_process(void) {
int i;
repeat:
if ((++last_pid)<0) last_pid=1;
for(i=0 ; i<64 ; i++)
if (task[i] && task[i]->pid == last_pid)
goto repeat;
for(i=1 ; i<64; i++)
if (!task[i])
return i;
return -EAGAIN;
}
|
一共三步,很简单。
第一步,判断 ++last_pid 是不是小于零了,小于零说明已经超过 long 的最大值了,重新赋值为 1,起到一个保护作用,这没什么好说的。
第二步,一个 for 循环,看看刚刚的 last_pid 在所有 task[] 数组中,是否已经被某进程占用了。如果被占用了,那就重复执行,再次加一,然后再次判断,直到找到一个 pid 号没有被任何进程用为止。
第三步,又是个 for 循环,刚刚已经找到一个可用的 pid 号了,那这一步就是再次遍历这个 task[] 试图找到一个空闲项,找到了就返回素组索引下标。
最终,这个方法就返回 task[] 数组的索引,表示找到了一个空闲项,之后就开始往这里塞一个新的进程吧。
由于我们现在只有 0 号进程,且 task[] 除了 0 号索引位置,其他地方都是空的,所以这个方法运行完,last_**pid 就是 1,也就是新进程被分配的 pid 就是 1**,然后即将要加入的 task[] 数组的索引位置,也是 1。
好的,那我们接下来就看,怎么构造这个进程结构,塞到这个 1 索引位置的 task[] 中?
来看 copy_process 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
| int copy_process(int nr,long ebp,long edi,long esi,
long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page();
if (!p)
return -EAGAIN;
task[nr] = p;
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority;
p->signal = 0;
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies;
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
p->tss.eip = eip;
p->tss.eflags = eflags;
p->tss.eax = 0;
p->tss.ecx = ecx; p->tss.edx = edx; p->tss.ebx = ebx;
p->tss.esp = esp; p->tss.ebp = ebp; p->tss.esi = esi;
p->tss.edi = edi; p->tss.es = es & 0xffff; p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++)
if (f=p->filp[i])
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}
|
艾玛,这也太多了!
别急,大部分都是 tss 结构的复制,以及一些无关紧要的分支,看我简化下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| int copy_process(int nr, ...) {
struct task_struct p =
(struct task_struct *) get_free_page();
task[nr] = p; *p = *current;
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->counter = p->priority;
..
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
...
copy_mem(nr,p);
...
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING;
return last_pid;
}
|
这个函数本来就是 fork 的难点了,所以我们慢慢来。
首先 get_free_page 会在主内存末端申请一个空闲页面,还记得我们之前在 第13回 内存初始化 mem_init 里是怎么管理内存的吧?
那 get_free_page 这个函数就很简单了,就是遍历 mem_map[] 这个数组,找出值为零的项,就表示找到了空闲的一页内存。然后把该项置为 1,表示该页已经被使用。最后,算出这个页的内存起始地址,返回。
然后,拿到的这个内存起始地址,就给了 task_struct 结构的 p。
1
2
3
4
5
6
7
| int copy_process(int nr, ...) {
struct task_struct p =
(struct task_struct *) get_free_page();
task[nr] = p;
*p = *current;
...
}
|
于是乎,一个进程结构 task_struct 就在内存中有了一块空间,但此时还没有赋值具体的字段。别急。
首先将这个 p 记录在进程管理结构 task[] 中。
然后下一句 *p = *current
很简单,就是把当前进程,也就是 0 号进程的 task_struct 的全部值都复制给即将创建的进程 p,目前它们两者就完全一样了。
嗯,这就附上值了,就完全复制之前的进程的 task_struct 而已,很粗暴。
最后的内存布局的效果就是这样。
然后,进程 1 和进程 0 目前是完全复制的关系,但有一些值是需要个性化处理的,下面的代码就是把这些不一样的值覆盖掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| int copy_process(int nr, ...) {
...
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->counter = p->priority;
..
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
...
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
...
}
|
不一样的值,一部分是 state,pid,counter 这种进程的元信息,另一部分是 tss 里面保存的各种寄存器的信息,即上下文。
这里有两个寄存器的值的赋值有些特殊,就是 ss0 和 esp0,这个表示 0 特权级也就是内核态时的 ss:esp 的指向。
根据代码我们得知,其含义是将代码在内核态时使用的堆栈栈顶指针指向进程 task_struct 所在的 4K 内存页的最顶端,而且之后的每个进程都是这样被设置的。
好了,进程槽位的申请,以及基本信息的复制,就讲完了。
今天就这么点内容,就是内存中找个地方存一个 task_struct 结构的东东,并添加到 task[] 数组里的空闲位置处,这个东东的具体字段赋值的大部分都是复制原来进程的。
接下来将是进程页表和段表的复制,这将会决定进程之间的内存规划问题,很是精彩,也是 fork 真正的难点所在。