computer_knowledge_notes/OS/XV6/专题分析/1_操作系统接口.md

7.4 KiB
Raw Blame History

操作系统的任务是在多个进程之间共享计算机,并提供比硬件本身支持更有用的服务。操作系统抽象和管理底层的硬件,以使上层的程序不必关心硬件的细节。它也在多个进程之间共享硬件,这样多个进程就可以同时运行了。最终,操作系统为程序提供了受控的交互方式,让它们可以共享数据或同时工作。

操作系统使用接口来为用户程序提供服务。设计一个好的接口是困难的。一方面我们希望它简单以便于正确地实现,另一方面我们又希望能提供大量复杂的特性。解决这一矛盾的技巧是,在设计接口的时候让它们依赖一些机制,这些机制可以组合起来提供更多的通用性。

xv6实现了Unix的基本接口并模仿了它的内部设计。

xv6有内核用以为运行的程序提供服务。运行的程序即进程有自己的内存用以包含指令、数据和栈。

进程通过系统调用使用系统服务。这样,进程就在内核空间和用户空间之间切换。

在内核层面实现了进程隔离的机制。

本章的其余部分概述了xv6提供的服务进程、内存、文件描述符、管道、文件系统并讨论了shell。

shell只是一个普通的程序它从用户那里读取命令并执行它们。它的代码在user/sh.c。

从内核态到用户态

在xv6启动的过程中0号核在main函数里会执行userinit函数标记initcode处的数据会复制到第一个用户进程的内存空间。标记initcode的数字是一段代码它对应的内容在user/initcode.S是要把文件init的内容放到内存里以用户态去执行。文件init编译自user/init.c。

user/init.c把文件sh放到内存里执行。文件sh编译自user/sh.c把用户输入的命令调入内存中执行它就是xv6的shell。shell就是不断地使用getcmd函数读取命令行的输入然后使用runcmd函数来执行命令行的输入。

从用户态到内核态

系统调用怎么从用户态到的内核态呢文件user/usys.pl生成user/usys.Susys.S就是用来完成从用户态到内核态的代码。从代码可见它是通过ecall产生一个异常来进入内核态。只要把usys.S生成的二进制文件链接进用户程序里就可以使用系统调用了。

进程和内存

xv6的进程包含两部分一是用户空间的内存一是仅内核可见的进程状态。

相关系统调用的实现请看kernel/sysproc.c。

fork

文件位置kernel/proc.c。作用创建一个进程新进程的内存和父进程是完全一样的。

fork函数一次调用两次返回这很怪异因为一般的调用都是只返回一次的。fork从父进程返回很正常但为什么会从子进程返回呢这是因为子进程继承了父进程的所有资源这一句*(np->tf) = *(p->tf)相当于子进程复制了父进程运行的“快照”子进程里也相当于进行了一个fork函数且从下一句开始执行。

  1. p为当前进程np为子进程。
  2. uvmcopy把父进程的内存复制给子进程。
  3. np->tf->a0 = 0保证了子进程返回0因为寄存器a0保存了函数的返回值。
  4. return pid对于子进程来说在汇编层面应该实际执行的返回寄存器a0的值。
exit

文件位置kernel/proc.c。作用让调用它的进程停止运行并释放资源(如内存、打开的文件等)。exit有一个整型的状态参数通常0代表成功1代表失败。

wait

文件位置kernel/proc.c。作用等待当前进程的某个子进程退出如果有子进程退出返回该进程的pid并把退出状态复制到传递给它的地址上。如果不关心子进程的退出状态可以直接给wait传递参数0。

exec

文件位置kernel/exec.c。作用把当前进程的内存替换为文件里保存的内存镜像并执行之。exec有两个参数,第一个是要执行的程序,第二个这个程序的参数(以字符串数组的形式出现)。

exec首先用namei来打开文件path。然后读取ELF header。xv6的程序是用ELF格式来读取的(ELF格式详见kernel/elf.h)。在一个ELF二进制里ELF header(struct elfhdr)在头部的位置,接下来是些程序头(struct proghdr),每个程序头都描述了一个必须被载入内存里的段(section)。xv6的程序只有一个程序头但其它系统指令和数据可能会有不同的段(section)。

第一步是快速检查文件是否正确包含了一个ELF二进制。ELF二进制的开头部分是一个魔数0x7f后面跟字符串"ELF"。如果魔数能对的上,exec就认为这个二进制是正确的。

exec使用proc_pagetable分配了一个没有映射的新页表,然后用uvmalloc来为每个ELF段分配内存并用loadseg把每个ELF段载入内存。loadseg使用walkaddr来找到将要写入ELF段的物理地址然后用readi来把文件中的内容载入到该地址。

可以用objdump -p来查看程序头的内容。在程序头里filesz可能比memsz小,这表示它们之间的间隙(gap)应该用0来填充而不是从文件里读取。

接下来该分配和初始化用户的栈了。它只为栈分配了一个页。exec依次把参数复制到栈顶,然后把到这些参数的指针记录在ustack里。在传给main函数的argv列表里,最末尾放了一个空指针。ustack的前三个入口分别是

sbrk

文件位置kernel/sysproc.c。作用为进程分配或回收内存。它有一个参数代表要分配的字节数。它返回的是新分配内存的地址。

这个系统调用是通过growproc来实现的。growproc使用uvmalloc来分配内存,如果给的参数是正数。或使用uvmdealloc来释放内存,如果给的参数是负数。

uvmalloc首先使用kalloc来分配物理内存,然后再用mappages把PTE加到用户的页表里。uvmdealloc调用uvmunmap实现其功能,uvmunmap首先用walk来找到对应的PTE然后使用kfree来释放相应的物理内存。

xv6里进程的页表不只是告诉硬件怎么映射到虚拟地址也是分配给那个进程的物理内存页的唯一记录。这就是为什么uvmunmap在释放用户内存的时候需要对用户页表进行检查。

shell

文件位置user/sh.c。shell结构简单main函数里可以看到它的主循环就是不断地使用getcmd来读取用户输入。然后使用fork来创建一个子进程。父进程调用wait来等待子进程执行命令。子进程调用runcmd来执行真正的命令。

既然fork之后肯定要执行exec为什么不把它们合而为一呢这是为了在I/O重定向的时候方便使用。fork复制的内存基本上是无用的,会在随后被exec替换掉,为了防止复制过程的资源浪费,可以使用虚拟内存的技术(如写时复制copy-on-write)。

在xv6里没有多用户的概念。用Unix术语来说所有xv6进程都是作为根用户来运行的。

I/O和文件描述符

相关系统调用的实现请看kernel/sysfile.c。

read

文件位置kernel/file.c。作用从文件描述符所指向的文件读取n个字节。

write

文件位置kernel/file.c。作用从文件描述符所指向的文件写入n个字节。

close
open
pipe
dup
chdir
mkdir
mknod
fstat

管道

文件系统