记录阅读《xv6: a simple, Unix-like teaching operating system》的一些摘要,这是第一章的内容:Operating system interfaces。
- fork函数作用:fork创建一个新进程,称为子进程,其内存内容与调用进程(称为父进程)完全相同。fork在父进程和子进程中都会返回。在父进程中,fork返回子进程的 PID;在子进程中,fork返回零。
- exit作用:exit系统调用使调用进程停止执行并释放诸如内存和打开的文件等资源。exit接收一个整数状态参数,按照惯例,0 表示成功,1 表示失败。
- wait作用:wait系统调用返回当前进程已退出(或被终止)的子进程的 PID,并将子进程的退出状态复制到传递给wait的地址;如果调用者的子进程都没有退出,wait会等待其中一个退出。如果调用者没有子进程,wait立即返回 -1。如果父进程不关心子进程的退出状态,它可以向wait传递一个地址为 0 的参数。
- exec作用:exec系统调用用从文件系统中存储的文件加载的新内存映像替换调用进程的内存。该文件必须具有特定的格式,该格式指定文件的哪个部分保存指令、哪个部分是数据、从哪个指令开始等等。xv6 使用 ELF 格式,当exec成功时,它不会返回到调用程序;相反,从文件加载的指令从 ELF 头中声明的入口点开始执行。exec接受两个参数:包含可执行文件的文件名和一个字符串参数数组。
- shell的原理:主循环使用getcmd从用户读取一行输入。然后它调用fork,这会创建一个 shell 进程的副本。父进程调用wait,而子进程运行命令。例如,如果用户向 shell 输入了 “echo hello”,那么runcmd将以 “echo hello” 作为参数被调用。runcmd(在user/sh.c:58)运行实际的命令。对于 “echo hello”,它将调用exec(在user/sh.c:78)。如果exec成功,那么子进程将执行来自echo的指令而不是runcmd。在某个时刻,echo将调用exit,这将使父进程从main函数中的wait返回(在user/sh.c:145)。
- 你可能会想为什么fork和exec不在一个单独的调用中;我们稍后会看到,在实现输入 / 输出重定向时,shell 利用了这种分离。为了避免创建一个重复的进程然后立即用exec替换它的浪费,操作系统内核通过使用诸如写时复制(见第 4.6 节)等虚拟内存技术来优化这种用例下fork的实现。进一步说明:为什么fork和exec是分开的调用是有帮助的了:在这两个调用之间,shell 有机会重定向子进程的 I/O,而不会干扰主 shell 的 I/O 设置。
- 隐式分配内存:fork为子进程分配复制父进程内存所需的内存,exec分配足够的内存来容纳可执行文件。一个在运行时需要更多内存(可能用于malloc)的进程可以调用sbrk(n)来将其数据内存增加n个字节;sbrk返回新内存的位置。
- 惯例:进程从文件描述符 0(标准输入)读取数据,将输出写入文件描述符 1(标准输出),并将错误消息写入文件描述符 2(标准错误)。shell 利用这个惯例来实现 I/O 重定向和管道。shell 确保它始终有三个打开的文件描述符(在user/sh.c:151),默认情况下这些是控制台的文件描述符。
- read作用:文件描述符fd中最多读取n个字节,将它们复制到buf中,并返回读取的字节数。每个指向文件的文件描述符都有一个与之相关联的偏移量。read从当前文件偏移量处读取数据,然后将该偏移量增加读取的字节数:后续的读取将返回第一次读取返回的字节之后的字节。当没有更多字节可读时,read返回 0 以表示文件结束。
- write作用:将buf中的n个字节写入文件描述符fd并返回写入的字节数。只有在发生错误时才会写入少于n个字节。与read类似,write在当前文件偏移量处写入数据,然后将该偏移量增加写入的字节数:每次写入都从上一次写入的位置继续。
- cat的说明:cat不知道它是从文件、控制台还是管道中读取数据。类似地,cat也不知道它是将内容输出到控制台、文件还是其他地方。文件描述符的使用以及文件描述符 0 是输入、文件描述符 1 是输出的惯例使得cat的实现很简单。
- close作用:close系统调用释放一个文件描述符,使其可以在未来的open、pipe或dup系统调用中被重用(见下文)。新分配的文件描述符始终是当前进程中未使用的编号最小的描述符。
- fork的进一步说明:文件描述符与fork相互作用,使得 I/O 重定向易于实现。fork会连同内存一起复制父进程的文件描述符表,因此子进程开始时与父进程具有完全相同的打开文件。系统调用exec会替换调用进程的内存,但保留其文件表。这种行为允许 shell 通过fork实现 I/O 重定向,在子进程中重新打开选定的文件描述符,然后调用exec来运行新程序。
- open作用:open的第二个参数由一组以位表示的标志组成,用于控制open的行为。可能的值在文件控制(fcntl)头文件(在kernel/fcntl.h:1 - 5)中定义:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)、O_CREATE(如果文件不存在则创建文件)和O_TRUNC(将文件截断为零长度)。
- dup作用:dup系统调用复制一个现有的文件描述符,返回一个新的文件描述符,该新描述符指向相同的底层 I/O 对象。两个文件描述符共享一个偏移量,就像由fork复制的文件描述符一样。
- 命令行
ls existing-file non-existing-file > tmp1 2>&1
解释:
这是执行 “ls” 命令,列出 “existing-file”(现有文件)和 “non-existing-file”(不存在的文件)的信息。
> tmp1
:
这是输出重定向操作。它将 “ls” 命令的标准输出(通常是文件和目录的列表信息)重定向到文件 “tmp1” 中。这意味着 “ls” 命令产生的正常输出内容将被写入到 “tmp1” 文件中,而不是显示在终端上。
2>&1
:
这里 “2” 代表标准错误输出(stderr),“1” 代表标准输出(stdout)。“2>&1” 的意思是将标准错误输出重定向到与标准输出相同的地方。
在这个例子中,由于前面已经将标准输出重定向到了 “tmp1” 文件,所以这个操作会使得 “ls” 命令在遇到 “non-existing-file”(不存在的文件)时产生的错误信息(标准错误输出)也被写入到 “tmp1” 文件中。 总的来说,这个命令的作用是执行 “ls” 命令列出两个文件的信息,并将正常输出和错误输出都写入到文件 “tmp1” 中。尽管xv6 shell 不支持错误文件描述符的 I/O 重定向,但现在你知道如何实现它了。
- 管道描述: 一个由内核管理的小缓冲区,以一对文件描述符的形式呈现给进程,一个用于读取,一个用于写入。向管道的一端写入数据,就可以从管道的另一端读取这些数据。管道为进程间通信提供了一种方式。
管道的优点:
- 首先,管道会自动清理自身。使用文件重定向时,shell 必须小心地在完成后删除 “/tmp/xyz” 这样的临时文件。如果不进行清理,临时文件可能会占用磁盘空间并逐渐积累,导致混乱。
- 其次,管道可以传递任意长度的数据流。而文件重定向需要磁盘上有足够的可用空间来存储所有数据。如果数据量非常大,可能会超出磁盘空间的限制,而管道则不受此限制,只要内存能够处理,就可以持续传递数据。
- 第三,管道允许管道的各个阶段并行执行。在使用管道时,各个阶段可以同时进行数据处理,提高效率。而使用文件的方法要求第一个程序完成后,第二个程序才能开始。这会导致时间上的延迟,特别是当处理大量数据或复杂任务时。
- 第四,如果正在实现进程间通信,管道的阻塞式读写比文件的非阻塞语义更高效。在管道中,当没有数据可读时,读操作会自动阻塞等待数据到来;当没有空间可写时,写操作会阻塞等待空间释放。这种机制使得进程可以更高效地同步和协调,而不需要不断地轮询文件状态。相比之下,使用文件进行进程间通信可能需要更复杂的同步机制,并且非阻塞的文件操作可能会导致频繁的检查和浪费资源。
- 程序解释:
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]);
}
- 子进程:
dup(p[0])
复制管道的读端文件描述符到最小可用的文件描述符,通常是 0,这样就将子进程的标准输入重定向到管道的读端。因为要重定向所以close(0),子进程调用close和dup以使文件描述符 0 指向管道的读端,关闭p中的文件描述符,并调用exec来运行wc。当wc从其标准输入读取时,它从管道中读取。 - 父进程:向管道的写端写入字符串"hello world\n",长度为 12 个字节。这样写入的数据可以在子进程中通过管道的读端读取。父进程关闭管道的读端,向管道写入数据,然后关闭写端。
- 进一步解释:如果子进程在执行wc命令时,没有关闭管道的写端,那么当wc从管道读取数据时,由于不知道是否还会有新数据写入管道,所以read操作会一直阻塞等待。只有当所有指向管道写端的文件描述符都被关闭时,read才会返回 0,表示到达文件末尾,wc才能正常结束执行。因此,为了确保wc能正确读取管道中的数据并在适当的时候结束,子进程在执行wc之前要关闭管道的写端。
- 如果没有数据可用,对管道的读操作会等待数据被写入或者所有指向写端的文件描述符都被关闭;在后一种情况下,read将返回 0,就好像到达了数据文件的末尾。read会阻塞直到不可能有新数据到达这一事实,是上述代码中在执行wc之前子进程要关闭管道写端的一个重要原因:如果wc的一个文件描述符指向管道的写端,那么wc将永远不会看到文件结束标志。
- 我的理解:父子进程都有文件描述符,为了使所有写端的文件描述符都被关闭让wc的read成功退出,那么我们需要保证读wc之前要关闭写端,否则wc的一个文件描述符指向管道的写端导致wc永远看不到文件结束标志。
- 命令行
grep fork sh.c | wc -l
解释:在文件 “sh.c” 中搜索包含 “fork” 的行,然后统计这些匹配行的数量 - shell 可能会创建一个进程树。这个树的叶子是命令,内部节点是等待左右子进程完成的进程。fork是相辅相成的,左端fork右端也需要fork:如对于p->left不进行fork操作,而是在内部进程中运行runcmd(p->left)。那么,例如,“echo hi | wc” 将不会产生输出,因为当 “echo hi” 在runcmd中退出时,内部进程退出并且永远不会调用fork来运行管道的右端。这种不正确的行为可以通过在内部进程的runcmd中不调用exit来修复,但这个修复会使代码变得复杂:现在runcmd需要知道它是否是一个内部进程。当对runcmd(p->right)也不进行fork操作时,也会出现复杂情况。例如,仅进行那个修改,“sleep 10 | echo hi” 将立即打印 “hi” 而不是在 10 秒后,因为 “echo” 立即运行并退出,而不是等待 “sleep” 完成。由于sh.c的目标是尽可能简单,所以它不会尝试避免创建内部进程。
- mkdir作用:创建新的目录,使用O_CREATE标志的open用于创建新的数据文件
- mknod作用:创建新的设备文件,该文件指向一个设备。与设备文件相关联的是主设备号和次设备号(mknod的两个参数),它们唯一地标识一个内核设备。当一个进程稍后打开一个设备文件时,内核将read和write系统调用转移到内核设备实现,而不是将它们传递给文件系统。
- 一个文件的名称与文件本身是不同的;同一个底层文件,称为索引节点(inode),可以有多个名称,称为链接。每个链接由目录中的一个条目组成;该条目包含一个文件名和对一个 inode 的引用。一个 inode 保存关于文件的元数据,包括它的类型(文件、目录或设备)、它的长度、文件内容在磁盘上的位置以及指向一个文件的链接数量。
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
struct stat {
int dev; // File system’s disk device
uint ino; // Inode number
short type; // Type of file
short nlink; // Number of links to file
uint64 size; // Size of file in bytes
};
- link作用:创建另一个文件系统名称,该名称与现有文件指向同一个索引节点(inode)。以下代码片段创建了一个新文件,它有两个名称 “a” 和 “b”。
open("a", O_CREATE|O_WRONLY);
link("a", "b");
- 从 “a” 读取或写入与从 “b” 读取或写入是相同的。每个索引节点(inode)由一个唯一的 inode 编号标识。在上述代码序列之后,可以通过检查fstat的结果来确定 “a” 和 “b” 指向相同的底层内容:两者将返回相同的 inode 编号(ino),并且链接计数(nlink)将被设置为 2。
- unlink作用:有点类似于引用计数,此时,虽然从文件系统中删除了名称 “a”,但文件的内容仍然存在,因为 “b” 仍然引用着该文件的 inode。只有当再次执行unlink(“b”)且没有其他文件描述符引用该文件时,文件的 inode 和占用的磁盘空间才会被释放。这体现了文件系统中链接计数的作用,确保在没有任何引用时才真正释放文件资源。
- 下面的程序是一种惯用法,用于创建一个没有名称的临时 inode。当进程关闭文件描述符 “fd” 或退出时,这个临时 inode 将会被清理掉。这样可以方便地创建临时文件,而无需在后续手动清理文件名,避免了文件名的混乱和资源泄漏的风险。
fd = open("/tmp/xyz", O_CREATE|O_RDWR);
unlink("/tmp/xyz");
- cd描述:cd不派生子进程:一个例外是 cd,它内置于 shell 中(在 user/sh.c:160)。cd 必须更改 shell 本身的当前工作目录。如果 cd 作为常规命令运行,那么 shell 将派生一个子进程,子进程将运行 cd,并且 cd 将更改子进程的工作目录。而父进程(即 shell)的工作目录将不会改变。
- 任何操作系统都必须将进程复用到底层硬件上,将进程彼此隔离,并提供受控的进程间通信机制