XV6操作系统:proc机制学习笔记

张开发
2026/4/10 13:42:46 15 分钟阅读

分享文章

XV6操作系统:proc机制学习笔记
梳理struct proc的结构如下通过分析一个父子进程的程序关系来理解process的工作原理#include stdio.h #include stdlib.h #include unistd.h #include fcntl.h #include sys/wait.h #include string.h int main() { int fd; char buffer[64]; const char *msg Hello!\n; pid_t pid fork(); if (pid 0) { exit(1); } else if (pid 0) { fd open(test.txt, O_WRONLY | O_CREAT | O_TRUNC, 0644); write(fd, msg, strlen(msg)); close(fd); exit(0); } else { wait(NULL); fd open(test.txt, O_RDONLY); read(fd, buffer, sizeof(buffer)); printf(父进程读取到子进程写下的: %s, buffer); close(fd); } return 0; }1.阶段一 fork... pid_t pid fork(); ...1.1 单核CPU情形一个父进程fork出子进程pid 0的过程首先父进程需要在内存中遍历进程表找到UNUSED的闲置进程。接下来为子进程复制父进程的各种资源openfile、cwd分配属于它自己的pagetable页表、trapframe。再把state从USED设置为RUNNABLE进入调度器就绪队列。调度器scheduler()一直在寻找RUNNABLE的进程通过上下文切换内存地址和寄存器的存储值指的是struct context状态state变为RUNNING。1.2 多核CPU情形单核CPU是不需要考虑多核的冲突问题的实际上要是另外的CPU闲着就有很大概率会来插手可能会导致proc槽位浪费亦或者程序冲突。这就是spinlock的价值。CPU0会首先调用 acquire(p-lock) 获取自旋锁, p是我们当前process的结构体名。此时如果其他 CPU 核心想要动这个 proc 结构体就会进入spin原地打转直到 CPU0 把进程状态安全地改写为 USED 并释放锁。仔细来看spinlock这里有一个误区需要辩解。proc结构体中有spinlock结构体spinlock结构体中有cpu结构体cpu结构体中又有proc结构体。那不是内存要被无限套娃撑爆。然而spinlock结构体中保存的只是指向cpu结构体的指针cpu结构体中也是这样的。struct spinlock { uint locked; // Is the lock held? char *name; // Name of lock. struct cpu *cpu; // The cpu holding the lock. }; struct cpu { struct proc *proc; // The process running on this cpu, or null. struct context context; // swtch() here to enter scheduler(). int noff; // Depth of push_off() nesting. int intena; // Were interrupts enabled before push_off()? }; struct proc { struct spinlock lock; enum procstate state; // Process state void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parents wait int pid; // Process ID struct proc *parent; // Parent process uint64 kstack; // Virtual address of kernel stack uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // User page table struct trapframe *trapframe; // data page for trampoline.S struct context context; // swtch() here to run process struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory char name[16]; // Process name (debugging) };这样的三角设计的设计导致1. proc 包含 lock锁保护进程状态保护进程内部成员变量2. lock 指向 cpu防止其他CPU篡改数据3. cpu 指向 proc内核随时能知道当前在跑哪个进程2.阶段二 trapframe子进程开始运行并调用了 open 和 write() 系统调用。... else if (pid 0) { fd open(test.txt, O_WRONLY | O_CREAT | O_TRUNC, 0644); write(fd, msg, strlen(msg)); ...2.1 用户态切换内核态trapframe.Suser程序没有权限直接操作硬盘。调用 open() 或者 write() 时RISC-V 处理器会执行 ecall 指令硬件立刻提高特权级通过蹦床内核态虚拟内存页表和用户态虚拟内存页表完全相同的地方这样进行转换时具有绝对地址才不会出问题跳入kernel态。以下为一次write系统调用示意图#include ritrampoline.Sscv.h #include memlayout.h .section trampsec .globl trampoline .globl usertrap trampoline: .aglign 4 .globl uservec .globl userret uservec: csrw sscratch, a0 # a0写入sscratch,用户态数据暂存 li a0, TRAPFRAME # 用trapframe替代a0 # save the user registers in TRAPFRAME sd ra, 40(a0) sd sp, 48(a0) sd gp, 56(a0) sd tp, 64(a0) sd t0, 72(a0) sd t1, 80(a0) sd t2, 88(a0) sd s0, 96(a0) sd s1, 104(a0) sd a1, 120(a0) sd a2, 128(a0) sd a3, 136(a0) sd a4, 144(a0) sd a5, 152(a0) sd a6, 160(a0) sd a7, 168(a0) sd s2, 176(a0) sd s3, 184(a0) sd s4, 192(a0) sd s5, 200(a0) sd s6, 208(a0) sd s7, 216(a0) sd s8, 224(a0) sd s9, 232(a0) sd s10, 240(a0) sd s11, 248(a0) sd t3, 256(a0) sd t4, 264(a0) sd t5, 272(a0) sd t6, 280(a0) csrr t0, sscratch sd t0, 112(a0) ld sp, 8(a0) ld tp, 32(a0) ld t0, 16(a0) ld t1, 0(a0) sfence.vma zero, zero //刷新TLB csrw satp, t1 // sfence.vma zero, zero // jalr t0 //JUMP TO t0(trap.c) userret: sfence.vma zero, zero csrw satp, a0 sfence.vma zero, zero li a0, TRAPFRAME # restore all but a0 from TRAPFRAME ld ra, 40(a0) ld sp, 48(a0) ld gp, 56(a0) ld tp, 64(a0) ld t0, 72(a0) ld t1, 80(a0) ld t2, 88(a0) ld s0, 96(a0) ld s1, 104(a0) ld a1, 120(a0) ld a2, 128(a0) ld a3, 136(a0) ld a4, 144(a0) ld a5, 152(a0) ld a6, 160(a0) ld a7, 168(a0) ld s2, 176(a0) ld s3, 184(a0) ld s4, 192(a0) ld s5, 200(a0) ld s6, 208(a0) ld s7, 216(a0) ld s8, 224(a0) ld s9, 232(a0) ld s10, 240(a0) ld s11, 248(a0) ld t3, 256(a0) ld t4, 264(a0) ld t5, 272(a0) ld t6, 280(a0) # restore user a0 ld a0, 112(a0) # return to user mode and user pc. # usertrapret() set up sstatus and sepc. sret2.2 保存现场uservec进入内核态的第一件事就是把子进程此刻用户态的所有寄存器如 a0 存放的文件路径指针a1 存放的打开模式等一股脑地保存到 p-trapframe陷入帧中。struct trapframe { /* 0 */ uint64 kernel_satp; // kernel page table /* 8 */ uint64 kernel_sp; // top of processs kernel stack /* 16 */ uint64 kernel_trap; // usertrap() /* 24 */ uint64 epc; // saved user program counter /* 32 */ uint64 kernel_hartid; // saved kernel tp /* 40 */ uint64 ra; /* 48 */ uint64 sp; /* 56 */ uint64 gp; /* 64 */ uint64 tp; /* 72 */ uint64 t0; /* 80 */ uint64 t1; /* 88 */ uint64 t2; /* 96 */ uint64 s0; /* 104 */ uint64 s1; /* 112 */ uint64 a0; /* 120 */ uint64 a1; /* 128 */ uint64 a2; /* 136 */ uint64 a3; /* 144 */ uint64 a4; /* 152 */ uint64 a5; /* 160 */ uint64 a6; /* 168 */ uint64 a7; /* 176 */ uint64 s2; /* 184 */ uint64 s3; /* 192 */ uint64 s4; /* 200 */ uint64 s5; /* 208 */ uint64 s6; /* 216 */ uint64 s7; /* 224 */ uint64 s8; /* 232 */ uint64 s9; /* 240 */ uint64 s10; /* 248 */ uint64 s11; /* 256 */ uint64 t3; /* 264 */ uint64 t4; /* 272 */ uint64 t5; /* 280 */ uint64 t6; };2.3 恢复现场userret当内核态子进程在硬盘上建好文件后会把文件描述符比如 3写进 trapframe-a0 中。随后执行 sret 指令退回用户态子进程醒来仿佛什么都没发生只是拿到了返回值 3。3.阶段三 打开文件表ofile当程序执行 open 时操作系统在底层构建了一条三级跳的映射链条这种设计实现了用户态与物理硬件的绝对隔离。文件描述符fd程序拿到的是一个简单的整数 fd。这个 fd 仅仅是该程序专属的 ofile 数组的下标。程序只能操作这个数字无法越权触碰内核的内存指针以此保证系统安全。动态运行时的 struct file内核通过 ofile 数组的下标找到对应的 struct file。设置这一层是因为同一个文件可以被并发访问。struct file 独立记录了本次 open 操作的专属上下文例如当前拥有的是只读还是读写权限以及具体的 off 偏移量。物理文件真身 inodestruct file 内部的 ip 指针最终指向内存中唯一的 inode。inode 包含了物理文件在底层磁盘上的真实扇区分布信息与元数据。4.阶段四 inode表、sleeplock当程序调用 write 准备将数据刷入磁盘时必须面对 CPU 高速运算与外设低速运转之间绝大的速度差。获取独占写入权内核找到目标 inode 后程序必须申请该 inode 绑定的 sleeplock。如果此时有其他任务正在写这个文件当前程序绝对不能使用 spinlock。因为 spinlock 会导致 CPU 空转在漫长的磁盘 I/O 期间空转是对算力的极大浪费。主动让出 CPU 核心拿不到 sleeplock 的程序会被内核变更为 SLEEPING 状态同时内核将目标 inode 的内存地址记录在该程序的 chan 字段中以此标记它具体在等待哪把锁。随后程序调用 swtch 触发上下文切换让出当前 CPU 核心去执行其他处于 RUNNABLE 状态的任务。详细实现见kernel/sleeplock.c硬件中断唤醒外设完成写入动作后会向 CPU 触发硬件中断。内核的中断处理例程随即介入检索所有处于 SLEEPING 状态且 chan 字段匹配该 inode 地址的程序将它们的状态回写为 RUNNABLE。调度器随后会重新安排其执行。5.阶段五 清理资源... close(fd); exit(0); } else { wait(NULL); ...子进程调用 exit()内核再次获取 p-lock自旋锁遍历 p-ofile 数组将所有打开的 struct file 的引用计数减一如果减到0就清理 inode。最后把状态改为 ZOMBIE。父进程调用 wait()由于父子并发父进程可能早就在 wait() 里等待了。发现子进程没死父进程会通过 sleep() 机制主动交出 CPU底层依赖 p-lock 保证检查状态和睡眠的原子性。当子进程变成 ZOMBIE 后唤醒父进程父进程终于读取子进程的残存状态并将其进程表项彻底抹平为 UNUSED。

更多文章