Xv6 I/O 与文件描述符

Meaning Unknown's Head Image

Xv6 I/O 与文件描述符

参考: xv6-riscv-book 1.2 I/O and File descriptors

Xv6 I/O 系统调用

本文会使用到如下 Xv6 的 I/O 系统调用:

系统调用 说明
int open(char *file, int flags) 打开一个文件,flags 用来指示读or写,返回一个文件描述符
int write(int fd, char *buf, int n) 从 buf 写 n 个字节到文件描述符 fd,返回写入的字节数
int read(int fd, char *buf, int n) 从文件描述符 fd 读 n 个字节到 buf,返回读取的字节数或 0 表示 EOF(文件结束)
int close(int fd) 释放打开的文件描述符 fd
int dup(int fd) 返回一个新的文件描述符,指向与 fd 相同的文件

open 的 flags 由 kernel/fcntl.h:1-5 提供:

flag 说明
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读和写
O_CREATE 不存在时新建
O_TRUNC 把文件截断到 0 长度

这些都是用 bit 描述的,可以做或运算:

1
fd = open("/dir/file", O_CREATE|O_WRONLY);

Xv6 文件描述符

文件描述符就是一个整数,用来代表一个打开的 IO 对象(如文件),通过文件描述符就可以对 IO 对象进行读写操作。程序一开始就会被分配给如下惯例文件描述符:

文件描述符 说明
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误

read & write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// useio.c
// Copies data from its standard input to its standard output.

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main() {
char buf[512];
int n;

for(;;) {
n = read(0, buf, sizeof buf); // 0: stdin

if (n == 0) {
break; // EOF
}
if (n < 0) {
fprintf(2, "read error\n"); // 2: stderr
exit(1);
}
if (write(1, buf, n) != n) { // 1: stdout
fprintf(2, "write error\n");
exit(1);
}
}

exit(0);
}

这个程序从标准输入读,写到标准输出,相当于一个简化的 cat:

1
2
3
4
$ useio > fff
123456 # 这是输入的
$ useio < fff
123456 # 这是输出的

重定向的实现

在通过如 open 的系统调用打开一个文件时,被分配给的文件描述符总是当前可用的描述符中最小的

用这个特性就可以实现输入输出的重定向。Xv6 的 Shell (user/sh.c:82) 里就是这么实现的。

下面的程序实现一个 cat < input.txt 的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// uredirection.c
// A simplified version of the code a shell runs for the command `cat < input.txt`

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h" // define O_RDONLY

int main() {
char *argv[2];
argv[0] = "cat";
argv[1] = 0;

if (fork() == 0) { // subprocess
close(0); // close stdin
// A newly allocated file descriptor is always the lowest-numbered unused descriptor of the current process.
open("input.txt", O_RDONLY); // 0 => input.txt
// exec replaces the calling process’s memory but preserves its file table.
exec("cat", argv);
}

exit(0);
}

编译运行:

1
2
3
4
$ cat > input.txt
<Input something here>
$ uredirection
<what is inputted above>

fork 和 exec 分离的一个好处就是 shell 可以在 fork 和 exec 之间优雅实现重定向,如上面的程序。如果把二者合并,提供一个 forkexec 系统调用,重定向的实现就很烦了:需要多传参数;或者在调用 forkexec 前设置 shell 进程自己的描述符,然后又改回去;或者让每个程序自己去支持重定向。

共享偏移

fork 的时候会拷贝文件描述符表,但每个文件的偏移量(读/写到哪)会在父子进程间共享。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// usharedoffset.c
// Although fork copies the file descriptor table, each underlying file offset is shared between parent and child.

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h"

int main() {
if (fork() == 0) {
write(1, "hello ", 6);
exit(0);
} else {
wait(0);
write(1, "world\n", 6);
}
exit(0);
}

运行效果:

1
2
3
4
$ echo "" > output.txt
$ usharedoffset > output.txt
$ cat output.txt
hello world

dup

dup “复制”一个现有的文件描述符,返回的新描述符指向和原来一样的 I/O 物体(比如文件)。类似于 fork,新旧文件描述符共享 offset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// udup.c 
//
// The dup system call duplicates an existing file descriptor,
// returning a new one that refers to the same underlying I/O object.
// Both file descriptors share an offset, just as the file descriptors
// duplicated by fork do.

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fcntl.h"


int main() {
int fd = dup(1);
write(1, "hello ", 6);
write(fd, "world\n", 6);

exit(0);
}

运行效果:

1
2
3
$ udup > output.txt
$ cat output.txt
hello world

利用 dup,shell 就可以实现 ls existing-file non-existing-file > tmp1 2>&1 了。 2>&1 就是 2 = dup(1),让标准错误和标准输出指向同一个文件,并且共享偏移(一直往后写)。


EOF


1
2
# By CDFMLR 2021-02-18
echo "See you.🧑‍💻"

顶部图片来自于小歪API,系随机选取的图片,仅用于检测屏幕显示的机械、光电性能,与文章的任何内容及观点无关,也并不代表本人局部或全部同意、支持或者反对其中的任何内容及观点。如有侵权,联系删除。