本章是对正常运行的Linux系统中内核提供的设备基础设施的基本考察。纵观Linux的历史,在内核如何向用户展示设备方面已经有了许多变化。我们将从传统的设备文件系统开始,看看内核如何通过sysfs提供设备配置信息。我们的目标是能够提取系统中的设备信息,以便了解一些基本的操作。后面的章节将更详细地介绍与特定类型设备的交互。
了解当出现新设备时,内核如何与用户空间进行交互是很重要的。udev系统使用户空间的程序能够自动配置和使用新设备。你将看到内核如何通过udev向用户空间进程发送消息的基本工作原理,以及该进程如何处理这些消息。
3.1 设备文件
操作Unix系统中的大多数设备是很容易的,因为内核将许多设备的I/O接口以文件形式呈现给用户进程。这些设备文件有时被称为设备节点。除了程序员使用常规的文件操作来处理设备外,一些设备也可以被像cat这样的标准程序访问。然而,你能用文件接口做的事情是有限的,所以不是所有的设备或设备功能都能用标准的文件I/O访问。
设备文件在/dev目录下,运行ls /dev可以看到/dev中的相当多的文件。那么,你是如何处理设备的呢?
$ echo blah blah > /dev/null
就像其他带有重定向输出的命令一样,这个命令将标准输出中的内容发送到文件中。然而,这个文件是/dev/null,是一个设备,所以内核绕过了通常的文件操作,对写入这个设备的数据使用了设备驱动程序。在/dev/null的情况下,内核只是接受了输入的数据并将其丢弃。
要识别一个设备并查看其权限,可以使用ls -l。下面是一些例子:
$ ls -l
brw-rw---- 1 root disk 8, 1 Sep 6 08:37 sda1
crw-rw-rw- 1 root root 1, 3 Sep 6 08:37 null
prw-r-r-- 1 root root 0 Mar 3 19:17 fdata
srw-rw-rw- 1 root root 0 Dec 18 07:43 log
注意每一行的第一个字符(文件模式的第一个字符)。如果这个字符是b、c、p或s,则该文件是一个设备。这些字母分别代表块、字符、管道和套接字:
-
块设备
程序以固定的块来访问块设备的数据。前面例子中的sda1是一个磁盘设备,是块设备的一种类型。磁盘可以很容易地被分割成数据块。因为块设备的总大小是固定的,并且容易索引,程序在内核的帮助下可以快速随机访问设备中的任何块。 -
字符设备
字符设备与数据流一起工作。你只能从字符设备中读取字符或向字符设备中写入字符,就像前面用/dev/null演示的那样。字符设备没有大小之分;当你从设备中读出或写入时,内核通常对它进行读或写操作。直接连接到你的计算机上的打印机是由字符设备表示的。值得注意的是,在字符设备的交互过程中,内核在将数据传递给设备或进程后,不能备份和重新检查数据流。 -
管道设备
命名的管道就像字符设备,在I/O流的另一端是另一个进程,而不是内核驱动。 -
套接字设备
套接字是特殊用途的接口,经常用于进程间通信。它们经常在/dev目录之外被发现。套接字文件代表Unix域套接字;你将在第10章中了解更多关于这些套接字的信息。
在来自ls -l的块和字符设备的文件列表中,日期前面的数字是主要和次要的设备号,内核用它来识别设备。类似的设备通常有相同的主设备号,比如sda3和sdb1(都是硬盘分区)。
注意
不是所有的设备都有设备文件,因为块和字符设备的I/O接口不是在所有情况下都合适。例如,网络接口没有设备文件。理论上,使用单一的字符设备与网络接口交互是可能的,但是由于这很困难,内核提供了其他的I/O接口。
3.2 sysfs设备路径
传统的Unix的/dev目录是一种方便的方式,用户进程可以引用和连接内核支持的设备,但这也是一种非常简单的方案。在/dev中的设备名称告诉你关于该设备的一些情况,但通常不够有用。另一个问题是,内核是按照找到设备的顺序来分配设备的,所以设备在重启之间可能有不同的名字。
为了提供基于实际硬件属性的附加设备的统一视图,Linux内核通过文件和目录系统提供sysfs接口。设备的基本路径是/sys/devices。例如,位于/dev/sda的SATA硬盘在sysfs中可能有如下路径:
/sys/devices/pci0000:00/0000:00:17.0/ata3/host0/target0:0:0/0:0:0:0/block/sda
你可以看到,与/dev/sda文件名相比,这个路径相当长,它也是一个目录。但你不能真正比较这两个路径,因为它们有不同的目的。/dev文件使用户进程能够使用设备,而/sys/devices路径是用来查看信息和管理设备的。如果你列出设备路径的内容,比如前面的那个,你会看到类似下面的内容:
alignment_offset discard_alignment holders removable size uevent
bdi events inflight ro slaves
capability events_async power sda1 stat
dev events_poll_msecs queue sda2 subsystem
device ext_range range sda5 trace
这里的文件和子目录主要是供程序而不是人阅读的,但是你可以通过查看/dev文件这样的例子来了解它们包含和代表的内容。在这个目录下运行cat dev会显示数字8:0,这恰好是/dev/sda的主设备号和次设备号。
在/sys目录下有一些快捷方式。例如,/sys/block应该包含一个系统上所有可用的块设备。然而,这些只是符号链接;你应该运行ls -l /sys/block来显示真正的sysfs路径。
要在/dev中找到一个设备的sysfs位置可能很困难。使用如下的udevadm命令来显示路径和其他一些有趣的属性:
$ udevadm info --query=all --name=/dev/sda
你会在第3.5节找到更多关于udevadm和整个udev系统的细节。
3.3 dd和设备
当你在处理块和字符设备时,dd程序是非常有用的。它的唯一功能是从输入文件或流中读出,然后写到输出文件或流中,在这个过程中可能会进行一些编码转换。对于块设备来说,dd的特别有用的功能是,你可以在文件的中间处理一大块数据,而忽略之前或之后的内容。
警告:
dd的功能非常强大,所以当你运行它时,请确保你知道你在做什么。如果不小心犯了错误,很容易损坏设备上的文件和数据。如果你不确定它将会做什么,通常可以将输出写入一个新的文件。
dd以固定大小的块复制数据。下面是如何在字符设备上使用dd,利用一些常见的选项:
$ dd if=/dev/zero of=new_file bs=1024 count=1
正如你所看到的,dd的选项格式与大多数其他Unix命令的选项格式不同;它是基于旧的IBM工作控制语言(JCL)风格。你不是用破折号(-)来表示一个选项,而是用等号(=)来命名一个选项并设置其值。前面的例子从/dev/zero复制了一个1,024字节的块(一个连续的零字节流)到new_file。
这些是重要的dd选项:
- if=file 输入文件。默认是标准输入。
- of=file 输出文件。默认是标准输出。
- bs=s