4 git checkout 088403ac98acf7991507715d29a282dcba222053
13 正确性:记录进程的EFLAGS、各种寄存器、栈、堆、程序本身
17 在正确性中:栈、堆、程序本身和页表关联,进程的EFLAGS、各种寄存器和中断上下文关联
21 根据Intel IA32手册[1],有两种情况
23 第一种是特权级不变,也就是内核代码切换到内核,或者Ring3用户代码切换到Ring3
25 栈上由高到低存放EFLAGS、CS、EIP、ERROR CODE,并且ESP指向ERROR CODE
27 lunaix使用中断来进行进程切换,那么看看怎么处理栈
29 中断程序如下,注:中断程序执行第一句时,栈空间已经是上述状态
31 可以看到,这个代码不管有没有权限改变都把除了ss的段寄存器push入栈(cs已经入栈了)
51 下面这段代码则是通过60(%esp)取出cs,假设为0x10
59 `eax`与0x3可以获得第2号段描述符,实际上设置了第二号为ring0代码段,所以这样可以判断是不是发生了权限切换
61 如果从ring3到ring0,则把ring0相关段地址填充一下
73 子函数其实是一个dispatcher,派发到不同的函数
77 这个函数又会根据栈上的eax的值,即陷入前根据eax的值来判断调用哪个系统调用
79 这样我们可以用变化的eax和一个中断向量号来执行多个系统调用(当然可以通过其他寄存器传参数,反正都保存在栈上了)
85 还是介绍一下它:fork用于创建一个进程,所创建的进程复制父进程的代码段/数据段/BSS段/堆/栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化。
92 struct proc_info* parent;
93 isr_param intr_ctx;//存储中断上下文
94 struct llist_header siblings;//与遍历兄弟姊妹有关
95 struct llist_header children;//与遍历孩子进程有关
96 struct proc_mm mm;//描述该进程的占用的虚拟内存范围和属性
97 void* page_table;//指向页目录
98 time_t created;//创建时的时间戳
102 struct lx_timer* timer;//与时间控制有关
106 下面是fork实现的第一段,用于初始化pcb
109 struct proc_info pcb;
111 pcb.mm = __current->mm;
112 pcb.intr_ctx = __current->intr_ctx;
113 pcb.parent = __current;
116 根据上面的描述,初始化就是复制父进程的代码段/数据段/BSS段/堆/栈、标记进程为创建状态等
119 setup_proc_mem(&pcb, PD_REFERENCED);
129 llist_init_head(&pcb.mm.regions);
130 struct mm_region *pos, *n;
131 llist_for_each(pos, n, &__current->mm.regions->head, head)
135 llish_for_each是一个遍历regions的宏,接下来就遍历__current的各种区域(regions也可视为一个链表)
138 region_add(&pcb, pos->start, pos->end, pos->attr);
144 if ((pos->attr & REGION_WSHARED)) {
152 uintptr_t start_vpn = PG_ALIGN(pos->start) >> 12;
153 uintptr_t end_vpn = PG_ALIGN(pos->end) >> 12;
154 for (size_t i = start_vpn; i < end_vpn; i++) {
157 后面又是一个循环,用于遍历该区域,如果不是共享写的话
160 x86_pte_t* curproc = &PTE_MOUNTED(PD_MOUNT_1, i);
161 x86_pte_t* newproc = &PTE_MOUNTED(PD_MOUNT_2, i);
164 if (pos->attr == REGION_RSHARED) {
166 *curproc = *curproc & ~PG_WRITE;
167 *newproc = *newproc & ~PG_WRITE;
173 PTE_MOUNTED 这个宏意思就是获得页目录的第几号页表项
175 PD_MOUNT_1这个页表就是当前进程的地址
178 if (pos->attr == REGION_RSHARED) {
180 *curproc = *curproc & ~PG_WRITE;[2]
181 *newproc = *newproc & ~PG_WRITE;
184 如果是共享读,就根据Intel manual 把页表设置成Read
192 否则视为私有,子进程不继承该页表,直接清空
196 最后看看TODO:setup_proc_mem(&pcb, PD_REFERENCED);
200 setup_proc_mem(struct proc_info* proc, uintptr_t usedMnt)
202 pid_t pid = proc->pid;
203 void* pt_copy = __dup_pagetable(pid, usedMnt);
204 vmm_mount_pd(PD_MOUNT_2, pt_copy);
205 // copy the kernel stack
206 for (size_t i = KSTACK_START >> 12; i <= KSTACK_TOP >> 12; i++) {
207 volatile x86_pte_t* ppte = &PTE_MOUNTED(PD_MOUNT_2, i);
210 void* ppa = vmm_dup_page(pid, PG_ENTRY_ADDR(p));
211 *ppte = (p & 0xfff) | (uintptr_t)ppa;
213 proc->page_table = pt_copy;
217 __dup_pagetable(pid, usedMnt);这个函数是复制全部kernel页表
219 先不看,加为TODO:__dup_pagetable(pid, usedMnt);
221 vmm_mount_pd(PD_MOUNT_2, pt_copy);复制到二号页目录挂载点
223 TODO:`__dup_pagetable(pid, usedMnt);` `vmm_mount_pd(PD_MOUNT_2, pt_copy);`
225 `vmm_dup_page(pid, PG_ENTRY_ADDR(p));`这个也加为TODO,最后的循环暂且知道是处理了每个页目录项即可。
231 vmm_unmount_pd(PD_MOUNT_2);
232 pcb.intr_ctx.registers.eax = 0;
233 push_process(&pcb);//把子进程放入执行队列,轮询制
241 这个函数作用是复制全部kernel页表、页目录
246 void* ptd_pp = pmm_alloc_page(pid, PP_FGPERSIST);
249 先调用pmm_alloc_page给当前pid分配一个物理页,篇幅有限,就不说了,后面的一些函数又是如此
254 x86_page_table* ptd = vmm_fmap_page(pid, PG_MOUNT_1, ptd_pp, PG_PREM_RW);
255 x86_page_table* pptd = (x86_page_table*)(mount_point | (0x3FF << 12));
258 把分配的物理页映射到这个挂载点,其实就是给挂载点分配物理空间,拿到页目录指针ptd
260 pptd指向kernel页目录第一项(这个kernel页表使用了循环引用,页目录最后一项指向页目录基址)
265 for (size_t i = 0; i < PG_MAX_ENTRIES - 1; i++) {
268 遍历所有page entry,除了最后一个
271 x86_pte_t ptde = pptd->entry[i];
272 if (!ptde || !(ptde & PG_PRESENT)) {
273 ptd->entry[i] = ptde;
278 取出kernel page directory entry,简单判断并复制页目录的每一项
281 x86_page_table* ppt = (x86_page_table*)(mount_point | (i << 12));
282 void* pt_pp = pmm_alloc_page(pid, PP_FGPERSIST);
283 x86_page_table* pt = vmm_fmap_page(pid, PG_MOUNT_2, pt_pp, PG_PREM_RW);
286 ppt指向 kernel page table entry,接着给二号页表挂载点分配物理页并映射
289 for (size_t j = 0; j < PG_MAX_ENTRIES; j++) {
290 x86_pte_t pte = ppt->entry[j];
291 pmm_ref_page(pid, PG_ENTRY_ADDR(pte));
294 ptd->entry[i] = (uintptr_t)pt_pp | PG_PREM_RW;
297 上面是这个循环中最后一段,复制页表,最后设置权限
299 pmm_ref_page(pid, PG_ENTRY_ADDR(pte));这个是增加页面引用计数,和内存共享有关
306 ptd->entry[PG_MAX_ENTRIES - 1] = NEW_L1_ENTRY(T_SELF_REF_PERM, ptd_pp);
319 void* pt_copy = __dup_pagetable(pid, usedMnt);
320 vmm_mount_pd(PD_MOUNT_2, pt_copy);
327 vmm_mount_pd(uintptr_t mnt, void* pde) {
328 x86_page_table* l1pt = (x86_page_table*)L1_BASE_VADDR;
329 l1pt->entry[(mnt >> 22)] = NEW_L1_ENTRY(T_SELF_REF_PERM, pde);
335 很简单,就是把kernel的页目录指向新页目录,于是我们可以通过虚拟地址访问这个页目录了
341 先回顾一下:它是在复制kernel栈的情况下使用的
344 // copy the kernel stack
345 for (size_t i = KSTACK_START >> 12; i <= KSTACK_TOP >> 12; i++) {
346 volatile x86_pte_t* ppte = &PTE_MOUNTED(PD_MOUNT_2, i);
349 void* ppa = vmm_dup_page(pid, PG_ENTRY_ADDR(p));
350 *ppte = (p & 0xfff) | (uintptr_t)ppa;
357 void* vmm_dup_page(pid_t pid, void* pa) {
358 void* new_ppg = pmm_alloc_page(pid, 0);
359 vmm_fmap_page(pid, PG_MOUNT_3, new_ppg, PG_PREM_RW);
360 vmm_fmap_page(pid, PG_MOUNT_4, pa, PG_PREM_RW);
366 :: "c"(1024), "r"(PG_MOUNT_3), "r"(PG_MOUNT_4)
367 : "memory", "%edi", "%esi");
369 vmm_unset_mapping(PG_MOUNT_3);
370 vmm_unset_mapping(PG_MOUNT_4);
387 void exit(int status) 立即终止调用进程。任何属于该进程的打开的文件描述符都会被关闭,该进程的子进程由进程 1 继承,初始化,且会向父进程发送一个 SIGCHLD 信号。
391 exit定义如下,依旧是通过中断陷入TRAP
393 static void _exit(int status) {
394 // 使用汇编语句设置寄存器ebx的值为status
395 asm("" :: "b"(status));
397 // 使用汇编语句触发一个中断,中断号为33(通常用于系统调用),功能号为8
398 asm volatile("int %1\n" : "=a"(v) : "i"(33), "a"(8));
403 为什么要设置ebx的值为status呢,因为上一次我们的中断框架规定了ebx传递第一个参数
406 terminate_proc(status);
408 terminate_proc(int exit_code)
410 __current->state = PROC_TERMNAT;
411 __current->exit_code = exit_code;
415 这个代码很简单,设置返回值为status和状态为终止,最后调用schedule
418 if (!sched_ctx.ptable_len) {
422 在schedule函数中,首先检查进程表长度,如果为空就不需要schedule了
425 cpu_disable_interrupt();
426 struct proc_info* next;
427 int prev_ptr = sched_ctx.procs_index;
431 ptr = (ptr + 1) % sched_ctx.ptable_len;
432 next = &sched_ctx._procs[ptr];
433 } while (next->state != PROC_STOPPED && ptr != prev_ptr);
435 sched_ctx.procs_index = ptr;
439 如果不为空,则(以轮询算法的方式)获得下一个进程指针存入next,而且该进程不能为停止状态
443 if (!(__current->state & ~PROC_RUNNING)) {
444 __current->state = PROC_STOPPED;
446 proc->state = PROC_RUNNING;
451 if (__current->page_table != proc->page_table) {
453 cpu_lcr3(__current->page_table);
454 // from now on, the we are in the kstack of another process
459 更新__current的信息,然后更新CR3寄存器
465 asm("mov %0, %%cr3" ::"r"(v));
471 apic_done_servicing();
472 asm volatile("pushl %0\n"
473 "jmp soft_iret\n" ::"r"(&__current->intr_ctx)
477 我们在中断context中已经保存了需要切换的进程的信息
484 先pop,也就是让esp等于(&__current->intr_ctx)
486 esp此时指向__current->intr_ctx这个结构体
507 unsigned int err_code;
513 } __attribute__((packed)) isr_param;
515 就是一堆寄存器,继续看soft_iret代码
541 这个过程是和中断wrapper调用时完全相反的,不多说了
543 总的来说,exit会返回exit code 并且中断跳到sched_ctx._procs数组中的一个进程
548 void push_process(struct proc_info* process);
550 挑push_process来说,它的作用是把一个进程放入轮询调度器
552 之前实现了timer就能很快实现调度器了,略过
554 push_process内容是对proc_info内容做一些检查最后写入数组,就没了
558 然后是创建0号进程的时候调用了push_process
564 struct proc_info proc0;
566 proc0.intr_ctx = (isr_param){ .registers = { .ds = KDATA_SEG,
571 .eip = (void*)__proc0,
573 .eflags = cpu_reflags() };
578 setup_proc_mem(&proc0, PD_REFERENCED);
583 asm volatile("movl %%cr3, %%eax\n"
584 "movl %%esp, %%ebx\n"
593 "movl %%eax, %%cr3\n"
594 "movl %%ebx, %%esp\n"
595 : "=m"(proc0.intr_ctx.registers.esp)
596 : "r"(proc0.page_table),
599 "r"(proc0.intr_ctx.eip)
600 : "%eax", "%ebx", "memory");
602 布置好中断栈,依次push eflags、代码段地址(cs)、eip、0(error code)、0(vector)
606 push_process(&proc0);
608 // 由于时钟中断未就绪,我们需要手动通知调度器进行第一次调度。这里也会同时隐式地恢复我们的eflags.IF位
616 假设我们调用fork执行了一个进程,之后调度器触发了进程切换
621 timer_update(const isr_param* param)
624 sched_ticks_counter++;
626 if (sched_ticks_counter >= sched_ticks) {
627 sched_ticks_counter = 0;
632 timer会每隔固定时间利用外部中断执行timer_update
633 timer_update函数会调用schedule
635 schedule也分析过了,它会取出其中一个PCB信息,并调用run函数
646 if (__current->timer) {
647 return __current->timer->counter / timer_context()->running_frequency;
650 struct lx_timer* timer =
651 timer_run_second(seconds, proc_timer_callback, __current, 0);
652 __current->timer = timer;
653 __current->intr_ctx.registers.eax = seconds;
654 __current->state = PROC_BLOCKED;
657 主要逻辑就是每秒运行proc_timer_callback
658 同时设置阻塞状态等,最后调用schedule函数
662 proc_timer_callback(struct proc_info* proc)
665 proc->state = PROC_STOPPED;
668 proc_timer_callback就是清空timer,设置为STOP
671 if (__current->timer) {
672 return __current->timer->counter / timer_context()->running_frequency;
675 sleep函数还有这一段没解释,当调用了proc_timer_callback,timer为空就不会返回
679 [1]Intel Manual, Vol 1, 6-15, Figure 6-7. Stack Usage on Transfers to Interrupt and Exception Handling Routines
681 [2]Intel Manual, Vol 3A, 4-13, Table 4-6. Format of a 32-Bit Page-Table Entry that Maps a 4-KByte Page