7-ps2_keyboard.md and 8-multitasking.md (#29)
authorFFreestanding <62629010+FFreestanding@users.noreply.github.com>
Mon, 26 Feb 2024 22:55:22 +0000 (06:55 +0800)
committerGitHub <noreply@github.com>
Mon, 26 Feb 2024 22:55:22 +0000 (22:55 +0000)
Add tutorial on ps2 driver and multitasking stuff
---------

Co-authored-by: ffreestanding <achillesweb@qq.com>
docs/tutorial/7-ps2_keyboard.md [new file with mode: 0644]
docs/tutorial/8-multitasking.md [new file with mode: 0644]

diff --git a/docs/tutorial/7-ps2_keyboard.md b/docs/tutorial/7-ps2_keyboard.md
new file mode 100644 (file)
index 0000000..d0faf9f
--- /dev/null
@@ -0,0 +1,370 @@
+## 准备工作
+
+```
+git checkout af336b49c908dc0d2b62846a19001d4dac7cad61
+```
+
+观看对应视频。
+
+## 代码分析
+
+### mutex
+
+```c
+#include <stdatomic.h>
+
+struct sem_t {
+    _Atomic unsigned int counter;
+    // FUTURE: might need a waiting list
+};
+```
+
+`stdatomic.h`里面的函数是可以使用的。我们会用到里面的原子操作。
+
+counter的类型是`unsigned int`的原子版本(_Atomic)
+
+如果sem->counter为0的话,就会一直等待。
+
+```c
+void sem_wait(struct sem_t *sem) {
+    while (!atomic_load(&sem->counter)) {
+        // TODO: yield the cpu
+    }
+    atomic_fetch_sub(&sem->counter, 1);
+}
+```
+
+增加变量的值
+
+```c
+void sem_post(struct sem_t *sem) {
+    atomic_fetch_add(&sem->counter, 1);
+    // TODO: wake up a thread
+}
+```
+
+mutex_lock需要获得锁,如果没有(sem->counter为0)就要等待。mutex_unlock会释放锁,sem->counter自增1。
+
+```c
+typedef struct sem_t mutex_t;
+
+static inline void mutex_init(mutex_t *mutex) {
+    sem_init(mutex, 1);
+}
+
+static inline unsigned int mutex_on_hold(mutex_t *mutex) {
+    return !atomic_load(&mutex->counter);
+}
+
+static inline void mutex_lock(mutex_t *mutex) {
+    sem_wait(mutex);
+}
+
+static inline void mutex_unlock(mutex_t *mutex) {
+    sem_post(mutex);
+}
+```
+
+### kernel/peripheral/ps2kbd.c
+
+`ps2_post_cmd`用于向端口写入命令
+
+`io_inb(PS2_PORT_CTRL_STATUS)`读取端口,读取后才能清空值。
+
+`PS2_PORT_CTRL_STATUS`代表Status Register,它在0x64。
+
+> Data should be written to the
+> controller's input buffer only if the input buffer's full bit in the
+> status register is equal to 0.[1]
+
+full bit就是第二位bit,如果输入缓冲区满,该bit为1
+
+> When the controller reads the input buffer, this bit will return to 0.[2]
+
+这里就是等待输入buffer被读取。如果没有被读取,就会阻塞在while。参数可以稍后再看。
+
+```c
+static void ps2_post_cmd(uint8_t port, char cmd, uint16_t arg) {
+    char result;
+    // 等待PS/2输入缓冲区清空,这样我们才可以写入命令
+    while((result = io_inb(PS2_PORT_CTRL_STATUS)) & PS2_STATUS_IFULL);
+
+    io_outb(port, cmd);
+    io_delay(PS2_DELAY);
+    
+    if (!(arg & PS2_NO_ARG)) {
+        // 所有参数一律通过0x60传入。
+        io_outb(PS2_PORT_ENC_CMDREG, (uint8_t)(arg & 0x00ff));
+        io_delay(PS2_DELAY);
+    }
+}
+```
+
+因为写入端口需要时间,所以要使用`io_delay`。`io_delay`是用一个循环来实现的。
+
+```c
+static inline void
+io_delay(int counter)
+{
+    asm volatile (
+        "   test %0, %0\n"
+        "   jz 1f\n"
+        "2: dec %0\n"
+        "   jnz 2b\n"
+        "1: dec %0"::"a"(counter));
+}
+```
+
+`ps2_issue_dev_cmd`调用`ps2_post_cmd`,最后要等待状态。之前等待的是`PS2_STATUS_IFULL`,这次等待的是`PS2_STATUS_OFULL`。这里循环里面进行了取反,表示output未满就等待。
+
+> The output buffer should be read only when the output buffer's full bit in the status register is 1.[3]
+
+```c
+static uint8_t ps2_issue_dev_cmd(char cmd, uint16_t arg) {
+    ps2_post_cmd(PS2_PORT_ENC_CMDREG, cmd, arg);
+
+    char result;
+    
+    // 等待PS/2控制器返回。通过轮询(polling)状态寄存器的 bit 0
+    // 如置位,则表明返回代码此时就在 0x60 IO口上等待读取。
+    while(!((result = io_inb(PS2_PORT_CTRL_STATUS)) & PS2_STATUS_OFULL));
+
+    return io_inb(PS2_PORT_ENC_CMDREG);
+}
+```
+
+`ps2_kbd_init`函数可以看注释。该函数中的下面两个函数需要接着分析。
+
+```c
+    intr_subscribe(PC_KBD_IV, intr_ps2_kbd_handler);
+    timer_run_ms(5, ps2_process_cmd, NULL, TIMER_MODE_PERIODIC);
+```
+
+`intr_ps2_kbd_handler`在键盘中断(键盘按下或抬起时触发)时被调用。该函数会对键盘发出的扫描码进行预处理得到`Lunaix Keycode`,保存到`kbd_keycode_t`中。
+
+每5毫秒会执行一次`ps2_process_cmd`,来进一步处理。
+
+### ps2_kbd_init
+
+用于初始化,略。
+
+### kbd_keycode_t
+
+根据资料,数字1的扫描码是0x16,所以`scancode_set2[0x16]`是`KEY_NUM(1)`。即数组下标是扫描码,值为lunaix-os自定义的*Lunaix Keycode*。如果不想查资料,可以在中断处理函数中打印接收的扫描码。
+
+```c
+// 大部分的扫描码(键码)
+static kbd_keycode_t scancode_set2[] = {
+    0, KEY_F9, 0, KEY_F5, KEY_F3, KEY_F1, KEY_F2, KEY_F12, 0, KEY_F10, KEY_F8, KEY_F6,
+    KEY_F4, KEY_HTAB, '`', 0, 0, KEY_LALT, KEY_LSHIFT, 0, KEY_LCTRL, 'q', KEY_NUM(1), 
+    0, 0, 0, 'z', 's', 'a', 'w', KEY_NUM(2), 0, 0, 'c', 'x', 'd', 'e', KEY_NUM(4), KEY_NUM(3), 
+    0, 0, KEY_SPACE, 'v', 'f', 't', 'r', KEY_NUM(5),
+    0, 0, 'n', 'b', 'h', 'g', 'y', KEY_NUM(6), 0, 0, 0, 'm', 'j', 'u', KEY_NUM(7), KEY_NUM(8),
+    0, 0, ',', 'k', 'i', 'o', KEY_NUM(0), KEY_NUM(9), 0, 0, '.', '/', 'l', ';', 'p', '-', 0, 0,
+    0, '\'', 0, '[', '=', 0, 0, KEY_CAPSLK, KEY_RSHIFT, KEY_LF, ']', 0, '\\', 0, 0, 0, 0, 0, 0, 0,
+    0, KEY_BS, 0, 0, KEY_NPAD(1), 0, KEY_NPAD(4), KEY_NPAD(7), 0, 0, 0, KEY_NPAD(0), ON_KEYPAD('.'),
+    KEY_NPAD(2), KEY_NPAD(5), KEY_NPAD(6), KEY_NPAD(8), KEY_ESC, KEY_NUMSLK, KEY_F11, ON_KEYPAD('+'),
+    KEY_NPAD(3), ON_KEYPAD('-'), ON_KEYPAD('*'), KEY_NPAD(9), KEY_SCRLLK, 0, 0, 0, 0, KEY_F7
+};
+```
+
+对于字符1,它的category是0x0,sequence是0x31(1的ASCII值)。KEY_NUM(1)的值就是这么来的。
+
+```c
+//      Lunaix Keycode
+//       15        7         0
+// key = |xxxx xxxx|xxxx xxxx|
+// key[0:7] = sequence
+// key[8:15] = category
+//   0x0: ASCII codes
+//   0x1: keypad keys
+//   0x2: Function keys
+//   0x3: Cursor keys (arrow keys)
+//   0x4: Modifier keys
+//   0xff: Other keys (Un-categorized)
+```
+
+再举一个例子,`KEY_F8`属于`Function keys`,`sequence`是0x07(这个值也是自定义的)。它的值是`(0x07 | 0x0200)`。
+
+可以理解成,Lunaix Keycode从两个维度来分类,一个是类型,一个是自定义序号。
+
+### intr_ps2_kbd_handler
+
+里面有一些对`kbd_state.state`的操作,用于切换视频中提到的状态机的状态。
+
+读取扫描码。
+
+```c
+       // Do not move this line. It is in the right place and right order.
+    // This is to ensure we've cleared the output buffer everytime, so it won't pile up across irqs.
+    uint8_t scancode = io_inb(PS2_PORT_ENC_DATA);
+    kbd_keycode_t key;
+```
+
+我们读取的不一定是scancode,还有可能是指令的返回码0xfa。对于这种数据不能用intr_ps2_kbd_handler处理。这个稍后来看。
+
+先来看看的一个case。如果`scancode == 0xf0`,说明释放了一个按键。状态改为`KBD_STATE_KRELEASED`。如果`scancode == 0xe0`,则使用特殊表`scancode_set2_ex`。如果都不是,就可以正常地获得Lunaix Keycode。
+
+```c
+switch (kbd_state.state)
+    {
+    case KBD_STATE_KWAIT:
+        if (scancode == 0xf0) { // release code
+            kbd_state.state = KBD_STATE_KRELEASED;       
+        } else if (scancode == 0xe0) {
+            kbd_state.state = KBD_STATE_KSPECIAL;
+            kbd_state.translation_table = scancode_set2_ex;
+        } else {
+            key = kbd_state.translation_table[scancode];
+            kbd_buffer_key_event(key, scancode, KBD_KEY_FPRESSED);
+        }
+        break;
+```
+
+第二个case。在特殊处理的状态下,如果没有释放,就正常地获得Lunaix Keycode。最后换成普通的表。
+
+```c
+    case KBD_STATE_KSPECIAL:
+        if (scancode == 0xf0) { //release code
+            kbd_state.state = KBD_STATE_KRELEASED;
+        } else {
+            key = kbd_state.translation_table[scancode];
+            kbd_buffer_key_event(key, scancode, KBD_KEY_FPRESSED);
+
+            kbd_state.state = KBD_STATE_KWAIT;
+            kbd_state.translation_table = scancode_set2;
+        }
+        break;
+```
+
+最后一个case略。
+
+### kbd_buffer_key_event
+
+state完整位图布局如下
+
+```tex
+15-10 保留
+9 右ALT
+8 左ALT
+7 右CTRL
+6 左CTRL
+5 右SHIFT
+4 左SHIFT
+3 CAPSLOCK
+2 NUMLOCK
+1 SCREENLOCK
+0 此位为1(KBD_KEY_FPRESSED)表示按下,为0表示抬起
+```
+
+这里是用来记录是否CAPSLOCK等键处于按下状态。简单来说下面代码是用于设置state的一个位。因为我们的状态是用一个bit表示的,所以处理起来有些麻烦。如果用一个byte表示一个状态,可以直接用更简单的异或来切换。
+
+```c
+    if (key == KEY_CAPSLK) {
+        kbd_state.key_state ^= KBD_KEY_FCAPSLKED & -state;
+    } else if (key == KEY_NUMSLK) {
+        kbd_state.key_state ^= KBD_KEY_FNUMBLKED & -state;
+    } else if (key == KEY_SCRLLK) {
+        kbd_state.key_state ^= KBD_KEY_FSCRLLKED & -state;
+    } 
+```
+
+state的值是0或者1。-state就是0x0或者0xffff。
+
+假如key == KEY_CAPSLK,而且state是1,kbd_state.key_state就会异或上0x8。
+
+假如key == KEY_CAPSLK,而且state是0,kbd_state.key_state就会异或上0。kbd_state.key_state不会改变
+
+所以按下CAPSLOCK再后抬起,kbd_state.key_state还是会CAPSLOCK锁定的状态。只有再次按下CAPSLOCK才会接触锁定。
+
+lunaix定义modifier的sequence刚好是每个modifier state bit相对于lshift的位移。这里的位移就是于根据key设置对应的modifier state。当按下lctrl,`key & 0xff == 2`,lshift 左移两位刚好等于lctrl state bit的位置,后面的state取补就是决定这个bit该不该被设置(如果release,就不用设置了)。
+
+```c
+} else {
+        if ((key & MODIFR)) {
+            kbd_kstate_t tmp = (KBD_KEY_FLSHIFT_HELD << (key & 0x00ff));
+            kbd_state.key_state = (kbd_state.key_state & ~tmp) | (tmp & -state);
+        }
+```
+
+如果按下了shift,就用`scancode_set2_shift`表。
+
+```c
+        else if (!(key & 0xff00) && (kbd_state.key_state & (KBD_KEY_FLSHIFT_HELD | KBD_KEY_FRSHIFT_HELD))) {
+            key = scancode_set2_shift[scancode];
+        }
+        state = state | kbd_state.key_state;
+        key = key & (0xffdf | -('a' > key || key > 'z' || !(state & KBD_KEY_FCAPSLKED)));
+```
+
+最后得到预处理好的key,存储到keyevent_pkt()。
+
+最后是让键盘亮灯的操作,但是这个操作会产生返回码干扰我们的状态机。
+
+```c
+    if (state & KBD_KEY_FPRESSED) {
+        // Ooops, this guy generates irq!
+        ps2_device_post_cmd(PS2_KBD_CMD_SETLED, (kbd_state.key_state >> 1) & 0x00ff);
+    }
+```
+
+所以`intr_ps2_kbd_handler`中代码使用了叠加掩码的方式来保护状态机
+
+```c
+#ifdef KBD_ENABLE_SPIRQ_FIX
+    if ((kbd_state.state & 0xc0)) {
+        kbd_state.state -= KBD_STATE_CMDPROCS;
+
+        return;
+    }
+#endif
+```
+
+`ps2_device_post_cmd`略
+
+### 接收key
+
+kernel/lxinit.c调用了`kbd_recv_key`。直接打印了keyevent.keycode的最低字节。
+
+```c
+    struct kdb_keyinfo_pkt keyevent;
+    while (1) {
+        if (!kbd_recv_key(&keyevent)) {
+            // yield();
+            continue;
+        }
+        if ((keyevent.state & KBD_KEY_FPRESSED) &&
+            (keyevent.keycode & 0xff00) <= KEYPAD) {
+            tty_put_char((char)(keyevent.keycode & 0x00ff));
+            tty_sync_cursor();
+        }
+    }
+```
+
+`kbd_buffer_key_event`中存储的`kdb_keyinfo_pkt`会在`kbd_recv_key`中读取。视频讲过它的原理,而且比较简单,所以略过。
+
+```c
+int kbd_recv_key(struct kdb_keyinfo_pkt* key_event) {
+    if (!key_buf.buffered_len) {
+        return 0;
+    }
+    mutex_lock(&key_buf.mutex);
+
+    struct kdb_keyinfo_pkt* pkt_current = &key_buf.buffer[key_buf.read_ptr];
+
+    *key_event = *pkt_current;
+    key_buf.buffered_len--;
+    key_buf.read_ptr = (key_buf.read_ptr + 1) % PS2_KBD_RECV_BUFFER_SIZE;
+
+    mutex_unlock(&key_buf.mutex);
+    return 1;
+}
+```
+
+## 参考
+
+[1]IBM_PC_AT_Technical_Reference_Mar84, 1-40 System Board, Input Buffer
+
+[2]IBM_PC_AT_Technical_Reference_Mar84, 1-38 System Board, Status-Register Bit Definition, Bit 1
+
+[3]IBM_PC_AT_Technical_Reference_Mar84, 1-40 System Board, Output Buffer
diff --git a/docs/tutorial/8-multitasking.md b/docs/tutorial/8-multitasking.md
new file mode 100644 (file)
index 0000000..243116b
--- /dev/null
@@ -0,0 +1,681 @@
+## 准备工作
+
+```sh
+git checkout 088403ac98acf7991507715d29a282dcba222053
+```
+
+观看对应视频
+
+### 实现多进程的要素
+
+连续性:记录切换进程的CS:IP
+
+正确性:记录进程的EFLAGS、各种寄存器、栈、堆、程序本身
+
+公平性:时间分配合理
+
+在正确性中:栈、堆、程序本身和页表关联,进程的EFLAGS、各种寄存器和中断上下文关联
+
+### 中断上下文
+
+根据Intel IA32手册[1],有两种情况
+
+第一种是特权级不变,也就是内核代码切换到内核,或者Ring3用户代码切换到Ring3
+
+栈上由高到低存放EFLAGS、CS、EIP、ERROR CODE,并且ESP指向ERROR CODE
+
+lunaix使用中断来进行进程切换,那么看看怎么处理栈
+
+中断程序如下,注:中断程序执行第一句时,栈空间已经是上述状态
+
+可以看到,这个代码不管有没有权限改变都把除了ss的段寄存器push入栈(cs已经入栈了)
+
+```assembly
+        pushl %esp
+
+        subl $16, %esp
+        movw %gs, 12(%esp)
+        movw %fs,  8(%esp)
+        movw %es,  4(%esp)
+        movw %ds,   (%esp)
+
+        pushl %esi
+        pushl %ebp
+        pushl %edi
+        pushl %edx
+        pushl %ecx
+        pushl %ebx
+        pushl %eax
+```
+
+下面这段代码则是通过60(%esp)取出cs,假设为0x10
+
+```assembly
+        movl 60(%esp), %eax
+        andl $0x3, %eax
+        jz somewheref
+```
+
+`eax`与0x3可以获得第2号段描述符,实际上设置了第二号为ring0代码段,所以这样可以判断是不是发生了权限切换
+
+如果从ring3到ring0,则把ring0相关段地址填充一下
+
+```assembly
+        movw $segment, %ax
+        movw %ax, %gs
+        movw %ax, %fs
+        movw %ax, %ds
+        movw %ax, %es
+```
+
+### 中断函数派发
+
+子函数其实是一个dispatcher,派发到不同的函数
+
+例如我们派发到系统调用函数
+
+这个函数又会根据栈上的eax的值,即陷入前根据eax的值来判断调用哪个系统调用
+
+这样我们可以用变化的eax和一个中断向量号来执行多个系统调用(当然可以通过其他寄存器传参数,反正都保存在栈上了)
+
+### fork函数实现
+
+这是一个非常经典的函数
+
+还是介绍一下它:fork用于创建一个进程,所创建的进程复制父进程的代码段/数据段/BSS段/堆/栈等所有用户空间信息;在内核中操作系统重新为其申请了一个PCB,并使用父进程的PCB进行初始化。
+
+先看看第一部分代码
+
+```c
+struct proc_info {
+    pid_t pid;
+    struct proc_info* parent;
+    isr_param intr_ctx;//存储中断上下文
+    struct llist_header siblings;//与遍历兄弟姊妹有关
+    struct llist_header children;//与遍历孩子进程有关
+    struct proc_mm mm;//描述该进程的占用的虚拟内存范围和属性
+    void* page_table;//指向页目录
+    time_t created;//创建时的时间戳
+    uint8_t state;
+    int32_t exit_code;
+    int32_t k_status;
+    struct lx_timer* timer;//与时间控制有关
+};
+```
+
+下面是fork实现的第一段,用于初始化pcb
+
+```c
+    struct proc_info pcb;
+    init_proc(&pcb);
+    pcb.mm = __current->mm;
+    pcb.intr_ctx = __current->intr_ctx;
+    pcb.parent = __current;
+```
+
+根据上面的描述,初始化就是复制父进程的代码段/数据段/BSS段/堆/栈、标记进程为创建状态等
+
+```c
+setup_proc_mem(&pcb, PD_REFERENCED);
+```
+
+PD_REFERENCED是页表虚拟地址
+
+这个函数作用是复制页表相关操作,稍后分析
+
+TODO:setup_proc_mem
+
+```c
+    llist_init_head(&pcb.mm.regions);
+    struct mm_region *pos, *n;
+    llist_for_each(pos, n, &__current->mm.regions->head, head)
+    {
+```
+
+llish_for_each是一个遍历regions的宏,接下来就遍历__current的各种区域(regions也可视为一个链表)
+
+```c
+        region_add(&pcb, pos->start, pos->end, pos->attr);
+```
+
+这个就是复制了,不多说
+
+```c
+if ((pos->attr & REGION_WSHARED)) {
+            continue;
+        }
+```
+
+如果这个区域共享写,就没什么要处理的了
+
+```c
+        uintptr_t start_vpn = PG_ALIGN(pos->start) >> 12;
+        uintptr_t end_vpn = PG_ALIGN(pos->end) >> 12;
+        for (size_t i = start_vpn; i < end_vpn; i++) {
+```
+
+后面又是一个循环,用于遍历该区域,如果不是共享写的话
+
+```c
+            x86_pte_t* curproc = &PTE_MOUNTED(PD_MOUNT_1, i);
+            x86_pte_t* newproc = &PTE_MOUNTED(PD_MOUNT_2, i);
+            cpu_invplg(newproc);
+
+            if (pos->attr == REGION_RSHARED) {
+                cpu_invplg(curproc);
+                *curproc = *curproc & ~PG_WRITE;
+                *newproc = *newproc & ~PG_WRITE;
+            } else {
+                *newproc = 0;
+            }
+```
+
+PTE_MOUNTED 这个宏意思就是获得页目录的第几号页表项
+
+PD_MOUNT_1这个页表就是当前进程的地址
+
+```c
+            if (pos->attr == REGION_RSHARED) {
+                cpu_invplg(curproc);
+                *curproc = *curproc & ~PG_WRITE;[2]
+                *newproc = *newproc & ~PG_WRITE;
+```
+
+如果是共享读,就根据Intel manual 把页表设置成Read
+
+```c
+            } else {
+                *newproc = 0;
+            }
+```
+
+否则视为私有,子进程不继承该页表,直接清空
+
+上面这个阶段就是复制页表
+
+最后看看TODO:setup_proc_mem(&pcb, PD_REFERENCED);
+
+```c
+void
+setup_proc_mem(struct proc_info* proc, uintptr_t usedMnt)
+{
+    pid_t pid = proc->pid;
+    void* pt_copy = __dup_pagetable(pid, usedMnt);
+    vmm_mount_pd(PD_MOUNT_2, pt_copy);
+    // copy the kernel stack
+    for (size_t i = KSTACK_START >> 12; i <= KSTACK_TOP >> 12; i++) {
+        volatile x86_pte_t* ppte = &PTE_MOUNTED(PD_MOUNT_2, i);
+        cpu_invplg(ppte);
+        x86_pte_t p = *ppte;
+        void* ppa = vmm_dup_page(pid, PG_ENTRY_ADDR(p));
+        *ppte = (p & 0xfff) | (uintptr_t)ppa;
+    }
+    proc->page_table = pt_copy;
+}
+```
+
+__dup_pagetable(pid, usedMnt);这个函数是复制全部kernel页表
+
+先不看,加为TODO:__dup_pagetable(pid, usedMnt);
+
+vmm_mount_pd(PD_MOUNT_2, pt_copy);复制到二号页目录挂载点
+
+TODO:`__dup_pagetable(pid, usedMnt);` `vmm_mount_pd(PD_MOUNT_2, pt_copy);`
+
+`vmm_dup_page(pid, PG_ENTRY_ADDR(p));`这个也加为TODO,最后的循环暂且知道是处理了每个页目录项即可。
+
+最后执行not cpoy段
+
+```c
+not_copy:
+    vmm_unmount_pd(PD_MOUNT_2);
+    pcb.intr_ctx.registers.eax = 0;
+    push_process(&pcb);//把子进程放入执行队列,轮询制
+    return pcb.pid;
+```
+
+### __dup_pagetable
+
+解决第一个TODO
+
+这个函数作用是复制全部kernel页表、页目录
+
+
+
+```c
+    void* ptd_pp = pmm_alloc_page(pid, PP_FGPERSIST);
+```
+
+先调用pmm_alloc_page给当前pid分配一个物理页,篇幅有限,就不说了,后面的一些函数又是如此
+
+
+
+```c
+    x86_page_table* ptd = vmm_fmap_page(pid, PG_MOUNT_1, ptd_pp, PG_PREM_RW);
+    x86_page_table* pptd = (x86_page_table*)(mount_point | (0x3FF << 12));
+```
+
+把分配的物理页映射到这个挂载点,其实就是给挂载点分配物理空间,拿到页目录指针ptd
+
+pptd指向kernel页目录第一项(这个kernel页表使用了循环引用,页目录最后一项指向页目录基址)
+
+
+
+```c
+    for (size_t i = 0; i < PG_MAX_ENTRIES - 1; i++) {
+```
+
+遍历所有page entry,除了最后一个
+
+```c
+       x86_pte_t ptde = pptd->entry[i];
+        if (!ptde || !(ptde & PG_PRESENT)) {
+            ptd->entry[i] = ptde;
+            continue;
+        }
+```
+
+取出kernel page directory entry,简单判断并复制页目录的每一项
+
+```c
+       x86_page_table* ppt = (x86_page_table*)(mount_point | (i << 12));
+        void* pt_pp = pmm_alloc_page(pid, PP_FGPERSIST);
+        x86_page_table* pt = vmm_fmap_page(pid, PG_MOUNT_2, pt_pp, PG_PREM_RW);
+```
+
+ppt指向 kernel page table entry,接着给二号页表挂载点分配物理页并映射
+
+```c
+        for (size_t j = 0; j < PG_MAX_ENTRIES; j++) {
+            x86_pte_t pte = ppt->entry[j];
+            pmm_ref_page(pid, PG_ENTRY_ADDR(pte));
+            pt->entry[j] = pte;
+        }
+        ptd->entry[i] = (uintptr_t)pt_pp | PG_PREM_RW;
+```
+
+上面是这个循环中最后一段,复制页表,最后设置权限
+
+pmm_ref_page(pid, PG_ENTRY_ADDR(pte));这个是增加页面引用计数,和内存共享有关
+
+
+
+循环结束后
+
+```c
+    ptd->entry[PG_MAX_ENTRIES - 1] = NEW_L1_ENTRY(T_SELF_REF_PERM, ptd_pp);
+    return ptd_pp;
+```
+
+同样做一个循环映射
+
+返回挂载点1的虚拟地址
+
+### vmm_mount_pd
+
+回顾:它的第二个参数是页目录
+
+```c
+void* pt_copy = __dup_pagetable(pid, usedMnt);
+    vmm_mount_pd(PD_MOUNT_2, pt_copy);
+```
+
+下面是它的代码
+
+```c
+void*
+vmm_mount_pd(uintptr_t mnt, void* pde) {
+    x86_page_table* l1pt = (x86_page_table*)L1_BASE_VADDR;
+    l1pt->entry[(mnt >> 22)] = NEW_L1_ENTRY(T_SELF_REF_PERM, pde);
+    cpu_invplg(mnt);
+    return mnt;
+}
+```
+
+很简单,就是把kernel的页目录指向新页目录,于是我们可以通过虚拟地址访问这个页目录了
+
+### vmm_dup_page
+
+
+
+先回顾一下:它是在复制kernel栈的情况下使用的
+
+```c
+    // copy the kernel stack
+    for (size_t i = KSTACK_START >> 12; i <= KSTACK_TOP >> 12; i++) {
+        volatile x86_pte_t* ppte = &PTE_MOUNTED(PD_MOUNT_2, i);
+        cpu_invplg(ppte);
+        x86_pte_t p = *ppte;
+        void* ppa = vmm_dup_page(pid, PG_ENTRY_ADDR(p));
+        *ppte = (p & 0xfff) | (uintptr_t)ppa;
+    }
+```
+
+
+
+```c
+void* vmm_dup_page(pid_t pid, void* pa) {    
+    void* new_ppg = pmm_alloc_page(pid, 0);
+    vmm_fmap_page(pid, PG_MOUNT_3, new_ppg, PG_PREM_RW);
+    vmm_fmap_page(pid, PG_MOUNT_4, pa, PG_PREM_RW);
+
+    asm volatile (
+        "movl %1, %%edi\n"
+        "movl %2, %%esi\n"
+        "rep movsl\n"
+        :: "c"(1024), "r"(PG_MOUNT_3), "r"(PG_MOUNT_4)
+        : "memory", "%edi", "%esi");
+
+    vmm_unset_mapping(PG_MOUNT_3);
+    vmm_unset_mapping(PG_MOUNT_4);
+
+    return new_ppg;
+}
+```
+
+参数pa是页表项的值取高20位
+
+最后实现 把pa指向页面值复制到新页面
+
+其实实现的就是页面复制到新地址,并返回
+
+
+
+### exit系统调用实现
+
+先来看看exit的说明
+void exit(int status) 立即终止调用进程。任何属于该进程的打开的文件描述符都会被关闭,该进程的子进程由进程 1 继承,初始化,且会向父进程发送一个 SIGCHLD 信号。
+
+我们的exit还是有一些不一样的
+
+exit定义如下,依旧是通过中断陷入TRAP
+```c
+static void _exit(int status) {
+    // 使用汇编语句设置寄存器ebx的值为status
+    asm("" :: "b"(status));
+    int v;
+    // 使用汇编语句触发一个中断,中断号为33(通常用于系统调用),功能号为8
+    asm volatile("int %1\n" : "=a"(v) : "i"(33), "a"(8));
+    // 返回中断处理后的结果
+    return (void)v;
+}
+```
+为什么要设置ebx的值为status呢,因为上一次我们的中断框架规定了ebx传递第一个参数
+
+```c
+terminate_proc(status);
+void
+terminate_proc(int exit_code)
+{
+    __current->state = PROC_TERMNAT;
+    __current->exit_code = exit_code;
+    schedule();
+}
+```
+这个代码很简单,设置返回值为status和状态为终止,最后调用schedule
+
+```c
+    if (!sched_ctx.ptable_len) {
+        return;
+    }
+```
+在schedule函数中,首先检查进程表长度,如果为空就不需要schedule了
+
+```c
+    cpu_disable_interrupt();
+    struct proc_info* next;
+    int prev_ptr = sched_ctx.procs_index;
+    int ptr = prev_ptr;
+
+    do {
+        ptr = (ptr + 1) % sched_ctx.ptable_len;
+        next = &sched_ctx._procs[ptr];
+    } while (next->state != PROC_STOPPED && ptr != prev_ptr);
+
+    sched_ctx.procs_index = ptr;
+
+    run(next);
+```
+如果不为空,则(以轮询算法的方式)获得下一个进程指针存入next,而且该进程不能为停止状态
+更新index,最后run
+
+```c
+    if (!(__current->state & ~PROC_RUNNING)) {
+        __current->state = PROC_STOPPED;
+    }
+    proc->state = PROC_RUNNING;
+```
+run函数也是先检查状态
+
+```c
+    if (__current->page_table != proc->page_table) {
+        __current = proc;
+        cpu_lcr3(__current->page_table);
+        // from now on, the we are in the kstack of another process
+    } else {
+        __current = proc;
+    }
+```
+更新__current的信息,然后更新CR3寄存器
+
+```c
+static inline void
+cpu_lcr3(reg32 v)
+{
+    asm("mov %0, %%cr3" ::"r"(v));
+}
+```
+更新CR3寄存器很简单
+
+```c
+    apic_done_servicing();
+    asm volatile("pushl %0\n"
+                 "jmp soft_iret\n" ::"r"(&__current->intr_ctx)
+                 : "memory");
+```
+将当前中断上下文push,然后中断返回
+我们在中断context中已经保存了需要切换的进程的信息
+
+```c
+    .global soft_iret
+    soft_iret:
+        popl %esp
+```
+先pop,也就是让esp等于(&__current->intr_ctx)
+
+esp此时指向__current->intr_ctx这个结构体
+回顾一下这个结构体
+```c
+typedef struct
+{
+    struct
+    {
+        reg32 eax;
+        reg32 ebx;
+        reg32 ecx;
+        reg32 edx;
+        reg32 edi;
+        reg32 ebp;
+        reg32 esi;
+        reg32 ds;
+        reg32 es;
+        reg32 fs;
+        reg32 gs;
+        reg32 esp;
+    } registers;
+    unsigned int vector;
+    unsigned int err_code;
+    unsigned int eip;
+    unsigned int cs;
+    unsigned int eflags;
+    unsigned int esp;
+    unsigned int ss;
+} __attribute__((packed)) isr_param;
+```
+就是一堆寄存器,继续看soft_iret代码
+
+```c
+        popl %eax
+        popl %ebx
+        popl %ecx
+        popl %edx
+        popl %edi
+        popl %ebp
+        popl %esi
+```
+这个就是把保存的寄存器都恢复出来
+
+```c
+        movw   (%esp), %ds
+        movw  4(%esp), %es
+        movw  8(%esp), %fs
+        movw 12(%esp), %gs
+```
+恢复段寄存器
+
+```c
+        movl 16(%esp), %esp
+        addl $8, %esp  
+        iret
+```
+这个过程是和中断wrapper调用时完全相反的,不多说了
+
+总的来说,exit会返回exit code 并且中断跳到sched_ctx._procs数组中的一个进程
+
+那么,接下来看看这个数组有哪些相关操作
+
+```c
+void push_process(struct proc_info* process);
+```
+挑push_process来说,它的作用是把一个进程放入轮询调度器
+
+之前实现了timer就能很快实现调度器了,略过
+
+push_process内容是对proc_info内容做一些检查最后写入数组,就没了
+
+最后看看push_process的使用
+
+然后是创建0号进程的时候调用了push_process
+
+那么看看为什么0号进程的创建和创建意义
+
+### 0号进程创建及其意义
+```c
+    struct proc_info proc0;
+    init_proc(&proc0);
+    proc0.intr_ctx = (isr_param){ .registers = { .ds = KDATA_SEG,
+                                                 .es = KDATA_SEG,
+                                                 .fs = KDATA_SEG,
+                                                 .gs = KDATA_SEG },
+                                  .cs = KCODE_SEG,
+                                  .eip = (void*)__proc0,
+                                  .ss = KDATA_SEG,
+                                  .eflags = cpu_reflags() };
+```
+上面这些是PCB的初始化,略过
+
+```c
+setup_proc_mem(&proc0, PD_REFERENCED);
+```
+复制内核的页目录到0号进程信息里面
+
+```c
+    asm volatile("movl %%cr3, %%eax\n"
+                 "movl %%esp, %%ebx\n"
+                 "movl %1, %%cr3\n"
+                 "movl %2, %%esp\n"
+                 "pushf\n"
+                 "pushl %3\n"
+                 "pushl %4\n"
+                 "pushl $0\n"
+                 "pushl $0\n"
+                 "movl %%esp, %0\n"
+                 "movl %%eax, %%cr3\n"
+                 "movl %%ebx, %%esp\n"
+                 : "=m"(proc0.intr_ctx.registers.esp)
+                 : "r"(proc0.page_table),
+                   "i"(KSTACK_TOP),
+                   "i"(KCODE_SEG),
+                   "r"(proc0.intr_ctx.eip)
+                 : "%eax", "%ebx", "memory");
+```
+布置好中断栈,依次push eflags、代码段地址(cs)、eip、0(error code)、0(vector)
+
+```c
+    // 向调度器注册进程。
+    push_process(&proc0);
+
+    // 由于时钟中断未就绪,我们需要手动通知调度器进行第一次调度。这里也会同时隐式地恢复我们的eflags.IF位
+    schedule();
+```
+最后进行注册和调度
+
+
+### 进程调用过程细节
+进程调用是由调度器管理
+假设我们调用fork执行了一个进程,之后调度器触发了进程切换
+
+```c
+
+static void
+timer_update(const isr_param* param)
+{
+    /*...*/
+    sched_ticks_counter++;
+
+    if (sched_ticks_counter >= sched_ticks) {
+        sched_ticks_counter = 0;
+        schedule();
+    }
+}
+```
+timer会每隔固定时间利用外部中断执行timer_update
+timer_update函数会调用schedule
+
+schedule也分析过了,它会取出其中一个PCB信息,并调用run函数
+run函数上面也分析了
+**就此进程管理大部分实现**
+
+### sleep实现
+
+```c
+    if (!seconds) {
+        return 0;
+    }
+
+    if (__current->timer) {
+        return __current->timer->counter / timer_context()->running_frequency;
+    }
+
+    struct lx_timer* timer =
+      timer_run_second(seconds, proc_timer_callback, __current, 0);
+    __current->timer = timer;
+    __current->intr_ctx.registers.eax = seconds;
+    __current->state = PROC_BLOCKED;
+    schedule();
+```
+主要逻辑就是每秒运行proc_timer_callback
+同时设置阻塞状态等,最后调用schedule函数
+
+```c
+static void
+proc_timer_callback(struct proc_info* proc)
+{
+    proc->timer = NULL;
+    proc->state = PROC_STOPPED;
+}
+```
+proc_timer_callback就是清空timer,设置为STOP
+
+```c
+    if (__current->timer) {
+        return __current->timer->counter / timer_context()->running_frequency;
+    }
+```
+sleep函数还有这一段没解释,当调用了proc_timer_callback,timer为空就不会返回
+
+## 参考
+
+[1]Intel Manual, Vol 1, 6-15, Figure 6-7. Stack Usage on Transfers to Interrupt and Exception Handling Routines
+
+[2]Intel Manual, Vol 3A, 4-13, Table 4-6. Format of a 32-Bit Page-Table Entry that Maps a 4-KByte Page