6.828 note 1, os interface

6.828 笔记

process 由 代码段, 数据段,堆栈段等组成

system call

当用户进程需要调用 内核服务时,会需要调用内核,这个过程为 system call

_wikis/6.828/assets/system-call.png

fork

fork 能创建一个子进程,copy 一份和parent 一样的新的数据段 (是copy一份独立的,并非 公用

除了以下4点不同 (see man fork):

  1. child process 有自己的唯一pid
  2. The child process has a different parent process ID? 没看懂, 结果并不是如此,见下面
  3. child process 的 file descriptor 会copy 一份 自己的 但是和 parent process 的 fd 所引用的对象是同一个。
  4. The child processes resource utilizations are set to 0; see setrlimit(2).

fork 会返回 child process 的pid,对于启动的child process,的代码从 fork 这里运行。child process 的fork 会返回 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int r;
printf("sssssssss?\n"); // 只会打印一次,child process 从下面才开始运行.
int pid = fork();
printf("lalala pid = %d\n", pid); // 会print 2次,第二次是0,说明 从child process 从 fork 开始运行
if(pid > 0){
printf("parent: child=%d, pid=%d, ppid=%d\n", pid,getpid(),getppid());
pid = wait(&r); // 参见 man 2 wait, wait and return the pid of an exited child of current process
printf("r=%d \n", r);
printf("child=%d is done, pid=%d, ppid=%d\n", pid,getpid(),getppid());
}else if(pid == 0){
printf("child: exiting\n");
exit(0);
}else{
printf("fork error\n");
}

result

1
2
3
4
5
6
7
sssssssss?
lalala pid = 80062
parent: child=80062, pid=80061, ppid=35277
lalala pid = 0
child: exiting
r=0
child=80062 is done, pid=80061, ppid=35277

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
2
3
4
FD	说明
0 stdin,标准输入
1 stdout,标准输出
2 stderr,标准错误输出

举例一个简版 cat 的例子,他从stdin 读数据,然后写入 stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char buf[512];
int n;

for(;;) {
n = read(0, buf, sizeof buf);
if (n == 0){
break;
}
if (n < 0){
fprintf(2, "read error\n");
exit(1);
}
if(write(1, buf, n)!=n){
fprintf(2, "write error\n");
exit(1);
}
}

编译: 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
M>N
# "M"是一个文件描述符, 如果没有明确指定的话默认为1.
# "N"是一个文件名.
# 文件描述符"M"被重定向到文件"N".

M>&N
# "M"是一个文件描述符, 如果没有明确指定的话默认为1.
# "N"是另一个文件描述符.

[j]<>filename
# 为了读写"filename", 把文件"filename"打开, 并且将文件描述符"j"分配给它.
# 如果文件"filename"不存在, 那么就创建它.
# 如果文件描述符"j"没指定, 那默认是fd 0, stdin.
#
# 这种应用通常是为了写到一个文件中指定的地方.
echo 1234567890 > File # 写字符串到"File".
exec 3<> File # 打开"File"并且将fd 3分配给它.
read -n 4 <&3 # 只读取4个字符.
echo -n . >&3 # 写一个小数点.
exec 3>&- # 关闭fd 3.
cat File # ==> 1234.67890
# 随机访问.

更多例子参考这里

代码实现

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 的过程

  1. fork 一个 child process
  2. 关闭child process 的 stdin
  3. 把需要读的文件内容用 stdin 来引用。(reuse stdin)
  4. exec 新程序
1
2
3
4
5
6
7
8
9
char *aa[3];
int re;
aa[0] = "mycat"; // 照惯例 第一个参数是 被执行文件的文件名(改成别的不影响程序运行) @see man 2 exec
aa[1] = 0; // 数组的最后一位应该是一个 NULL pointer, 在 c 语言中,0 == NULL
if (fork() == 0) {
close(0);
open("ioredirtest.c", O_RDONLY);
re = execv("mycat", aa); // 或者改成系统的 /bin/cat
}

这里的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
2
3
4
5
6
7
8
9
10
int r;
write(2, "123\n", 4);
fprintf(stderr, "abcerr\n");
if (fork() == 0) {
close(2); // close stderr
dup(1); // make stderr use stdout
write(1, "123\n", 4);
fprintf(stderr, "22222\n");
}
wait(&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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int p[2];  // pair of file desc, [read, write]
char *argv[2];

argv[0] = "wc";
argv[1] = 0;

pipe(p);

if (fork() == 0) {
close(0);
int d = dup(p[0]); // 将 pipe 的 read fd 挂载到 child process 的stdin
close(p[0]);
close(p[1]);
execv("/usr/bin/wc", argv); // 将 pipe 的read 挂给 wc 的 stdin
} else {
write(p[1], "hello world!\n", 13);
close(p[0]);
close(p[1]);
}
return 0;

tip

省略的include 可以通过 man 查看 e.g man 3 exit , man 3 fprintf

reference

http://xstarcd.github.io/wiki/shell/exec_redirect.html

https://pdos.csail.mit.edu/6.828/2014/schedule.html

https://pdos.csail.mit.edu/6.828/2014/xv6/book-rev8.pdf

avatar

lelouchcr's blog