18 shell 重定向以及文件描述符

  • 1.对重定向的理解
  • 2.硬件设备和文件描述符
    • 文件描述符到底是什么
  • 3.Linux Shell 输出重定向
  • 4.Linux Shell 输入重定向
  • 5.结合Linux文件描述符谈重定向
  • 6.Shell 对文件描述符的操作
  • 7.改变当前shell环境的重定向目标
  • 8.示例

1.对重定向的理解

  • Linux Shell 重定向分为两种,一种输入重定向,一种是输出重定向;从字面上理解,输入输出重定向就是「改变输入与输出的方向」的意思。
  • 输入方向就是数据从哪里流向程序。标准输入方向是指数据从键盘流向程序,如果改变了它的方向,数据就从其它地方流入,这就是输入重定向。
  • 输出方向就是数据从程序流向哪里。标准输出方向是指数据从程序流向显示器,如果改变了它的方向,数据就流向其它地方,这就是输出重定向。

2.硬件设备和文件描述符

计算机的硬件设备有很多,常见的输入设备有键盘、鼠标、麦克风、手写板等,输出设备有显示器、投影仪、打印机等。不过,在 Linux 中,标准输入设备指的是键盘,标准输出设备指的是显示器

Linux系统中把一切都看做文件,包括普通文件-、目录文件d、字符设备文件c、块设备文件b、符号链接文件l以及<span>标准输入设备(键盘)和标准输出设备(显示器)<span>在内的所有计算机硬件都是文件</span></span>

文件描述符是内核为了高效管理已被打开的文件所创建的索引(一个非负整数),用于指代已被打开的文件。

Linux下所有的的I/O操作的系统调用都是通过文件描述符执行,一个文件描述符只是一个和打开的文件相关联的整数,它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器,甚至是一个网络连接。例如0表示标准输入(键盘)、1表示标准输出(显示器)、2表示标准错误(显示器),文件描述符会在这个基础上递增。

文件描述符文件名类型硬件
0

/dev/stdin -> /proc/self/fd/0

/proc/self/fd/0 -> /dev/pts/2

标准输入文件键盘
1

/dev/stdout -> /proc/self/fd/1

/proc/self/fd/1 -> /dev/pts/2

标准输出文件显示器
2

/dev/stderr -> /proc/self/fd/2

/proc/self/fd/2 -> /dev/pts/2

标准错误输出文件显示器

在Linux中,每一个进程打开时都会自动获取3个文件描述符0、1和2,分别表示标准输入、标准输出、和标准错误,如果要打开其他文件,则文件描述符必须从3开始标识。对于我们人为要打开的描述符,建议使用9以内的描述符,超过9的描述符可能已经被系统内部分配给其他进程。

文件描述符说白了就是系统为了跟踪这个打开的文件而分配给它的一个数字,这个数字和文件绑定在一起,数据流入描述符的时候也表示流入文件。

程序在打开文件描述符的时候,有三种可能的行为:从描述符中读、向描述符中写、可读也可写。从lsof的FD列可以看出程序打开这个文件是为了从中读数据,还是向其中写数据,亦或是既读又写。例如,tail命令监控文件时,就是打开文件从中读数据的(3r的r是read,w是write,u是read and write)。

lsof -n | grep "/a.sh" | column -t                 

tail  13563  root  3r  REG  8,2  182  69632966  /root/a.sh

文件描述符到底是什么

文件描述符: 在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。

一个 Linux 进程启动后,会在内核空间中创建一个 PCB 控制块,PCB 内部有一个文件描述符表(File descriptor table),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。

除了文件描述符表,系统还需要维护另外两张表:

  • 打开文件表(Open file table)
  • i-node 表(i-node table)

文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个,它们三者之间的关系如下图所示。

18 shell 重定向以及文件描述符

文件描述符表: 进程级的列表,也就是用户区的一部分,进程每打开一个文件就会新建一个文件描述符。
系统级打开文件表: 系统级的列表,对当前系统的所有进程都共享,每条条目包含文件偏移量、访问模式以及指向它的文件描述符的条目计数
文件系统索引节点表: inode索引节点表(UID、GID、ctime、mtime、atime、读写执行权限、链接数、block位置)

进程级文件描述符系统级打开文件表i-node表

1.文件描述符标志

2.文件指针(open file handle)

通过文件描述符,可以找到文件指针,从而进入打开文件表

1.文件偏移量,也就是文件内部指针偏移量。调用 read() 或者 write() 函数时,文件偏移量会自动更新,当然也可以使用 lseek() 直接修改。

2.状态标志,比如只读模式、读写模式、追加模式、覆盖模式等。

3.i-node 表指针。

要想真正读写文件,要通过打开文件表的 i-node 指针进入 i-node 表

1.文件类型,例如常规文件、套接字或 FIFO。

2.文件大小。

3.时间戳,比如创建时间、更新时间。

4.文件锁。

对上图的进一步说明:

  • 在进程 A 中,文件描述符 1 和 20 都指向了同一个打开文件表项,标号为 23(指向了打开文件表中下标为 23 的数组元素),这可能是通过调用 dup()、dup2()、fcntl() 或者对同一个文件多次调用了 open() 函数形成的。
  • 进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件,这可能是在调用 fork() 后出现的(即进程 A、B 是父子进程关系),或者是不同的进程独自去调用 open() 函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
  • 进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项,但这些表项均指向 i-node 表的同一个条目(标号为 1976);换言之,它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件,也会发生类似情况。 


有了以上对文件描述符的认知,我们很容易理解以下情形:

  • 同一个进程的不同文件描述符可以指向同一个文件;
  • 不同进程可以拥有相同的文件描述符;
  • 不同进程的同一个文件描述符可以指向不同的文件(一般也是这样,除了 0、1、2 这三个特殊的文件);
  • 不同进程的不同文件描述符也可以指向同一个文件。

文件描述符、文件、进程之间的关系

  • 每个文件描述符都指向一个打开的文件相对应
  • 不同的文件描述符可能指向同一个打开的文件
  • 相同的文件可能被不同的进程打开,也可以在被同一个进程打开多次

3.Linux Shell 输出重定向

输出重定向是指命令的结果不再输出到显示器上,而是输出到其它地方,一般是文件中。这样做的最大好处就是把命令的结果保存起来,当我们需要的时候可以随时查询。在输出重定向中,>代表的是覆盖,>>代表的是追加。

注意点:

  1. 输出重定向的写法是fd>file或者fd>>file,其中 fd 表示文件描述符,如果不写,默认为 1,也就是标准输出文件,即command 1>file与command >file相同。当文件描述符为大于 1 的值时,比如 2,就必须写上。
  2. fd>之间不能有空格,否则 Shell 会解析失败;>file之间的空格可有可无
  3. /dev/null 文件。如果想要将输出结果丢弃,可以将命令结果重定向到 /dev/null 文件中,即 ls -l &>/dev/null,任何放入垃圾箱的数据都会被丢弃,不能恢复。

表1:fd>之间不能有空格

命令说明

若fd与>之间有空格:

echo "c.biancheng.net" 1 >log.txt

cat log.txt

则输出结果为:c.biancheng.net 1

实际执行命令为:echo "c.biancheng.net" 1 1>log.txt

表2:Bash 支持的输出重定向符号

类 型符 号作 用
标准输出重定向command >file以覆盖的方式,把 command 的正确输出结果输出到 file 文件中。
command >>file以追加的方式,把 command 的正确输出结果输出到 file 文件中。
标准错误输出重定向command 2>file以覆盖的方式,把 command 的错误信息输出到 file 文件中。
command 2>>file以追加的方式,把 command 的错误信息输出到 file 文件中。
正确输出和错误信息同时保存command >file 2>&1以覆盖的方式,把正确输出和错误信息同时保存到同一个文件(file)中。
command >>file 2>&1以追加的方式,把正确输出和错误信息同时保存到同一个文件(file)中。
command >file1 2>file2以覆盖的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。
command >>file1  2>>file2以追加的方式,把正确的输出结果输出到 file1 文件中,把错误信息输出到 file2 文件中。
command >file 2>file不推荐】这两种写法会导致 file 被打开两次,引起资源竞争,所以 stdout 和 stderr 会互相覆盖
command >>file 2>>file

4.Linux Shell 输入重定向

输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而是使用文件作为命令的输入。和输出重定向类似,输入重定向的完整写法是fd<file,其中 fd 表示文件描述符,如果不写,默认为 0,也就是标准输入文件。

表3:Bash 支持的输出重定向符号

符号说明举栗例子中的知识点
command <file将 file 文件中的内容作为 command 的输入。

统计 readme.txt 文件中有多少行文本:

cat readme.txt #预览一下文件内容

aa

bb

cc

dd

wc -l <readme.txt #输入重定向

4

Linux wc 命令可以用来对文本进行统计,包括单词个数、行数、字节数,它的用法如下:

wc  [选项]  [文件名]

其中,-c选项统计字节数,-w选项统计单词数,-l选项统计行数。

command <<END从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止(分界符可以是任意的字符串,用户自己定义)。统计用户在终端输入的文本的行数。
wc -l <<END

> 123

> 789

> abc

> xyz

> END

4

wc 命令会一直等待用输入,直到遇见分界符 END 才结束读取。

输入重定向符号<<,这个符号的作用是使用特定的分界符作为命令输入的结束标志,而不使用 Ctrl+D 键。

<<之后的分界符可以自由定义,只要再碰到相同的分界符,两个分界符之间的内容将作为命令的输入(不包括分界符本身)。

command <file1 >file2将 file1 作为 command 的输入,并将 command 的处理结果输出到 file2。

wc -l < test.txt >result.txt

cat result.txt 

4

 

代码块重定向

{}<file1

代码块重定向,即把一组命令同时重定向到一个文件逐行读取文件内容。
#!/bin/bash
while read str; do
    echo $str
done <readme.txt

运行结果:

aa

bb

cc

dd

 

5.结合Linux文件描述符谈重定向

Linux 系统每次读写文件时,都从文件描述符下手,通过文件描述符找到文件指针,然后进入打开文件表和 i-node 表,打开文件表和i-node表中保存了与打开文件相关的各种信息。

文件指针是一个内存地址,是文件描述符和真实文件之间最关键的“纽带”,当我们改变了文件指针的指向,就可以改变文件描述符对应的真实文件,比如文件描述符 1 本来对应显示器,但是我们偷偷将文件指针指向了 log.txt 文件,那么文件描述符 1 也就和 log.txt 对应起来了。

Linux 系统提供的函数可以修改文件指针,比如 dup()、dup2();Shell 也能通过重定向修改文件指针,在发生重定向时,Linux 会用文件描述符表(一个结构体数组)中的一个元素给另一个元素赋值,或者用一个结构体变量给数组元素赋值,文件描述符并没有改变,改变的是文件描述符对应的文件指针。对于标准输出,Linux 系统始终向文件描述符 1 中输出内容,而不管它的文件指针指向哪里;只要我们修改了文件指针,就能向任意文件中输出内容。

6.Shell 对文件描述符的操作

文件描述符操作符: >,<和<>

分类用法举栗
输出

n>filename

以输出的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 1,也即标准输出文件。

 

n>&m

[n]>&word :将文件描述符n复制于word 代表的文件或描述符。可以理解为文件描述符n重用word代表的文件或描述符,即word原来对应哪个文件,现在n作为它的副本也对应这个文件。n不指定则默认为1(标准输出就是1),表示标准输出也将输出到word所代表的文件或描述符中。

3>&1表示fd=3复制于fd=1,而fd=1目前的重定向目标文件是/dev/stdout,因此fd=3也重定向到/dev/stdout,以后进程将数据写入fd=3的时候,将直接输出到屏幕。

这里的3>&1等价于3>&/dev/stdout。如果用"复制"来理解,就是fd=3是当前fd=1的一个副本,即指向/dev/stdout设备。如果后面改变了fd=1的输出目标(如file1),由于fd=3的目标仍然是/dev/stdout,所以可以拿fd=3来还原fd=1使其目标变回/dev/stdout。

n>&-

关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 1。

关闭文件描述符的方式是将 [n]>&word中的word使用符号"-",这表示释放fd=n描述符,且关闭其指向的文件。

[n]>&digit-

将文件描述符digit代表的输出文件移动到n上,并关闭digit值的描述符。

 

&>filename

将正确输出结果和错误信息全部重定向到 filename。

 
输入

n<filename

以输入的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 0,也即标准输入文件。

 

n<&m

[n]<&word :将文件描述符n复制于word 代表的文件或描述符。可以理解为文件描述符n重用word代表的文件或描述符,即word原来对应哪个文件,现在n作为它的副本也对应这个文件。n不指定则默认为0(标准输入就是0),表示标准输入也将输入到word所代表的文件或描述符中。

cat <&1表示fd=0复制于fd=1上,而此时fd=1的重定向文件是/dev/stdout,所以fd=0也指向这个/dev/stdout文件,而cat从fd=0中读取标准输入,于是/dev/stdout既是标准输入设备,也是标准输出设备,也就是说进程从/dev/stdout(屏幕)接受输入,输入后再直接输出到/dev/stdout。

cat 0<&1 #进入交互模式

aa #输入aa

aa #输出aa

^C

n<&-

关闭文件描述符 n 及其代表的文件。n 可以不写,默认为 0。

关闭文件描述符的方式是将 [n]<&word 中的word使用符号"-",这表示释放fd=n描述符,且关闭其指向的文件。

[n]<&digit-

将文件描述符digit代表的输入文件移动到n上,并关闭digit值的描述符。

 
输入和输出

n<>filename

同时以输入和输出的方式打开文件 filename,并绑定到文件描述符 n,相当于 n>filename 和 n<filename 的总和。。n 可以不写,默认为 0,若filename文件不存在,则先创建filename文件。

exec 3<> /tmp/a.log

lsof -n | grep "/a.log" | column -t 

bash  13637  root  3u  REG  8,2  292018  69632965  /tmp/a.log

实例过程

command >file 2>&1

等价于&>file

表示标准输出和标准错误都重定向到file中

先打开file,再将fd=1重定向到file文件上,这样file文件就成了标准输出的输出目标;之后再将fd=2复制于fd=1,而fd=1此时已经重定向到file文件上,因此fd=2也重定向到file上。所以,最终的结果是标准输出重定向到file上,标准错误也重定向到file上。

command 2>&1 1>file

先将fd=2复制于fd=1,而此时fd=1重定向的文件是默认的/dev/stdout,所以fd=2也重定向到/dev/stdout;之后再将fd=1重定向到file文件上。

即最终的结果是标准错误输出到/dev/stdout,即屏幕上,而标准输出将输出到file文件中。

echo "aa" 10>log.txt >&10先执行10>log.txt,即打开log.txt,并给它分配文件描述符 10;接着执行>&10,即将fd=1复制与fd=10,而fd=10此时重庆向到log.txt,因此fd=1也重定向到log.txt上,所以该语句等价与echo "aa" >log.txt,之所以写得这么绕,是为了理解各种操作符的用法

文件描述符的移动

exec 3<> /tmp/a.log

lsof -n | grep "/a.log" | column -t 

bash  13637  root  3u  REG  8,2  292018  69632965  /tmp/a.log

exec 1>&3-  # 将3移动到1上,关闭3

lsof -n | grep "/a.log" | column -t   # 在另一个bash窗口查看

bash  13637  root  1u  REG  8,2  292018  69632965  /tmp/a.log

可见,fd=3移动到fd=1后,原本与fd=3关联的/tmp/a.log已经关联到fd=1上。

7.改变当前shell环境的重定向目标

如果在命令中直接改变重定向的位置,那么命令执行结束时描述符会自动还原。正如上面的ls /boot 2>&1 >/tmp/a.log命令,在ls执行结束后,fd=2还原回默认的/dev/stderr,fd=1还原回默认的/dev/stdout。但是如果我们想要在当前shell环境中一直改变重定向目标时,可以使用exec命令。

exec 是 Shell 内置命令,它有两种用法,一种是执行 Shell 命令,一种是操作文件描述符。使用exec命令改变重定向方向后,只有在当前shell退出或者再次执行 exec 命令时才会恢复或改变描述符。

exec 的用法举栗说明举栗

exec 文件描述符操作

eg: exec 2>&3

echo "重定向未发生"

重定向未发生

exec >log.txt

echo "aa"

echo "bb"

exec >&2

echo "重定向已恢复"

重定向已恢复

cat log.txt

aa

bb

  • exec >log.txt将当前 Shell 进程的所有标准输出重定向到 log.txt 文件,它等价于exec 1>log.txt
  • 后面的两个 echo 命令都没有在显示器上输出,而是输出到了 log.txt 文件。
  • exec >&2用来恢复重定向,让标准输出重新回到显示器,它等价于exec 1>&2。2 是标准错误输出的文件描述符,它也是输出到显示器,并且没有遭到破坏,我们用1来复制2,就能修复 1,让 1 重新指向显示器。
  • 接下来的 echo 命令将结果输出到显示器上,证明exec >&2奏效了。
  • 最后我们用 cat 命令来查看 log.txt 文件的内容,发现就是中间两个 echo 命令的输出。

重定向的恢复

  • /dev/tty 文件代表的就是显示器,将标准输出重定向到 /dev/tty 即可,也就是 exec >/dev/tty。
  • 如果还有别的文件描述符指向了显示器,那么也可以别的文件描述符来恢复标号为 1 的文件描述符,例如 exec >&2。但是如果文件描述符 2 也被重定向了,那么这种方式就无效了。
  1. #!/bin/bash
  2. exec 6<&#先将0号文件描述符保存
  3. exec <nums.txt #输入重定向
  4. sum=0
  5. while read ndo
  6. ((sum += n))
  7. done
  8. echo "sum=$sum"
  9. exec 0<&6<&- #恢复输入重定向,并关闭文件描述符6

将代码保存到 test.txt,并执行下面的命令:

cat nums.txt
80
33
bash ./test.sh
sum=113

8.示例

示例脚本说明
描述符的使用

echo 1234567890 > File # (1)写字符串到"File".

exec 3<> File          # (2)打开"File"并且给它分配fd 3.

read -n 4 <&3          # (3)只读4 个字符.

echo -n . >&3          # (4)写一个小数点.

exec 3>&-              # (5)关闭fd 3.

cat File               # (6)1234.67890

(1)向文件File中写入几个字符。

(2)打开文件File以备read/write,并分配fd=3给该文件。

(3)将fd=0复制于fd=3上,而fd=3的重定向目标为File,所以fd=0的目标也是File,即从File中读取数据。这里读取4个字符,由于read命令中没有指定变量,因此分配给默认变量REPLY。注意,这个命令执行结束后,fd=0的重定向目标会变回/dev/stdin。

(4)将fd=1复制于fd=3上,而fd=3的重定向目标文件为File,所以fd=1的目标也是File,即数据写入到File中。这里写入一个小数点。注意,这个命令结束后,fd=1的重定向目标回变回/dev/stdout。

(5)关闭fd=3,这也会关闭其指向的文件File。

(6)File文件中已经写入了一个小数点。如果此时执行echo $REPLY,将输出"1234"。

关于描述符恢复、关闭

exec 6>&1                   # (1)

exec > /tmp/file.txt        # (2)

echo "---------------"      # (3)

exec 1>&6 6>&-              # (4)

echo "==============="      # (5)

(1)首先将fd=6复制于fd=1,此时fd=1的重定向目标为/dev/stdout,因此fd=6的重定向目标为/dev/stdout。 

(2)将fd=1重定向到/tmp/file.txt文件。此后所有标准输出都将写入到/tmp/file.txt中。 

(3)写入数据。该数据将写入到/tmp/file.txt中。 

(4)将fd=1重新复制回fd=6,此时fd=6的重定向目标为/dev/stdout,因此fd=1将恢复到/dev/stdout上。最后将fd=6关闭。 

(5)写入数据,这段数据将输出在屏幕上。

1.为什么要先将fd=1复制于fd=6,再用fd=6来恢复fd=1,恢复的时候直接将fd=1重定向回/dev/stdout不就可以了吗?

答:在这里借用fd=6这个中转描述符是为了方便操作,在恢复fd=1的重定向目标的时候,应该重定向到`/dev/{伪终端字符设备}`上,而不是/dev/stdout。因为/dev/stdout是软链接,其目标指向/proc/self/fd/1,而/proc/self/fd/1文件还是软链接,它指向/dev/{伪终端字符设备}。同理/dev/stdin和/dev/stderr都一样。

:/data/1learn_linux$ ll /dev/stdout /proc/self/fd/1

lrwxrwxrwx 1 root   root   15 Sep 12 03:02 /dev/stdout -> /proc/self/fd/1

lrwx------ 1 roaddb roaddb 64 Sep 16 08:08 /proc/self/fd/1 -> /dev/pts/2

pts/2是当前所在的终端!!

因此,如果你当前所在的终端如果是pts/2,那么可以使用下面的命令来实现上面同样的功能:

exec > /tmp/file.txt

echo "---------------"

exec >/dev/pts/2

echo "==============="

2.exec >/dev/tty  # 这样更方便

如果不借用fd=6这个中转描述符,你要先去获取并记住当前shell所在的终端,很不方便。但可以使用/dev/tty这个文件来表示当前所在终端,这会方便的多。

但如果要恢复的不是终端相关的文件,那么可能就只能通过文件描述符的备份、还原来恢复了。

一个比较厉害的重定向

将本机的public key添加到目标机器上,实现免密登录

ssh -p2242 ‘mkdir -p .ssh && cat >> .ssh/authorized_keys‘ < ~/.ssh/id_rsa.pub

ssh ",表示登录远程主机;

‘mkdir .ssh && cat >> .ssh/authorized_keys‘,表示登录后在远程shell上执行的命令

mkdir -p .ssh"的作用是,如果用户主目录中的.ssh目录不存在,就创建一个;

‘cat >> .ssh/authorized_keys‘ < ~/.ssh/id_rsa.pub的作用是,将本地的公钥文件~/.ssh/id_rsa.pub,重定向追加到远程文件authorized_keys的末尾。

写入authorized_keys文件后,公钥登录的设置就完成了。

 

相关推荐