6.828 note 1, os interface
6.828 笔记
process 由 代码段, 数据段,堆栈段等组成
system call
当用户进程需要调用 内核服务时,会需要调用内核,这个过程为 system call
fork
fork 能创建一个子进程,copy 一份和parent 一样的新的数据段 (是copy一份独立
的,并非 公用
)
除了以下4点不同 (see man fork):
- child process 有自己的唯一pid
- The child process has a different parent process ID? 没看懂, 结果并不是如此,见下面
- child process 的 file descriptor 会copy 一份 自己的 但是和 parent process 的 fd 所引用的对象是同一个。
- The child processes resource utilizations are set to 0; see setrlimit(2).
fork 会返回 child process 的pid,对于启动的child process,的代码从 fork 这里运行。child process 的fork 会返回 0
1 | int r; |
result
1 | sssssssss? |
exec 与 fork 与 system
The exec family of functions replaces the current process image with a new process image.
exec 不产生新的child process,
exec命令在执行时会把当前的shell process关闭,然后换到后面的命令继续执行。
系统调用exec是以新的进程去代替原来的进程,但进程的PID保持不变。
因此,可以这样认为,exec系统调用并没有创建新的进程,只是替换了原来进程上下文的内容。
原进程的代码段,数据段,堆栈段被新的进程所代替。
参考 man 3 exec
会发现 exec是一个函数簇,由6个函数组成。
执行exec系统调用,一般都是这样,用fork()函数新建立一个进程,然后让进程去执行exec调用。我们知道,在fork()建立新进程之后,父进各与子进程共享代码段,但数据空间是分开的,但父进程会把自己数据空间的内容copy到子进程中去,还有上下文也会copy到子进程中去。
而为了提高效率,采用一种写时copy的策略,即创建子进程的时候,并不copy父进程的地址空间,父子进程拥有共同的地址空间,只有当子进程需要写入数据时(如向缓冲区写入数据),这时候会复制地址空间,复制缓冲区到子进程中去。
从而父子进程拥有独立的地址空间。而对于fork()之后执行exec后,这种策略能够很好的提高效率,如果一开始就copy,那么exec之后,子进程的数据会被放弃,被新的进程所代替。
fork copy
一份 一样的内存。。。
exec与system的区别
exec是直接用新的进程去代替原来的程序运行,运行完毕之后不回到原先的程序中去。
system是调用shell执行你的命令,system=fork+exec+waitpid,执行完毕之后,回到原先的程序中去。继续执行下面的部分。
总之,如果你用exec调用,首先应该fork一个新的进程,然后exec. 而system不需要你fork新进程,已经封装好了。
IO 与 File descriptors (fd)
fd 是一个给 user process 读写数据的 内核对象(kernel-managed object) 引用, 用int 表示
一般来说,一个进程默认有3个 fd
1 | FD 说明 |
举例一个简版 cat
的例子,他从stdin 读数据,然后写入 stdout
1 | char buf[512]; |
编译: clang -Wall -g cat.c -o mycat
mycat 后面会用到
注,这里的cat 并不是shell 自带的cat 那么强大,他不支持 直接 读取文件名。
只能从stdin 读取,所以 原本shell 可以 cat cat.c
需要用 ./cat < cat.c
替代
那么这个 <
是如何让右边 cat.c 文件里面的数据 写入到 cat 的stdin的呢?
io redirect
先看下常用的 shell 重定向
比如 后台运行程序时常见的 nohup xxx >xx.log 2>&1 &
&n
使用系统调用 dup (2) 复制文件描述符 n 并把结果用作标准输出
2>&1
将前面的命令的 stdout 和 stderr 合并 ( 严格的说是通过复制文件描述符 1 来建立文件描述符 2 , 结果是合并了2个流 )
&
关闭stdout
综上: stdout 和 stderr 被写入 xx.log 并关闭 monitor 的 stdout
1 | M>N |
更多例子参考这里
代码实现
close
: 可以释放一个fd,被释放的fd 包括 0,1,2 (std in/out/err) 可以被重新使用。
新申请的 fd 总是当前进程中最小的未使用的fd,比如 0 被close了,新申请的一定是 0
fork
: 出的child process 和 parent 的 fd 一样,指向同一个 fd object.
shell 实现 io redirect 的过程
- fork 一个 child process
- 关闭child process 的 stdin
- 把需要读的文件内容用 stdin 来引用。(reuse stdin)
- exec 新程序
1 | char *aa[3]; |
这里的child process 关闭 fd 0 后,这里的open 就会申请新的最小的 fd 数字,于是就是0了
然后在 exec 之前的 mycat,注,这时,mycat 的 stdin 其实是 ioredirtest.c
fd了.
这里有人会问,为啥要 fork,不fork 其实也行。但是 这里是模拟shell 的操作,如果shell 不fork,直接exec,那么结果就是 shell 做redirect 的命令后,自己也会退出。。
我们再来看下 2>&1
的代码实现
dup: 复制一个已经存在的 fd,返回一个fd, 这个fd 和之前的fd 指向同一个IO对象。 这种关系 就和 forked child process 的 std in/out/err 和 parent process 的 std in/out/err 一样。
1 | int r; |
pipe
pipe的定义: 一个有一对 读/写 fd 的内核缓冲 (A pipe is a small kernel buffer exposed to processes as a pair of file descriptors, one for reading and one for writing)
在shell 里面 有 管道 |
, 下面用代码来实现下 echo "hello world\!" |wc
这个命令
1 | int p[2]; // pair of file desc, [read, write] |
tip
省略的include 可以通过 man 查看 e.g man 3 exit
, man 3 fprintf
reference
http://xstarcd.github.io/wiki/shell/exec_redirect.html