0%

从ucore来总结操作系统(5)----用户线程

用户线程

0X00 环境准备

本lab基于前所有lab,但是需要对其进行一些扩展:

  • alloc_proc函数:添加对proc_struct::wait_state以及proc_struct::cptr/optr/yptr成员的初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

static struct proc_struct *alloc_proc(void)
{
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL)
{
// .....
// Lab5 code
proc->wait_state = 0;
proc->cptr = proc->optr = proc->yptr = NULL;
}
return proc;
}

proc结构体中的 parent, cptr, yptr, optr

proc->parent是指向父进程的指针, proc->cptr是指向子进程的指针, proc->optr是指向更老兄弟进程的指针, proc->yptr是指向更年轻兄弟进程的指针

因为子进程并不唯一, 父进程的cptr指针指向最近建立的子进程, yptr和optr是子进程之间通过时间顺序形成的双向链表, 但是这个双向链表两边都是NULL,并不是严格的双向链表

1
2
3
4
5
6
7
8
9
10
11
12
13

+----------------+
| parent process |
+----------------+
parent ^ \ ^ parent
/ \ \
/ \ cptr \
/ yptr V \ yptr
+-------------+ --> +-------------+ --> NULL
| old process | | New Process |
NULL <-- +-------------+ <-- +-------------+
optr optr

  • idt_init函数:设置中断T_SYSCALL的触发特权级位DPL_USER
1
2
3
4
5
6
7
8
9

void idt_init(void)
{
// ......
// Lab5 code
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
// ......
}

  • trap_dispatch函数:设置每100次时间中断后,当前正在执行的进程被调度,也注释掉输出100ticks的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

static void trap_dispatch(struct trapframe *tf)
{
char c;
int ret=0;
switch (tf->tf_trapno)
{
// ......
case IRQ_OFFSET + IRQ_TIMER:
ticks++;
if(ticks % TICK_NUM == 0){
// Lab5 Code
assert(current != NULL);
current->need_resched = 1;
//print_ticks();
}
break;
// ......
}
}

  • do_fork函数:添加对当前程序等待状态的检查,以及使用set_link函数来设置进程之间的关系
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

int do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf)
{
// ..........
if ((proc = alloc_proc()) == NULL)
goto fork_out;
proc->parent = current;
// Lab5: 确保当前进程的wait状态为空
assert(current->wait_state == 0);
if (setup_kstack(proc) != 0)
goto bad_fork_cleanup_proc;
if (copy_mm(clone_flags, proc) != 0)
goto bad_fork_cleanup_kstack;
copy_thread(proc, stack, tf);
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc);
// Lab5: 设置进程间的关系
set_links(proc);
}
local_intr_restore(intr_flag);
wakeup_proc(proc);
ret = proc->pid;
// ..........
}

只有当前进程不在等待中,才能进入fork函数里

set_link函数,是设置proc结构体中的 cptr, yptr, optr, 因为子进程并不唯一, 这个函数位置保证, 父进程的cptr指针指向最近建立的子进程, yptr和optr是子进程之间通过时间顺序形成的双向链表。

0X01 加载应用程序并执行

一般我们fork出子进程后会立刻执行execv函数,从而加载到另一个程序(这也是为什么大多需要写时复制,因为新程序不需要父进程的上下文)。

kern/process/proc.c的do_execv函数加载并解析一个处于内存中的ELF执行文件格式(因为此时没有文件系统,所以所有用户程序需要随内核编译,参见实验指导书)的应用程序,建立相应的用户内存空间来放置应用程序的代码段、数据段等,且要设置好proc_struct结构中的成员变量trapframe中的内容,确保在执行此进程后,能够从应用程序设定的起始执行地址开始执行。需设置正确的trapframe内容。

而上述函数调用load_icode函数来完成具体的实现,所以我们需要完成kern/process/proc.c中的load_icode函数。

不过在实现这个函数之前,我们先分析一下创建内核线程的过程

  • 首先用户程序随内核编译,放在内核代码的后面,然后随内核一起被bootloader加载到内存中。
  • 在本实验中第一个用户进程是由第二个内核线程initproc通过把hello应用程序执行码覆盖到initproc的用户虚拟内存空间来创建的,其会通过调用KERNEL_EXECVE(hello)来创建用户线程
  • KERNEL_EXECVE(hello)会调用kernel_execve函数来调用SYS_exec系统调用(这里面会做一些解析传递用户线程地址的工作,非常的简单)
  • 系统调用发生后,会进入中断处理函数(中断的调用栈此处就忽略了)
  • 在中断处理函数中,会进入到我们在准备工作中写的那个分支,会直接调用syscall函数。
  • 在syscall函数中,由于此时还未中断返回,所以syscall会从中断帧中取出系统调用代号,并使用一个函数指针数组来调用对应的系统调用处理函数,此时就是sys_exec函数。
  • sys_exec函数会解析传递进来的参数并转换一下(因为各个系统调用需要的参数数目不同,所以传递的是类似'变长参数'的东西(其实是一个数组,各个函数各取所需)),然后调用do_execv函数来加载应用程序并执行。
  • 当各个子函数都执行完,就正确加载了程序并建立了上下文,但是建立上下文不代表能执行,想要让上下文正确被加载,需要中断返回,当中断返回之后,就进入新加载的程序的上下文中和对应权限下执行了()

所以我们先从do_execv函数开始分析(前面一些函数比较简单,就是参数倒来倒去)。

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
61

// kern/process/proc.c

// do_execve - call exit_mmap(mm)&put_pgdir(mm) to reclaim memory space of current process
// - call load_icode to setup new memory space accroding binary prog.
// 真正处理execve系统调用的函数
int do_execve(const char *name, size_t len, unsigned char *binary, size_t size)
{
// 需要完成从内核态到用户态的转换,所以需要先清空mm
struct mm_struct *mm = current->mm;
// 检查在mm中,从name开始的len个字节是否都能被用户线程访问
// 而mm是内核栈,所以这些是保险性监测
// user_mem_check在vmm.c中
if (!user_mem_check(mm, (uintptr_t)name, len, 0))
{
return -E_INVAL;
}
if (len > PROC_NAME_LEN)
{
len = PROC_NAME_LEN;
}

// 存储副本
// 猜测原因:因为name可能在用户空间,而用户空间可能会被修改
char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);

// 如果mm不为空,那么就需要清空mm
// 因为内核线程常驻内存,所以内核线程的mm为NULL
// 如果是用户线程的话,因为需要置换,所以mm不为NULL
if (mm != NULL)
{
lcr3(boot_cr3);
// mm的引用计数为0
if (mm_count_dec(mm) == 0)
{
exit_mmap(mm);
put_pgdir(mm);
mm_destroy(mm);
}
current->mm = NULL;
}

// 然后执行load_icode,这个函数会加载ELF格式的程序
// 申请内存、建立用户态地址空间、加载程序等等
int ret;
if ((ret = load_icode(binary, size)) != 0)
{
goto execve_exit;
}

// 设置线程名字
set_proc_name(current, local_name);
return 0;

execve_exit:
do_exit(ret);
panic("already exit: %e.\n", ret);
}

在确定了用户进程的执行代码和数据,以及用户进程的虚拟空间布局后,我们可以来创建用户进程了。在本实验中第一个用户进程是由第二个内核线程initproc通过把hello应用程序执行码覆盖到initproc的用户虚拟内存空间来创建的,期间通过一系列调用,最终通过do_execve函数来完成用户进程的创建工作。此函数的主要工作流程如下:

  • 首先为加载新的执行码做好用户态内存空间清空准备。如果mm不为NULL,则设置页表为内核空间页表,且进一步判断mm的引用计数减1后是否为0,如果为0,则表明没有进程再需要此进程所占用的内存空间,为此将根据mm中的记录,释放进程所占用户空间内存和进程页表本身所占空间。最后把当前进程的mm内存管理指针为空。由于此处的initproc是内核线程,所以mm为NULL,整个处理都不会做。
  • 接下来的一步是加载应用程序执行码到当前进程的新创建的用户态虚拟空间中。这里涉及到读ELF格式的文件,申请内存空间,建立用户态虚存空间,加载应用程序执行码等。load_icode函数完成了整个复杂的工作。

可以看到,重点其实就是load_icode函数,这也是我们需要实现的函数

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202

// kern/process/proc.c

/* load_icode - load the content of binary program(ELF format) as the new content of current process
* @binary: the memory addr of the content of binary program
* @size: the size of the content of binary program
*/
// 从内存中地址位置为binary的地方加载一个ELF格式的程序,加载到当前进程的内存空间中
// size是binary的大小
// 因为没有文件管理系统,所以只能从内存中加载
// 而应用程序随内核编译,被bootloader加载到内存中(参见实验指导书)
static int load_icode(unsigned char *binary, size_t size)
{
// 需要保证当前进程的内存空间是空的
// 这在do_execve中已经保证了
if (current->mm != NULL)
{
panic("load_icode: current->mm must be empty.\n");
}

int ret = -E_NO_MEM;
struct mm_struct *mm;
//(1) create a new mm for current process
// 1. 创建一个新的mm_struct结构体,其实就是为新程序创建地址空间
if ((mm = mm_create()) == NULL)
{
goto bad_mm;
}

//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
// 2. 创建一个新的页目录表
if (setup_pgdir(mm) != 0)
{
goto bad_pgdir_cleanup_mm;
}

//(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
// 3. 加载程序,需要解析各种东西
struct Page *page;
//(3.1) get the file header of the bianry program (ELF format)
// 3.1 获取ELF文件头
struct elfhdr *elf = (struct elfhdr *)binary;
//(3.2) get the entry of the program section headers of the bianry program (ELF format)
// 3.2 获取ELF程序头表
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
// 3.3 检查ELF文件头是否合法(魔数)
if (elf->e_magic != ELF_MAGIC)
{
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}

uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;
for (; ph < ph_end; ph++)
{
//(3.4) find every program section headers
// 3.4 寻找每个程序段
if (ph->p_type != ELF_PT_LOAD)
{
continue;
}
if (ph->p_filesz > ph->p_memsz)
{
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap;
}
if (ph->p_filesz == 0)
{
continue;
}
//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
// 3.5 设置新的虚拟内存区域
vm_flags = 0, perm = PTE_U;
if (ph->p_flags & ELF_PF_X)
vm_flags |= VM_EXEC;
if (ph->p_flags & ELF_PF_W)
vm_flags |= VM_WRITE;
if (ph->p_flags & ELF_PF_R)
vm_flags |= VM_READ;
if (vm_flags & VM_WRITE)
perm |= PTE_W;
if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0)
{
goto bad_cleanup_mmap;
}
unsigned char *from = binary + ph->p_offset;
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);

ret = -E_NO_MEM;

//(3.6) alloc memory, and copy the contents of every program section (from, from+end) to process's memory (la, la+end)
// 3.6 分配内存和复制上下文
end = ph->p_va + ph->p_filesz;
//(3.6.1) copy TEXT/DATA section of bianry program
// 3.6.1 复制TEXT/DATA段
while (start < end)
{
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL)
{
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la)
{
size -= la - end;
}
memcpy(page2kva(page) + off, from, size);
start += size, from += size;
}

//(3.6.2) build BSS section of binary program
// 3.6.2 参数段
end = ph->p_va + ph->p_memsz;
if (start < la)
{
/* ph->p_memsz == ph->p_filesz */
if (start == end)
{
continue;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la)
{
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end)
{
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL)
{
goto bad_cleanup_mmap;
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la)
{
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
start += size;
}
}
//(4) build user stack memory
// 设置用户栈
vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0)
{
goto bad_cleanup_mmap;
}
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP - PGSIZE, PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP - 2 * PGSIZE, PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP - 3 * PGSIZE, PTE_USER) != NULL);
assert(pgdir_alloc_page(mm->pgdir, USTACKTOP - 4 * PGSIZE, PTE_USER) != NULL);

//(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
// 设置当前线程的mm表、cr3寄存器和CR3寄存器
mm_count_inc(mm);
current->mm = mm;
current->cr3 = PADDR(mm->pgdir);
lcr3(PADDR(mm->pgdir));

//(6) setup trapframe for user environment
// 设置用户环境的trapframe
struct trapframe *tf = current->tf;
memset(tf, 0, sizeof(struct trapframe));
// 需要通过中断帧来实现中断返回
// 这样才能正确切换到目标线程的上下文里
// 此处修改的是current里的trapframe,即当前线程的tf
/* LAB5:EXERCISE1 YOUR CODE
* should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
* NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
* tf_cs should be USER_CS segment (see memlayout.h)
* tf_ds=tf_es=tf_ss should be USER_DS segment
* tf_esp should be the top addr of user stack (USTACKTOP)
* tf_eip should be the entry point of this binary program (elf->e_entry)
* tf_eflags should be set to enable computer to produce Interrupt
*/
// 用户堆栈
tf->tf_cs = USER_CS;
tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
tf->tf_esp = USTACKTOP;
// 设置下一条指令为程序入口
tf->tf_eip = elf->e_entry;
tf->tf_eflags = FL_IF;
ret = 0;
out:
return ret;
bad_cleanup_mmap:
exit_mmap(mm);
bad_elf_cleanup_pgdir:
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
goto out;
}

load_icode函数的主要工作就是给用户进程建立一个能够让用户进程正常运行的用户环境。此函数有一百多行,完成了如下重要工作:

  • 调用mm_create函数来申请进程的内存管理数据结构mm所需内存空间,并对mm进行初始化
  • 调用setup_pgdir来申请一个页目录表所需的一个页大小的内存空间,并把描述ucore内核虚空间映射的内核页表(boot_pgdir所指)的内容拷贝到此新目录表中,最后让mm->pgdir指向此页目录表,这就是进程新的页目录表了,且能够正确映射内核虚空间
  • 根据应用程序执行码的起始位置来解析此ELF格式的执行程序,并调用mm_map函数根据ELF格式的执行程序说明的各个段(代码段、数据段、BSS段等)的起始位置和大小建立对应的vma结构,并把vma插入到mm结构中,从而表明了用户进程的合法用户态虚拟地址空间
  • 调用根据执行程序各个段的大小分配物理内存空间,并根据执行程序各个段的起始位置确定虚拟地址,并在页表中建立好物理地址和虚拟地址的映射关系,然后把执行程序各个段的内容拷贝到相应的内核虚拟地址中,至此应用程序执行码和数据已经根据编译时设定地址放置到虚拟内存中了
  • 需要给用户进程设置用户栈,为此调用mm_mmap函数建立用户栈的vma结构,明确用户栈的位置在用户虚空间的顶端,大小为256个页,即1MB,并分配一定数量的物理内存且建立好栈的虚地址<–>物理地址映射关系
  • 至此,进程内的内存管理vma和mm数据结构已经建立完成,于是把mm->pgdir赋值到cr3寄存器中,即更新了用户进程的虚拟内存空间,此时的initproc已经被hello的代码和数据覆盖,成为了第一个用户进程,但此时这个用户进程的执行现场还没建立好
  • 先清空进程的中断帧,再重新设置进程的中断帧,使得在执行中断返回指令“iret”后,能够让CPU转到用户态特权级,并回到用户态内存空间,使用用户态的代码段、数据段和堆栈,且能够跳转到用户进程的第一条指令执行,并确保在用户态能够响应中断

当创建一个用户态进程并加载了应用程序后,CPU是如何让这个应用程序最终在用户态执行起来的

  • 创建一个新的用户态进程后(可以参考一下proc:init_main),内核线程调用schedule函数,schedule函数在proc_list队列中查找下一个处于“就绪”态的用户态进程,然后调用proc_run来运行新进程。
  • 随后proc_run根据上一个实验的调用流程,找到了kernel_thread_entry。而kernel_thread_entry先将edx保存的输入参数(NULL)压栈,然后通过call指令,跳转到ebx指向的函数,即user_main,user_main先打印userproc的pid和name信息,然后调用kernel_execve
  • kernel_execve调用了exec系统调用,从而转入到了系统调用的处理例程。之后进行正常的中断处理例程,然后控制权转移到了syscall.c中的syscall函数,然后根据系统调用号转移给了sys_exec函数,在该函数中调用了do_execve函数来完成指定应用程序的加载。其调用流程为:vector128 -> __alltraps -> trap -> trap_dispatch -> syscall -> sys_exec -> do_execve。
  • 在do_execve中进行了若干设置,包括推出当前进程的页表,换用内核的PDT,调用load_icode函数完成对整个用户线程内存空间的初始化,包括堆栈的设置以及将ELF可执行文件的加载,之后通过current->tf指针修改了当前系统调用的trapframe,使得最终中断返回的时候能够切换到用户态,并且同时可以正确地将控制权转移到应用程序的入口处。
  • 在完成了do_exec函数之后,进行正常的中断返回的流程,由于中断处理例程的栈上面的eip已经被修改成了应用程序的入口处,而CS上的CPL是用户态,因此iret进行中断返回的时候会将堆栈切换到用户的栈,并且完成特权级的切换,并且跳转到要求的应用程序的入口处,开始执行应用程序的第一条代码。

0X02 父进程复制自己的内存空间给子进程

创建子进程的函数do_fork在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制。具体是通过copy_range函数(位于kern/mm/pmm.c中)实现的,我们需要完善这个函数,从而保证复制正常执行。

其调用栈为do_fork()---->copy_mm()---->dup_mmap()---->copy_range()。在lab4中,copy_mm是空的,因为内核线程常驻内存,不需要mm和调度,所以不需要设置子进程的mm,但是用户线程需要复制父进程的内存空间。

我们先看一下这些函数。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157

// copy_mm - process "proc" duplicate OR share process "current"'s mm according clone_flags
// - if clone_flags & CLONE_VM, then "share" ; else "duplicate"
static int copy_mm(uint32_t clone_flags, struct proc_struct *proc)
{
struct mm_struct *mm, *oldmm = current->mm;

/* current is a kernel thread */
// 内核线程不需要复制地址空间
if (oldmm == NULL)
{
return 0;
}

// 写时复制,目前没有,不会进入此if
if (clone_flags & CLONE_VM)
{
mm = oldmm;
goto good_mm;
}

// 一般情况下,进程复制
int ret = -E_NO_MEM;
// 创建子进程的mm
if ((mm = mm_create()) == NULL)
{
goto bad_mm;
}
// 设置子进程的页目录
if (setup_pgdir(mm) != 0)
{
goto bad_pgdir_cleanup_mm;
}

lock_mm(oldmm);
{
// 执行复制
ret = dup_mmap(mm, oldmm);
}
unlock_mm(oldmm);

if (ret != 0)
{
goto bad_dup_cleanup_mmap;
}

good_mm:
mm_count_inc(mm);
proc->mm = mm;
proc->cr3 = PADDR(mm->pgdir);
return 0;
bad_dup_cleanup_mmap:
exit_mmap(mm);
put_pgdir(mm);
bad_pgdir_cleanup_mm:
mm_destroy(mm);
bad_mm:
return ret;
}

// 复制mm的函数
int dup_mmap(struct mm_struct *to, struct mm_struct *from)
{
assert(to != NULL && from != NULL);
list_entry_t *list = &(from->mmap_list), *le = list;
while ((le = list_prev(le)) != list)
{
struct vma_struct *vma, *nvma;
vma = le2vma(le, list_link);
nvma = vma_create(vma->vm_start, vma->vm_end, vma->vm_flags);
if (nvma == NULL)
{
return -E_NO_MEM;
}

insert_vma_struct(to, nvma);

bool share = 0;
// 逐个复制其中的每个VMM
if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0)
{
return -E_NO_MEM;
}
}
return 0;
}

/* copy_range - copy content of memory (start, end) of one process A to another process B
* @to: the addr of process B's Page Directory
* @from: the addr of process A's Page Directory
* @share: flags to indicate to dup OR share. We just use dup method, so it didn't be used.
*
* CALL GRAPH: copy_mm-->dup_mmap-->copy_range
*/
int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share)
{
assert(start % PGSIZE == 0 && end % PGSIZE == 0);
assert(USER_ACCESS(start, end));
// copy content by page unit.
do
{
// call get_pte to find process A's pte according to the addr start
// 从父进程中的页表中找到对应的页表项
pte_t *ptep = get_pte(from, start, 0), *nptep;
if (ptep == NULL)
{
start = ROUNDDOWN(start + PTSIZE, PTSIZE);
continue;
}
// call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT
// 然后在子进程里找到对应的,找不到就新建一个,然后把父进程的复制过去
if (*ptep & PTE_P)
{
if ((nptep = get_pte(to, start, 1)) == NULL)
{
return -E_NO_MEM;
}
uint32_t perm = (*ptep & PTE_USER);
// get page from ptep
struct Page *page = pte2page(*ptep);
// alloc a page for process B
struct Page *npage = alloc_page();
assert(page != NULL);
assert(npage != NULL);
int ret = 0;
/* LAB5:EXERCISE2 YOUR CODE
* replicate content of page to npage, build the map of phy addr of nage with the linear addr start
*
* Some Useful MACROs and DEFINEs, you can use them in below implementation.
* MACROs or Functions:
* page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
* page_insert: build the map of phy addr of an Page with the linear addr la
* memcpy: typical memory copy function
*
* (1) find src_kvaddr: the kernel virtual address of page
* (2) find dst_kvaddr: the kernel virtual address of npage
* (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
* (4) build the map of phy addr of nage with the linear addr start
*/
// 复制page的内容到npage,建立npage的物理地址与线性地址开始的映射。

// 获取源(src)页面所在的虚拟地址(注意,此时的PDT是内核状态下的页目录表)
void *kva_src = page2kva(page);
// 获取目标(dst)页面所在的虚拟地址
void *kva_dst = page2kva(npage);
// 页面数据复制
memcpy(kva_dst, kva_src, PGSIZE);
// 将该页面设置至对应的PTE中
ret = page_insert(to, npage, start, perm);

assert(ret == 0);
}
start += PGSIZE;
} while (start != 0 && start < end);
return 0;
}

其实copy_range就是将进程A的内存复制到进程B里,不过这里并不是写时复制,而是全部复制,这样会浪费比较严重。

0X03 系统调用函数们

  • do_fork
    • 分配并初始化进程控制块(alloc_proc函数)。
    • 分配并初始化内核栈(setup_stack函数)。
    • 对当前进程等待状态的检查。
    • 根据clone_flag标志复制或共享进程内存管理结构(copy_mm函数)。
    • 设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread函数)。
    • 把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中,并且设置set_links。
    • 自此,进程已经准备好执行了,把进程状态设置为“就绪”态。
    • 设置返回码为子进程的id号。
  • do_execve
    • 调用exit_mmap(mm)和put_pgdir(mm)来回收当前进程内存空间的所有资源(仅保留了PCB)。
    • 调用load_icode根据可执行文件分配特定位置的虚拟内存空间。
    • 该函数会释放除了PCB以外所有的原进程的资源
  • do_wait
    • 检查当前进程分配的内存区域是否正常。
    • 查找特定/所有子进程中是否存在某个等待的父进程回收的子进程(PROC_ZOMBIE状态)。如果有则回收该进程返回,如果没有则设置当前进程为PROC_SLEEPING并执行schedule调度其他进程运行。当前进程的某个子进程结束运行后,当前进程会被唤醒,并在do_wait函数中回收子进程的PCB内存资源。
  • do_exit
    • 调用exit_mmap,put_pgdir和mm_destroy来释放进程几乎所有的内存空间。
    • 将进程状态设置为僵死模式,然后调用wakeup_proc(parent)来让父进程回收这个进程。
    • 调用scheduler来切换到其他进程。

这些函数是具体处理系统调用的函数,这些系统调用函数都被组织成数组,以快速访问。

1
2
3
4
5
6
7
8
9
10
11
12
13

static int (*syscalls[])(uint32_t arg[]) = {
[SYS_exit] sys_exit,
[SYS_fork] sys_fork,
[SYS_wait] sys_wait,
[SYS_exec] sys_exec,
[SYS_yield] sys_yield,
[SYS_kill] sys_kill,
[SYS_getpid] sys_getpid,
[SYS_putc] sys_putc,
[SYS_pgdir] sys_pgdir,
};

当系统调用中断发生后,会执行syscall函数,这个函数会从上述数组中选择出对应的系统调用以执行。

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

void syscall(void)
{
struct trapframe *tf = current->tf;
uint32_t arg[5];
int num = tf->tf_regs.reg_eax;
if (num >= 0 && num < NUM_SYSCALLS)
{
if (syscalls[num] != NULL)
{
arg[0] = tf->tf_regs.reg_edx;
arg[1] = tf->tf_regs.reg_ecx;
arg[2] = tf->tf_regs.reg_ebx;
arg[3] = tf->tf_regs.reg_edi;
arg[4] = tf->tf_regs.reg_esi;
// 从数组中选择出函数并执行
tf->tf_regs.reg_eax = syscalls[num](arg);
return;
}
}
print_trapframe(tf);
panic("undefined syscall %d, pid = %d, name = %s.\n",
num, current->pid, current->name);
}

等相应的内核函数结束后,程序通过之前保留的trapframe返回用户态。一次系统调用结束。

  • 在fork完成后,会修改子进程状态为可执行,但是父进程不变
  • exec不修改当前进程的状态,但是会替换内存空间的所有数据和代码
  • wait会先检测是否存在子进程,如果存在僵尸子进程,就回收;否则会阻塞父进程为睡眠状态,并等待子进程唤醒
  • exit会将当前进程的状态设置为僵尸状态,并唤醒父进程,使其为可运行状态

0X04 总结

通过这个lab5,我们学习了以下内容:

  • 学习用户进程和内核进程的区别
  • 了解在没有文件系统的情况下,用户线程是如何加载的(execve)
  • 系统调用的中断的处理,以及系统调用的实现
  • 创建用户子进程时的处理,包括加载用户程序,复制内存空间,复制trapframe等
  • 创建完成后如何进行中断返回从而到的对应的上下文中
  • fork是复制一个一模一样的线程、execve是执行另一个线程。本lab中是直接在内核线程中加载用户线程并切换到相应的上下文