0%

I/O重定向与Linux

第一次接触重定向这个概念,是在读C Primer Plus时。书中提出了一个问题:如何将程序的输出写到文件中?答案就是重定向或者显式使用特定的函数打开文件
这篇文章主要介绍重定向在Linux上如何实现。

重定向

重定向概念中最为重要的是:重定向只能连接一个可执行程序和一个数据文件(同管道区分开来),且不能读取多个文件的输入,也不能把输出定向到多个文件。
重定向有四种符号:>、>>、<、<<,三类用法:重定向输入、重定向输出、组合重定向。这些符号使用比较简单,无论是书中还是网上都有比较详细的介绍(组合重定向的用法可以关注一下),这里就不再赘述。

C Primer Plus P222中提到

重定向是一个命令行的概念,与操作系统有关,与C无关。
程序并不关心输入的内容来自文件还是键盘。
因为C把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备。(指从I/O设备到文件的重定向)

当初无法理解这几句话,尤其是后两句。在读了关于Linux编程的书后,发现这就是Linux一种设计哲学的体现——一切皆文件(Windows不清楚)。也就是说,输出到设备或文件的过程是没有两样的。
可能有人还是不明白,设备和文件怎么可能一样呢?要真正想明白,只有实践,而实践最好的方式,就是复现。


重定向在Linux中的实现

我们来看看书中如何描述重定向的实现。

重定向由底层I/O实现

底层I/O是什么?前面提到过,将程序的输出写到文件中有两种方式:重定向和显式使用特定的函数打开文件,现在来谈谈后者。
我们知道,I/O有两种级别:标准高级I/O(standard high-level I/O)和底层I/O(low-level I/O)。标准高级I/O由C标准库定义(实际上是由底层I/O实现的),底层I/O由操作系统提供。在标准高级I/O中,使用文件指针(FILE *fp)进行文件操作,在底层I/O中,使用文件描述符(int fd)进行文件操作。

了解了底层I/O的定义,再来看第一句话:

重定向是一个命令行的概念,与操作系统有关,与C无关。
这就说得通了!底层I/O与操作系统有关,而Linux的底层就是用C写的,所以Linux的重定向由C实现就一点也不奇怪了。至于其它的操作系统用什么我们一点也不关心,这就是为什么说“与C无关”。


如何用底层I/O实现重定向?

这需要一些底层I/O的基本知识,可以看这篇文章
还要了解一个原则:最低可用文件描述符(Lowest-Available-fd)原则,即进程打开文件时,为此文件安排的描述符总是可用的文件描述符中最小的。(已有的文件描述符被认为是不可用的)

原理:

  1. 实现重定向,只需要将进程的输出(这里是一种抽象的表达)写到文件就行了。
  2. 创建进程时,标准输入(stdin)、标准输出(stdout)、标准错误(stderr)的文件描述符(默认为0、1、2)就会被打开,并连接到终端上。
  3. 进程的输出总是默认为标准输出(printf等等函数的输出)

这样看来,我们要做事只有一件,使0(或1)成为一个文件的文件描述符。联想到最低可用文件描述符(Lowest-Available-fd)原则,我们有了下面的三种方法(摘自Unix/Linux编程实践教程)。

  1. close->open
    关闭0或1的文件描述符,再打开文件,分配给文件的文件描述符只能是0或1
  2. open->close(0或1)->dup->close(被复制的)
    dup是一个系统调用,只有一个参数,作用为复制参数中的文件描述符(虽控制同一个文件,被同时影响(偏移等),但两个文件描述符的数值并不相等),成功时返回新的文件描述符,失败时返回-1。
    由于dup遵循最低可用文件描述符(Lowest-Available-fd)原则,因此这个方案是可行的
  3. open->dup2->close(被复制的)
    dup也是一个系统调用,有两个参数(oldfd、newfd),作用为复制oldfd给newfd(同样的,两个文件描述符都控制一个文件),返回值与dup同理。

这些方案的可行性都是不证自明的,接下来对新的文件描述符(读取、写入(对应着重定向中的>或>>、<或<<))就行了。
但是方法中在运行过程中会存在差异,在dup与close(被复制的)之间,旧的fd可能会被使用。而dup2是一种原子操作,不用担心这种问题。具体请查看man page。

现在在来看这两句话,就觉得豁然开朗了把:

程序并不关心输入的内容来自文件还是键盘。
因为C把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备。(指从I/O设备到文件的重定向)

书上还提到,其中有些方法并不适合于文件,但使用管道的时候,这些方法都是必要的(还搞不懂)

Shell、子进程的重定向

  • 对于子进程的重定向,记住一点即可:fork与exec并不会改变文件描述符,其余的东西与一般重定向别无两样。
  • 我们知道,Shell执行程序就是fork+exec,shell中的重定向就是对子进程的重定向。因此,Shell使用fork产生子进程与子进程调用exec之间的间隔来重定向。

例子

下面是一个书中的例子,运行who命令并将结果储存在userlist文件中。我只是修改了一些注释和一些细节,仅供参考。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* redirect_to_file.c
* 我的代码(已删除):当>或<作为命令行参数使用时,仍然会被当作重定向...(因此不能在shell中模拟重定向)
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <wait.h>

int main(void)
{
pid_t pid;
int fd;

printf("About to run who into a file\n");
/* create a new process or quit */
if ((pid = fork()) == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
/* child does the work */
if (pid == 0)
{
/* 第一种方式 */
close(1); // close
fd = open("userlist", O_CREAT | O_WRONLY); // then open
/* 第二种方式 */
// fd = open("userlist", O_CREAT | O_WRONLY);
// dup2(fd, 1);
execlp("who", "who", NULL); // and run
perror("execlp");
exit(EXIT_FAILURE);
}
/* parent waits then reports */
if (pid != 0)
{
wait(NULL);
printf("Done running who, results in userlist\n");
}

return 0;
}

总结

书上总结得很好,这里就直接引用了:

共有三个基本的概念,利用它们使得Unix下的程序可以轻易地将标准输入、输出、和错误信息输出连接到文件
(1)标准输入、输出、和错误信息输出分别对应于文件描述符0、1、2
(2)内核总是使用最低可用描述符
(3)文件描述符集合通过exec调用传递,且不会被改变
(4)Shell使用fork产生子进程与子进程调用exec之间的间隔来重定向。

感谢浏览!