linux-特殊设备文件

linux-特殊设备文件

在Linux系统中,一切皆为文件,设备也不例外,会被描述为设备文件,比如常见硬盘会被描述为支持随机存取和寻址的块设备文件,键盘终端会被描述成不支持随机存取的字符设备文件。此外,Linux操作系统还有一些特殊的设备,例如伪设备、标准输入输出设备、终端设备等等,它们为操作系统提供了硬件的抽象化功能,简化的系统结构提升了系统架构的可理解性。

伪设备

在类Unix操作系统中,设备节点并不一定要对应物理设备。没有这种对应关系的设备是伪设备或虚拟设备。而对程序而言,这些伪拟设备文件则会被当成真实的文件对待。程序可以向伪设备请求数据(当具有可读权限时),所得到的数据将由操作系统提供。注意,这些数据并不是从磁盘上读取到的,而是由操作系统动态生成的。程序也可以向伪设备写入数据(当具有可写权限时),程序会根据伪设备的特性相应地处理写入的数据。部分经常使用到的伪设备包括:

  • /dev/null:接受并丢弃所有输入;。
  • /dev/zero:产生连续的NUL字元的串流(数值为0)。
  • /dev/full:永远在被填满状态的设备。
  • /dev/random:产生一个虚假随机的任意长度字元串流。(Blocking,阻塞式)
  • /dev/urandom:产生一个虚假随机的任意长度字元串流。(Non-Blocking,非阻塞式)

/dev/null

1$ ls -l /dev/null
2crw-rw-rw- 1 root root      1,   3 Nov 18 18:57 null

对于以上伪设备,我们/dev/null应该算是最常见的了。/dev/null可称为空设备,它会接受任何写入的内容并把它们统统扔掉(但报告写入操作成功),就像黑洞一样,通常用于丢弃不需要的数据输出。在程序员行话,尤其是Unix行话中,/dev/null又被称为位桶(bit bucket)或者黑洞(black hole)。从权限来看,/dev/null还是可读的,读取它则会立即得到一个EOF(END Of File的缩写,表示终止符)标志。

总结:所有写入\dev\null的内容都会永远丢失,而尝试从它那儿读取内容则什么也读不到。

/dev/zero

1$ ls -l /dev/zero
2crw-rw-rw- 1 root root 1, 5 Nov 18 18:57 /dev/zero

在类UNIX 操作系统中,/dev/zero是一个特殊的文件,当你读它的时候,它会提供无限的空字符(NULL, ASCII NUL, 0x00)。它的典型用法包括用它提供的字符流来覆盖信息,以及产生一个特定大小的空白文件等。

1# 创建一个名为test-dev-zero、大小为1 MiB的文件,以ASCII码为“0”(NULL)的字符填充:
2$ dd if=/dev/zero of=test-dev-zero count=1024 bs=1024
31024+0 records in
41024+0 records out
51048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00280503 s, 374 MB/s
6$ ls -lh test-dev-zero
7-rw-r--r-- 1 lelouch lelouch 1.0M Dec  6 22:34 test-dev-zero
8# 彻底覆盖某一分区的数据(低格),谨慎操作
9$ dd if=/dev/zero of=/dev/<destination partition>

/dev/null类似,/dev/zero也可以作为一个数据接收点,所有写往/dev/zero将返回成功,没有其他影响;/dev/null也是一样,但是/dev/null更常用。

/dev/full

1$ ls -l /dev/full
2crw-rw-rw- 1 root root 1, 7 Nov 18 18:57 /dev/full

注意啊/dev/full/dev/null只有一个字母之差,不要打错或看错了。/dev/full也是可读写的,任何进程在向其写入时总是返回设备无剩余空间(错误码为ENOSPC, Error No Space的缩写),在Debian的返回结果如下所示;通常被用来测试程序在遇到磁盘无剩余空间错误时的行为

1$ echo "hello" > /dev/full
2-bash: echo: write error: No space left on device

读取时则与/dev/zero一样,可返回无限多的空字符(NULL, ASCII NUL, 0x00)。它的典型用法和/dev/zero包括用它提供的字符流来覆盖信息,以及产生一个特定大小的空白文件等,但是正常情况下,用/dev/zero更多。

1# 创建一个名为test-dev-full、大小为1 MiB的文件,以ASCII码为“0”(NULL)的字符填充:
2$ dd if=/dev/full of=test-dev-full count=1024 bs=1024
31024+0 records in
41024+0 records out
51048576 bytes (1.0 MB, 1.0 MiB) copied, 0.00283009 s, 371 MB/s
6$ ls -lh test-dev-full
7-rw-r--r-- 1 lelouch lelouch 1.0M Dec  6 22:38 test-dev-full

/dev/random/dev/urandom

1$ ls -l /dev/random /dev/urandom
2crw-rw-rw- 1 root root 1, 8 Nov 18 18:57 /dev/random
3crw-rw-rw- 1 root root 1, 9 Nov 18 18:57 /dev/urandom

/dev/random/dev/urandom是Linux系统中提供的随机性的伪设备,这两个设备的任务,是提供永不为空的随机字节数据流。很多解密程序与安全应用程序(如SSH Keys,SSL Keys等)需要它们提供的随机数据流。读取这个两个设备都会返回一堆随机乱码。

这两个设备的差异在于:/dev/random的random pool依赖于系统中断,因此在系统的中断数不足时,/dev/random设备会一直封锁,尝试读取的进程就会进入等待状态,直到系统的中断数充分够用, /dev/random设备可以保证数据的随机性。/dev/urandom不依赖系统的中断,也就不会造成进程忙等待,但是数据的随机性也不高。如果不是研究特定问题,一般是用不到这两个设备的。

标准输入输出设备

还有一类常用的特殊设备就是标准输入输出设备,它们和我们的操作最息息相关。所有程序默认的输入都来自标准输入设备,所有回显的内容都会输出到标准输出设备,而所有的操作异常、错误信息都会给到标准错误设备。可以说标准输入输出设备是和我们直接交互的设备,简单说,我们通过标准输入设备给程序输入数据,再从标准输出(错误)设备得知程序运算的结果。它们三个分别表示为三个设备文件:标准输入设备/dev/stdin,标准输出设备/dev/stdout,标准错误设备/dev/stderr

1$ ls -l /dev/std*
2lrwxrwxrwx 1 root root 15 Nov 18 18:57 /dev/stderr -> /proc/self/fd/2
3lrwxrwxrwx 1 root root 15 Nov 18 18:57 /dev/stdin -> /proc/self/fd/0
4lrwxrwxrwx 1 root root 15 Nov 18 18:57 /dev/stdout -> /proc/self/fd/1

从上面的结果来看,这三个设备分别了链接到三个文件描述符(file descriptor, fd),习惯上,标准输入(standard input)的文件描述符是 0,标准输出(standard output)是 1,标准错误(standard error)是 2。尽管这种习惯并非Unix内核的特性,但是因为一些 shell 和很多应用程序都使用这种习惯,因此,基本常见内核都遵循这种习惯。

/dev/stdin或0

STDIN标准输入是指输入至程序(进程)的资料、文件等数据流,此数据流默认从标准输入设备获取,最常见的标准输入设备是键盘,在以ssh远程登陆的linux中也会指向虚拟终端pts。需要指出,并不是所有程序都需要输入,如dir或ls程序(显示一个目录中的文件名)运行时不用任何输入。

  • 默认终端登录:/dev/stdin -> /proc/self/fd/0 -> /dev/tty1
  • SSH登录:/dev/stdin -> /proc/self/fd/0 -> /dev/pts/0

/dev/stdout或1

STDOUT(fd:1)标准输出是指程序输出资料、数据、图像的数据流,标准输出的默认对应设备是终端(这是个广阔的概念,linux中以tty指代),在以ssh远程登陆的linux中也会指向虚拟终端pts。需要指出:并非所有程序都要求输出。如mv或ren程序在成功完成时是沉默的。Linux的设计理念有一条即是没有消息就是好消息

  • 默认终端登录:/dev/stdout -> /proc/self/fd/1 -> /dev/tty1
  • SSH登录:/dev/stdout -> /proc/self/fd/1 -> /dev/pts/0

/dev/stderr或2

STDERR(fd:2)标准错误是另一输出流,用于输出错误消息或诊断。它独立于标准输出,且可以分别被重定向。常见的默认目的则为启始这个程序的终端(和STDOUT一致)。

  • 默认终端登录:/dev/stderr -> /proc/self/fd/2 -> /dev/tty1
  • SSH登录:/dev/stderr -> /proc/self/fd/2 -> /dev/pts/0

我们发现不管是通过终端还是ssh登录,最终标准输入、输出、错误设备都会被定位到同一个设备,/dev/tty1/dev/pts/0,这都是linux使用虚拟终端的结果,linux使用虚拟终端来同一管理这些数据流和相应设备的驱动、协议等。下一节我们就介绍终端设备,例如tty与pts。

终端设备

Linux的终端设备是与我们直接交互的设备,我们这里仅介绍以命令行为主的文字终端设备。终端设备,或TTY设备是一类特殊的字符设备,所有可以被用来当作控制终端的设备都可以被称为终端设备,目前常见的包括虚拟终端、串口和伪终端

从物理终端到虚拟终端

计算机在早期都是庞大、复杂且昂贵的,即使发展到20世纪80,90年代,一台个人电脑(PC)依旧是很罕见的,在Linux诞生的1991年,一台便宜的Apple Macintosh PowerBook的价格是2,299美元,要知道那可是1991年的2,299美元,当年中国人民的人均GDP才333美元。所以在计算机发展早期,大多用户是通过物理终端(外设)连接到大型计算机或中型计算机的,共享一台计算机。来看看著名的IBM 308X系列计算机(以下房间中所有设备都是IBM 3081计算机的一部分): IBM_3081.jpg 由于早期的计算机都是多用户共享的,而每个用户需要一套单独的外设(电传机,teletype,简称tty以及显示器)去连接主机,因此系统中就要有相应的tty终端对接程序,Linux中保留的可以通过ctrl+alt+F1~F6切换的tty1-tty6(/dev/tty1~/dev/tty6)六个终端就是这个时期历史遗留的产物(在具有桌面环境的Linux发行版中,X Window Systemy一般在/dev/tty7上运行,也有把桌面终端放在tty1和tty2的比如Ubuntu,依各Linux发行版自己决定)。除了通过tty连接的终端,早期电脑上还自带一个可以直接操作和能够显示系统信息的控制台(console),在linux中也有一个对应的设备/dev/console

那时tty和console还是有很大的区别的,很多系统配置只能通过console修改,tty只能执行特定的用户程序,得到用户程序返回的信息。这同样说明,控制台是计算机的基本设备,而终端是附加外设。

这个设计也保留到了如今的Linux操作系统中,与终端不相关的信息,比如内核消息,后台服务消息,只显示到控制台上,但不会显示到终端上。比如在启动和关闭Linux系统时,我们可以在控制台上看到很多的内核信息,而如果通过telnet、ssh等方式连接上Linux系统,这些开关机信息是不会显示的。

后来,由于终端硬件设备越来越多样化,厂商也各不相同,于是Linux就将对接外设的部分单独拎出来模块化并设计了名叫TTY的子系统,对于每一个终端,TTY driver都会创建一个TTY设备与它对应,如果有多个终端连接过来,那么看起来就是这个样子的:

 1          +----------------------------------------+
 2          |  TTY 驱动   |
 3          |       |
 4          |  +-------+  | +------------------------+
 5+------------+    |  |      |<-------->| 用户进程 A |
 6|  终端  A   |<---------|->| tty1 |    +------------+
 7+------------+    |  |      |<-------->| 用户进程 B |
 8          |  +-------+  | +------------------------+
 9          |        
10          |  +-------+  | +------------------------+
11+------------+    |  |      |<-------->| 用户进程 C |
12|   终端  B  |<---------|->| tty2 |    +------------+
13+------------+    |  |      |<-------->| 用户进程 D |
14          |  +-------+  | +------------------------+
15          |       |
16          +----------------------------------------+

后来随着计算机的不断发展,teletype,console这些特定的外设逐渐消失,我们不再需要专门的终端设备了,每个机器都有自己的键盘和显示器,每台机器还可以是其它机器的终端,即远程的操作通过ssh来实现,但是内核TTY驱动这一架构一直没有发生变化,我们想要和系统中的进程进行I/O交互,还是需要通过TTY设备,于是出现了各种终端模拟软件,最终物理终端都变成了虚拟终端埋进了内核中。我们的输入和程序的输出都通过终端模拟软件转变为过去的tty设备信息流与内核交互,可以称之为隐藏在内核深处的计算机历史痕迹😆。

 1         +-----------------------------------------+
 2              内核                  | 用户空间      |
 3         |     +-----+    +------+  |    +---------+ 
 4+--------+     |     |<-->| tty1 |<-|--->| Shell 1 |
 5| 键  盘  |--->| 终端 |    +------+  |    +---------+
 6+--------|     | 模拟 |<-->| tty2 |<-|--->| Shell 2 |
 7| 显示器  |<---| 软件 |    +------+  |    +---------+
 8+--------+     |     |<-->| tty3 |<-|--->| Shell 3 |
 9         |     +-----+    +------+  |--------------+
10         |                          |              |
11         +-----------------------------------------+

需要指出,软件仿真终端和tty是运行在内核态的。过去的/dev/console被默认连接到当前登录的虚拟终端(/dev/tty0也同样指向当前的虚拟终端,但是/dev/tty指向的当前任意终端,既可以是虚拟终端也可使伪终端)。我们可以通过把终端用ctrl+alt+F6切换到tty6做以下尝试:

 1#/dev/tty        Current TTY device
 2#/dev/console    System console, defalut current virtual console
 3#/dev/tty0       Current virtual console
 4$ tty
 5/dev/tty6
 6$ sudo bash -c 'echo "Hello from tty6" > /dev/console'
 7Hello from tty6
 8$ sudo bash -c 'echo "Hello from tty6" > /dev/tty'
 9Hello from tty6
10$ sudo bash -c 'echo "Hello from tty6" > /dev/tty0'
11Hello from tty6

从这三个设备都得到了重定向的消息,说明它们三个都被连接到了当前终端。但是,如果用伪终端,只有sudo bash -c 'echo "Hello from tty6" > /dev/tty'会有回显。例如,我们用Ubuntu自带的伪终端尝试结果如下:

伪终端回显

伪终端和虚拟终端有什么不同呢?我们下面看看伪终端。

伪终端

当我们需要终端模拟器有更灵活的功能而又不想动用内核时,我们也可以让终端模拟程序运行在用户区。如果我们让终端模拟程序运行在用户区,就需要伪终端(pseudoterminal或pseudotty, PTY)。PTY是描述并非单个设备文件,而是一对可双向通信的虚拟字符设备,称为 PTY master(ptmx)和 PYT slave(pts)。当前PTY有BSD(master名/dev/pty[p-za-e][0-9a-f],salve名/dev/tty[p-za-e][0-9a-f])和UNIX 98(master名/dev/ptmx,salve名/dev/pts/*)两种命名与实现方式,但是目前大多数都用的是UNIX98标准。

从用户空间的程序来看,使用虚拟终端还是伪终端都是一样的。此外,如果系统由多个伪终端连接,ptmx和pts合力完成多个伪终端的会话管理。但由于PTY运行在用户区,更加安全和灵活,同时仍然保留了TTY驱动的功能,因此我们目前在Linux桌面发行版中调出来的命令行工具、telnet、ssh、VNC远程连接几乎都是伪终端。常用的伪终端有xterm,gnome-terminal,以及远程终端ssh等。

伪终端的具体工作流程比较复杂,如果想具体了解可以参考2010年的《The Linux Programming Interface》的64章。对一般用户而言,我们只需要知道他是一种运行在用户空间的tty就可以了~

串行端口终端

串行端口终端是基于RS-232接口连接到主机的终端设备,作为最古老的设备接口之一,早期的外设一般都使用串行端口终端。同样的,它也是一类字符设备,用设备描述符/dev/ttySX表示,"X"代表一数字。现在串口协议一般用在嵌入式单片机或工业机上,大多数个人电脑现在连串口接口都没保留。下图以纪念我单片机焊板子的生活:

RS232串口

标准输入输出(错误)设备和终端设备的关系

默认情况下,标准输入输出(错误)设备会被链接到当前正在使用的终端设备,可以是虚拟设备也可以是伪设备,重要点是正在操作哪个终端设备。