MIT 6.828学习笔记

  1. 第0章 操作系统接口

以下包含 xv6-book阅读笔记以及xv6源码剖析

第0章 操作系统接口

xv6使用了传统的内核概念: 内核是一个向其他运行中程序提供服务的特殊程序. 每一个运行中程序(进程)都包含指令,数据,栈的内存空间. 指令实现程序运算, 数据是用于运算过程的变量, 栈管理程序的过程调用.

进程通过系统调用使用内核服务,系统调用进入内核,内核执行服务并返回.进程总是在用户空间内核空间之间交替运行.

内核使用CPU__硬件保护机制__保证用户进程只能访问自己的内存空间.内核有实现保护机制的硬件权限而用户程序没有.

用户程序进行系统调用时,硬件会提升特权级来执行一些内核中定义的功能.

xv6的进程包含两部分: 1. 用户内存空间(指令,数据,栈) 2. 仅内核可见的进程状态.

分时特性: 在可用CPU中不断切换,决定哪一个等待中的进程被执行.当一个进程不在进行时,xv6会保存它CPU寄存器,当再次执行时恢复寄存器的值. 内核将进程和一个pid(process identifier)关联起来.

一个进程可以通过fork()创建一个子进程,一个进程调用fork() 后, 系统优先给新进程分配资源, 然后把原来进程的全部资源都复制到新的进程中, 然后寄存器指向新进程. fork()函数在父进程,子进程中都返回一个值(一次调用两次返回,子进程的返回值是在子进程的栈中构造好数据之后从栈中获取), 对于父进程返回pid,对子进程返回0.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int pid
pid = fork();

if (pid > 0) {
printf("parent: child="%d"\n", pid);
pid = wait();
printf("child %d is done\n", pid);
} else if (pid == 0) {
printf("child: exiting\n");
} else {
printf("fork error\n");
}
//输出结果
//parent: child=1234
//child: exiting

在执行fork()函数之前,只有一个进程这段代码, 调用后就成为了两个进程在执行.因此父进程中的pid被赋值1234(随便写的),子进程的pid被赋值0,所以就打印了这样的一个结果.

这个过程可以理解为链表, 父进程的pid(pointer)指向子进程的pid,子进程的子进程等于NULL,所以子进程的pid==0.

补充: 子进程拥有自己独立的内存空间, 在fork()函数执后开始执行下面的内容.

fork的实现分为两步: 1. 复制当前进程资源 2.执行fork出的进程

复制进程的资源包括以下几步: 1. 进程pid 2.程序体(即代码数据段) 3.用户栈 4.内核栈 5.虚拟内存池 6.页表

代码入下:

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
// 将父进程的pcb、虚拟地址位图拷贝给子进程

static int32_t copy_pcb_vaddrbitmap_stack0(task_struct *child_thread, task_struct *parent_thread) {
/* a 复制pcb所在的整个页,里面包含进程pcb信息及特级0极的栈,里面包含了返回地址, 然后再单独修改个别部分 */
memcpy(child_thread, parent_thread, PG_SIZE);
child_thread->pid = fork_pid();
child_thread->elapsed_ticks = 0;
child_thread->status = TASK_READY;
child_thread->ticks = child_thread->priority; // 为新进程把时间片充满
child_thread->parent_pid = parent_thread->pid;
child_thread->general_tag.prev = child_thread->general_tag.next = NULL;
child_thread->all_list_tag.prev = child_thread->all_list_tag.next = NULL;
block_desc_init(child_thread->u_block_desc);
/* b 复制父进程的虚拟地址池的位图 */
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
void *vaddr_btmp = get_kernel_pages(bitmap_pg_cnt);
if (vaddr_btmp == NULL)
return -1;
/* 此时child_thread->userprog_vaddr.vaddr_bitmap.bits还是指向父进程虚拟地址的位图地址
* 下面将child_thread->userprog_vaddr.vaddr_bitmap.bits指向自己的位图vaddr_btmp */
memcpy(vaddr_btmp, child_thread->userprog_vaddr.vaddr_bitmap.bits, bitmap_pg_cnt * PG_SIZE);
child_thread->userprog_vaddr.vaddr_bitmap.bits = vaddr_btmp;

ASSERT(strlen(child_thread->name) < 11); // pcb.name的长度是16,为避免下面strcat越界
strcat(child_thread->name, "_fork");
return 0;
}

// 复制子进程的进程体(代码和数据)及用户栈
static void copy_body_stack3(task_struct *child_thread, task_struct *parent_thread, void *buf_page) {
uint8_t *vaddr_btmp = parent_thread->userprog_vaddr.vaddr_bitmap.bits;
uint32_t btmp_bytes_len = parent_thread->userprog_vaddr.vaddr_bitmap.btmp_bytes_len;
uint32_t vaddr_start = parent_thread->userprog_vaddr.vaddr_start;
uint32_t idx_byte = 0;
uint32_t idx_bit = 0;
uint32_t prog_vaddr = 0;

/* 在父进程的用户空间中查找已有数据的页 */
while (idx_byte < btmp_bytes_len) {
if (vaddr_btmp[idx_byte]) {
idx_bit = 0;
while (idx_bit < 8) {
if ((BITMAP_MASK << idx_bit) & vaddr_btmp[idx_byte]) {
prog_vaddr = (idx_byte * 8 + idx_bit) * PG_SIZE + vaddr_start;
/* 下面的操作是将父进程用户空间中的数据通过内核空间做中转,最终复制到子进程的用户空间 */

/* a 将父进程在用户空间中的数据复制到内核缓冲区buf_page,
目的是下面切换到子进程的页表后,还能访问到父进程的数据*/
memcpy(buf_page, (void *)prog_vaddr, PG_SIZE);

/* b 将页表切换到子进程,目的是避免下面申请内存的函数将pte及pde安装在父进程的页表中 */
page_dir_activate(child_thread);
/* c 申请虚拟地址prog_vaddr */
get_a_page_without_opvaddrbitmap(PF_USER, prog_vaddr);

/* d 从内核缓冲区中将父进程数据复制到子进程的用户空间 */
memcpy((void *)prog_vaddr, buf_page, PG_SIZE);

/* e 恢复父进程页表 */
page_dir_activate(parent_thread);
}
idx_bit++;
}
}
idx_byte++;
}
}


// 为子进程构建thread_stack和修改返回值
static int32_t build_child_stack(task_struct *child_thread) {
/* a 使子进程pid返回值为0 */
/* 获取子进程0级栈栈顶 */
intr_stack *intr_0_stack = (intr_stack *)((uint32_t)child_thread + PG_SIZE - sizeof(intr_stack));
/* 修改子进程的返回值为0 */
intr_0_stack->eax = 0;

/* b 为switch_to 构建 struct thread_stack,将其构建在紧临intr_stack之下的空间*/
uint32_t *ret_addr_in_thread_stack = (uint32_t *)intr_0_stack - 1;

/* ebp在thread_stack中的地址便是当时的esp(0级栈的栈顶),
即esp为"(uint32_t*)intr_0_stack - 5" */
uint32_t *ebp_ptr_in_thread_stack = (uint32_t *)intr_0_stack - 5;

/* switch_to的返回地址更新为intr_exit,直接从中断返回 */
*ret_addr_in_thread_stack = (uint32_t)intr_exit;

/* 把构建的thread_stack的栈顶做为switch_to恢复数据时的栈顶 */
child_thread->self_kstack = ebp_ptr_in_thread_stack;
return 0;
}
// 拷贝父进程本身所占资源给子进程
static int32_t copy_process(task_struct *child_thread, task_struct *parent_thread) {
/* 内核缓冲区,作为父进程用户空间的数据复制到子进程用户空间的中转 */
void *buf_page = get_kernel_pages(1);
if (buf_page == NULL) {
return -1;
}

/* a 复制父进程的pcb、虚拟地址位图、内核栈到子进程 */
if (copy_pcb_vaddrbitmap_stack0(child_thread, parent_thread) == -1) {
return -1;
}

/* b 为子进程创建页表,此页表仅包括内核空间 */
child_thread->pgdir = create_page_dir();
if (child_thread->pgdir == NULL) {
return -1;
}

/* c 复制父进程进程体及用户栈给子进程 */
copy_body_stack3(child_thread, parent_thread, buf_page);

/* d 构建子进程thread_stack和修改返回值pid */
build_child_stack(child_thread);

/* e 更新文件inode的打开数 */
update_inode_open_cnts(child_thread);

mfree_page(PF_KERNEL, buf_page, 1);
return 0;
}


// fork子进程,内核线程不可直接调用
pid_t sys_fork(void) {
task_struct *parent_thread = running_thread();
task_struct *child_thread = get_kernel_pages(1); // 为子进程创建pcb(task_struct结构)
if (child_thread == NULL) {
return -1;
}
ASSERT(INTR_OFF == intr_get_status() && parent_thread->pgdir != NULL);

if (copy_process(child_thread, parent_thread) == -1) {
return -1;
}

/* 添加到就绪线程队列和所有线程队列,子进程由调试器安排运行 */
ASSERT(!elem_find(&thread_ready_list, &child_thread->general_tag));
list_append(&thread_ready_list, &child_thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &child_thread->all_list_tag));
list_append(&thread_all_list, &child_thread->all_list_tag);

return child_thread->pid; // 父进程返回子进程的pid
}

执行进程就是将该进程加入就绪队列.

script>