> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec01-introduction-and-examples/1.5-some-systemcalls.md).

# 1.5 read, write, exit系统调用

接下来，我将讨论对于应用程序来说，系统调用长成什么样。因为系统调用是操作系统提供的服务的接口，所以系统调用长什么样，应用程序期望从系统调用得到什么返回，系统调用是怎么工作的，这些还是挺重要的。你会在第一个lab中使用我们在这里介绍的系统调用，并且在后续的lab中，扩展并提升这些系统调用的内部实现。

我接下来会展示一些简单的例子，这些例子中会执行系统调用，并且我会在XV6中运行这些例子。XV6是一个简化的类似Unix的操作系统，而Unix是一个老的操作系统，但是同时也是很多现代操作系统的基础，例如Linux，OSX。所以Unix使用的非常广泛。而作为我们教学用的操作系统，XV6就要简单的多。它是受Unix启发创造的，有着相同的文件结构，但是却要比任何真实的Unix操作系统都要简单的多。因为它足够简单，所以你们极有可能在几周内很直观的读完所有的代码，同时也把相应的书也看完，这样你们就能理解XV6内部发生的一切事情了。

XV6运行在一个RISC-V微处理器上，而RISC-V是MIT6.004课程讲解的处理器，所以你们很多人可能已经知道了RISC-V指令集。理论上，你可以在一个RISC-V计算机上运行XV6，已经有人这么做了。但是我们会在一个QEMU模拟器上运行XV6。

我这里会写下来，我们的操作系统是XV6，它运行在RISC-V微处理器上，当然不只是RISC-V微处理器，我们假设有一定数量的其他硬件存在，例如内存，磁盘和一个console接口，这样我们才能跟操作系统进行交互。但是实际上，XV6运行在QEMU模拟器之上。这样你们都能在没有特定硬件的前提下，运行XV6。

![](/files/-MHvHK3kmYjL1DhU1p9L)

接下来，我会展示一下代码。首先，我会在我的笔记本上设置好XV6。首先输入make qemu，你会发现你在实验中会经常用到这个命令。这个命令会编译XV6，而XV6是用C语言写的。我首先执行一下make clean，这样你们就能看到完整的编译过程。

![](/files/-MHvI3IEg_-jUl9_3x0f)

之后我输入make qemu，这条指令会编译并构建xv6内核和所有的用户进程，并将它们运行在QEMU模拟器下。

![](/files/-MHvISJCORVXBFpreV8l)

编译需要花费一定的时间。

![](/files/-MHvJT2VC3So_Db0nrEm)

现在xv6系统已经起来并运行了。$表示Shell，这是参照Unix上Shell的命令行接口。如果你用过Athena工作站，它的Shell与这里的非常像。XV6本身很小，并且自带了一小部分的工具程序，例如ls。我这里运行ls，它会输出xv6中的所有文件，这里只有20多个。

![](/files/-MHvKJSFZoCQxQ3djoa_)

可以看到，这里还有grep，kill，mkdir和rm，或许你们对这些程序很熟悉，因为它们在Unix中也存在。

我向你们展示的第一个系统调用是一个叫做copy的程序。

![](/files/-MHvKyEGRPn_ArkwFtsl)

它的源代码只有不到一页。你们这里看到的是一个程序，它从第8行的main开始，这是C程序的风格。它在第12行进入到一个循环中，在循环中，它会在第13行从输入读取一些数据，并在第16行，将数据写入到输出。如果我在XV6中运行这个copy程序，

![](/files/-MHvLiHZyFnyTyRXIPY9)

它会等待输入。我随便输入一些字符，程序会读取我输入的字符，并将相同的字符输出给我。

![](/files/-MI-P0KuhASrhk5FjhrN)

所以这是一个非常简单的程序。如你所看到的，这个程序是用C语言写的，如果你不懂C语言，那最好还是去读一本标准的[C编程语言](https://zh.wikipedia.org/wiki/C%E7%A8%8B%E5%BA%8F%E8%AE%BE%E8%AE%A1%E8%AF%AD%E8%A8%80_\(%E4%B9%A6\))。这个程序里面执行了3个系统调用，分别是read，write和exit。

如果你看第13行的read，它接收3个参数：

* 第一个参数是文件描述符，指向一个之前打开的文件。Shell会确保默认情况下，当一个程序启动时，文件描述符0连接到console的输入，文件描述符1连接到了console的输出。所以我可以通过这个程序看到console打印我的输入。当然，这里的程序会预期文件描述符已经被Shell打开并设置好。这里的0，1文件描述符是非常普遍的Unix风格，许多的Unix系统都会从文件描述符0读取数据，然后向文件描述符1写入数据。
* read的第二个参数是指向某段内存的指针，程序可以通过指针对应的地址读取内存中的数据，这里的指针就是代码中的buf参数。在代码第10行，程序在栈里面申请了64字节的内存，并将指针保存在buf中，这样read可以将数据保存在这64字节中。
* read的第三个参数是代码想读取的最大长度，sizeof(buf)表示，最多读取64字节的数据，所以这里的read最多只能从连接到文件描述符0的设备，也就是console中，读取64字节的数据。

read的返回值可能是读到的字节数，在上面的截图中也就是6（xyzzy加上结束符）。read可能从一个文件读数据，如果到达了文件的结尾没有更多的内容了，read会返回0。如果出现了一些错误，比如文件描述符不存在，read或许会返回-1。在后面的很多例子中，比如第16行，我都没有通过检查系统调用的返回来判断系统调用是否出错，但是你应该比我更加小心，你应该清楚系统调用通常是通过返回-1来表示错误，你应该检查所有系统调用的返回值以确保没有错误。

如果你想知道所有的系统调用的参数和返回值是什么，在XV6书籍的第二章有一个表格。

> 学生提问：如果read的第三个参数设置成1 + sizeof(buf)会怎样？
>
> Robert教授：如果第三个参数是65字节，操作系统会拷贝65个字节到你提供的内存中（第二个参数）。但是如果栈中的第65个字节有一些其他数据，那么这些数据会被覆盖，这里是个bug，或许会导致你的代码崩溃，或者一些异常的行为。所以，作为一个程序员，你必须要小心。C语言很容易写出一些编译器能通过的，但是最后运行时出错的代码。虽然很糟糕，但是现实就是这样。

有一件事情需要注意的事，这里的copy程序，或者说read，write系统调用，它们并不关心读写的数据格式，它们就是单纯的读写，而copy程序会按照8bit的字节流处理数据，你怎么解析它们，完全是用应用程序决定的。所以应用程序可能会解析这里的数据为C语言程序，但是操作系统只会认为这里的数据是按照8bit的字节流。
