烦恼一般都是想太多了。

0%

Bash中的IO与重定向

我们知道,进程在打开的时候,就默认已经打开了三个文件描述符 0, 1, 2 方便大不了 标准输入,标准输出,标准错误。而我们要进行重定向的时候,比如到一个文件的时候,实际上进程不会关心他到底是被重定向到什么地方了,他只需要依然向这三个描述符进行相关的 IO 操作就行了。所以,我们需要来了解一下文件描述符背后的一些内容。

File Descriptor

APUE 中我们可以看到,对于打开的文件其实上有三个数据结构:

digraph { rankdir = LR subgraph cluster_process_table{ label="process table entry" T[shape=record,label="{fd flags | file pointers}|{ fd0 | p0}|{ fd1 | p1}|{ fd2 | p2}"] } subgraph cluster_open_file_table{ label="file table entry\n maintain by kernel" F1[shape=record,label=" flag | offset| v-node pointer"] F2[shape=record,label=" flag | offset| v-node pointer"] } subgraph cluster_v_node_table{ label="v-node table entry\n" V1[shape=record, label=" v-node infomation 1 | v_data"] V2[shape=record, label=" v-node infomation 2 | v_data"] I1[shape=record, label=" i-node infomation 1 | i_vnode"] I2[shape=record, label=" i-node infomation 2 | i_vnode"] } T:p0-> F1:flag T:p1 -> F2:flag F1:vp -> V1:v F2:vp -> V2:v V1:data -> I1:i V2:data ->I2:i I1:v -> V1:v I2:v ->V2:v }

对于上面这个图做简单的解释:

  • Process table entry 进程表项。在 进程表中,每个进程都会有一个记录项。每个进程表中的记录都会有已经打开的文件描述符表。每个文件描述符都于一个指向打开的文件表项指针。
  • File table entry 文件表项 对于所有打开的文件,内核将其维护在一个表内。每个表项都有一个指向 v-node 信息的指针。
  • V-node V 节点信息 包含了此类型文件的信息,以及在此文件上进行操作的函数指针。
    • V-node 信息由打开文件的时候从磁盘读入
    • V-node 也包含了此文件的 i-node 节点信息
    • Linux 没有 v-node,而是用了一个通用的 i-node 结构。其使用了一个文件系统无关的 i-node和一个文件系统相关的 i-node

所以呢,对于一个文件,是否是同一个文件,要看 v-node/i-node 是一样的,才能说明打开的是同一个文件。
而如果同一个文件被不同的进程打开两次,那么在内核维护的打开文件表中就会有两项。

digraph { rankdir = LR subgraph cluster_process_table_A{ label="process table entry" T1[shape=record,label="{fd flags | file pointers}|{ fd0 | p0}|{ fd1 | p1}|{ fd2 | p2}"] } subgraph cluster_process_table_B{ label="process table entry" T2[shape=record,label="{fd flags | file pointers}|{ fd0 | p0}|{ fd1 | p1}|{ fd2 | p2}"] } subgraph cluster_open_file_table{ label="file table entry\n maintain by kernel" F1[shape=record,label=" flag | offset| v-node pointer"] F2[shape=record,label=" flag | offset| v-node pointer"] } subgraph cluster_v_node_table{ label="v-node table entry\n" V1[shape=record, label=" v-node infomation 1 | v_data"] I1[shape=record, label=" i-node infomation 1 | i_vnode"] } T1:p0-> F1:flag T2:p1 -> F2:flag F1:vp -> V1:v F2:vp -> V1:v V1:data -> I1:i I1:v -> V1:v }

重定向

bash 中的重定向操作符有两类三个:

  • 输入重定向 <, <<, <<<
  • 输出重定向 >, >>, >>>

其本质上是对文件描述符的复制。

文件描述符复制

在一个进程中,所谓的文件描述符的复制,说的是,为一个 文件表项在进程表项 中添加一个指针。
当我们进行重定向的时候,需要重定向的那个文件描述符 S,将成为定向到的文件描述符 D 的副本,也即是说,将文件描述符 D 复制到了 文件描述符 S。

digraph { rankdir = LR subgraph cluster_process_table_A{ label="process table entry" T1[shape=record,label="{fd flags | file pointers}|{ fd0 | p0}|{ fd1 | p1}|{ fd2 | p2} |{ fdS| pS}|{ fdD| pD}"] } F1[shape=record,label=" flag | offset| v-node pointer"] T1:pS -> F1:flag T1:pD -> F1:flag }

就是感官上有点怪异,我需要的是重定向是S,结果却是将要定向到的 D 复制给了 S。

dup 系统调用

有两个底层函数来完成文件描述符的复制:

#include <unistd.h>

int
dup(int fildes);

int
dup2(int fildes, int fildes2);
  • dup() 会返回文件描述符表中最小的那个整数,让此整数代表指向 fildes 的文件表项。
  • dup2() 就有所不同,当 fildes2 在使用的话,那么就会先关闭 fildes2,然后再进行复制。

所以我猜想,对于输入输出的重定向,要么是利用:

close(1);
dup()

或者是

dup2(fd, 1)

这样的形式来实现的。

重定向的顺序

经常这谈到的一个区别就是:

ls  > dirlist  2>&1
digraph { fd1 fd2 ft1 ftdirlist[shape=record] fd1 -> ftdirlist fd2 -> ftdirlist }

ls 2>&1 > dirlist
digraph { fd1 fd2 ft1 ftdirlist[shape=record] fd2 -> ft1 fd1 -> ftdirlist }

的区别。

这是再于,对于第二种情况,标准错误只是使用了标准输出的文件表项,而标准输出使用了 dirlist 的文件表项。

[>]>&

这个操作符实际上是用来进行表示复制文件描述符的意思的,我们可以用 > 重定向到文件,但是如果是要重定向描述符的时候就只能用这个。

[n]<&word
  • 当 Word 不是数字就会出错。
  • 如果 word 是 -,那么会关闭描述符 n
  • 如果 n 未指定,那么就会使用 0
[n]>&word
  • 当 Word 不是数字就会出错。
  • 如果 word 是 -,那么会关闭描述符 n
  • 如果 n 未指定,那么就会使用 1
  • 特殊情况:如果 n 未指定,word 不是数字,也不是 -(它是个文件名),那么标准输出,标准错误都会被重定向到 word 代表的文件。

<< here-document

[n] <<[-] word
here-document
delimiter

在这个命令中, << 表示跟随其后的是一个文档,而 word 表示文档的结束符,delimiter 表征结束。如:

wc << EOF
a b c
EOF

使用这个的时候需要注意:

在 word 上 不会进行 参数和变量展开,命令替换,算数展开,或文件名展开

如果 word 有部分是加了引用的,那么这个 delimiter 应该是 word 去除引号后的结果。并且 her-document 中的所有内容不会进行展开。

export VVVV="vvvvvv"
cat << "F"F
$VVVV
`expr 1 + 2`
FF
$VVVV
`expr 1 + 2`

如果未引用word,则对本文的所有行进行参数扩展,命令替换和算术扩展,字符序列\ newline将被忽略,并且必须使用’\’来引用字符’\’,’$ ‘和’`’。

export VVVV="vvvvvv"
cat << FF
$VVVV
`expr 1 + 2`
FF
vvvvvv
3

<<- 是一种特殊情况,所有开头的 tab 会被去掉,同时会将 delimiter 包含进来。

export VVVV="vvvvvv"
cat <<- FF
$VVVV
`expr 1 + 2`
FF
vvvvvv
3

<<<

[n] <<< word

直接就是长串文本的搞法了:

cat <<< "FF
$VVVV
`expr 1 + 2`
FF"