operating system

xv6是一个简单的操作系统,由 ANSI C 实现,多进程。涉及进程、内存、文件描述符、管道和文件系统

0 操作系统接口

操作系统的功能是合理分配资源给多个程序,管理和抽象底层硬件。

用户通过 shell 提供系统调用,shell 是用户程序,不是内核组成部分,因此可以被替代。现代 Unix 系统由各式各样的 shell 给我们使用,默认的大都是 bash,但是为了珍惜生命我更喜欢用 zsh。xv6 的 shell 实现在 console.c 中

  1. 进程和内存

    通过调用 fork 产生子进程,fork 会复制一份相同的资源给子进程用,原来的资源还是父进程拥有。fork 的返回值不同,可以利用返回值辨别父子关系。

    exit 系统调用会终止程序,释放资源(内存,文件)

    wait 系统调用会返回一个使用了 exit 的子进程的编号。如果没有,那么 wait 愿意等。

    exec 系统调用会是用文件系统中的镜像替代已有的内存。文件有固定格式,xv6 使用 ELF 格式,第二章讲。当你调用了该函数,那么将不会返回,直接执行镜像内存。exec 的参数有两个,分别是执行文件路径和参数数组。

    fork 用来创建进程,exec 用来加载程序,二者通常一起使用,但是为什么分开设计,后面会讲。

    xv6 隐性分配内存,调用 fork 分配和父进程一样的内存,调用 exec 分配容纳文件的内存,程序运行时调用 sbrk 分配运行内存。

    在 xv6 中,所有进程都在 root 下运行。

  2. I/O 和文件描述符

    Unix 中一切皆文件,因为内核管理的对象都是由不同的整数所代表,我们称之为文件描述符,一个进程打开一个文件或者设备,就能获得该文件的描述符;创建一个管道,复制一个存在的描述符,文件描述符抽象了文件、管道和设备,让它们看起来都是字节流。

    read 和 write 都是系统调用,read(fd, buf, n) 将读取 fd 所代表的文件到 buf 数组中,一次最多读 n 个字节,当没有更多可读的时候,read 会返回读取的字节数。每次读取,文件指针都会偏移,读完后指针会重置为0。

    write(fd, buf, n) 和 read 差不多,从 buf 中读取最多 n 字节,存放到 fd 中。

    cat 指令可以利用 read 和 write 实现,就是从一个文件描述符中读取,然后写入到另一个文件描述符中,但是我们其实并不知道文件描述符代表的是终端、管道还是文件。

    每一个进程都有自己的文件符表,记录着自己所拥有的文件符,每次重新打开一个新的文件,总是会用最小的没有被使用的文件符。

    文件描述符和 fork 相互作用使得 IO 重定向变得简单。fork 产生子进程,并把文件表带了过来,exec 执行替换了内存,但是保留了文件表。这种特性允许 shell 去实现 IO 重定向通过 fork、打开关闭的文件符,然后执行新程序。

    close 系统调用会关闭一个文件描述符,然后调用 open 就肯定会使用 0 这个描述符,从而达到重定向。

    这也就是 fork 和 exec 要分开实现的原因,我们可以在两者之间做一些调整,从而达到我们想要的结果。

    父子进程之间共享文件描述符,因此文件指针也共享。试着用子进程写 hello,父进程写 world。

    当然你可以利用 dup 实现,dup 可以复制一个已经存在的文件描述符,返回一个新的并指向相同的文件相同的位置。可以试着用 dup 来编写一个 hello world 程序。

    只有通过 fork 和 dup 实现的文件描述符才可以共用文件指针,因为本来就是同一个文件;open 就不行,open 打开的完全是另一个文件,不共享偏移。

    XV6 不支持 IO 重定向,但是你可以去实现。

    总结起来,文件描述符是高度的抽象,因为它隐藏了你所连接的设备区别。

  3. 管道

    管道是一个小的内核缓冲区,作为一对文件描述符暴露在进程中,一个作为输入,一个作为输出。因此,管道使进程间可以通信。