Linux工作原理3设备

3 设备

本章是对正常运行的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=size 区块大小。dd一次读写这么多字节的数据。为了缩写大块的数据,你可以用b和k分别表示512和1024个字节。因此,前面的例子可以读取bs=1k,而不是bs=1024。
  • ibs=size, obs=size输入和输出块的大小。如果你能对输入和输出使用相同的块大小,请使用bs选项;如果不能,请对输入和输出分别使用ibs和obs。
  • count=num 要复制的块的总数。当处理巨大的文件时,或者处理提供无尽数据流的设备,比如/dev/zero,你希望dd在固定的点上停止;否则,你可能会浪费大量的磁盘空间、CPU时间,或者两者都浪费。使用count和跳过参数,从大文件或设备中复制一小段。
  • skip=num 跳过输入文件或数据流中的第一个num块,不把它们复制到输出。

3.4 设备名称摘要

有时很难找到设备的名称(例如,在给磁盘分区时)。这里有几个方法可以找出它是什么:

  • 用udevadm查询udevd(见第3.5节)。
  • 在/sys目录下寻找设备。
  • 从journalctl -k命令(打印内核信息)或内核系统日志(见第7.1节)的输出中猜测其名称。这个输出可能包含了对你系统中设备的描述。
  • 对于系统已经可见的磁盘设备,你可以检查mount命令的输出。
  • 运行cat /proc/devices来查看你的系统目前有驱动的块和字符设备。每一行都包括编号和名称。数字是设备的主要编号,如第3.1节所述。如果你能从名称中猜出设备,在/dev中寻找具有相应主要编号的字符或块设备,你就找到了设备文件。

在这些方法中,只有第一个方法是可靠的,但它确实需要udev。如果你遇到udev不可用的情况,可以尝试其他方法,但要记住,内核可能没有适合你的硬件的设备文件。

下面的章节列出了最常见的Linux设备和它们的命名规则。

3.4.1 硬盘/dev/sd*

大多数连接到当前Linux系统的硬盘对应于带有sd前缀的设备名,比如/dev/sda,/dev/sdb,等等。这些设备代表整个磁盘;内核为磁盘上的分区制作单独的设备文件,如/dev/sda1和/dev/sda2。
这个命名规则需要解释一下。名称中的sd部分代表SCSI磁盘。小型计算机系统接口(SCSI)最初是作为一种硬件和协议标准开发的,用于磁盘和其他外围设备等设备之间的通信。虽然传统的SCSI硬件在大多数现代机器中没有使用,但由于SCSI协议的适应性,它无处不在。例如,USB存储设备使用它进行通信。SATA(串行ATA,PC上常见的存储总线)磁盘上的情况要复杂一些,但Linux内核在与它们交谈时,仍然在一定程度上使用SCSI命令。
要列出你系统上的SCSI设备,可以使用一个工具来行走由sysfs提供的设备路径。其中一个最简洁的工具是lsscsi。当你运行它时,你可以看到以下内容:

$ lsscsi
[0:0:0:0]1  disk2  ATA     WDC WD3200AAJS-2  01.0  /dev/sda3
[2:0:0:0]    disk    FLASH   Drive UT_USB20    0.00  /dev/sdb

第一列标识了系统中设备的地址,第二列描述了它是什么类型的设备,最后一列3出在哪里可以找到设备文件。其他的都是厂商信息。
Linux按照其驱动程序遇到设备的顺序将设备分配给设备文件。所以,在前面的例子中,内核首先找到了磁盘,其次才是闪存驱动器。
不幸的是,当你重新配置硬件时,这种设备分配方案历来会引起问题。举例来说,你有一个有三个磁盘的系统: /dev/sda,/dev/sdb,和/dev/sdc。如果/dev/sdb爆炸了,你必须把它移走,以便机器能够重新工作,那么以前的/dev/sdc就会移到/dev/sdb上,而不再有/dev/sdc了。如果你直接参考fstab文件中的设备名称(见第4.2.8节),你就必须对该文件做一些修改,以便使事情(大部分)恢复正常。为了解决这个问题,许多Linux系统使用通用唯一标识符(UUID;见第4.2.4节)和/或逻辑卷管理器(LVM)来稳定磁盘设备映射。
关于如何在Linux系统上使用磁盘和其他存储设备,本文的讨论几乎没有触及表面。关于使用磁盘的更多信息,见第4章。在本章的后面,我们将研究SCSI支持在Linux内核中是如何工作的。

3.4.2 虚拟磁盘/dev/xvd, /dev/vd

一些磁盘设备为虚拟机进行了优化,如AWS实例和VirtualBox。Xen虚拟化系统使用/dev/xvd前缀,而/dev/vd是一种类似的类型。

3.4.3 非易失性内存设备/dev/nvme*

一些系统现在使用非易失性内存快车(NVMe Non-Volatile Memory Express)接口来与某些类型的固态存储进行对话。在Linux中,这些设备显示在/dev/nvme*。你可以使用nvme list命令来获得你系统上这些设备的列表。

3.4.4 设备映射器: /dev/dm-, /dev/mapper/

在一些系统上,比磁盘和其他直接块存储更高级别的是LVM,它使用一个叫做设备映射器的内核系统。如果你看到以/dev/dm-开头的块设备和/dev/mapper的符号链接,你的系统可能使用了它。你将在第4章中了解这一切。

3.4.5 CD和DVD驱动器: /dev/sr*

Linux将大多数光存储驱动器识别为SCSI设备/dev/sr0、/dev/sr1,等等。然而,如果驱动器使用旧的接口,它可能会显示为一个PATA设备,如下所述。/dev/sr*设备是只读的,它们只用于从磁盘上读取。对于光学设备的写入和重写能力,你将使用"通用 "SCSI设备,如/dev/sg0。

3.4.6 PATA硬盘/dev/hd*

PATA(Parallel ATA)是一种较早的存储总线。Linux块设备/dev/hda、/dev/hdb、/dev/hdc和/dev/hdd在旧版本的Linux内核和旧硬件上很常见。这些是基于接口0和1的设备对的固定分配。 有时,你可能会发现SATA驱动器被识别为这些磁盘之一。这意味着该SATA驱动器在兼容模式下运行,这阻碍了性能。检查你的BIOS设置,看看你是否可以将SATA控制器切换到其原始模式。

3.4.7 终端: /dev/tty, /dev/pts/, 和 /dev/tty

终端是在用户进程和I/O设备之间移动字符的设备,通常用于向终端屏幕输出文字。终端设备接口可以追溯到很久之前,当时的终端是基于打字机的设备,很多都是连接在一台机器上的。

大多数终端是伪终端设备,是理解真正终端的I/O特性的仿真终端。内核不是与真正的硬件对话,而是将I/O接口呈现给软件,例如你可能在其中输入大部分命令的shell终端窗口。

两个常见的终端设备是/dev/tty1(第一个虚拟控制台)和/dev/pts/0(第一个伪终端设备)。/dev/pts目录本身是一个专门的文件系统。

/dev/tty设备是当前进程的控制终端。如个程序当前正在从终端读写,这个设备就是该终端的同义词。进程不需要连接到终端。

  • 显示模式和虚拟控制台

Linux有两种主要的显示模式:文本模式和图形模式(第14章介绍了使用这种模式的窗口系统)。尽管Linux系统传统上是以文本模式启动的,但现在大多数发行版使用内核参数和临时图形显示机制(bootsplashes,如plymouth)来完全隐藏系统启动时的文本模式。在这种情况下,系统会在启动过程接近尾声时切换到全图形模式。

Linux支持虚拟控制台来复用显示。每个虚拟控制台可以在图形或文本模式下运行。当处于文本模式时,你可以通过ALT功能键组合在控制台之间进行切换--例如,ALT-F1会带你到/dev/tty1,ALT-F2会到/dev/tty2,以此类推。许多这样的虚拟控制台可能被运行登录提示的getty进程所占据,如第7.4节所述。

在图形模式下使用的虚拟控制台略有不同。除非被指示使用一个特定的虚拟控制台,否则图形环境会接管空闲的虚拟控制台,而不是从初始配置中获得一个虚拟控制台的分配。例如,如果你有getty进程在tty1和tty2上运行,新的图形环境会占用tty3。此外,一旦进入图形模式,你通常必须按CTRL-ALT-功能键组合来切换到另一个虚拟控制台,而不是更简单的ALT-功能键组合。

所有这些的结果是,如果你想在系统启动后看到你的文本控制台,按CTRL-ALT-F1。要返回到图形环境,按ALT-F2、ALT-F3,以此类推,直到你进入图形环境。

注意:有些发行版在图形模式下使用tty1。在这种情况下,你将需要尝试其他控制台。

如果你在切换控制台时由于输入机制的故障或其他情况而遇到麻烦,你可以尝试用chvt命令强迫系统改变控制台。例如,要切换到tty1,以root身份运行以下命令:

# chvt 1

3.4.8 串行端口: /dev/ttyS, /dev/ttyUSB, /dev/ttyACM*

较早的RS-232类型和类似的串行端口被表示为真正的终端设备。你不能在命令行上对串口设备做很多事情,因为有太多的设置需要担心,比如波特率和流量控制,但是你可以使用screen命令,通过添加设备路径作为参数来连接到终端。你可能需要该设备的读写权限;有时你可以通过将自己添加到特定的组(如dialout)来实现。

在Windows上被称为COM1的端口是/dev/ttyS0;COM2是/dev/ttyS1;以此类推。插入式USB串行适配器显示为USB和ACM,名称为/dev/ttyUSB0、/dev/ttyACM0、/dev/ttyUSB1、/dev/ttyACM1等等。

一些涉及到串行端口的最有趣的应用是基于微控制器的板子,你可以把它插入你的Linux系统进行开发和测试。例如,你可以通过USB串行接口访问CircuitPython板的控制台和读-评-印循环。你所需要做的就是插上一个,寻找设备(通常是/dev/ttyACM0),然后用屏幕连接到它。

3.4.9 并行端口/dev/lp0和/dev/lp1

单向并口设备/dev/lp0和/dev/lp1代表着一种接口类型,在很大程度上已经被USB和网络所取代,在Windows中对应于LPT1:和LPT2:。你可以用cat命令将文件(如要打印的文件)直接发送到并口,但你可能需要在之后给打印机一个额外的进纸或复位。像CUPS这样的打印服务器在处理与打印机的交互方面要好得多。
双向的并行端口是/dev/parport0和/dev/parport1。

3.4.10 音频设备: /dev/snd/*, /dev/dsp, /dev/audio, 以及更多

Linux有两套音频设备。有独立的设备用于高级Linux声音架构(ALSA)系统接口和较早的开放声音系统(OSS)。ALSA设备在/dev/snd目录下,但很难直接使用它们。使用ALSA的Linux系统支持OSS的后向兼容设备,如果目前加载了OSS的内核支持。

对OSS的dsp和音频设备可以进行一些基本的操作。例如,计算机会播放你发送到/dev/dsp的任何WAV文件。然而,由于频率不匹配,硬件可能做不到你期望的那样。此外,在大多数系统上,该设备往往在你登录后就开始忙碌。

注意:由于涉及到许多层次,Linux的声音是一个混乱的主题。我们只谈了内核级的设备,但通常还有用户空间的服务器,如pulseaudio,管理来自不同来源的音频,作为声音设备和其他用户空间进程之间的中介。

3.4.11 创建设备文件

在任何合理的最近的Linux系统上,你都不会创建自己的设备文件;它们是由devtmpfs和udev创建的(见第3.5节)。然而,看看如何创建设备文件是很有意义的,在罕见的情况下,你可能需要创建命名的管道或套接字文件。

mknod命令创建设备。你必须知道设备的名称以及它的主号和次号。例如,创建/dev/sda1只需使用下面的命令:


# mknod /dev/sda1 b 8 1

b 8 1指定了一个主数为8、次数为1的块设备。对于字符或命名的管道设备,使用c或p而不是b(对于命名的管道,省略主要和次要数字)。

在旧版本的Unix和Linux中,维护/dev目录是一个挑战。随着每一次重要的内核升级或驱动程序的增加,内核可以支持更多种类的设备,这意味着将有一组新的主要和次要数字被分配给设备文件名。为了解决这个维护难题,每个系统都有一个MAKEDEV程序,在/dev中创建设备组。当你升级你的系统时,你将试图找到MAKEDEV的更新,然后运行它以创建新的设备。

这个静态的系统变得很难看,所以一个替换是必要的。解决这个问题的第一个尝试是devfs,一个内核空间的/dev实现,包含了当前内核支持的所有设备。然而,有一些限制,这导致了udev和devtmpfs的发展。

3.5 udev

我们已经谈到了内核中不必要的复杂性是很危险的,因为你太容易引入系统的不稳定性。设备文件管理就是一个例子:你可以在用户空间创建设备文件,那么你为什么要在内核中这样做?Linux内核可以在检测到系统中的新设备时(例如,当有人安装了U盘时)向udevd的用户空间进程发送通知。这个udevd进程可以检查新设备的特性,创建设备文件,然后执行任何设备初始化。

注意:你几乎肯定会看到udevd以systemd-udevd的形式在你的系统上运行,因为它是你将在第六章看到的启动机制的一部分。

这就是理论。不幸的是,这种方法有一个问题--设备文件在启动过程的早期是必需的,所以udevd也必须提前启动。但是为了创建设备文件,udevd不能依赖于任何它应该创建的设备,它需要非常快速地执行其初始启动,这样系统的其他部分就不会因为等待udevd的启动而被耽搁。

3.5.1 devtmpfs

devtmpfs文件系统是为了解决启动过程中的设备可用性问题而开发的(关于文件系统的更多细节,见第4.2节)。这个文件系统类似于较早的devfs支持,但有所简化。内核在必要时创建设备文件,但它也会通知udevd新的设备是可用的。收到这个信号后,udevd不会创建设备文件,但它会执行设备初始化,同时设置权限并通知其他进程新设备可用。此外,它在/dev中创建一些符号链接,以进一步识别设备。你可以在/dev/disk/by-id目录下找到例子,每个连接的磁盘都有一个或多个条目。

例如,考虑典型的磁盘(连接在/dev/sda)和它在/dev/disk/by-id中的分区的链接:


$ ls -l /dev/disk/by-id
lrwxrwxrwx 1 root root  9 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671 -> ../../sda
lrwxrwxrwx 1 root root 10 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671-part1 ->
../../sda1
lrwxrwxrwx 1 root root 10 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671-part2 ->
../../sda2
lrwxrwxrwx 1 root root 10 Jul 26 10:23 scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671-part5 ->
../../sda5

udevd进程按接口类型命名链接,然后按制造商和型号信息、序列号和分区(如果适用)命名。

注意:devtmpfs中的"tmp"表示文件系统驻留在主内存中,具有用户空间进程的读/写能力;这个特性使udevd能够创建这些符号链接。我们将在第4.2.12节看到更多细节。

但udevd如何知道要创建哪些符号链接,以及如何创建它们?下一节将描述udevd是如何工作的。然而,你不需要知道这些或本章中的任何其他材料来继续阅读本书。事实上,如果这是你第一次研究Linux设备,我们强烈建议你跳到下一章,开始学习如何使用磁盘。

3.5.2 udevd的操作和配置

udevd守护进程的运作方式如下:

  • 内核通过内部网络链接向udevd发送通知事件,称为uevent。
  • udevd加载uevent中的所有属性。
  • udevd解析其规则,根据这些规则过滤和更新uevent,并采取相应的行动或设置更多属性。
    udevd从内核收到的传入的uevent可能是这样的(你会在第3.5.4节学习如何用udevadm monitor --property命令获得这个输出):

ACTION=change
DEVNAME=sde
DEVPATH=/devices/pci0000:00/0000:00:1a.0/usb1/1-1/1-1.2/1-1.2:1.0/host4/
target4:0:0/4:0:0:3/block/sde
DEVTYPE=disk
DISK_MEDIA_CHANGE=1
MAJOR=8
MINOR=64
SEQNUM=2752
SUBSYSTEM=block
UDEV_LOG=3

这个特殊的事件是对设备的改变。在接收到uevent后,udevd知道了设备的名称、sysfs设备路径和其他一些与属性相关的属性;它现在准备开始处理规则。
规则文件在/lib/udev/rules.d和/etc/udev/rules.d目录下。/lib中的规则是默认的,而/etc中的规则是重写的。对规则的全面解释会很乏味,你可以从udev(7)手册中了解更多,但这里有一些关于udevd如何读取规则的基本信息:

  • udevd从规则文件的开始到结束读取规则。
  • 在读完一条规则并可能执行其动作后,udevd 继续阅读当前的规则文件,寻找更多适用的规则。
  • 有一些指令(如GOTO)可以在必要时跳过规则文件的部分内容。这些指令通常放在规则文件的顶部,如果它与udevd正在配置的特定设备无关,就跳过整个文件。

让我们看一下第3.5.1节中/dev/sda例子中的符号链接。这些链接是由/lib/udev/rules.d/60-persistent-storage.rules中的规则定义的。在里面,你会看到以下几行:

# ATA
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{vendor}=="ATA", IMPORT{program}="ata_id --export $devnode"

# ATAPI devices (SPC-3 or later)
KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{type}=="5",ATTRS{scsi_level}=="[6-9]*", IMPORT{program}="ata_id --export $devnode"

这些规则与通过内核的SCSI子系统(见第3.6节)呈现的ATA磁盘和光学介质相匹配。你可以看到有一些规则来捕捉设备可能的不同表现方式,但想法是udevd将尝试匹配以sd或sr开头但没有数字的设备(用KERNEL"sd[!0-9]|sr"表达式),以及子系统(SUBSYSTEMS"scsi"),最后还有一些其他属性,取决于设备的类型。如果所有这些条件表达式在任何一条规则中都为真,udevd就会转到下一个也是最后表达式:


IMPORT{program}="ata_id --export $tempnode"

这不是条件。相反,它是指令,从/lib/udev/ata_id命令中导入变量。如果你有这样一个磁盘,自己在命令行上试试。它看起来会像这样:

# /lib/udev/ata_id --export /dev/sda
ID_ATA=1
ID_TYPE=disk
ID_BUS=ata
ID_MODEL=WDC_WD3200AAJS-22L7A0
ID_MODEL_ENC=WDC\x20WD3200AAJS22L7A0\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20
\x20\x20\x20\x20\x20\x20\x20\x20\x20
ID_REVISION=01.03E10
ID_SERIAL=WDC_WD3200AAJS-22L7A0_WD-WMAV2FU80671
--snip--

现在,导入设置了环境,使这个输出中的所有变量名都被设置为所示的值。例如,接下来的任何规则现在都会将 ENV{ID_TYPE} 识别为磁盘。

在我们到目前为止看到的两条规则中,特别值得注意的是ID_SERIAL。在每条规则中,这个条件都出现在第二条:

env{id_serial}!="?*"

如果ID_SERIAL没有被设置,这个表达式会评估为真。因此,如果ID_SERIAL被设置了,条件就是假的,整个当前规则就不适用,udevd就会转到下一条规则。
为什么会出现在这里?这两条规则的目的是运行ata_id来查找磁盘设备的序列号,然后将这些属性添加到uevent的当前工作副本中。你会在许多udev规则中发现这个一般模式。
设置了ENV{ID_SERIAL}后,udevd现在可以在后面的规则文件中评估这个规则,它寻找任何连接的SCSI磁盘:

KERNEL=="sd*|sr*|cciss*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="?*",SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}"

你可以看到,这条规则要求ENV{ID_SERIAL}被设置,它有一个指令:

SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}"

这个指令告诉udevd为进入的设备添加一个符号链接。所以,现在你知道设备的符号链接是怎么来的了!
你可能想知道如何区分条件表达式和指令。条件表达式用两个等号(==)或砰的一声等号(!=)表示,指令用一个等号(=)、一个加号(+=)或一个冒号等号(:=)表示。

3.5.3 udevadm

udevadm程序是udevd的管理工具。 你可以重新加载udevd规则和触发事件,但udevadm最强大的功能可能是搜索和探索系统设备的能力,以及udevd从内核接收uevent时监控uevent的能力。不过,命令的语法可能有点儿复杂。大多数选项都有长短两种形式,我们在这里使用长的。
让我们从检查一个系统设备开始。回到第3.5.2节中的例子,为了查看所有的udev属性,以及与/dev/sda这样的设备的规则一起生成的属性,运行以下命令:

$ udevadm info --query=all --name=/dev/sda
P: /devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda
N: sda
S: disk/by-id/ata-WDC_WD3200AAJS-22L7A0_WD-WMAV2FU80671
S: disk/by-id/scsi-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671
S: disk/by-id/wwn-0x50014ee057faef84
S: disk/by-path/pci-0000:00:1f.2-scsi-0:0:0:0
E: DEVLINKS=/dev/disk/by-id/ata-WDC_WD3200AAJS-22L7A0_WD-WMAV2FU80671 /dev/disk/by-id/scsi
-SATA_WDC_WD3200AAJS-_WD-WMAV2FU80671 /dev/disk/by-id/wwn-0x50014ee057faef84 /dev/disk/by
-path/pci-0000:00:1f.2-scsi-0:0:0:0
E: DEVNAME=/dev/sda
E: DEVPATH=/devices/pci0000:00/0000:00:1f.2/host0/target0:0:0/0:0:0:0/block/sda
E: DEVTYPE=disk
E: ID_ATA=1
E: ID_ATA_DOWNLOAD_MICROCODE=1
E: ID_ATA_FEATURE_SET_AAM=1
--snip--

每行的前缀表示设备的一个属性或其他特征。在这个例子中,顶部的P:是sysfs设备路径,N:是设备节点(也就是给/dev文件起的名字),S:表示udevd根据其规则放在/dev中的设备节点的符号链接,E:是udevd规则中提取的额外设备信息。(这个例子中的输出远远超过了这里需要展示的内容;自己尝试一下这个命令,感受一下它的作用)。

3.5.4 设备监控

要用udevadm监视uevents,请使用monitor命令:

$ udevadm monitor
KERNEL[658299.569485] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2 (usb)
KERNEL[658299.569667] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0 (usb)
KERNEL[658299.570614] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/host15 
(scsi)
KERNEL[658299.570645] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/ 
host15/scsi_host/host15 (scsi_host)
UDEV [658299.622579] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2 (usb)
UDEV [658299.623014] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0 (usb)
UDEV [658299.623673] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/host15 
(scsi)
UDEV [658299.623690] add /devices/pci0000:00/0000:00:1d.0/usb2/2-1/2-1.2/2-1.2:1.0/
host15/scsi_host/host15 (scsi_host)
--snip--

在这个输出中,每条信息都有两份,因为默认行为是同时打印来自内核的传入信息(用KERNEL标记)和来自udevd的处理信息。 要想只看到内核事件,请添加--kernel选项,要想只看到udevd处理事件,请使用--udev。要看到整个传入的uevent,包括3.5.2节中显示的属性,使用--property选项。--udev和--property选项一起显示处理后的uevent。
你还可以按子系统过滤事件。例如,要想只看到与SCSI子系统的变化有关的内核信息,使用这个命令:

$ udevadm monitor --kernel --subsystem-match=scsi

关于udevadm的更多信息,请看udevadm(8)手册页。
udev的内容还有很多。例如,有一个叫做udisksd的守护进程,它监听事件,以便自动连接磁盘,并通知其他进程有新的磁盘可用。

3.6 深入了解: SCSI和Linux内核

在这一节中,我们将看一下Linux内核中的SCSI支持,作为探索Linux内核结构的一部分。你不需要为了使用磁盘而了解这些信息,所以如果你急于使用磁盘,请继续阅读第四章。此外,这里的材料比你到目前为止所看到的更高级,更具有理论性,所以如果你想保持动手能力,你肯定应该跳到下一章。
让我们从一点背景开始。传统的SCSI硬件设置是一个主机适配器通过SCSI总线与一连串的设备相连,如图3-1所示。主机适配器被连接到计算机上。主机适配器和设备都有一个SCSI ID,根据SCSI版本,每条总线可以有8或16个ID。一些管理员可能会用SCSI目标这个词来指代设备和它的SCSI ID,因为在SCSI协议中,会话的一端被称为目标。

图3-1:带有主机适配器和设备的SCSI总线

任何设备都可以通过SCSI命令集以点对点的关系与另一个设备进行通信。计算机没有直接连接到设备链上,所以它必须通过主机适配器才能与磁盘和其他设备通信。通常情况下,计算机向主机适配器发送SCSI命令以转达给设备,而设备则通过主机适配器转达响应。

较新版本的SCSI,如串行连接SCSI(SAS),提供了卓越的性能,但你可能不会在大多数机器中找到真正的SCSI设备。你会更经常地遇到使用SCSI命令的USB存储设备。此外,支持ATAPI的设备(如CD/DVD-ROM驱动器)使用SCSI命令集的一个版本。

SATA磁盘也作为SCSI设备出现在你的系统上,但它们略有不同,因为它们中的大多数是通过libata库中的转换层进行通信的(见3.6.2节)。一些SATA控制器(特别是高性能的RAID控制器)在硬件中进行这种转换。
这一切是如何组成的?考虑一下下面系统中显示的设备:

$ lsscsi
[0:0:0:0]diskATAWDC WD3200AAJS-201.0/dev/sda
[1:0:0:0]cd/dvdSlimtypeDVD A DS8A5SHXA15/dev/sr0
[2:0:0:0]diskUSB2.0CardReader CF0100/dev/sdb
[2:0:0:1]diskUSB2.0CardReader SM XD0100/dev/sdc
[2:0:0:2]diskUSB2.0CardReader MS0100/dev/sdd
[2:0:0:3]diskUSB2.0CardReader SD0100/dev/sde
[3:0:0:0]diskFLASHDrive UT_USB200.00/dev/sdf

方括号内的数字从左到右分别是SCSI主机适配器号、SCSI总线号、设备SCSI ID和LUN(逻辑单元号,是设备的进一步细分)。在这个例子中,有四个连接的适配器(scsi0、scsi1、scsi2和scsi3),每个都有个总线(都是总线号0),每个总线上只有一个设备(都是目标0)。位于2:0:0的USB读卡器有四个逻辑单元,但每一种闪存卡都可以被插入。内核给每个逻辑单元分配了一个不同的设备文件。

尽管不是SCSI设备,NVMe设备有时会在lsscsi输出中显示出N作为适配器编号。
注意:如果你想自己尝试lsscsi,你可能需要把它作为一个额外的软件包来安装。
图3-2显示了这个特定系统配置的内核中的驱动和接口层次,从单个设备驱动到块驱动。它不包括SCSI通用(sg)驱动。
尽管这是一个庞大的结构,一开始可能会让人不知所措,但是图中的数据流是非常线性的。让我们从SCSI子系统和它的三层驱动开始剖析:
顶层处理一类设备的操作。例如,sd(SCSI磁盘)驱动程序就在这一层;它知道如何将来自内核块设备接口的请求翻译成SCSI协议中的磁盘特定命令,反之亦然。
中间层在顶层和底层之间调节和路由SCSI信息,并跟踪所有的SCSI总线和连接到系统的设备。
底层处理特定的硬件动作。这里的驱动程序向特定的主机适配器或硬件发送传出的SCSI协议信息,并从硬件提取传入的信息。与顶层分离的原因是,尽管SCSI消息对于一个设备类别(如磁盘类别)是统一的,但不同种类的主机适配器有不同的发送相同消息的程序。

图3-2: Linux SCSI子系统原理图

上层和下层包含许多不同的驱动,但是重要的是要记住,对于你系统上的任何一个设备文件,内核(几乎总是)使用一个上层驱动和一个下层驱动。在我们的例子中,对于位于/dev/sda的磁盘,内核使用sd顶层驱动和ATA桥层驱动。
有些时候,你可能会为一个硬件设备使用不止一个上层驱动(见第3.6.3节)。对于真正的硬件SCSI设备,比如连接到SCSI主机适配器或硬件RAID控制器的磁盘,下层驱动程序直接与下面的硬件对话。然而,对于你发现连接到SCSI子系统的大多数硬件来说,情况就不同了。

3.6.1 USB存储和SCSI

为了让SCSI子系统与普通的USB存储硬件对话,如图3-2所示,内核需要的不仅仅是一个低层的SCSI驱动。一个由/dev/sdf代表的U盘可以理解SCSI命令,但是为了和驱动器进行实际的通信,内核需要知道如何通过USB系统进行对话。
从抽象的角度来看,USB与SCSI非常相似--它有设备类别、总线和主机控制器。因此,Linux内核包括一个与SCSI子系统非常相似的三层USB子系统也就不足为奇了,它的顶端是设备类驱动程序,中间是总线管理核心,底部是主机控制器驱动程序。与SCSI子系统在其组件之间传递SCSI命令一样,USB子系统在其组件之间传递USB信息。甚至还有一个lsusb命令,与lsscsi相似。
我们在这里真正感兴趣的部分是顶部的USB存储驱动器。这个驱动充当了翻译者的角色。在一端,驱动程序说的是SCSI,而在另一端,它说的是USB。因为存储硬件在其USB信息中包含SCSI命令,所以驱动程序的工作相对容易:它主要是重新包装数据。
有了SCSI和USB子系统,你几乎拥有了与闪存盘对话所需的一切。最后缺失的环节是SCSI子系统中的下层驱动,因为USB存储驱动是USB子系统的一部分,而不是SCSI子系统。(由于组织上的原因,这两个子系统不应该共享一个驱动。)为了使子系统能够相互交谈,一个简单的、低层的SCSI桥接驱动连接到USB子系统的存储驱动。

3.6.2 SCSI和ATA

图3-2中的SATA硬盘和光驱都使用相同的SATA接口。为了将内核的SATA驱动连接到SCSI子系统,内核采用了一个桥接驱动,就像对待USB驱动器一样,但是有不同的机制和额外的复杂情况。光驱说的是ATAPI,这是ATA协议中编码的SCSI命令的一个版本。然而,硬盘不使用ATAPI,也不对任何SCSI命令进行编码!
Linux内核使用一个叫做libata的库的一部分来调和SATA(和ATA)驱动器与SCSI子系统。对于讲ATAPI的光驱来说,这是一个相对简单的任务,即把SCSI命令打包并提取到ATA协议中去。但对硬盘来说,这项任务要复杂得多,因为库必须做一个完整的命令转换。
光驱的工作类似于将一本英文书打入电脑。你不需要为了完成这项工作而理解书的内容,甚至也不需要理解英语。但硬盘的任务更像是阅读一本德语书,并将其作为英文翻译输入电脑。在这种情况下,你需要理解两种语言以及书中的内容。
尽管有这样的困难,libata还是执行了这项任务,并使将ATA/SATA接口和设备连接到SCSI子系统成为可能。(除了图3-2中所示的一个SATA主机驱动程序外,通常还涉及更多的驱动程序,但为了简单起见,我们没有显示出来)。

3.6.3 通用SCSI设备

当用户空间进程与SCSI子系统通信时,它通常是通过块设备层和/或其他位于SCSI设备类驱动(如sd或sr)之上的内核服务进行的。换句话说,大多数用户进程不需要知道任何关于SCSI设备或它们的命令。
然而,用户进程可以绕过设备类驱动程序,通过其通用设备直接向设备发出SCSI协议命令。例如,考虑第3.6节中描述的系统,但这次,看看当你为lsscsi添加-g选项以显示通用设备时会发生什么:

$ lsscsi -g
[0:0:0:0]   disk    ATA       WDC WD3200AAJS-2  01.0  /dev/sda 1/dev/sg0
[1:0:0:0]   cd/dvd  Slimtype  DVD A DS8A5SH     XA15  /dev/sr0   /dev/sg1
[2:0:0:0]   disk    USB2.0    CardReader CF     0100  /dev/sdb   /dev/sg2
[2:0:0:1]   disk    USB2.0    CardReader SM XD  0100  /dev/sdc   /dev/sg3
[2:0:0:2]   disk    USB2.0    CardReader MS     0100  /dev/sdd   /dev/sg4
[2:0:0:3]   disk    USB2.0    CardReader SD     0100  /dev/sde   /dev/sg5
[3:0:0:0]   disk    FLASH     Drive UT_USB20    0.00  /dev/sdf   /dev/sg6

除了通常的块设备文件外,每个条目在最后一列1中列出SCSI通用设备文件。例如,位于/dev/sr0的光驱的通用设备是/dev/sg1。
为什么你想使用一个通用设备?答案是与内核中代码的复杂性有关。随着任务变得越来越复杂,最好把它们留在内核之外。考虑一下CD/DVD的写入和读取。读取光盘是相当简单的操作,而且有专门的内核驱动。
然而,写光盘要比读光盘困难得多,而且没有关键的系统服务依赖于写光盘的操作。没有理由用这种活动来威胁内核空间。因此,要在Linux中写入光盘,你需要运行一个用户空间程序,与一个通用的SCSI设备对话,比如/dev/sg1。这个程序可能比内核驱动的效率低一些,但它更容易建立和维护。

3.6.4 单一设备的多种访问方法

图3-3展示了Linux SCSI子系统从用户空间访问光驱的两个点(sr和sg)(SCSI下层的任何驱动都被省略了)。进程A使用sr驱动从驱动器中读取,进程B使用sg驱动向驱动器写入。然而,像这样的进程通常不会同时运行来访问同一个设备。

图3-3:光学设备驱动原理图
在图3-3中,进程A从块设备中读取数据。但是,用户进程真的会以这种方式读取数据吗?通常情况下,答案是否定的,不是直接的。在块设备上面还有更多的层,甚至还有更多的硬盘访问点,你将在下一章中学习。

posted @ 2023-05-31 21:10  磁石空杯  阅读(308)  评论(0编辑  收藏  举报