SciRui 2019-12-19
计算机的硬件设备有很多,常见的输入设备有键盘、鼠标、麦克风、手写板等,输出设备有显示器、投影仪、打印机等。不过,在 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),记录着当前进程所有可用的文件描述符,也即当前进程所有打开的文件。
除了文件描述符表,系统还需要维护另外两张表:
文件描述符表每个进程都有一个,打开文件表和 i-node 表整个系统只有一个,它们三者之间的关系如下图所示。
文件描述符表: 进程级的列表,也就是用户区的一部分,进程每打开一个文件就会新建一个文件描述符。
系统级打开文件表: 系统级的列表,对当前系统的所有进程都共享,每条条目包含文件偏移量、访问模式以及指向它的文件描述符的条目计数
文件系统索引节点表: 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.文件锁。 |
对上图的进一步说明:
有了以上对文件描述符的认知,我们很容易理解以下情形:
文件描述符、文件、进程之间的关系
输出重定向是指命令的结果不再输出到显示器上,而是输出到其它地方,一般是文件中。这样做的最大好处就是把命令的结果保存起来,当我们需要的时候可以随时查询。在输出重定向中,>
代表的是覆盖,>>
代表的是追加。
注意点:
fd>file
或者fd>>file
,其中 fd 表示文件描述符,如果不写,默认为 1,也就是标准输出文件,即command 1>file与command >file相同。
当文件描述符为大于 1 的值时,比如 2,就必须写上。fd
和>
之间不能有空格,否则 Shell 会解析失败;>
和file
之间的空格可有可无表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 |
输入重定向就是改变输入的方向,不再使用键盘作为命令输入的来源,而是使用文件作为命令的输入。和输出重定向类似,输入重定向的完整写法是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 [选项] [文件名] 其中, |
command <<END | 从标准输入(键盘)中读取数据,直到遇见分界符 END 才停止(分界符可以是任意的字符串,用户自己定义)。 | 统计用户在终端输入的文本的行数。 wc -l <<END > 123 > 789 > abc > xyz > END 4 wc 命令会一直等待用输入,直到遇见分界符 END 才结束读取。 | 输入重定向符号
|
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 |
Linux 系统每次读写文件时,都从文件描述符下手,通过文件描述符找到文件指针,然后进入打开文件表和 i-node 表,打开文件表和i-node表中保存了与打开文件相关的各种信息。
文件指针是一个内存地址,是文件描述符和真实文件之间最关键的“纽带”,当我们改变了文件指针的指向,就可以改变文件描述符对应的真实文件,比如文件描述符 1 本来对应显示器,但是我们偷偷将文件指针指向了 log.txt 文件,那么文件描述符 1 也就和 log.txt 对应起来了。
Linux 系统提供的函数可以修改文件指针,比如 dup()、dup2();Shell 也能通过重定向修改文件指针,在发生重定向时,Linux 会用文件描述符表(一个结构体数组)中的一个元素给另一个元素赋值,或者用一个结构体变量给数组元素赋值,文件描述符并没有改变,改变的是文件描述符对应的文件指针。对于标准输出,Linux 系统始终向文件描述符 1 中输出内容,而不管它的文件指针指向哪里;只要我们修改了文件指针,就能向任意文件中输出内容。
文件描述符操作符: >,<和<>
分类 | 用法 | 举栗 |
---|---|---|
输出 | n>filename 以输出的方式打开文件 filename,并绑定到文件描述符 n。n 可以不写,默认为 1,也即标准输出文件。 | |
n>&m [n]>&word :将文件描述符n复制于word 代表的文件或描述符。可以理解为文件描述符n重用word代表的文件或描述符,即word原来对应哪个文件,现在n作为它的副本也对应这个文件。n不指定则默认为1(标准输出就是1),表示标准输出也将输出到word所代表的文件或描述符中。 |
这里的 | |
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 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上。 |
如果在命令中直接改变重定向的位置,那么命令执行结束时描述符会自动还原。正如上面的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 |
重定向的恢复
|
将代码保存到 test.txt,并执行下面的命令: cat nums.txt 80 33 bash ./test.sh sum=113 |
示例 | 脚本 | 说明 | |
---|---|---|---|
描述符的使用 | 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都一样。
因此,如果你当前所在的终端如果是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文件后,公钥登录的设置就完成了。 |