|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Lec01:System Call & Shell |
| 4 | +category: "MIT6.S081 Note" |
| 5 | +date: 2025-11-14 |
| 6 | +--- |
| 7 | + |
| 8 | + |
| 9 | +> 课程翻译:https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec01-introduction-and-examples |
| 10 | +> |
| 11 | +> xv6教学文档:https://th0ar.gitbooks.io/xv6-chinese/content/index.html |
| 12 | +
|
| 13 | +## OS 整体设计 |
| 14 | + |
| 15 | +**操作系统的目标** |
| 16 | + |
| 17 | +* 抽象硬件 |
| 18 | +* 多个应用程序共用硬件资源,即multiplex |
| 19 | +* 应用程序之间相互隔离,当某个程序出现故障时不能影响其他程序 |
| 20 | +* 各个应用程序之间需要数据交互 |
| 21 | +* 操作系统用户的共享与隐私性 |
| 22 | +* 向应用程序提供高性能支持 |
| 23 | +* 操作系统需要支持不同的应用场景,如服务器或PC |
| 24 | + |
| 25 | +**操作系统结构** |
| 26 | + |
| 27 | +用户空间 \ 内核空间 |
| 28 | + |
| 29 | +内核空间Kernel内的服务 |
| 30 | + |
| 31 | +* 硬件资源管理 |
| 32 | +* 管理文件系统和进程管理系统 |
| 33 | +* 内存的分配 |
| 34 | +* Access Control |
| 35 | + |
| 36 | +应用程序与Kernel的交互通过**系统调用**实现 |
| 37 | + |
| 38 | +系统调用 `open, write, fork` 简单例子如下,ta们将实际运行在系统内核中,并执行内核中对应的代码实现 |
| 39 | + |
| 40 | +````c |
| 41 | +int fd = open("out", 1); |
| 42 | +write(fd, "hello\n", 6); |
| 43 | +int pid = fork() |
| 44 | +```` |
| 45 | +
|
| 46 | +## System Call是如何使用的 |
| 47 | +
|
| 48 | +1. 使用 **`open` 系统调用**创建文件描述符 |
| 49 | +
|
| 50 | +````c |
| 51 | +# open.c 仅是演示代码,不是open系统调用的实现 |
| 52 | +#include "kernel/types.h" |
| 53 | +#include "user/user.h" |
| 54 | +#include "kernel/fcntl.h" |
| 55 | +
|
| 56 | +int main(){ |
| 57 | + int fd = open("output", O_WRONLY | O_CREATE); |
| 58 | + write(fd, "ooo\n", 4); |
| 59 | + exit(0); |
| 60 | +} |
| 61 | +```` |
| 62 | + |
| 63 | +`open` 系统调用会返回一个新分配的文件描述符,是一个递增的数字 |
| 64 | + |
| 65 | +2. 使用 **`read \ write \ exit` 系统调用**实现复制 console 输入,粘贴到 console 输出 |
| 66 | + |
| 67 | +`read` 系统调用,接收3个参数:文件描述符、指向某段内存的指针、读取的最大长度 |
| 68 | + |
| 69 | +````c |
| 70 | +# copy.c |
| 71 | +#include "kernel/types.h" |
| 72 | +#include "user/user.h" |
| 73 | + |
| 74 | +int main(){ |
| 75 | + char buf[64]; |
| 76 | + while(1){ |
| 77 | + int n = read(0, buf, sizeof(buf)); |
| 78 | + if(n <= 0) break; |
| 79 | + write(1, buf, n); |
| 80 | + } |
| 81 | + exit(0); |
| 82 | +} |
| 83 | +```` |
| 84 | + |
| 85 | +其中文件描述符`0`,在 shell 程序中指代 console 的输入,文件描述符 `1` 指代console 的输出 |
| 86 | + |
| 87 | +优化copy程序的思路:捕获系统调用可能的报错,即返回值,并处理 |
| 88 | + |
| 89 | +3. 使用 **`fork` 系统调用** 创建新进程 |
| 90 | + |
| 91 | +````c |
| 92 | +# fork.c 仅是演示代码,不是fork系统调用的实现 |
| 93 | +#include "kernel/types.h" |
| 94 | +#include "user/user.h" |
| 95 | + |
| 96 | +int main(){ |
| 97 | + int pid = fork(); |
| 98 | + |
| 99 | + if(pid == 0){ |
| 100 | + printf("child\n"); |
| 101 | + }else{ |
| 102 | + printf("parent\n"); |
| 103 | + } |
| 104 | + |
| 105 | + exit(0); |
| 106 | +} |
| 107 | +```` |
| 108 | + |
| 109 | +`fork` 会拷贝当前进程的内存,并创建一个新的进程,这里的内存包含了进程的指令和数据 |
| 110 | + |
| 111 | +> 父子进程在 xv6 的实现中完全相同,包括文件描述符表单的拷贝 |
| 112 | +
|
| 113 | +* 在原始的进程中,fork系统调用会返回大于0的整数,即是新创建进程的ID。 |
| 114 | +* 在新创建的进程中,fork系统调用会返回0。 |
| 115 | + |
| 116 | +执行该程序 `fork.c` 将导致父子进程同时运行,使得 console 输出时看到两个进程混淆在一起的字符输出 |
| 117 | + |
| 118 | +在 Shell 中运行程序,实际上 Shell 会创建一个新的进程来运行输入的每一个指令,新的进程运行指令也需要进行系统调用 `exec` |
| 119 | + |
| 120 | +4. 使用 **`exec \ wait` 系统调用**执行其他指令并接收子进程运行状态 |
| 121 | + |
| 122 | +````c |
| 123 | +# exec.c 仅是演示代码,不是exec系统调用的实现 |
| 124 | + |
| 125 | +#include "kernel/types.h" |
| 126 | +#include "user/user.h" |
| 127 | + |
| 128 | +int main(){ |
| 129 | + char *argv[] = {"echo", "this", "is", "echo", 0}; |
| 130 | + exec("echo", argv); |
| 131 | + printf("exec failed!\n"); |
| 132 | + exit(1); |
| 133 | +} |
| 134 | +```` |
| 135 | + |
| 136 | +执行 `exec` 系统调用将丢弃当前进程的内存,转而加载的目标文件中的指令并执行。在这一过程中能够传入命令行参数数组 `char *argv[]` |
| 137 | + |
| 138 | +* `exec` 系统调用会保留当前的文件描述符表单 |
| 139 | +* `exec` 系统调用正常执行不会返回,因为内存被完全替换,相当于当前进程不复存在了,所以 `exec` 系统调用已经没有地方能返回了 |
| 140 | +* `exec` 系统调用只会当出错时才会返回,比如某些错误会阻止操作系统运行文件中的指令 |
| 141 | + |
| 142 | +因此 `exec.c` 后半段的 `printf, exit` 在程序正常执行过程中不会被执行到 |
| 143 | + |
| 144 | +Shell 会将 `fork` 和 `exec` 连起来使用,如 |
| 145 | + |
| 146 | +````c |
| 147 | +# forkexec.c |
| 148 | + |
| 149 | +int main(){ |
| 150 | + int pid, status, child; |
| 151 | + pid = fork(); |
| 152 | + |
| 153 | + if(pid == 0){ |
| 154 | + char *argv[] = {"echo", "this", "is", "echo", 0}; |
| 155 | + exec("exec", argv); |
| 156 | + printf("exec failed!\n"); |
| 157 | + exit(1); |
| 158 | + }else{ |
| 159 | + printf("parent waiting."); |
| 160 | + child = wait(&status); |
| 161 | + printf("child %d exited with status: %d\n", child, status); |
| 162 | + } |
| 163 | + exit(0); |
| 164 | +} |
| 165 | + |
| 166 | +```` |
| 167 | + |
| 168 | +子进程(`pid=0`)会用 `echo` 命令来代替自己,`echo` 执行完成之后就退出,将操作系统控制权还给父进程 |
| 169 | + |
| 170 | +父进程部分,执行的 `wait` 系统调用,接收参数 `&status`,是一种让退出的子进程以一个整数格式与等待的父进程通信方式,即将 `status` 对应的地址传递给内核,内核会向这个地址写入子进程向 `exit` 传入的参数;`wait` 系统调用返回接收到退出的子进程的PID号 |
| 171 | + |
| 172 | +一般来说,`exit` 的参数选择是固定的 |
| 173 | + |
| 174 | +* 如果一个程序成功的退出了,那么 `exit` 的参数会是0 |
| 175 | +* 如果出现了错误,会向 `exit` 传递1 |
| 176 | + |
| 177 | +> 如果子进程调用了 `wait`,由于子进程自己没有子进程了,所以 `wait` 会立即返回 `-1`,表明出现错误:当前的进程并没有任何子进程。 |
| 178 | +
|
| 179 | +优化思路:默认的 `fork` 系统调用会拷贝当前进程的所有内存,但实际情况中并不需要这么多的资源,因此可以进一步优化,仅拷贝执行 `exec` 所需要的内存 |
| 180 | + |
| 181 | +> 在编译之后,C程序就是一些在内存中的指令,这些指令存在于内存中;所以这些指令可以被拷贝,因为它们就是内存中的字节,它们可以被拷贝到别处 |
| 182 | +
|
| 183 | +## Shell & Redirect |
| 184 | + |
| 185 | +当在 Shell 中输入内容时,实际上是告诉 Shell 运行相应的程序。 |
| 186 | + |
| 187 | +比如当输入 `ls` 时,实际的意义是我要求 Shell 运行名为 `ls` 的程序,文件系统中会有一个文件名为 `ls`,这个文件中包含了一些计算机指令,最终 Shell 将运行位于文件 `ls` 内的这些计算机指令。 |
| 188 | + |
| 189 | +Shell 中重定向符号的简单使用 |
| 190 | + |
| 191 | +````bash |
| 192 | +ls > out # 将 ls 程序返回的内容写入 out 文件 |
| 193 | +grep x < out # 将 out 文件的内容导入 grep 程序中 |
| 194 | +```` |
| 195 | + |
| 196 | +程序实现重定向 |
| 197 | + |
| 198 | +````c |
| 199 | +# redirect.c 仅是演示代码,不是重定向符号的实现 |
| 200 | + |
| 201 | +int main(){ |
| 202 | + int pid; |
| 203 | + |
| 204 | + pid = fork(); |
| 205 | + if(pid == 0){ |
| 206 | + close(1); |
| 207 | + open("output", O_WRONLY | O_CREATE); |
| 208 | + |
| 209 | + char *argv[] = {"echo", "this", "is", "redirect", "echo", 0}; |
| 210 | + exec("echo", argv); |
| 211 | + printf("exec failed\n"); |
| 212 | + }else{ |
| 213 | + wait((int *) 0); |
| 214 | + } |
| 215 | + exit(0); |
| 216 | +} |
| 217 | +```` |
| 218 | + |
| 219 | +运行该程序将导致 `echo` 本来输出到 console 的字符,输入到了 `output` 文件中 |
| 220 | + |
| 221 | +这是因为子进程中的 `close(1)` 改变了 Shell 程序中默认的文件描述符,具体来说,程序手动关闭了文件描述符`1`;又调用 `open` 打开了一个新的文件描述符,而 `open` 会返回当前进程未使用的最小文件描述符序号,这里即是 `1` |
| 222 | + |
| 223 | +> 文件描述符 `0` 由于没有被 `close`,因此依旧代表 console 的输入 |
| 224 | +> |
| 225 | +> 子进程中调用 `close`,不会影响到父进程的文件描述符 |
| 226 | +
|
| 227 | +因此此时子进程中的文件描述符 `1` 不再指代 console 的输出,而是指代对 `output` 文件的写入 |
| 228 | + |
| 229 | +而 `echo` 指令默认会输出到文件描述符 `1`,进而输入到了 `output` 文件中,就此完成了重定向 |
| 230 | + |
| 231 | +> `echo` 指令始终将输出指向文件描述符 `1`,不管文件描述符 `1`是指代的什么 |
0 commit comments