Xv6 I/O 与文件描述符

440 阅读3分钟

这是我参与8月更文挑战的第19天,活动详情查看:8月更文挑战

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 描述的,可以做或运算:

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

Xv6 文件描述符

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

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

read & write

先来看一个例子:

// useio.c

#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:

$ useio > fff
123456  # 这是输入的
$ useio < fff
123456  # 这是输出的

重定向的实现

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

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

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

// uredirection.c

#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);
}

编译运行:

$ cat > input.txt
<Input something here>
$ uredirection
<what is inputted above>

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

共享偏移

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

// usharedoffset.c

#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);
}

运行效果:

$ echo "" > output.txt
$ usharedoffset > output.txt
$ cat output.txt
hello world

dup

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

// udup.c

#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);
}

运行效果:

$ 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

By CDFMLR 2021-02-18

echo "See you.🧑‍💻"

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