原文链接:https://pdos.csail.mit.edu/6.828/2021/xv6/book-riscv-rev2.pdf

原文标题:XV6:一种简单、类Unix的教学操作系统

第一章

操作系统接口

操作系统的任务是让多个程序共享计算机;相对于在只有硬件的情况下,提供一套更有用的服务。操作系统管理和抽象底层硬件,比如,文本编辑器不需要考虑使用哪一种存储硬件(硬盘/U盘)。操作系统让多个程序分享硬件,同时(或者说看起来像)运行。操作系统向应用程序提供可控的交互方法,让它们能够共享数据或者协同工作。

操作系统通过一个接口向用户程序提供服务。根据以往经验,设计出一个好的接口是困难的。另外,我们需要接口简单、紧凑,这样执行起来才更容易。而且,我们总是试图向应用程序提供精确的接口。解决的的办法是设计几种接口,这些接口通过一些机构联合起来提供普遍性。

本书使用一种操作系统,作为固定的范例来描述操作系统的概念。这种操作系统,xv6,提供由Ken Thompson和Dennis Richie发明的Unix基本接口,复刻了Unix的核心设计。Unix提供了一种紧凑的接口,该接口的机构能够很好的集成,提供了非凡的普遍性。这个接口是如此的成功,以至于现代操作系统——BSD, Linux,macOS, Solaris, 甚至某种程度上的Microsoft Windows,都拥有类Unix的接口。理解xv6是理解这些或者其它许多操作系统的良好开端。

如图1.1,xv6采用了内核的传统形式,内核是用来运行程序的特殊程序。一个正在运行的程序,叫做进程,在内存中存有指令,数据和栈。指令执行程序的运行。数据是程序运行的变量。栈管理程序的过程调用。一般情况下,一台计算机可以拥有多个进程,但是只拥有一个内核。

一个进程请求内核服务,就是请求系统调用,系统调用是一种操作系统接口的调用。系统调用深入内核;内核执行服务和返回。因此进程在用户空间和内核空间之间切换执行。

内核使用CPU的硬件保护机制,来确保每一个在用户空间运行的进程只能访问属于自己的内存。内核程序执行的时候,带有硬件特权,需要启用这些保护;用户程序执行的时候,没有这些特权。当用户程序执行系统调用的时候,硬件提升特权等级,开始执行在内核预先设定好的功能。

 内核提供的系统调用集合是用户程序看到的接口。xv6内核提供一个服务和系统调用的子集,这些子集从传统上说,是Unix内核所拥有的。图1.2列出了所有xv6的系统调用。

本章的其余内容概括了xv6的服务——进程,内存,文件描述符,管道,还有一个文件系统——用代码段对它们加以说明;还包括shell,这个Unix的命令行用户界面,如何使用它们的探讨。

shell是一种普通程序,它解读并执行用户的命令。事实上,shell是用户程序,不是内核的一部分,它说明了系统调用接口的能力:shell没有什么特别之处。这也意味着shell很容易被取代;结果是,现代Unix系统拥有各种shell以供选择,每个都有自己的用户界面和脚本功能。xv6的shell本质上是一个简化版的Unix Bourne shell。可以在(user/sh.c:1)找到它。

1.1进程和内存

一个xv6进程包含用户空间内存(指令,数据和堆栈),每个进程的状态在内核中是私有的。Xv6时分进程:它为等待执行的众多进程透明地切换可用的CPU。当一个进程没被执行的时候,xv6保存它的CPU寄存器,等到它下次运行的时候再恢复寄存器。内核把每一个进程与进程标识符(process identifier, PID)相关联。

进程可以使用 fork 系统调用创建一个新进程。Fork 给予新进程与调用进程完全相同的内存内容(指令和数据)。在原始进程中,fork 返回新进程的PID。在新进程中,fork 返回 0 。原始进程和新进程通常被称作 parent 和 child。

例如,考虑下面的C语言程序段:

int pid = fork();

if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait((int *) 0);
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit(0);
} else {
printf("fork error\n");
}

......

exit系统调用让调用进程停止执行,释放内存和打开的文件等资源。Exit 使用一个整型数,通常用0来表示成功,1来表示失败。wait系统调用返回一个PID,它是当前进程一个已退出的(或者是已关闭的)的子进程的PID,并且把子进程的退出状态复制到传递给wait的地址上;如何调用进程的子进程都没有退出,wait就等待它们退出。如何调用进程没有子进程,wait立即返回-1。如果父进程不在意子进程的退出状态,也可以向wait传递0 。

在例程中,输出行为:

parent: child=1234

child: exiting

两行输出的先后顺序不确定(甚至可能会混在一起输出),取决于父进程或子进程谁先调用printf。子进程退出后,父进程的wait返回,导致父进程打印

parent: child 1234 is done

尽管最初子进程与父进程有着相同的内存内容,父进程和子进程执行着不同的内存和寄存器:改变其中一个进程的变量不会影响另外一个。例如,当wait的返回值存储到父进程的pid中时,存储在子进程变量pid并不改变。子进程的pid值保持为0。

exec系统调用从存储在文件系统中的一个文件加载内存映像,替换调用进程的内存。该文件必须拥有特殊的格式,指定文件的哪一部分存放指令,哪一部分存放数据,指令从哪里开始。xv6使用ELF格式,将在第3章中详细讨论。 exec 执行完毕后,并没有返回到调用程序;取而代之的是,由文件加载的指令从ELF头(header)声明的起点开始执行。Exec带有两个参数:可执行文件的名称和字符串数组参数。例如:

char *argv[3]

argv[0] = "echo";

argv[1] = "hello";

argv[2] = 0;

exec("/bin/echo", argv);

printf("exec error\n");

这个代码段用与参数清单 echo hello一起运行的程序实体 /bin/echo 替换调用程序。参数数组的第一个元素一般是程序的名称,大多数程序会忽略它。

xv6 shell使用上述调用来代表用户运行程序。shell的主要结构很简单;参见main (user/sh.c:145). main 循环使用 getcmd 命令自用户读取一行输入。然后调用fork,fork 在shell进程中创建一个副本。父进程调用 wait, 子进程运行命令。例如,如果用户在shell中输入"echo hello",就会把"echo hello"作为参数调用runcmd命令。runcmd(user/sh.c:58)运行实际命令。对于"echo hello"来说,会调用 exec(user/sh.c:78)。如果 exec 完成,则子进程将从echo执行指令,而不是exec。echo从某一点调用exit, 这将引发父进程从main(user/sh.c:145)函数里的wait返回。

你或许会疑惑为什么fork和exec没有合并成一个调用; 稍后我们会看到,shell在I/O重定向的执行中会充分利用这种分离。为了避免复制进程后立即(用exec)替换它而产生的浪费,运行中的内核通过使用虚拟内存技术,比如copy-on-write(见4.6节),为这种情形优化fork的执行。

xv6在后台中分配大多数用户空间内存:fork为父进程内存的子进程副本分配内存,exec为可执行文件分配足够的内存。运行中的进程需要更多的内存,它可以调用 sbrk(n)来增加 n字节数据内存; sbrk返回新内存的地址。


 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐