0%

Ucore-Lab5

实验简述

  • 了解第一个用户进程创建过程
  • 了解系统调用框架的实现机制
  • 了解ucore如何实现系统调用 sys_fork,sys_exec,sys_exit,sys_wait 来进行进程管理

ucore在lab4中实现了进程/线程机制,能够创建并进行内核线程的调度,通过上下文的切换令线程分时的获得CPU,使得不同线程能够并发的运行

在lab5中需要更进一步,实现我们平常开发接触到的、运行在用户态的进程/线程机制

  • 用户线程通常用于承载和运行应用程序,为了保护操作系统内核,避免其被不够鲁棒的应用程序破坏,应用程序都运行在 低特权级 中,无法直接访问 高特权级 的内核数据结构,也无法通过程序指令直接的访问各种外设
  • 但应用程序访问高特权级数据、外设的需求是不可避免的,因此ucore在lab5中也实现了 系统调用机制 ,应用程序平常运行在用户态,在有需要时可以通过系统调用的方式间接的访问外设等受到保护的资源

在ucore lab5中,提供了一些用户态的demo应用程序,并在内核实现了诸如 fork、exit、kill、yield、wait 等等系统调用功能以及C实现的应用程序系统调用库

通过lab5的学习,可以更深入的了解操作系统中用户态应用程序的加载、运行和退出机制,以及系统调用的工作原理

系统调用

  • 系统调用是操作系统提供的一种特殊api接口,底层是通过中断实现的,应用程序调用系统中断时,其CPL特权级会被暂时的提升到ring0,因此便获得了访问外设、内核数据的能力
  • 这一提升CPL特权级从外层用户态到里层内核态的过程,也被称为陷入内核,系统调用会陷入内核,但是陷入内核的方式除了系统调用外,还包括触发保护异常等
  • 由于系统调用是操作系统的开发人员精心设计的,且对传入的参数等等有着很严格的控制,确保了系统调用不会对内核造成破坏,同时,在系统调用中断返回时,也会将其CPL特权级对应用程序透明的还原到用户态

ELF文件结构

因为后续要分析ELF文件加载到进程的过程,所以我们先了解一下ELF文件的结构(这里我们主要关注一下执行视图

值得注意的这一点是:

  • ELF头部程序头部表 是两个不同的东西
  • 前者用于描述整个 ELF 文件的信息
  • 后者是一个结构体数组,数组中的每个结构体元素是一个段表头(program header),每个程序头描述一个段(segment)

进程&文件&段

描述进程

proc_struct 结构体专门用于描述进程的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct proc_struct {
enum proc_state state; // 当前进程的状态
int pid; // 进程ID
int runs; // 当前进程被调度的次数
uintptr_t kstack; // 内核栈
volatile bool need_resched; // 是否需要被调度
struct proc_struct *parent; // 父进程ID
struct mm_struct *mm; // 当前进程所管理的虚拟内存页,包括其所属的页目录项PDT
struct context context; // 保存的进程上下文,用于进程切换
struct trapframe *tf; // 中断帧指针,指向内核栈的某个位置(保存有中断上下文)
uintptr_t cr3; // 页目录表的地址
uint32_t flags; // 当前进程的相关标志
char name[PROC_NAME_LEN + 1]; // 进程名称(可执行文件名)
list_entry_t list_link; // 进程链表
list_entry_t hash_link; // 进程哈希表
int exit_code; /* lab5新增:描述线程退出时的原因 */
uint32_t wait_state; /* lab5新增:描述线程进入wait阻塞态的原因 */
struct proc_struct *cptr, *yptr, *optr; /* lab5新增:用于组织子进程链表 */
/*
lab5新增:cptr(child ptr),指针指向当前进程的子进程中,最晚创建的那个子进程
lab5新增:yptr(younger sibling ptr),指向与当前进程共享同一个父进程,但比当前进程的创建时间更晚的进程(younger)
lab5新增:optr(older sibling ptr),指向与当前进程共享同一个父进程,但比当前进程的创建时间更早的进程(older)
*/
};

描述文件

当文件加载到内存时,CPU必须要执行 load_icode 函数来把该文件加载到进程,因此,load_icode 函数必须要获取文件的相关信息

elfhdr 结构体(也被称为ELF文件头,或者文件头)专门用于描述文件的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct elfhdr {
uint32_t e_magic; // 魔数(一定是魔数ELF_MAGIC-用于表示ELF文件)
uint8_t e_elf[12];
uint16_t e_type; // 文件的类型(1=可重新定位,2=可执行,3=共享对象,4=核心镜像)
uint16_t e_machine; // 处理器型号(3=x86, 4=68K,等等)
uint32_t e_version; // 文件版本(常常是"1")
uint32_t e_entry; // 文件入口点(如果是可执行文件)
uint32_t e_phoff; // 程序头表(段头表结构体数组)的文件位置
uint32_t e_shoff; // 节头的文件位置
uint32_t e_flags; // 特定于架构的标志(通常为0)
uint16_t e_ehsize; // 这个elf头的大小
uint16_t e_phentsize; // 程序头表中条目的大小
uint16_t e_phnum; // 程序头表中的条目数
uint16_t e_shentsize; // 节头中条目的大小
uint16_t e_shnum; // 节头中的条目数
uint16_t e_shstrndx; // 包含节名字符串的节号
};

描述段

ELF文件有多个段表,每个段表都有一个用于描述信息的段表头(program header),而各个段表头又组织在程序头表中

  • proghdr:专门用来描述段信息的结构体
1
2
3
4
5
6
7
8
9
10
struct proghdr {
uint32_t p_type; // 段类型(可加载的代码或数据、动态链接信息)
uint32_t p_offset; // 段的文件偏移量
uint32_t p_va; // 映射段的虚拟地址
uint32_t p_pa; // 物理地址(未使用)
uint32_t p_filesz; // 文件中段的大小:告诉elf文件中该可调节段的实际/精确大小
uint32_t p_memsz; // 内存中段的大小(如果包含bss,则更大):内存中段的总大小
uint32_t p_flags; // 读read/写write/执行execute 位
uint32_t p_align; // 所需的对齐方式(其值始终为硬件页面大小)
};

练习0-把 lab4 的内容复制粘贴到 lab5

不过相比 lab4,lab5 新添&修改了一些内容:

  • proc_struct:进程控制块结构体(上文已经提过)

  • idt_init:对中断描述符表进行初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static inline void
lidt(struct pseudodesc *pd) {
asm volatile ("lidt (%0)" :: "r" (pd) : "memory");
}

void
idt_init(void) {
extern uintptr_t __vectors[]; /* 声明:中断入口(每一项对应于中断描述符的中断服务例程的入口地址) */
int i;
for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
/* 遍历IDT数组(中断描述符表),将其中的内容(中断描述符)设置进IDT中断描述符表中(默认的DPL特权级(描述符特权级)都是内核态DPL_KERNEL-0) */
SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);
}
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
/* lab5新增:为T_SYSCALL(系统调用的中断描述符)设置用户态权限(DPL_USER) */
lidt(&idt_pd); /* IDT */
}
  • trap_dispatch:实现中断处理分发逻辑,也实现了对应的中断服务例程(用于处理T_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
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
#define T_PGFLT                 14  /* 页错误异常(page fault) */

#define IRQ_OFFSET 32 // 中断请求偏移(Interrupt Request,IRQ)
#define IRQ_TIMER 0 // 中断请求-计时器中断
#define IRQ_KBD 1 // 中断请求-键盘中断
#define IRQ_COM1 4 // 中断请求-串口中断
#define IRQ_IDE1 14 // 中断请求-IDE通道1中断
#define IRQ_IDE2 15 // 中断请求-IDE通道2中断
#define IRQ_ERROR 19
#define IRQ_SPURIOUS 31

#define T_SYSCALL 0x80 /* 系统调用(syscall) */

#define T_SWITCH_TOU 120 /* 中断请求-内核到用户空间触发的中断 */
#define T_SWITCH_TOK 121 /* 中断请求-用户空间到内核触发的中断 */

static void
trap_dispatch(struct trapframe *tf) {
/* 根据trapframe中断帧中的标志位,来执行具体的中断服务历程 */
char c;
int ret=0;
switch (tf->tf_trapno) {
case T_PGFLT: /* 页错误异常(page fault) */
if ((ret = pgfault_handler(tf)) != 0) {
print_trapframe(tf);
/* lab5新增:与进程相关的if判断语句 */
if (current == NULL) {
panic("handle pgfault failed. ret=%d\n", ret);
}
else {
if (trap_in_kernel(tf)) {
panic("handle pgfault failed in kernel mode. ret=%d\n", ret);
}
cprintf("killed by kernel.\n");
panic("handle user mode pgfault failed. ret=%d\n", ret);
do_exit(-E_KILLED);
}
}
break;
case T_SYSCALL: /* 系统调用(syscall) */
syscall();
break;
case IRQ_OFFSET + IRQ_TIMER: /* 中断请求-计时器中断 */
#if 0
LAB3 : If some page replacement algorithm(such as CLOCK PRA) need tick to change the priority of pages,
then you can add code here.
#endif
ticks ++; /* lab5新增:用于记录"中断请求-计时器中断"执行的次数 */
if (ticks % TICK_NUM == 0) { /* 当前进程的时间片已用完,需要重新调度 */
assert(current != NULL);
current->need_resched = 1; /* 设置需要重新调度 */
}
break;
case IRQ_OFFSET + IRQ_COM1: /* 中断请求-串口中断 */
c = cons_getc();
cprintf("serial [%03d] %c\n", c, c);
break;
case IRQ_OFFSET + IRQ_KBD: /* 中断请求-键盘中断 */
c = cons_getc();
cprintf("kbd [%03d] %c\n", c, c);
break;
case T_SWITCH_TOU: /* 中断请求-内核到用户空间触发的中断 */
case T_SWITCH_TOK: /* 中断请求-用户空间到内核触发的中断 */
panic("T_SWITCH_** ??\n");
break;
case IRQ_OFFSET + IRQ_IDE1: /* 中断请求-IDE通道1中断 */
case IRQ_OFFSET + IRQ_IDE2: /* 中断请求-IDE通道2中断 */
/* 本实验不涉及这一部分 */
break;
default:
/* lab5改动:完善了报错处理 */
print_trapframe(tf); /* 打印trapframe结构体(中断帧,用于存储执行中断的信息) */
if (current != NULL) {
cprintf("unhandled trap.\n");
do_exit(-E_KILLED);
}
panic("unexpected trap in kernel.\n");
}
}
  • alloc_proc:分配一个 proc_struct,用于描述进程的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct)); /* 进行分配 */
if (proc != NULL) { /* 对相关proc_struct结构体条目进行初始化 */
proc->state = PROC_UNINIT;
proc->pid = -1;
proc->runs = 0;
proc->kstack = 0;
proc->need_resched = 0;
proc->parent = NULL;
proc->mm = NULL;
memset(&(proc->context), 0, sizeof(struct context));
proc->tf = NULL;
proc->cr3 = boot_cr3;
proc->flags = 0;
memset(proc->name, 0, PROC_NAME_LEN);
proc->wait_state = 0; /* lab5新增:设置进程为等待态 */
proc->cptr = proc->optr = proc->yptr = NULL; /* lab5新增:进程的兄弟父母节点为空 */
}
return proc;
}
  • do_fork:创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同,在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态
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
int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
int ret = -E_NO_FREE_PROC;
struct proc_struct *proc;
if (nr_process >= MAX_PROCESS) {
goto fork_out;
}
ret = -E_NO_MEM;
if ((proc = alloc_proc()) == NULL) { /* 分配一个物理页,作为proc_struct */
goto fork_out;
}

proc->parent = current; /* 设置父进程为当前进程 */
assert(current->wait_state == 0); /* lab5新增:断言进程为等待态 */

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(); /* 为进程分配唯一的PID */
hash_proc(proc); /* 将proc添加到进程哈希链表中 */
set_links(proc); /* lab5改动:取消list_add,采用set_links */
}
local_intr_restore(intr_flag); /* 解除中断的阻塞 */
wakeup_proc(proc); /* 设置新的子进程可执行(唤醒该进程) */
ret = proc->pid; /* 设置返回值为pid */

fork_out:
return ret;

bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}

static void /* lab5新增:set_links就是在list_add的基础上,对部分proc_struct条目进行了初始化 */
set_links(struct proc_struct *proc) {
list_add(&proc_list, &(proc->list_link)); /* 照常插链 */
proc->yptr = NULL; /* 后续对进程结构体proc_struct进行设置 */
if ((proc->optr = proc->parent->cptr) != NULL) {
proc->optr->yptr = proc; /* 链入子进程链表 */
}
proc->parent->cptr = proc;
nr_process ++;
}

练习1-加载应用程序并执行

do_execv 函数调用 load_icode 来加载并解析一个处于内存中的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
#define ELF_PT_LOAD                     1

static int
load_icode(unsigned char *binary, size_t size) {
// binary:"已经加载到内存中的文件"的头文件(ELF格式)
// size:"已经加载到内存中的文件"的大小

if (current->mm != NULL) { /* 检查当前进程是否为NULL */
panic("load_icode: current->mm must be empty.\n");
}

int ret = -E_NO_MEM;
struct mm_struct *mm; /* mm_struct结构体:用于描述虚拟内存区域(vma)的各种信息 */
if ((mm = mm_create()) == NULL) { /* 创建一片虚拟内存 */
goto bad_mm; /* 创建失败,直接返回 */
}
if (setup_pgdir(mm) != 0) { /* 新建一个页目录表,每个进程都需要一个页目录表 */
goto bad_pgdir_cleanup_mm; /* 创建失败,执行mm_destroy */
}

struct Page *page;
struct elfhdr *elf = (struct elfhdr *)binary; /* 获取的二进制文件的文件头 */
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff); /* 获取二进制文件的程序头表入口 */

if (elf->e_magic != ELF_MAGIC) { /* 检查该程序的魔数是否正确 */
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}

/* <---- 遍历程序头表,并构建vma ----> */

uint32_t vm_flags, perm;
struct proghdr *ph_end = ph + elf->e_phnum;
/* ph_end:计算出程序头表的结尾地址 */
/* elf->e_phnum:程序头表中的条目数 */
for (; ph < ph_end; ph ++) { /* 遍历整个程序头表(ph就是各个段头表) */
if (ph->p_type != ELF_PT_LOAD) {
/* 遍历寻找到ELF_PT_LOAD为止,在ucore中,该段是TEXT/DATA */
continue ;
}
if (ph->p_filesz > ph->p_memsz) {
/* 文件中段的大小 > 内存中段的大小 */
/* 内存中p_memsz大于p_filesz的原因是,可加载段可能包含一个.bss部分,没有此部分则是等于状态,绝对不可能是小于状态 */
ret = -E_INVAL_ELF;
goto bad_cleanup_mmap; /* 调用exit_mmap */
}
if (ph->p_filesz == 0) {
continue ;
}

/* <---- 根据标志位进行初始化,准备构建vma ----> */

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) {
/* 调用mm_map,为目标段构建新的vma */
goto bad_cleanup_mmap; /* 调用exit_mmap */
}

/* <---- 建立并分配页目录表,复制TEXT/DATA段到进程的内存(建立映射) ----> */

unsigned char *from = binary + ph->p_offset; /* 获取TEXT/DATA的段地址 */
size_t off, size;
uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);
/* start:初始化为段起始地址(映射段的虚拟地址) */
/* la(可变参数):start进行内存页对齐后(只舍不进)的地址 */

ret = -E_NO_MEM;
end = ph->p_va + ph->p_filesz;
/* end:初始化为段结束地址(映射段的虚拟地址+文件中段的大小) */

while (start < end) { /* 持续为pgdir分配页表,直到整个段都完成映射 */
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
/* 分配一块物理页(作为页表),设置页表项(对应la),插入页表目录(pgdir) */
goto bad_cleanup_mmap; /* 调用exit_mmap */
}
off = start - la; /* 更新偏移 */
/* 第一次: off='start为了页对齐而舍弃的数值'(正) */
/* 后续: off='0' */
size = PGSIZE - off; /* 更新已分配的段大小(每次增加PGSIZE) */
la += PGSIZE; /* 更新当前的物理地址(每次增加PGSIZE) */

if (end < la) {
size -= la - end; /* 获取准确的段大小 */
}
memcpy(page2kva(page) + off, from, size);
/* 获取页目录表的虚拟地址,通过off计算出对应页目录表项,用memcpy在其中填入from(TEXT/DATA段的起始地址) */
start += size;
from += size;
/* 第一次: start增加的值比la小一些 */
/* 后续: start和la都增加相同的值(PGSIZE),并且地址也相同 */
}

/* <---- 分配内存,建立并分配页目录表,建立BSS段(建立映射) ----> */

end = ph->p_va + ph->p_memsz; /* end:初始化为段结束地址(映射段的虚拟地址+内存中段的大小) */
if (start < la) { /* start最后会小于等于la,以下代码就是为了当"start<la"时,实现"start=la",并且置空原start距新start多出的部分 */
if (start == end) {
continue ;
}
off = start + PGSIZE - la, size = PGSIZE - off;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
/* 获取页目录表的虚拟地址,通过off计算出对应页目录表项,并使用memset置空 */
start += size;
assert((end < la && start == end) || (end >= la && start == la));
}
while (start < end) { /* 持续为pgdir分配页表,直到整个段都完成映射 */
if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
goto bad_cleanup_mmap; /* 调用exit_mmap */
}
off = start - la, size = PGSIZE - off, la += PGSIZE;
if (end < la) {
size -= la - end;
}
memset(page2kva(page) + off, 0, size);
/* 获取页目录表的虚拟地址,通过off计算出对应页目录表项,并使用memset置空 */
start += size;
}
}

/* <---- 构建用户堆栈内存 ----> */

vm_flags = VM_READ | VM_WRITE | VM_STACK;
if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) {
goto bad_cleanup_mmap; /* 调用exit_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);

/* <---- 设置当前进程的mm,sr3,设置CR3寄存器 ----> */

mm_count_inc(mm); /* 设置并返回"共享该虚拟内存空间mva的进程数" */
current->mm = mm; /* 设置当前进程的"proc_struct->mm"为该虚拟内存空间mva */
current->cr3 = PADDR(mm->pgdir); /* 设置当前进程的"proc_struct->cr3"为该页目录表的地址 */
lcr3(PADDR(mm->pgdir)); /* 设置CR3寄存器为当前页目录表的物理地址 */

/* <---- 为用户环境设置trapframe ----> */

struct trapframe *tf = current->tf;
memset(tf, 0, sizeof(struct trapframe)); /* 把trapframe清零 */

/* <---- lab start ----> */

/* 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
*/

/* <---- lab end ----> */

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; /* 直接返回 */
}

其他函数的代码:

  • mm_create:创建一片虚拟内存,完成各个条目的初始化
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
struct mm_struct {
list_entry_t mmap_list; /* 按vma的起始地址排序的线性列表链接(vma链表) */
struct vma_struct *mmap_cache; /* 当前访问的vma,用于速度目的 */
pde_t *pgdir; /* 这些vma的PDT(页目录表) */
int map_count; /* 这些vma的计数 */
void *sm_priv; /* 用于指向swap manager的某个链表 */
int mm_count; /* 共享mm的进程数 */
lock_t mm_lock; /* 关于锁的标记 */
};

struct mm_struct *
mm_create(void) {
struct mm_struct *mm = kmalloc(sizeof(struct mm_struct)); /* 申请物理内存 */

if (mm != NULL) {
list_init(&(mm->mmap_list)); /* vma链表初始化 */
mm->mmap_cache = NULL;
mm->pgdir = NULL;
mm->map_count = 0;

if (swap_init_ok) swap_init_mm(mm); /* 如果swap_manager程序初始化完毕,就对mm进行swap初始化 */
else mm->sm_priv = NULL; /* 否则禁用swap链表(可交换的已分配物理页链表) */

set_mm_count(mm, 0); /* 设置共享进程的数目为0 */
lock_init(&(mm->mm_lock)); /* 初始化锁 */
}
return mm;
}

static inline void
list_init(list_entry_t *elm) {
elm->prev = elm->next = elm; /* 把prev和next都设置为它自己 */
}

static inline void
set_mm_count(struct mm_struct *mm, int val) {
mm->mm_count = val; /* 设置共享进程的数目为val */
}

static inline void
lock_init(lock_t *lock) {
*lock = 0; /* 初始化锁 */
}
  • mm_destroy:遍历并释放 vma 链表中的所有 vma,最后释放 mm
1
2
3
4
5
6
7
8
9
10
11
12
void
mm_destroy(struct mm_struct *mm) {
assert(mm_count(mm) == 0);

list_entry_t *list = &(mm->mmap_list), *le;
while ((le = list_next(list)) != list) { /* 遍历并释放vma链表中的所有vma */
list_del(le);
kfree(le2vma(le, list_link));
}
kfree(mm);
mm=NULL;
}
  • setup_pgdir:申请一个页目录表
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
#define PGSHIFT         12      		 // 1<<12=4096(一页的大小)
struct Page *pages; // 页目录表首地址
pde_t *boot_pgdir = &__boot_pgdir; // 这里的boot_pgdir可能是所有pgdir的模板

struct Page {
int ref; // 当前页被引用的次数,与内存共享有关
uint32_t flags; // 标志位的集合,与eflags寄存器类似
unsigned int property; // 空闲的连续page数量,这个成员只会用在连续空闲page中的第一个page
list_entry_t page_link; // 两个分别指向上一个和下一个非连续空闲页的两个指针
list_entry_t pra_page_link; // 用于连接上一个和下一个"可交换已分配"的物理页
uintptr_t pra_vaddr; // 用于保存该物理页所对应的虚拟地址
};

static int
setup_pgdir(struct mm_struct *mm) {
struct Page *page;
if ((page = alloc_page()) == NULL) { /* 分配物理页,作为页目录表 */
return -E_NO_MEM;
}
pde_t *pgdir = page2kva(page); /* 获取页目录表的虚拟地址 */
memcpy(pgdir, boot_pgdir, PGSIZE); /* 把boot_pgdir拷贝到pgdir(这里两边都是虚拟地址,C语言的'&'符号用于获取虚拟地址) */
pgdir[PDX(VPT)] = PADDR(pgdir) | PTE_P | PTE_W; /* PADDR转为物理地址后装入对应的页目录表项 */
mm->pgdir = pgdir; /* 更新mm_struct结构体中的pgdir条目 */
return 0;
}

static inline ppn_t
page2ppn(struct Page *page) {
return page - pages; /* 获取对应page管理的那一页的索引(物理地址偏移) */
/* 其中pages可以认为是存储所有struct Page的首地址(页目录表) */
}

static inline uintptr_t
page2pa(struct Page *page) {
return page2ppn(page) << PGSHIFT; /* 得到"page2ppn(page)"的物理地址 */
}

static inline void *
page2kva(struct Page *page) {
return KADDR(page2pa(page)); /* 返回物理地址"page2pa(page)"对应的虚拟地址 */
}
  • mm_count_inc:设置并返回“共享该虚拟内存空间mva的进程数”
1
2
3
4
5
static inline int
mm_count_inc(struct mm_struct *mm) {
mm->mm_count += 1; /* 共享该虚拟内存空间mva的进程数增加 */
return mm->mm_count;
}

本实验的目的就是叫我们完善 load_icode 函数,代码很长很复杂,不过我都解析完了,可以参考上面的注释

其实 load_icode 函数就只有一个地方没有完成了:为用户环境设置 trapframe

下面是实现代码:(根据提示初始化几个数值就可以了)

1
2
3
4
5
6
7
8
struct trapframe *tf = current->tf; /* 构建中断帧 */
memset(tf, 0, sizeof(struct trapframe)); /* 初始化置空为'0' */
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;

练习2-父进程复制自己的内存空间给子进程

创建子进程的函数 do_fork 在执行中将拷贝当前进程(即父进程)的用户内存地址空间中的合法内容到新进程中(子进程),完成内存资源的复制,具体是通过 copy_range 函数实现的,请补充 copy_range 的实现,确保能够正确执行

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
int /* 将一个进程A的内存内容(start,end)复制到另一个进程B */
copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
// to:进程B(子进程)页面目录的地址
// from:进程A(父进程)页面目录的地址
// start:需要复制的复制起始地址
// end:需要复制的结束地址
// share:指示dup或share的标志(我们只是使用dup方法,所以没有使用)

assert(start % PGSIZE == 0 && end % PGSIZE == 0); /* 断言start和end已经对齐 */
assert(USER_ACCESS(start, end)); /* 断言start和end在合理的范围 */
do {
pte_t *ptep = get_pte(from, start, 0), *nptep;
/* 找到进程A的二级页表项的虚拟地址 */
if (ptep == NULL) { /* 如果没有找到,则跳转下一页继续 */
start = ROUNDDOWN(start + PTSIZE, PTSIZE);
continue ;
}
if (*ptep & PTE_P) { /* 表示物理内存页存在 */
if ((nptep = get_pte(to, start, 1)) == NULL) {
/* 找到进程B的二级页表项的虚拟地址 */
return -E_NO_MEM;
}
uint32_t perm = (*ptep & PTE_USER); /* 标记用户状态,获取对应的物理地址 */
struct Page *page = pte2page(*ptep); /* 根据二级页表项获取一张物理页 */
struct Page *npage= alloc_page(); /* 分配一张物理页 */
assert(page!=NULL);
assert(npage!=NULL);
int ret=0;

/* <---- lab start ----> */

/* 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
*/

/* <---- lab end ----> */

assert(ret == 0);
}
start += PGSIZE; /* 更新start,继续参与循环 */
} while (start != 0 && start < end);
return 0;
}
  • copy_range 函数的目的是把A进程(from,父进程)的内存资源,拷贝到B进程(to,子进程)
  • 它选择在循环中,以页为单位对进程内容进行复制(和 load_icode 建立段映射的逻辑很像)
  • 复制需要使用 memcpy ,而这个函数需要虚拟地址,程序已经获取了对应的物理页地址,所以我们只需要调用 page2kva 即可
  • 最后需要把已经复制完成的物理页添加到对应的页目录表项中,需要调用 page_insert ,而它的参数我们都已知

具体实现:

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
int /* 将一个进程A的内存内容(start,end)复制到另一个进程B */
copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {

assert(start % PGSIZE == 0 && end % PGSIZE == 0); /* 断言start和end已经对齐 */
assert(USER_ACCESS(start, end)); /* 断言start和end在合理的范围 */
do {
pte_t *ptep = get_pte(from, start, 0), *nptep;
/* 找到进程A的二级页表项的虚拟地址 */
if (ptep == NULL) { /* 如果没有找到,则跳转下一页继续 */
start = ROUNDDOWN(start + PTSIZE, PTSIZE);
continue ;
}
if (*ptep & PTE_P) { /* 表示物理内存页存在 */
if ((nptep = get_pte(to, start, 1)) == NULL) {
/* 创建进程B的二级页表项,并返回其虚拟地址 */
return -E_NO_MEM;
}
uint32_t perm = (*ptep & PTE_USER); /* 标记用户状态,获取对应的权限 */
struct Page *page = pte2page(*ptep); /* 根据二级页表项获取一张物理页 */
struct Page *npage= alloc_page(); /* 分配一张物理页 */
assert(page!=NULL);
assert(npage!=NULL);
int ret=0;

/* <---- lab start ----> */

void * kva_src = page2kva(page); /* 获取源页面所在的虚拟地址 */
void * kva_dst = page2kva(npage); /* 获取目标页面所在的虚拟地址 */
memcpy(kva_dst, kva_src, PGSIZE); /* 页面数据复制 */
ret = page_insert(to, npage, start, perm); /* 将该页面设置至对应的PTE中 */

/* <---- lab end ----> */

assert(ret == 0);
}
start += PGSIZE; /* 更新start,继续参与循环 */
} while (start != 0 && start < end);
return 0;
}

练习3-理解 fork/exec/wait/exit/syscall 的实现

  • do_fork:创建当前内核线程的一个副本
    • 它们的执行上下文、代码、数据都一样,但是存储位置不同,在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态(已经实现)
    • 这个函数我们已经实现了,详情可以看前面代码
  • do_execve:可执行程序的加载和运行
    • 检查当前进程所分配的内存区域是否存在异常
    • 回收当前进程的所有资源,包括已分配的内存空间/页目录表等等
    • 读取可执行文件,并根据 ELFheader 分配特定位置的虚拟内存,并加载代码与数据至特定的内存地址,最后分配堆栈并设置 trapframe 属性
    • 设置新进程名称
    • do_execve 本身完成的这4步操作都是一些简单的“边角料”,真正核心且复杂的 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
int
do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
struct mm_struct *mm = current->mm;

/* 检查当前进程所分配的内存区域是否存在异常 */
if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
return -E_INVAL;
}
if (len > PROC_NAME_LEN) {
len = PROC_NAME_LEN;
}

/* 回收当前进程的所有资源,包括已分配的内存空间/页目录表等等 */
char local_name[PROC_NAME_LEN + 1];
memset(local_name, 0, sizeof(local_name));
memcpy(local_name, name, len);

if (mm != NULL) {
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
exit_mmap(mm);
put_pgdir(mm);
mm_destroy(mm);
}
current->mm = NULL;
}
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);
}
  • do_wait:程序会使某个进程一直等待,直到(特定)子进程退出后,该进程才会回收该子进程的资源并函数返回,该函数的具体操作如下:
    • 检查当前进程所分配的内存区域是否存在异常
    • 查找特定/所有子进程中是否存在某个等待父进程回收的子进程(PROC_ZOMBIE)
      • 如果有,则回收该进程并函数返回
      • 如果没有,则设置当前进程状态为 PROC_SLEEPING 并执行 schedule 调度其他进程
      • 当该进程的某个子进程结束运行后,当前进程会被唤醒,并在 do_wait 函数中回收子进程的PCB内存资源
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
int
do_wait(int pid, int *code_store) {
struct mm_struct *mm = current->mm;
if (code_store != NULL) {
if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) {
return -E_INVAL;
}
}

struct proc_struct *proc;
bool intr_flag, haskid;
repeat:
haskid = 0;
if (pid != 0) {
proc = find_proc(pid);
if (proc != NULL && proc->parent == current) {
haskid = 1;
if (proc->state == PROC_ZOMBIE) {
goto found;
}
}
}
else {
proc = current->cptr;
for (; proc != NULL; proc = proc->optr) {
haskid = 1;
if (proc->state == PROC_ZOMBIE) {
goto found;
}
}
}
if (haskid) {
current->state = PROC_SLEEPING;
current->wait_state = WT_CHILD;
schedule();
if (current->flags & PF_EXITING) {
do_exit(-E_KILLED);
}
goto repeat;
}
return -E_BAD_PROC;

found:
if (proc == idleproc || proc == initproc) {
panic("wait idleproc or initproc.\n");
}
if (code_store != NULL) {
*code_store = proc->exit_code;
}
local_intr_save(intr_flag);
{
unhash_proc(proc);
remove_links(proc);
}
local_intr_restore(intr_flag);
put_kstack(proc);
kfree(proc);
return 0;
}
  • do_exit:退出操作
    • 回收所有内存(除了PCB,该结构只能由父进程回收)
    • 设置当前的进程状态为 PROC_ZOMBIE
    • 设置当前进程的退出值 current->exit_code
    • 如果有父进程,则唤醒父进程,使其准备回收该进程的PCB
      • 正常情况下,除了 initprocidleproc 以外,其他进程一定存在父进程
    • 如果当前进程存在子进程,则设置所有子进程的父进程为 initproc
      • 这样倘若这些子进程进入结束状态,则 initproc 可以代为回收资源
    • 执行进程调度,一旦调度到当前进程的父进程,则可以马上回收该终止进程的 PCB
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
int
do_exit(int error_code) {
if (current == idleproc) {
panic("idleproc exit.\n");
}
if (current == initproc) {
panic("initproc exit.\n");
}

struct mm_struct *mm = current->mm;
if (mm != NULL) {
lcr3(boot_cr3);
if (mm_count_dec(mm) == 0) {
exit_mmap(mm);
put_pgdir(mm);
mm_destroy(mm);
}
current->mm = NULL;
}
current->state = PROC_ZOMBIE;
current->exit_code = error_code;

bool intr_flag;
struct proc_struct *proc;
local_intr_save(intr_flag);
{
proc = current->parent;
if (proc->wait_state == WT_CHILD) {
wakeup_proc(proc);
}
while (current->cptr != NULL) {
proc = current->cptr;
current->cptr = proc->optr;

proc->yptr = NULL;
if ((proc->optr = initproc->cptr) != NULL) {
initproc->cptr->yptr = proc;
}
proc->parent = initproc;
initproc->cptr = proc;
if (proc->state == PROC_ZOMBIE) {
if (initproc->wait_state == WT_CHILD) {
wakeup_proc(initproc);
}
}
}
}
local_intr_restore(intr_flag);

schedule();
panic("do_exit will not return!! %d.\n", current->pid);
}
  • syscall:系统调用
    • syscall 是内核程序为用户程序提供内核服务的一种方式
    • 在用户程序中,若需用到内核服务,则需要执行 sys_xxxx 函数(例如sys_kill
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static inline int
syscall(int num, ...) {
va_list ap;
va_start(ap, num);
uint32_t a[MAX_ARGS];
int i, ret;
for (i = 0; i < MAX_ARGS; i ++) {
a[i] = va_arg(ap, uint32_t);
}
va_end(ap);

asm volatile (
"int %1;"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a[0]),
"c" (a[1]),
"b" (a[2]),
"D" (a[3]),
"S" (a[4])
: "cc", "memory");
return ret;
}
  • 该函数会设置 %eax, %edx, %ecx, %ebx, %edi, %esi 五个寄存器的值
  • 分别为 syscall调用号、参数1、参数2、参数3、参数4、参数5
  • 然后执行int中断进入中断处理例程
  • 在中断处理例程中:程序会根据中断号执行 syscall 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void /* 和前面那个syscall同名,但不是同一个函数 */
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);
}