Bash 重定向

2016-09-23 bash language

所谓 IO 重定向简单来说就是一个过程,这个过程捕捉一个文件、命令、程序、脚本、甚至脚本中的代码块的输出,然后把捕捉到的输出,作为输入发送给另外一个文件、命令、程序、或者脚本。

这里简单介绍常用命令及其实现。

重定向

文件描述符 (File Descriptor) 是进程对其所打开文件的索引,形式上是个非负整数。在 Linux 系统中,系统为每一个打开的文件指定一个文件标识符以便系统对文件进行跟踪。

和 C 语言编程里的文件句柄相似,文件标识符是一个数字,不同数字代表不同的含义,默认情况下,系统占用了 3 个,分别是 0 标准输入 (stdin) , 1 标准输出 (stdout) , 2 标准错误 (stderr) ,另外 3-9 是保留的标识符,可以把这些标识符指定成标准输入,输出或者错误作为临时连接。通常这样可以解决很多复杂的重定向请求。

使用方法

可以简单地用 <> ,默认相当于使用 0<1>;管道 (| pipe line),把上一个命令的 stdout 接到下一个命令的 stdin;tee 命令的作用是在不影响原本 IO 的情况下,将 stdout 复制一份到文件中去,例如 cat file.txt | tee backup

重新定义文件标识符可以用 i>&j 命令,表示把文件标识符 i 重新定向到 j ,你可以把 "&" 理解为 “取地址” 。

常见用例

cmd > file

把 cmd 命令的输出重定向到文件 file 中。如果 file 已经存在,则清空原有文件,使用 bash 的 noclobber 选项可以防止覆盖原有文件。

----- 阻止文件重定向操作对一个文件的覆盖,或者使用 set -C
set -o noclobber
----- 恢复文件重定向操作对一个文件的覆盖
set +o noclobber
cmd >| file

功能同 > ,但即便在设置了 noclobber 时也会覆盖 file 文件,注意用的是 | 而非一些资料中说的 ! ,目前仅在 csh 中仍沿用 >! 实现这一功能。

exec 是 Shell 的内建命令,通常用于替换 Shell 执行命令。在重定向中,exec 用来操作文件描述符,当不指定命令时,会修改当前 shell 的文件描述符。

示例

# ls /dev 1>filename               # 将标准输出重定向到文件中, "1" 和 ">" 中间没有空格
# ls /dev  >filename               # 与上等价,系统默认是 1 ,因此 1 可以省略
# ls /dev >>filename               # 追加到文件,而非创建
# ls -qw /dev 2>filename           # 将标准错误重定向到文件
# ls /dev &>filename               # 将 stdio/stderr 都输入到文件,"&" 在这里代表 stdio/stderr
# ls /dev >&filename               # 与上同
# ls /dev >filename 2>&1           # 与上同
# ls 2>&1 > filename               # 只有标准输出重定向到 filename

# exec 5>&1                        # 把文件标识符 5 定向到标准输出,通常用来临时保存标准输入
# grep word <filename              # 重定向标准输入,与下面相同
# grep word 0<filename             # 把文件 filename 作为 grep 命令的标准输入,而不是从键盘输入

# echo 123456789 >file             # 把字符串写到文件 file 中
# exec 3<>file                     # 把文件 file 打开,并指定文件标识符为 3 ,默认为 0
# read -n 4 <&3                    # 从文件中读 4 个字符,句柄已经指到第四个字符末尾
# echo -n . >&3                    # 在第 5 个字符处写一个点,覆盖第 5 个字符, -n 表示不换行
# exec 3>&-                        # 关闭文件标识符 3
# cat file                         # file 文件的结果就成了 1234.6789

# touch filename
# cat filename
# set -o noclobber
# echo 2 >filename
bash: filename: cannot overwrite existing file
# echo 2 >| filename
# cat filename
2
# set +o noclobber

DUP

如下的两个函数均为复制一个现存的文件的描述,通常使用这两个系统调用来重定向一个打开的文件描述符。

#include <unistd.h>
int dup(int fd);
int dup2(int fd1, int fd2);

dup 返回的新文件描述符是当前最小的可用文件描述,用 dup2() 则可以用 fd2 参数指定新的描述符数值,如果 fd2 已经打开,则先关闭,若 fd1=fd2,则 dup2 返回 fd2,而不关闭它。

#include <string.h>
#include <stdio.h>

void flush(FILE *stream)
{
	int duphandle;

    /* flush TC's internal buffer */
    fflush(stream);

    /* make a duplicate file handle */
    duphandle = dup(fileno(stream));

    /* close the duplicate handle to flush the buffer */
    close(duphandle);
}

int main(void)
{
    FILE    *fp;
    char    msg[] = "This is a test";

    fp = fopen("DUMMY.FIL","w"); /*create a file*/

    /*write some date to the file*/
    fwrite(msg, strlen(msg), 1, fp);
    printf("Press any key to flush DUMMY.FIL");
    getchar();

    /*flush the data to DUMMY.FIL without closing it*/
    flush(fp);
    printf("\n File was flushed, Press any key to quit:");
    getchar();

    return 0;
}

使用示例

这里简单列举一些常见的方法,将 stdout 定向到文件有 3 种方法:

1. close open

在 open 时操作系统会选择最小的一个文件描述符,所以可以使用类似如下的方法,不过只适用于单个的重定向。

close(1);       // stdout/1 成为最小的空闲文件描述符
fd = open("/tmp/stdout", O_WRONLY | O_CREAT);

2. open close dup close

简单来说操作步骤如下。

fd = open("/tmp/stdout", O_WRONLY | O_CREAT); // 打开需要重定向的文件
close(1);   // 关闭标准输出,1成为最小描述符
dup(fd);    // 复制文件描述符fd,复制时会使用最小的文件描述符也就是1
close(fd);  // 将原有的文件描述符关闭

第一次打开文件获取的描述符非 1 ,因为 1 还在打开着。

3. open dup2 close

这里使用到了 dup2() 函数,可以指定复制的目标文件描述符,如下是其声明。

#include <unistd.h>
int dup2(oldfd, newfd);

oldfd 需要复制的文件描述符,newfd 为期望复制 oldfd 后得到的文件描述符,成功则返回 newfd,否则返回 -1 。

如下是简单将 stdout 重定向到 stdout 文件。

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int fd;

    fd = open("stdout", O_RDWR | O_CREAT, 0644);
    close(1);
    if (dup2(fd, 1) != 1) {
        fprintf(stderr, "Failed to dup2(%d, 1), %s\n",
                fd, strerror(errno));
        exit(1);
    }
    printf("Print sth\n");
    fprintf(stderr, "Done running\n");

    return 0;
}

如下是通过命名管道 FIFO 将子进程的输出重定向到父进程,也可以使用匿名管道 PIPE 。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define FIFO "stdout_fifo"

int main(void)
{
    /* Make a fifo that redirect the data from stdout of child */
    unlink(FIFO);
    mkfifo(FIFO, 0777);

    /* child process */
    if(fork() == 0) {
        int fd = open(FIFO, O_WRONLY);
        dup2(fd, 1); /* readirect the stdout */
        execl("/bin/ls", "-a", NULL);
    } else { /* parent process */
        char child_stdout[1024] = {0};

        /* Read and print the data from stdout of child */
        int fd = open(FIFO, O_RDONLY), rc;
        rc = read(fd, child_stdout, sizeof(child_stdout));
        child_stdout[rc] = 0;
        printf("Parent: %s\nFinished\n", child_stdout);

        /* Wait for child process */
        wait(NULL);
    }

    return 0;
}

单进程

假设进程 A 拥有一个已打开的文件描述符 fd3,它的状态如下。

------------
fd0 0   | p0
------------
fd1 1   | p1 -------> 文件表1 ---------> vnode1
------------
fd2 2   | p2
------------
fd3 3   | p3 -------> 文件表2 ---------> vnode2
------------
... ...
... ...
------------

经调用 nfd = dup2(fd3, STDOUT_FILENO); 后进程状态如下:

------------
fd0 0   | p0
------------
nfd 1   | p1 ------------+
------------             |
fd2 2   | p2             |
------------             V
fd3 3   | p3 -------> 文件表2 ---------> vnode2
------------
... ...
... ...
------------

如上的函数表示,nfdfd3 共享一个文件表项,它们的文件表指针指向同一个文件表项,nfd 在文件描述符表中的位置为 STDOUT_FILENO 的位置,而原先的 STDOUT_FILENO 所指向的文件表项被关闭,所以会有如下的特点:

  1. 第一个参数必须为已打开的合法 filedes 。
  2. 第二个参数可以是任意合法范围的 filedes 值。

内核结构

每个进程都对应一个结构体 struct task_struct,里面包含一个数据成员:

struct task_struct {
	/* open file information */
	struct files_struct *files;
}

其中,struct files_struct 结构体定义如下:

/* Open file table structure */
struct files_struct {
	/* read mostly part */
	atomic_t count;
	struct fdtable __rcu *fdt;
	struct fdtable fdtab;

	/* written part on a separate cache line in SMP */
        spinlock_t file_lock ____cacheline_aligned_in_smp;
        int next_fd;
        struct embedded_fd_set close_on_exec_init;
        struct embedded_fd_set open_fds_init;
        struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

对于上面的两个句柄 nfd fd3 他们在 files_struct 里面的数组 fd_array 里面对应的数值是相等的。