Xv 6 Chapter 0
Operating system interfaces
操作系统的工作是分享电脑资源给多个程序,并且提供一系列服务而不是只让硬件提供。
操作系统通过接口给用户程序提供服务,所以设计一个好的接口就显得很重要。
这里操作系统xv6提供基础的接口,由Ken Thompson 和Dennis Ritchie的操作系统介绍,同时还有模仿Unix的内部设计。
每一个运行的程序叫做进程,他们都有自己的空间,其中包括指令,数据和栈。指令实现了程序的计算功能,数据是那些计算中的变量。而栈则决定了程序的运行。
当一个进程需要请求内核指令的时候,他会在操作系统的接口中请求一个程序。这样的程序叫做系统调用。系统调用进入到内核,内核就提供服务并且返回。因此一个进程在用户内存和内核内存中互相切换。
内核使用CPU的硬件保护机制来保证每个进程在用户内存中运行并只能访问自己的空间。内核运行的时候利用硬件提供的特权来实现这些保护,同时用户程序没有这些优先权。当一个用户程序请求系统调用的时候,这个硬件就提升他的优先级并运行一个在内核实现安排好的程序。
Processes and memory
一个xv6的进程包括了用户内存(指令,数据,还有栈),和每个进程的对于内核的私有状态。Xv6可以进程间共享时间: 他在可利用的并等待执行的CPU之间互相切换。当一个进程不在执行,xv6就会保存它cpu的寄存器状态,直到下一次运行这个进程。内核与进程的进程号联系,也叫PID用来联系每个进程。
进程可以使用fork系统调用来创建一个新的进程。Fork创建的新的进程叫做子进程,父子进程使用同样的内存空间。Fork不仅返回父进程,也会返回子进程。(C语言函数可以返回两个值得最好例子) 在父进程中,fork返回子进程的pid,在子进程中,fork返回的是0.
如下:
int pid = fork();
if(pid > 0){
printf("parent: child=%d\n", pid);
pid = wait();
printf("child %d is done\n", pid);
} else if(pid == 0){
printf("child: exiting\n");
exit();
} else {
printf("fork error\n");
}
Exit系统调用会让进程停止运行并释放资源,比如内存和打开的文件等。Wait系统调用返回一个当前进程退出的子进程pid,如果当前进程的所有子进程都没有退出,wait函数将会一直等待。
Exec系统调用会把当前进程的内存空间从文件系统中替换成一个新的内存。这样的文件一定要特定的格式,比如明确哪一部分存储指令,哪一部分是数据,从哪个指令开始等等。Xv6使用ELF格式。当exec执行成功后,它不会返回到当前函数,相反从文件中加载的指令将会在ELF开头声明的地方开始运行。Exec需要两个参数:可执行文件的名字和一串字符串参数。
比如:
char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;
exec("/bin/echo", argv);
printf("exec error\n");
这个代码用程序“/bin/echo”代替了原始程序,并且运行参数列表的“echo hello”。大部分的程序无视第一个参数,因为一般来所只是程序的名字。
Xv6隐式的分配大部分的用户内存空间:fork 分配子进程复制父进程所需要的内存,exec为可执行文件分配足够的内存。一个进程在运行的时候需要更多内存的时候可以调用sbrk(n)去增加n个字节。Sbrk返回新内存的地址。
Xv6不提供用户这个概念,或者保护每一个用户。在Unix的角度,所有的xv6进程都是以root用户来运行。
I/O and File descriptors
一个文件描述符是一个很小的整数,表示一个由内核管理的东西(Object),并且进程可以读写。一个进程也可以通过打开一个文件,目录,或者一个设备,又或者创建一个管道,复制一个已存在的描述符等等来很多方法获得文件描述符。简单来说我们经常把一个指向一个东西(Object)的描述符当做文件“file”。文件描述符的接口抽象并将文件,管道还有设备都看作一样的字符流。
在内部,xv6内核使用文件描述符当做在进程表中的查找指引。所以每个进程都有文件描述符的私有空间,文件描述符由0开始。一般来说,进程从文件描述符0(standard input 标准输入)中读取,从文件描述符1(standard input 标准输入)中写入,从文件描述符2(standard error 标准错误输入)写入错误信息。正如我们所知,shell利用这些惯例来实现I/O的重定向和管道。Shell确保了时刻都会有三个文件描述符打开,这些已经被控制台默认。
Close系统调用会释放文件描述符,并且让它可以被以后的open和pipe或者dup系统调用再次使用。一个新创建的文件描述符总是从该进程中没用过的最小数字开始。
文件描述符和fork共同使得I/O重定向容易被实现。Fork复制父进程的内存和文件描述,所以子进程的文件描述符从和父进程一样的地方开始计算。Exec系统调用替换了当前进程的内存但是保存了它的文件表。这样就允许shell利用fork实现了I/O重定向,并且重新打开之前选择的文件描述符,然后执行新的程序。一下是一个简单的在shell运行命令cat命令command cat < input.txt:的例子
char *argv[2];
argv[0] = "cat";
argv[1] = 0;
if(fork() == 0) {
close(0);
open("input.txt", O_RDONLY);
exec("cat", argv);
}
在子进程关闭文件描述符0之后,open就能确保使用的是新打开的文件input.txt,0将会是最小的可利用的文件描述符。Cat之后再执行文件描述符0并指向input.txt。
尽管fork复制文件描述符表,每个潜在的文件偏移量也会在父子进程中共享。这是一个例子:
if(fork() == 0) {
write(1, "hello ", 6);
exit();
} else {
wait();
write(1, "world\n", 6);
}
Dup系统调用可以复制一个已存在的文件描述符,返回一个新的并且指向相同。两个文件共享偏移量就如同文件描述符被fork复制一样。这是另一种方法写:
fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);
两个文件共享偏移量仅仅当他们被同一个文件dup或者fork,即使open打开同一个文件也不会共享偏移量。
Pipes
管道是一个小的内核缓冲区,让一对文件描述符使用。一个是来读一个是用来写。一下是一个例子:
int p[2];
char *argv[2];
argv[0] = "wc";
argv[1] = 0;
pipe(p);
if(fork() == 0) {
close(0);
dup(p[0]);
close(p[0]);
close(p[1]);
exec("/bin/wc", argv);
} else {
close(p[0]);
write(p[1], "hello world\n", 12);
close(p[1]);
}
刚关闭文件描述符0或者1的时候,dup分配最小可利用的描述符,也就是刚关闭的
如果所有指向write的描述符都被关闭,read将会返回0。事实上read一直阻塞直到关闭管道的写端。如果wc的文件描述符指向管道的写端,wc将会永远读不到文件结尾。
有些地方翻译的不是很到位,如果有错误欢迎指出谢谢。后续还会有xv6的源码阅读。