MIT 6.S081入门lab2 操作系统调用
MIT 6.S081入门lab2 系统调用
一、参考资料阅读与总结
1.xv6 book书籍阅读(操作系统架构)
a. 总览
- 操作系统的核心: 对多个活动的支持,即多路复用、隔离、交互。
- xv6前提: 64位操作系统;多核RISC-V:包括RAM、ROM、串口、磁盘;宏内核。
b.抽象系统资源
- RTOS等实时操作系统: 将系统调用实现为一个库,进程直接和硬件进行交互(合作方案) -> 前提:进程之间相互信任(定时放弃处理器+没有bug)
- Unix方案(强隔离): 使用固定接口,限制应用与存储的交互,同时将设备抽象为文件路径名称;操作系统作为接口的实现者管理硬件;Ex:CPU的分时复用和多CPU的调度、exec的内存管理的实现(swap分区)。
- Unix文件描述符(fd): 抽象了底层实现,同时简化了交互方式。
c.用户态,核心态,以及系统调用
- 强隔离的实现目标: 用户和硬件隔离(寄存器);用户和操作系统隔离(数据结构和代码);用户之间隔离(内存)。
- 强隔离的硬件基础(CPU): RISC-V为例:
模式 | 作用 |
---|---|
机器模式 | CPU配置(start.s) |
管理模式 | 特权指令(中断、页表地址的寄存器操作等)[内核空间] |
用户模式 | 非特权指令[用户空间] |
- 注意: RISC-V提供跳转指令:ecall;同时内核进入点需要由内核控制。
d.内核组织
- 宏内核与微内核:
宏内核:整个操作系统在管理模式运行。优点: 便于操作系统内部的交互;缺点: 接口复杂;一个bug就会会导致操作系统崩溃
微内核:只有必须部分在管理模式运行,采用客户/服务器机制,内核部分只提供低级接口(启动用户程序、IPC、底层硬件设备访问)。优点:防止了系统崩溃; 缺点:沟通复杂。
e.代码(XV6架构篇)
-
代码位置: kernel/
使用模块化概念进行分隔,模块接口在def.h定义kernel代码文件
文件 描述 bio.c 文件系统的磁盘块缓存 console.c 连接到用户的键盘和屏幕 entry.S 首次启动指令 exec.c exec()系统调用 file.c 文件描述符支持 fs.c 文件系统 kalloc.c 物理页面分配器 kernelvec.S 处理来自内核的陷入指令以及计时器中断 log.c 文件系统日志记录以及崩溃修复 main.c 在启动过程中控制其他模块初始化 pipe.c 管道 plic.c RISC-V中断控制器 printf.c 格式化输出到控制台 proc.c 进程和调度 sleeplock.c CPU提供的锁机制(睡眠锁) spinlock.c CPU为提供的锁机制;程序实现(自旋锁) start.c 机器模式启动代码 string.c 字符串和字节数组库 swtch.c 线程切换 syscall.c 调度系统接口函数 sysfile.c 文件相关的系统调用 sysproc.c 进程相关的系统调用 trampoline.S 用于在用户和内核之间切换的汇编代码 trap.c 对陷入指令和中断进行处理并返回的C代码 uart.c 串口控制台设备驱动程序 virtio_disk.c 磁盘设备驱动程序 vm.c 管理页表和地址空间
f.进程总览
- 进程本质: 隔离操作的最小单位
- 进程数据存储实现: 页表提供地址空间(虚拟地址),RISC-V页表将虚拟地址映射为物理地址,其中包括.text段、.data段、栈、堆、切换内核操作所需部分(trampoline、trapframe)。
注: RISC-V的64位指针中,只有低39位使用,其中低38位为用户的虚拟地址(kernel/riscv.h:348) - 进程状态: 一个进程的状态有许多元素组成,其被抽象为一个proc结构体(kernel/proc.h:86)。其中重要的有页表、内核栈、运行状态等。执行进程时,会将相应的页表装载到页表寄存器satp中
- 线程: 针对与每个进程,其都有一个硬件线程(软件线程分时复用);线程被挂起时,其状态被保存在栈上。
注意: 栈存在两个,用户栈和内核栈,根据执行指令的级别进行应用。 - 用户<->内核 切换过程:
ecall(进入内核)-> 提升级别 ->
pc跳转至内核进入点entry point -> 切换至内核栈 -> 内核执行指令 ->
切换至用户栈 -> sret(进入用户空间)->
恢复用户指令
g.代码(启动XV6和第一个进程)
-
xv6启动流程:
bootloader[程序重定位到0x80000000]
-> kernel/entry.S[看门狗+栈设置]entry.S
# qemu -kernel loads the kernel at 0x80000000 # and causes each CPU to jump there. # kernel.ld causes the following code to # be placed at 0x80000000. .section .text _entry: # set up a stack for C. # stack0 is declared in start.c, # with a 4096-byte stack per CPU. # sp = stack0 + (hartid * 4096) la sp, stack0 li a0, 1024*4 csrr a1, mhartid addi a1, a1, 1 mul a0, a0, a1 add sp, sp, a0 # jump to start() in start.c call start spin: j spin
-> kernel/start.c:[修改模式+设置中断/异常管理+设置定时器+禁用MMU](注意: 这里的模式切换是和MMU是直接通过修改寄存器实现的)
start.c
#include "types.h" #include "param.h" #include "memlayout.h" #include "riscv.h" #include "defs.h" void main(); void timerinit(); // entry.S needs one stack per CPU. __attribute__ ((aligned (16))) char stack0[4096 * NCPU]; // scratch area for timer interrupt, one per CPU. uint64 mscratch0[NCPU * 32]; // assembly code in kernelvec.S for machine-mode timer interrupt. extern void timervec(); // entry.S jumps here in machine mode on stack0. void start() { // set M Previous Privilege mode to Supervisor, for mret. unsigned long x = r_mstatus(); x &= ~MSTATUS_MPP_MASK; x |= MSTATUS_MPP_S; w_mstatus(x); // set M Exception Program Counter to main, for mret. // requires gcc -mcmodel=medany w_mepc((uint64)main); // disable paging for now. w_satp(0); // delegate all interrupts and exceptions to supervisor mode. w_medeleg(0xffff); w_mideleg(0xffff); w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE); // ask for clock interrupts. timerinit(); // keep each CPU's hartid in its tp register, for cpuid(). int id = r_mhartid(); w_tp(id); // switch to supervisor mode and jump to main(). asm volatile("mret"); } // set up to receive timer interrupts in machine mode, // which arrive at timervec in kernelvec.S, // which turns them into software interrupts for // devintr() in trap.c. void timerinit() { // each CPU has a separate source of timer interrupts. int id = r_mhartid(); // ask the CLINT for a timer interrupt. int interval = 1000000; // cycles; about 1/10th second in qemu. *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval; // prepare information in scratch[] for timervec. // scratch[0..3] : space for timervec to save registers. // scratch[4] : address of CLINT MTIMECMP register. // scratch[5] : desired interval (in cycles) between timer interrupts. uint64 *scratch = &mscratch0[32 * id]; scratch[4] = CLINT_MTIMECMP(id); scratch[5] = interval; w_mscratch((uint64)scratch); // set the machine-mode trap handler. w_mtvec((uint64)timervec); // enable machine-mode interrupts. w_mstatus(r_mstatus() | MSTATUS_MIE); // enable machine-mode timer interrupts. w_mie(r_mie() | MIE_MTIE); }
-> kernel/main.c [初始化设备和子系统] (执行到userinit())
main.c
#include "types.h" #include "param.h" #include "memlayout.h" #include "riscv.h" #include "defs.h" volatile static int started = 0; // start() jumps here in supervisor mode on all CPUs. void main() { if(cpuid() == 0){ consoleinit(); printfinit(); printf("\n"); printf("xv6 kernel is booting\n"); printf("\n"); kinit(); // physical page allocator kvminit(); // create kernel page table kvminithart(); // turn on paging procinit(); // process table trapinit(); // trap vectors trapinithart(); // install kernel trap vector plicinit(); // set up interrupt controller plicinithart(); // ask PLIC for device interrupts binit(); // buffer cache iinit(); // inode cache fileinit(); // file table virtio_disk_init(); // emulated hard disk userinit(); // first user process 执行到这里创建第一个进程 __sync_synchronize(); started = 1; } else { while(started == 0) ; __sync_synchronize(); printf("hart %d starting\n", cpuid()); kvminithart(); // turn on paging trapinithart(); // install kernel trap vector plicinithart(); // ask PLIC for device interrupts } scheduler(); }
-> kernel/proc.c:212:第一个进程userinit(分配proc进程结构、初始化用户虚拟内存、申请页表、设置陷阱帧[程序epc]、设置程序栈指针、设置进程名[与用户空间initcode一致]、设置工作目录、设置进程状态、释放进程锁)
proc.c:userinit
// a user program that calls exec("/init") // od -t xC initcode uchar initcode[] = { 0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02, 0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02, 0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00, 0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00, 0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69, 0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Set up first user process. void userinit(void) { struct proc *p; p = allocproc(); initproc = p; // allocate one user page and copy init's instructions // and data into it. uvminit(p->pagetable, initcode, sizeof(initcode)); p->sz = PGSIZE; // prepare for the very first "return" from kernel to user. p->trapframe->epc = 0; // user program counter p->trapframe->sp = PGSIZE; // user stack pointer safestrcpy(p->name, "initcode", sizeof(p->name)); p->cwd = namei("/"); p->state = RUNNABLE; release(&p->lock); }
-> 调度器执行 user/initcode.S 进入用户空间,使用exec进入内核装载init.c ,从init返回用户空间:
initcode.S
# Initial process that execs /init. # This code runs in user space. #include "syscall.h" # exec(init, argv) .globl start start: la a0, init la a1, argv li a7, SYS_exec ecall # for(;;) exit(); exit: li a7, SYS_exit ecall jal exit # char init[] = "/init\0"; init: .string "/init\0" # char *argv[] = { init, 0 }; .p2align 2 argv: .long init .long 0
-> /user/init.c 从内核返回用户空间,创建控制台设备节点并打开文件描述符,使用exec打开shell
init.c
// init: The initial user-level program #include "kernel/types.h" #include "kernel/stat.h" #include "kernel/spinlock.h" #include "kernel/sleeplock.h" #include "kernel/fs.h" #include "kernel/file.h" #include "user/user.h" #include "kernel/fcntl.h" char *argv[] = { "sh", 0 }; int main(void) { int pid, wpid; if(open("console", O_RDWR) < 0){ mknod("console", CONSOLE, 0); open("console", O_RDWR); } dup(0); // stdout dup(0); // stderr for(;;){ printf("init: starting sh\n"); pid = fork(); if(pid < 0){ printf("init: fork failed\n"); exit(1); } if(pid == 0){ exec("sh", argv); printf("init: exec sh failed\n"); exit(1); } for(;;){ // this call to wait() returns if the shell exits, // or if a parentless process exits. wpid = wait((int *) 0); if(wpid == pid){ // the shell exited; restart it. break; } else if(wpid < 0){ printf("init: wait returned an error\n"); exit(1); } else { // it was a parentless process; do nothing. } } } }
h.总结、xv6与真实操作系统的差异
- 真实设备中,同时应用了宏内核和微内核。
- 现代操作系统支持进单进程多线程,使单一进程可以多核并行,其核心机制有潜在的接口更改、控制线程和进程序的资源共享等
二、涉及函数及其文件:
-
kernel/proc.h: 核心进程管理的重要结构
kernel/proc.h
// Saved registers for kernel context switches. 内核上下文切换寄存器 struct context { uint64 ra; //返回地址 uint64 sp; //堆栈指针 // callee-saved uint64 s0; uint64 s1; uint64 s2; uint64 s3; uint64 s4; uint64 s5; uint64 s6; uint64 s7; uint64 s8; uint64 s9; uint64 s10; uint64 s11; }; // Per-CPU state. cpu状态结构体 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()? 中断使能 }; extern struct cpu cpus[NCPU]; //SMP数组 // per-process data for the trap handling code in trampoline.S. // sits in a page by itself just under the trampoline page in the // user page table. not specially mapped in the kernel page table. // the sscratch register points here. // uservec in trampoline.S saves user registers in the trapframe, // then initializes registers from the trapframe's // kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap. // usertrapret() and userret in trampoline.S set up // the trapframe's kernel_*, restore user registers from the // trapframe, switch to the user page table, and enter user space. // the trapframe includes callee-saved user registers like s0-s11 because the // return-to-user path via usertrapret() doesn't return through // the entire kernel call stack. //进程在用户模式下的所有寄存器状态,用来保存进程上下文 struct trapframe { /* 0 */ uint64 kernel_satp; // kernel page table /* 8 */ uint64 kernel_sp; // top of process's 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; }; enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; //进程状态结构 // Per-process state 进程状态 struct proc { struct spinlock lock; // p->lock must be held when using these: enum procstate state; // Process state struct proc *parent; // Parent process void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent's wait int pid; // Process ID // these are private to the process, so p->lock need not be held. 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) };
-
kernel/defs.h:s所有的系统调用API和系统结构声明
defs.h中函数作用表格
函数名称 函数作用 所在文件 binit
初始化缓冲区 bio.c bread
读取缓冲区 bio.c brelse
释放缓冲区 bio.c bwrite
写入缓冲区 bio.c bpin
固定缓冲区 bio.c bunpin
解除固定缓冲区 bio.c consoleinit
初始化控制台 console.c consoleintr
控制台中断处理 console.c consputc
控制台输出字符 console.c exec
执行程序 exec.c filealloc
分配文件结构 file.c fileclose
关闭文件 file.c filedup
复制文件描述符 file.c fileinit
初始化文件系统 file.c fileread
读取文件 file.c filestat
获取文件状态 file.c filewrite
写入文件 file.c fsinit
初始化文件系统 fs.c dirlink
创建目录链接 fs.c dirlookup
查找目录项 fs.c ialloc
分配索引节点 fs.c idup
复制索引节点 fs.c iinit
初始化索引节点 fs.c ilock
锁定索引节点 fs.c iput
释放索引节点 fs.c iunlock
解锁索引节点 fs.c iunlockput
解锁并释放索引节点 fs.c iupdate
更新索引节点 fs.c namecmp
比较文件名 fs.c namei
查找索引节点 fs.c nameiparent
查找父索引节点 fs.c readi
读取索引节点 fs.c stati
获取索引节点状态 fs.c writei
写入索引节点 fs.c itrunc
截断索引节点 fs.c ramdiskinit
初始化RAM磁盘 ramdisk.c ramdiskintr
RAM磁盘中断处理 ramdisk.c ramdiskrw
RAM磁盘读写 ramdisk.c kalloc
内核内存分配 kalloc.c kfree
释放内核内存 kalloc.c kinit
初始化内核内存 kalloc.c initlog
初始化日志 log.c log_write
写入日志 log.c begin_op
开始文件系统操作 log.c end_op
结束文件系统操作 log.c pipealloc
分配管道 pipe.c pipeclose
关闭管道 pipe.c piperead
读取管道 pipe.c pipewrite
写入管道 pipe.c printf
格式化输出 printf.c panic
内核错误处理 printf.c printfinit
初始化格式化输出 printf.c cpuid
获取CPU ID proc.c exit
进程退出 proc.c fork
创建进程 proc.c growproc
增加进程大小 proc.c proc_pagetable
获取进程页表 proc.c proc_freepagetable
释放进程页表 proc.c kill
终止进程 proc.c mycpu
获取当前CPU proc.c getmycpu
获取当前CPU proc.c myproc
获取当前进程 proc.c procinit
初始化进程系统 proc.c scheduler
调度器 proc.c sched
进程调度 proc.c setproc
设置当前进程 proc.c sleep
进程睡眠 proc.c userinit
初始化用户进程 proc.c wait
等待进程结束 proc.c wakeup
唤醒进程 proc.c yield
放弃CPU proc.c either_copyout
拷贝到用户空间 proc.c either_copyin
从用户空间拷贝 proc.c procdump
打印进程信息 proc.c swtch
上下文切换 swtch.S acquire
获取自旋锁 spinlock.c holding
检查自旋锁 spinlock.c initlock
初始化自旋锁 spinlock.c release
释放自旋锁 spinlock.c push_off
禁用中断 spinlock.c pop_off
恢复中断 spinlock.c acquiresleep
获取睡眠锁 sleeplock.c releasesleep
释放睡眠锁 sleeplock.c holdingsleep
检查睡眠锁 sleeplock.c initsleeplock
初始化睡眠锁 sleeplock.c memcmp
内存比较 string.c memmove
内存移动 string.c memset
内存设置 string.c safestrcpy
安全字符串拷贝 string.c strlen
字符串长度 string.c strncmp
字符串比较 string.c strncpy
字符串拷贝 string.c argint
获取整数参数 syscall.c argstr
获取字符串参数 syscall.c argaddr
获取地址参数 syscall.c fetchstr
获取字符串 syscall.c fetchaddr
获取地址 syscall.c syscall
系统调用 syscall.c trapinit
初始化陷阱 trap.c trapinithart
初始化陷阱处理器 trap.c usertrapret
用户陷阱返回 trap.c uartinit
初始化UART uart.c uartintr
UART中断 uart.c uartputc
UART输出字符 uart.c uartputc_sync
同步UART输出字符 uart.c uartgetc
UART获取字符 uart.c kvminit
初始化内核虚拟内存 vm.c kvminithart
初始化虚拟内存处理器 vm.c kvmpa
虚拟内存到物理地址 vm.c kvmmap
映射内核虚拟内存 vm.c mappages
映射页面 vm.c uvmcreate
创建用户虚拟内存 vm.c uvminit
初始化用户虚拟内存 vm.c uvmalloc
分配用户虚拟内存 vm.c uvmdealloc
释放用户虚拟内存 vm.c uvmcopy
拷贝用户虚拟内存 vm.c uvmfree
释放用户虚拟内存 vm.c uvmunmap
取消映射用户虚拟内存 vm.c uvmclear
清除用户虚拟内存 vm.c walkaddr
遍历地址 vm.c copyout
拷贝到用户空间 vm.c copyin
从用户空间拷贝 vm.c copyinstr
拷贝字符串到用户空间 vm.c plicinit
初始化PLIC plic.c plicinithart
初始化PLIC处理器 plic.c plic_claim
声明PLIC plic.c plic_complete
完成PLIC处理 plic.c virtio_disk_init
初始化虚拟磁盘 virtio_disk.c virtio_disk_rw
虚拟磁盘读写 virtio_disk.c virtio_disk_intr
虚拟磁盘中断 virtio_disk.c defs.h中结构体作用表格
结构体名称 结构体作用 所在文件 struct buf
缓冲区结构,通常用于文件系统的块缓存 buf.h struct context
上下文结构,用于保存进程或线程的上下文(如寄存器状态) proc.h, swtch.S struct file
文件结构,代表一个打开的文件 file.h struct inode
索引节点结构,表示文件系统中的一个文件 file.h struct pipe
管道结构,用于进程间通信 pipe.c struct proc
进程结构,表示一个进程 proc.h struct spinlock
自旋锁结构,用于多线程同步 spinlock.h struct sleeplock
睡眠锁结构,另一种同步机制 sleeplock.h struct stat
状态结构,存储文件或目录的信息 stat.h struct superblock
超级块结构,存储文件系统的全局信息 fs.h -
kernel/entry.S: 内核启动汇编代码,见上文
-
kernel/main.c: 内核主函数,见上文
-
user/initcode.S: 用户空间第一个进程的初始化,见上文
-
user/init.c: 用户空间的的第一个进程,其创建控制台设备节点并打开文件描述符,同时使用exec打开shell,见上文。
-
略读:
-
kernel/proc.c: 是进程管理的核心,其中包含的程序如下:
proc.c中进程管理函数及其作用
函数名称 函数作用 参数和返回值 procinit
初始化进程表 无参数,无返回值 cpuid
返回当前CPU的ID 无参数,返回值:int mycpu
返回当前CPU的cpu结构体指针 无参数,返回值:struct cpu* myproc
返回当前正在执行的进程的proc结构体指针 无参数,返回值:struct proc* allocpid
为新进程分配一个唯一的进程ID 无参数,返回值:int allocproc
在进程表中查找未使用的进程项并进行初始化 无参数,返回值:struct proc* freeproc
释放一个进程结构和它占用的资源 参数:struct proc *p,无返回值 proc_pagetable
为给定进程创建一个用户页表 参数:struct proc *p,返回值:pagetable_t proc_freepagetable
释放进程的页表和它引用的物理内存 参数:pagetable_t pagetable, uint64 sz,无返回值 userinit
设置第一个用户进程 无参数,无返回值 growproc
增加或减少进程的内存大小 参数:int n,返回值:int fork
创建一个新进程,复制父进程的状态 无参数,返回值:int reparent
将进程的孩子进程重新分配给init进程 参数:struct proc *p,无返回值 exit
结束当前进程并保留其状态 参数:int status,无返回值 wait
等待子进程退出并返回其pid 参数:uint64 addr,返回值:int scheduler
CPU的进程调度器 无参数,无返回值 sched
切换到调度器 无参数,无返回值 yield
放弃CPU一个调度轮次 无参数,无返回值 forkret
fork子进程的第一次调度后执行的函数 无参数,无返回值 sleep
使进程进入睡眠状态 参数:void *chan, struct spinlock *lk,无返回值 wakeup
唤醒睡眠在chan上的所有进程 参数:void *chan,无返回值 wakeup1
用于唤醒等待进程的函数 参数:struct proc *p,无返回值 kill
结束指定pid的进程 参数:int pid,返回值:int either_copyout
将数据复制到用户地址或内核地址(取决于usr_dst) 参数:int user_dst, uint64 dst, void *src, uint64 len,返回值:int either_copyin
从用户地址或内核地址复制数据(取决于usr_src) 参数:void *dst, int user_src, uint64 src, uint64 len,返回值:int procdump
打印进程列表,用于调试 无参数,无返回值 -
kernel/exec.c: 执行程序的核心代码
exec函数的流程图:
mermaid源代码
graph TD A[开始] --> B[检查文件] B -->|不存在| Z[返回-1] B --> C[锁定inode] C --> D[检查ELF头是否有效] D -->|无效| Z D --> E[创建页表] E --> F[加载程序段到内存] F --> G[解锁inode并结束文件操作] G --> H[准备用户栈] H --> I[推入参数和agrv指针] I --> J[设置trapframe(包括程序入口点和栈指针)] J --> K[切换用户进程镜像(更换页表)] K --> L[返回参数个数(作为main的第一个参数)] L --> M[结束] K --> |无法切换|Z H --> |用户栈申请失败|Z E --> |创建页表失败|Z
三、课程视频观看笔记
-
隔离: 在复用的同时达到强隔离,通过对硬件的抽象实现。 例外:只有基于RTOS的系统是基于库的,即为协作式调度,由于其中的应用程序都为可信的。
- 进程(fork)是对CPU及其资源的抽象
- exec是对内存的抽象;
- 文件是对磁盘块的抽象
- 硬件基础:用户/内核模式(多权限模式)、虚拟内存/页表(MMU)
-
注意: 操作系统应该是防御性的,其能够处理恶意程序的注入;同时,应该保证应用程序和内核的强隔离。
-
内核\用户模式:
- 内核模式中CPU可以执行特权指令、用户模式只能使用非特权指令;特权指令主要指的是硬件相关的寄存器,如中断、时钟设置等。
- 虚拟内存(MMU):使用页表,将虚拟地址映射到物理地址,针对每一个进程提供自己的页表。->强页表隔离
-
系统调用:
- ecall: RISC-V 指令进入内核,根据系统调用编号进入内核的特定的位置。
例:fork() ->ecall<sys_fork> -|-> syscall->fork() - syscall:进行参数检查
- ecall: RISC-V 指令进入内核,根据系统调用编号进入内核的特定的位置。
-
内核理论:
内核可被视为可信任计算基础(KGB,没有bug)、内核必须将用户程序视作恶意的(安全思维模式)- 宏内核:整个操作系统都在内核中(桌面级操作系统)
优点:OS能够更好的协作,效率更高;
缺点:bug危险,且代码量庞大 - 微内核:内核级只有IPC、页表、复用代码等核心代码,大部分操作系统在用户层 (嵌入式操作系统)
优点:小,更少的bug;
缺点:系统调用复杂,性能容易受限
- 宏内核:整个操作系统都在内核中(桌面级操作系统)
-
xv6:
QEMU模拟RISC-V:读取指令->解码->执行 + 寄存器状态 -
syscall: 调用ecall后进入syscall,syscall根据传入的信息调用内核中相应的函数
syscall函数
void syscall(void) { int num; struct proc *p = myproc(); num = p->trapframe->a7; if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { p->trapframe->a0 = syscalls[num](); } else { printf("%d %s: unknown sys call %d\n", p->pid, p->name, num); p->trapframe->a0 = -1; } }
四、完成lab及其代码
主要是跟着操作即可,两个实验的目的都是熟悉系统调用的过程:
系统调用的过程:
user/user.h: 系统调用声明
->user/usys.S: 由usys.pl生成,为从用户台跳至内核态的跳板函数,使用ecall[中断处理]从用户态送入内核态,通过寄存器a7传递参数
->kernel/syscall.c: 到达内核态统一处理函数syscall(),根据表中查找函数并执行
->kernel/sysproc.c:进行具体内核操作,到达sys_()系列函数。
用户态配置修改:
-
user.h下加入相应的系统调用声明
user.h
...省略... struct sysinfo; //lab2 ...省略... // system calls ...省略... int trace(int); //lab1 int sysinfo(struct sysinfo *); //lab2 ...省略...
-
usys.pl下添加相应参数,完成对跳转代码的生成
usys.pl
...省略... entry("trace"); //lab1 entry("sysinfo"); //lab2 ...省略...
内核态配置修改:
-
syscall.h中加入相应系统调用编号
syscall.h
...省略... #define SYS_trace 22 //lab1 #define SYS_sysinfo 23 //lab2
-
syscall.c中加入相应的内核调用函数,并补充syscalls映射表
syscall.c
...省略... extern uint64 sys_trace(void); //lab1 extern uint64 sys_sysinfo(void); //lab2 static uint64 (*syscalls[])(void) = { ...省略... [SYS_trace] sys_trace, //lab1 [SYS_sysinfo] sys_sysinfo, //lab2 }; ...省略...
具体lab1实现(/kernel目录下,即内核态):
-
修改proc.h中proc结构体定义,添加跟踪系统调用的mask参数:
proc.h
// Per-process state struct proc { ...省略.. uint64 tracemask; //tracemask };
-
proc.c 中在创建新进程时对mask参数初始化、释放进程时清理、创建子进程时候复制
proc.c
...省略... static struct proc* allocproc(void)//创建新进程 { ...省略... // Set up new context to start executing at forkret, // which returns to user space. memset(&p->context, 0, sizeof(p->context)); p->context.ra = (uint64)forkret; p->context.sp = p->kstack + PGSIZE; p->tracemask = 0; //初始化mask参数 return p; } // free a proc structure and the data hanging from it, // including user pages. // p->lock must be held. static void freeproc(struct proc *p) //释放进程 { ...省略... p->tracemask = 0; ...省略... } ...省略... // Create a new process, copying the parent. // Sets up child kernel stack to return as if from fork() system call. int fork(void) { int i, pid; struct proc *np; struct proc *p = myproc(); ...省略... pid = np->pid; np->tracemask = p->tracemask; np->state = RUNNABLE; ...省略... }
-
在sysproc.c中实现相应syscall的代码,其负责将参数注入到proc结构体中:
sysproc.c
// sys_trace trace the syscall uint64 sys_trace(void) { int mask; if(argint(0, &mask) < 0) //读取trapframe,获得参数。 return -1; myproc()->tracemask |= mask; return 0; }
-
由于syscall实现所有内核调用,因此在syscall中根据mask侦测syscall;并加入代号到名字(string)的mapping,便于打印:
syscall.c
//内核调用代码与相应名称的mapping static char *syscall_name[] = { [SYS_fork] "fork", [SYS_exit] "exit", [SYS_wait] "wait", [SYS_pipe] "pipe", [SYS_read] "read", [SYS_kill] "kill", [SYS_exec] "exec", [SYS_fstat] "fstat", [SYS_chdir] "chdir", [SYS_dup] "dup", [SYS_getpid] "getpid", [SYS_sbrk] "sbrk", [SYS_sleep] "sleep", [SYS_uptime] "uptime", [SYS_open] "open", [SYS_write] "write", [SYS_mknod] "mknod", [SYS_unlink] "unlink", [SYS_link] "link", [SYS_mkdir] "mkdir", [SYS_close] "close", [SYS_trace] "trace", }; void syscall(void) { int num; struct proc *p = myproc(); num = p->trapframe->a7; //将系统调用从a7寄存器中提出 if(num > 0 && num < NELEM(syscalls) && syscalls[num]) { //判断是否为合法的系统调用 p->trapframe->a0 = syscalls[num](); //返回值存放在a0寄存器中 if (p->tracemask & (1 << num)) { printf("%d: syscall %s -> %d\n", p->pid, syscall_name[num], p->trapframe->a0); //根据mask判定是否需要trace这一调用 } } else { printf("%d %s: unknown sys call %d\n", p->pid, p->name, num); p->trapframe->a0 = -1; } }
具体lab2实现(/kernel目录下,即内核态):
-
根据kernel/sysinfo.h结构体,在def.h相应位置声明需要获取内存和进程信息所需要的函数
def.h
// kalloc.c void* kalloc(void); void kfree(void *); void kinit(void); uint64 kfreemem_bytes(void); //剩余内存获取函数 // proc.c ...省略... void procdump(void); uint64 nproc(void); //现有进程数量函数
-
在相应的文件中( kalloc.c和proc.c)实现内存获取函数(通过遍历相应链表)和现有进程数量函数(通过遍历相应数组)
kalloc.c
struct run { struct run *next; }; struct { struct spinlock lock; struct run *freelist; } kmem; ...省略... // kfreemem_bytes to know how much bytes left in mem uint64 kfreemem_bytes(void) { struct run *r; uint64 freemen_bytes = 0; acquire(&kmem.lock); //获取锁 r = kmem.freelist; while(r) { //遍历链表 freemen_bytes += PGSIZE; //获取剩余内存字节数 r = r->next; } release(&kmem.lock);//释放锁 return freemen_bytes; }
proc.c
struct proc proc[NPROC]; ...省略... // count how many procs is not in UNUSED state now uint64 nproc(void) { struct proc *p; uint64 proc_count = 0; for(p = proc; p < &proc[NPROC]; p++) { //遍历数组 acquire(&p->lock); //加锁 if(p->state != UNUSED) { proc_count++; //获取使用进程数量 } release(&p->lock); //解锁 } return proc_count; }
-
参考sys_fstat()(kernel/sysfile.c)和filestat()(kernel/file.c),使用copyout将数据传回用户空间
int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
sysproc.c
#include "sysinfo.h" ...省略... uint64 sys_sysinfo(void) { struct proc *p = myproc(); //获取当前进程结构体 uint64 addr; //传回用户空间的地址 struct sysinfo s; if(argaddr(0, &addr) < 0) //获取相应参数 return -1; s.freemem = kfreemem_bytes(); s.nproc = nproc(); //调用内核函数填充结构体 if(copyout(p->pagetable, addr, (char *)&s, sizeof(s)) < 0) //将数据回用户空间 return -1; return 0; }
参考文献
2020版xv6手册:https://pdos.csail.mit.edu/6.S081/2020/xv6/book-riscv-rev1.pdf
xv6手册与代码笔记:https://zhuanlan.zhihu.com/p/350949057
xv6阅读笔记:https://ghostasky.github.io/2022/07/12/XV6/
xv6手册中文版:http://xv6.dgs.zone/tranlate_books/book-riscv-rev1/c1/s3.html
28天速通MIT 6.S081操作系统公开课:https://zhuanlan.zhihu.com/p/625526955
MIT6.s081操作系统笔记:https://juejin.cn/post/7006016963029762056