第3章 设备管理

第3章 设备管理

 图像说明文字

本章介绍与Linux系统内核提供的设备相关的基础设施。纵观Linux发展史,内核向用户呈现设备的方式发生了很大变化。我们将从传统的设备文件系统开始,介绍内核如何通过sysfs来提供设备配置信息。我们的目标是能够通过在系统上收集设备信息来了解一些基本操作。后面的章节将进一步介绍一些具体设备的管理。

理解内核怎样在用户空间呈现新设备很关键。udev系统让用户空间进程能够自动配置和使用新设备。我们将介绍内核如何通过udev向用户空间进程发送消息,以及进程如何处理这些消息。

3.1 设备文件

在Unix系统中操纵大多数设备都很容易,因为很多I/O接口都是以文件的形式由内核呈现给用户的。这些设备文件有时又叫作设备节点。开发人员可以像操作文件一样来操作设备,一些Unix标准命令(如cat)也可以访问设备,所以不仅仅开发人员,普通用户也能够访问设备。然而对文件接口所能执行的操作是有限制的,所以并不是所有设备或设备功能都能够通过标准文件I/O方式来访问。

Linux处理设备文件的方式和Unix一样。设备文件存放在/dev目录中,可以使用ls /dev命令来查看。

我们从下面这个命令开始:

$ echo blah blah > /dev/null

这个命令将执行结果从标准输出重定向到一个文件,这个文件是/dev/null,它是一个设备,内核决定如何处理设备的数据写入。对/dev/null来说,内核直接忽略输入数据。

你可以使用ls -l来查看设备及其权限。

例3-1 设备文件

$ 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(block)、c(character)、p(pipe)和s(socket)代表设备文件。下面是详细介绍。

  • 块设备

    程序从块设备中按固定的块大小读取数据。前面的例子中,sda1是一个磁盘设备,它是块设备的一种。我们能够轻松地将磁盘划分成数据区块。因为磁盘的容量是固定的,索引起来也很方便,所以进程能够通过内核访问磁盘上的任意区块。

  • 字符设备

    字符设备处理流数据。你只能对字符设备读取和写入字符数据,如前面例子中的/dev/null。字符设备没有固定容量,当你对字符设备进行读写时,内核对相应的设备进行读写操作。字符设备的一个例子是打印机,值得注意的是,内核在流数据送达设备和进程后不会备份和再次验证。

  • 管道设备

    命名管道设备和字符设备类似,不同的是输入输出端不是内核驱动程序,而是另外一个进程。

  • 套接字设备

    套接字设备是跨进程通信经常用到的特殊接口。它们经常会存放于/dev目录之外。套接字文件代表Unix域套接字,我们将在第10章详细介绍。

在例3-1的第1、2行中,日期前的两个数字代表主要次要设备号,它们是内核用来识别设备的数字。相同类型的设备一般有相同的主设备号,比如sda3和sdb1(它们都是磁盘分区)。

注解:并不是所有的设备都有对应的设备文件,因为块设备和字符设备并不是适合所有场合的。例如,网络接口没有设备文件,虽然其理论上可以使用字符设备来代表,但是实现起来实在很困难,所以内核采用了其他的I/O接口。

3.2 sysfs设备路径

传统的Unix /dev目录为用户进程与使用内核支持的设备进行引用与交互提供了便利,但是它过于简单。/dev目录中的文件名包含有关设备的一些信息,但不是很详尽。另一个问题是内核根据其找到设备的顺序为设备文件命名,所以系统每次重新启动后,设备文件名有可能不同。

Linux内核通过一个文件和目录系统提供sysfs界面,旨在基于硬件属性统一显示设备的相关信息。设备以/sys/devices为root路径。例如,/dev/sda代表的SATA硬盘在sysfs中的路径可能是:

/sys/devices/pci0000:00/0000:00:1f.2/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

注解udevadm命令在/sbin目录下,如果你的路径中没有,可以将该目录加到你的路径中。

udevadm和udev系统将在3.5节详细介绍。

3.3 dd命令和设备

dd命令对于块设备和字符设备非常有用,它的主要功能是从输入文件和输入流读取数据然后写入输出文件和输出流,在此过程中可能涉及到编码转换。

dd命令复制固定大小的数据块,例如下面的代码显示如何借助字符设备使用dd命令:

$ dd if=/dev/zero of=new_file bs=1024 count=1

dd命令的格式选项和大多数其他Unix命令不同,它沿袭了从前的IBM Job Control Language(JCL)的风格。它使用等号=而不是减号-来设定选项和参数值。上面的例子是从/dev/zero复制一个大小为1024字节的数据块到文件new_file。

以下是dd命令的一些重要选项。

  • if=file:代表输入文件,默认是标准输入。
  • of=file:代表输出文件,默认是标准输出。
  • bs=size:代表数据块大小。dd命令一次读取或者写入数据的大小。对于海量数据,你可以在数字后设置bk来分别代表512字节和1024字节。如:bs=1kbs=1024一样。
  • ibs=size,obs=size:代表输入和输出块大小。如果输入输出块大小相同,你可以使用bs选项,如果不相同的话,可以使用ibsobs分别指定。
  • count=num:代表复制块的总数。在处理大文件或者无限数据流(/dev/zero)的时候,你可能会需要在某个地方停止dd复制,不然的话将会消耗大量硬盘空间和CPU时间。这时你可以使用countskip选项从大文件或设备中复制一小部分数据。
  • skip=num:代表跳过前面的num个块,不将它们复制到输出。

警告dd命令功能非常强大,你需要先对其充分了解再使用,否则稍一疏忽就会损坏文件和设备上的数据。dd命令通常用来将输出数据写入到新文件。

3.4 设备名总结

有时候查找设备的名称不是很方便(比如在为硬盘分区的时候),下面我们介绍一些简便的方法。

  • 使用udevadm命令来查询udevd(见3.5节)。

  • 在/sys目录下查找设备。

  • dmesg(它显示最新的内核消息,见7.2节)命令的输出或者内核系统日志中查获设备名。这些地方通常会有系统设备的描述信息。

  • 对系统已经找到的硬盘设备,可以使用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 disk。小型计算机系统接口(Small Computer System Inteface,以下简称SCSI)最初是作为设备之间通信的硬件协议标准而开发的,虽然现在的计算机并没有使用传统的SCSI硬件,但是SCSI协议的运用却非常广泛。例如,USB存储设备就使用SCSI协议进行通信。SATA硬盘的情况相对复杂一些,但是Linux内核仍然在某些场合使用SCSI命令和它们通信。

我们可以使用sysfs系统提供的命令来查看系统中的SCSI设备。最常用的命令之一是lsscsi。运行结果如下例所示:

$ lsscsi
[0:0:0:0]➊   disk➋  ATA      WDC WD3200AAJS-2 01.0 /dev/sda➌
[1:0:0:0]    cd/dvd  Slimtype DVD A   DS8A5SH  XA15 /dev/sr0
[2:0:0:0]    disk    FLASH    Drive  UT_USB20  0.00 /dev/sdb

上例中,➊字段是指设备在系统中的地址,➋字段是设备的描述信息,➌字段则是设备文件的路径。其余的是设备提供商的相关信息。

Linux按照设备驱动程序检测到设备的顺序来分配设备文件。在前面的例子中,内核先检测到磁盘,然后是cd/dvd,最后是闪存盘。

悲剧的是,这种方式在重新配置硬件时会导致一些问题。比如说你的系统有三块硬盘:/dev/sda、/dev/sdb和/dev/sdc,如果/dev/sdb损坏了,你必须将其移除才能使系统正常工作,然而/dev/sdc已经不存在了,之前的/dev/sdc现在成了/dev/sdb。如果你在fstab文件(见4.2.8节)中引用了/dev/sdc,你就必须更新此文件。为了解决这个问题,大部分现代的Linux系统使用通用唯一标识符(Universally Unique Identifier,以下简称UUID,见4.2.4节)来访问设备。

这里提到的内容不涉及硬盘和其他存储设备的使用细节,相关内容我们将在第4章介绍。在本章稍后我们会介绍SCSI如何支持Linux内核的运行。

3.4.2 CD和DVD:/dev/sr*

Linux系统能够将大多数光学存储设备识别为SCSI设备,如/dev/sr0、/dev/sr1等。但是如果光驱使用的是老接口的话,可能会被识别为PATA设备。/dev/sr*设备是只读的,它们只用于从光盘上读取数据。可读写光盘驱动用/dev/sg0这样的设备文件表示,g代表“generic”。

3.4.3 PATA硬盘:/dev/hd*

老版本的Linux内核常用设备文件/dev/hda、/dev/hdb、/dev/hdc和/dev/hdd来代表老的块设备。这是基于主从设备接口0和1的固定设置方式。SATA设备有时候也会被这样识别,这表示SATA设备在兼容模式中运行,会造成性能损失。你可以检查你的BIOS设置,看看能否将SATA控制器切换到它原有的模式。

3.4.4 终端设备/dev/tty/*、/dev/pts/*和/dev/tty

终端设备负责在用户进程和输入输出设备之间传送字符,通常是在终端显示屏上显示文字。终端设备接口由来已久,一直可以追溯到手动打字机时代。

伪终端设备模拟终端设备的功能,由内核为程序提供I/O接口,而不是真实的I/O设备,shell窗口就是伪终端。

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

/dev/tty代表当前进程正在使用的终端设备,虽然不是每个进程都连接到一个终端设备。

显示模式和虚拟控制台

Linux系统有两种显示模式:文本模式和X Windows系统服务器(即图形模式,通常是通过显示管理器)。通常系统是在文本模式下启动,但是很多Linux发行版通过内核参数和内置图形显示机制(如plymouth)将文本模式完全屏蔽起来,这样系统从始至终是在图形模式下启动。

Linux系统支持虚拟控制台来实现多个终端的显示,虚拟控制台可以在文本模式和图形模式下运行。在文本模式下,你可以使用ALT-Function在控制台之间进行切换,例如ALT-F1切换到/dev/tty1,ALT-F2切换到/dev/tty2等等。这些控制台通常会被getty进程占用以显示登录提示符,详见7.4节。

X server在图形模式下使用的虚拟控制台稍微有些不同,它不是从init配置中获得虚拟控制台,而是由X server来控制一个空闲的虚拟控制台,除非另外指定。例如,如果tty1和tty2上运行着getty进程,X server就会使用tty3。此外,X server将虚拟控制台设置为图形模式后,通常需要按CTRL-ALT-Function而不是ALT-Function来切换到其他虚拟控制台。

如果你想在系统启动后使用文本模式,可以按CTRL-ALT-F1。按ALT-F2、ALT-F3等返回X11会话。

如果在切换控制台的时候遇到问题,你可以尝试chvt命令强制系统切换工作台。例如:使用root运行以下命令切换到tty1:

# chvt 1

3.4.5 串行端口:/dev/ttyS*

老式的RS-232和串行端口是特殊的终端设备,串行端口设备在命令行上运用不太广,原因是需要处理诸如波特律和流控制等参数的设置。

Windows上的COM1端口在Linux中表示为/dev/ttyS0,COM2表示为/dev/ttyS1,以此类推。可插拔USB串行适配器在USB和ACM模式下分别表示为:/dev/ttyUSB0、/dev/ttyACM0、/dev/ttyUSB1、/dev/ttyACM1等。

3.4.6 并行端口:/dev/lp0和/dev/lp1

单向并行端口设备,目前被USB广泛取代的一种接口类型,表示为:/dev/lp0和/dev/lp1,分别代表Windows中的LPT1:和LPT2:。你可以使用cat命令将整个文件(比如说要打印的文件)发送到并行端口,执行完毕后你可能需要向打印机发送另外的指令(form feedreset)。像CUPS这样的打印服务相比打印机来说提供了更好的用户交互体验。双向并行端口表示为:/dev/parport0和/dev/parport1。

3.4.7 音频设备:/dev/snd/*、/dev/dsp、/dev/audio和其他

Linux系统有两组音频设备,分别是高级Linux声音架构(Advanced Linux Sound Architecture,以下简称ALSA)和开放声音系统(Open Sound System,以下简称OSS)。ALSA在/dev/snd目录下,要直接使用不太容易。如果Linux系统中加载了OSS内核支持,则ALSA可以向后兼容OSS设备。

OSS dsp和音频设备支持一些基本的操作。例如,可以将WAV文件发送给/dev/dsp来播放。然而如果频率不匹配的话,硬件有可能无法正常工作。并且在大多数系统中,音频设备在你登录时通常处于忙状态。

注解:Linux的音频处理非常复杂,因为涉及很多层细节。我们刚刚介绍的是内核级设备,通常在用户空间中还有pulsaudio这样的服务来负责处理不同来源和声音设备的音频处理。

3.4.8 创建设备文件

在现代Linux系统中,你不需要创建自己的设备文件,这项工作是由devtmpfs和udev(见3.5节)来完成。不过了解一下这个过程总是有益的,以备不时之需。

mknod命令用来创建设备。你必须知道设备名以及主要和次要编号。例如,可以使用以下命令创建设备/dev/sda1:

# mknod /dev/sda1 b 8 2

参数b 8 2分别代表块设备、主要编号8和次要编号2。字符设备使用c,命名管道使用p(主要和次要编号可忽略)。

mknod命令来创建临时的命名管道很方便,也可以用于在系统恢复的时候创建丢失的设备文件。

在老版本的Unix和Linux系统中,维护/dev目录不是一件容易的事情。内核每次更新和增加新的驱动程序,能支持的设备就更多,同时也意味着一些新的主要和次要编号被指定给设备文件。为了方便维护,系统使用/dev目录下的MAKEDEV程序来创建设备组。在系统升级的时候你可以看看有没有新版本的MAKEDEV,如果有的话可以运行它来创建新设备。

这样的静态管理系统非常不好用,所以出现了一些新的选择。首先是devfs,它是/dev在内核空间的一个实现版本,包含内核支持的所有设备。但是它的种种局限使得人们又开发了udev和devtmpfs。

3.5 udev

我们已经介绍过,内核中的一些豪无必要的复杂功能会降低系统的稳定性。设备文件管理就是一个很好的例子:如果你可以在用户空间内创建设备文件的话,就不需要在内核空间做。Linux系统内核在检测到新设备的时候(如发现一个USB存储器),会向用户空间进程发送消息(称为udevd)。用户空间进程会在另一端验证新设备的属性,创建设备文件,执行初始化。

理论上是如此,但实际上这个方法有一些问题:系统启动前期即需要设备文件,所以udevd需要在其之前启动。udevd不能依赖于任何设备来创建设备文件,它必须尽快启动以免拖延整个系统。

3.5.1 devtmpfs

devtmpfs文件系统正是为了解决上述问题而开发的(详见4.2节)。它类似老的devfs系统,但是更简单。内核根据需要创建设备文件,并且在新设备可用时通知udevdudevd在收到通知后并不创建设备文件,而是进行设备初始化以及发送消息通知。此外还在/dev目录中为设备创建符号链接文件。你可以在/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使用接口类型名称、厂商、型号、序列号以及分区(如果有的话)的组合来命名符号链接。

下一节介绍udevd是怎样创建符号链接文件的,不过你现在并不需要马上了解。实际上,如果你是第一次接触Linux设备管理,你可以直接跳到下一章去了解如何使用硬盘。

3.5.2 udevd的操作和配置

udevd守护进程是这样工作的。

1. 内核通过一个内部网络链接向udevd发送一个通知事件,称作uevent。

2. udevd加载uevent中的所有属性信息。

3. udevd通过规则解析来决定执行哪些操作和增加哪些属性信息。

呼入uevent是udevd从内核接收到的消息,如下面代码所示:

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)帮助手册。现在让我们看一下3.5.1一节中/dev/sda一例中的符号链接。这些链接是在/lib/udev/rules.d/60-persistent-storage.rules中定义的。你能够在其中找到如下内容:

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

这些规则和内核SCSI子系统呈现的ATA硬盘相匹配(参见3.6节)。你可以看到udevd尝试匹配以sd或者sr开头,但是不包含数字的设备名(通过表达式:KERNEL=="sd\*[!0-9]|sr\*")、匹配子系统(SUBSYSTEMS=="scsi")和其他一些属性。如果上述所有条件都满足,则进行下一步:

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

这不是一个条件,而是一个指令,它从/lib/udev/ata_id命令导入变量。如果你有匹配的设备,可以试着执行以下命令行:

$ sudo /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}的值对后面的规则都为disk

ID_SERIAL需要特别注意一下,在每一个规则中都有这个条件行:

ENV{ID_SERIAL}!="?*"

意思是如果ID_SERIAL变量没有被设置,条件语句返回true,反之如果变量被设置,则为false,当前规则返回falseudevd继续解析下一规则。

这是什么意思呢?这两条规则的目的是找出硬盘设备的序列号,如果ENV{ID_SERIAL}被设置,udevd就能够解析下面的规则:

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规则,触发消息。它功能强大之处在于搜寻和浏览系统设备以及监控udevd从内核接收的消息。使用udevadm需要掌握一些命令行语法。

我们首先来看看如何检验系统设备。回顾一下3.5.2节中的例子,我们使用以下命令来查看设备(如/dev/sda)的udev属性和规则:

$ 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中监控uevent可以使用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选项,只看udev发送的消息可以用--udev。查看呼入消息的所有属性(见3.5.2节)可以使用--property选项。

你还可以使用子系统来过滤消息。例如,如果只想看和SCSI有关的内核消息,可以使用下面的命令:

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

udevadm的更多内容可以参考udevadm(8)使用手册。

关于udev还有很多内容,比如处理中间通信的D-Bus系统有一个守护进程叫作udisks-daemon,它通过监听udevd的呼出消息来自动通知桌面应用系统发现了新的硬盘。

3.6 详解SCSI和Linux内核

本节我们将介绍Linux内核对SCSI的支持,借此机会了解一下Linux内核的架构。本节的内容偏理论,如果你想先了解如何使用硬盘,可以直接跳到第4章。

首先我们介绍一下SCSI的背景知识,传统的SCSI硬件设置是通过SCSI总线链接设备到主机适配器,如图3-1所示。主机适配器和设备都有一个SCSI ID,每个总线有8到16个ID(不同版本数量不同)。SCSI目标指的是设备及其SCSI ID。

图像说明文字

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

计算机并不和设备链直接连接,所以必须通过主机适配器和设备通信。主机适配器通过SCSI命令集与设备进行一对一通信,设备向其发送响应消息。

更新版本的SCSI如Serial Attached SCSI (SAS)的性能更出色,不过大部分计算机中并没有真正意义的SCSI设备。更多的是那些使用SCSI命令的USB存储设备。支持ATAPI的设备(如CD/DVD-ROM)也使用某个版本的SCSI命令集。

SATA硬盘在系统中通常由一个处于libata层(见3.6.2节)的转换机制呈现为SCSI设备。一些SATA控制器(特别是高性能RAID控制器)使用硬件来实现这个转换。

让我们用下面这个例子将上述内容整合起来:

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

方括号中的数字是SCSI主机适配器编号,SCSI总线编号,设备SCSI ID,以及LUN(逻辑元件编号,设备的字设备)。本例中有4个适配器(scsi0、scsi1、scsi2和scsi3),它们都有一个单独的总线(总线编号都是0),每个总线上有一个设备(target编号都是0)。编号为2:0:0的USB读卡器有4个逻辑单元,每个代表一个可插入闪存盘。内核为每个逻辑单元指定一个不同的设备文件。

图3-2显示内核中该部分的驱动和接口程序结构,从单个设备驱动上到块设备驱动,但是不包括SCSI通用驱动。

图像说明文字

图3-2 Linux SCSI 子系统示意图

上图看上去复杂,实际上整个结构是非常线性的。我们先从SCSI子系统和它的三层驱动开始。

  • 最顶层负责处理某一类设备。例如,sd(SCSI硬盘)驱动就在这一层,它负责将来自内核块设备接口的请求消息翻译为SCSI协议中的硬盘相关命令,反之亦然。
  • 中间层在上下层之间调控和分流SCSI消息,并且负责管理系统中的所有SCSI总线和设备。
  • 最底层负责处理硬件相关操作。该层中的驱动程序向特定的主机适配器发送SCSI协议消息,并且提取从硬件发送过来的消息。该层和最顶层分开的原因是,虽然SCSI消息对某类设备是统一的,但是不同类型的主机适配器处理同类消息的方式会不一样。

最顶层和最底层中有许多各式各样的驱动程序,但是需要注意,对每一个设备文件,内核都使用一个顶层中的驱动程序和一个底层中的驱动程序。对我们例子中的/dev/sda硬盘来说,内核使用顶层的sd和底层的ATA bridge。

有时候你可能需要使用一个以上的顶层驱动程序(见3.6.3节)。对于真正的SCSI设备,如连接到SCSI主机适配器或者硬件RAID的硬盘,底层驱动程序直接和下方的硬件通信,这与大部分SCSI子系统中的设备不同。

3.6.1 USB存储设备和SCSI

如图3-2所示,内核需要更多那样的底层SCSI驱动来支持SCSI子系统和USB存储设备硬件的通信。/dev/sdf代表的USB闪存驱动支持SCSI命令,但是不和驱动通信,所以由内核来负责和USB系统的通信。

理论上,USB和SCSI很类似,包括设备类别、总线、主机控制器。所以和SCSI类似,Linux内核也有一个三层USB子系统。最顶层是同类设备驱动,中间层是总线管理,最底层是主机控制驱动。和SCSI类似,USB子系统通过USB消息在其组件之间通信,它还有一个和lsscsi类似的命名叫lsusb

最顶层是我们介绍的重点,在这里驱动程序如同一个翻译,它对一方使用SCSI协议通信,对另一方使用USB协议,并且存储硬件在USB消息中包含了SCSI命令,所以启动程序要做的翻译工作仅仅是重新打包消息数据。

有了SCSI和USB子系统,你就能够和闪存驱动通信了。还有不要忘了SCSI子系统更底层的驱动程序,因为USB存储驱动是USB子系统的一部分,而非SCSI子系统。(出于某些原因,两个子系统不能共享驱动程序)。如果一个子系统要和其他子系统通信,需要使用一个简单的底层SCSI桥接驱动来连接USB子系统的存储驱动程序。

3.6.2 SCSI和ATA

图3-2中的SATA硬盘和光驱使用的都是SATA接口。和USB驱动一样,内核需要一个桥接驱动来将SATA驱动连接到SCSI子系统,不过使用的是另外的更复杂的方式。光驱使用ATAPI协议通信,它是使用了ATA协议编码的一种SCSI命令。然而硬盘不使用ATAPI和编码的SCSI命令。

Linux内核使用libata库来协调SATA(以及ATA)驱动和SCSI子系统。对于支持ATAPI的光驱,问题变得很简单,只需要提取和打包往来于ATA协议上的SCSI命令即可。对于硬盘就复杂得多,libata库需要一整套命令翻译机制。

光驱的作用类似于把一本英文书敲入计算机。你不需要了解书的内容,甚至不懂英文也没关系。硬盘的工作则类似把一本德文书翻译成英文并敲入计算机。所以你必须懂两种语言,以及了解书的内容。

libata能够将SCSI子系统连接到ATA/SATA接口和设备。(为了简单起见,图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 ➊/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

除了常见的块设备文件,上面的每一行还在➊字段位置显示SCSI通用设备文件。例如光驱/dev/sr0的通用设备是/dev/sg1。

那么我们为什么需要SCSI通用设备呢?原因来自内核代码的复杂度。当任务变得越来越复杂的时候,最好是将其从内核移出来。我们可以考虑下CD/DVD的读写操作,写数据操作比读复杂得多,并且没有任何关键的系统服务需要依赖于CD/DVD的写数据操作。使用用户空间进程来写数据也许比使用内核服务要慢,但是却更容易开发和维护,并且如果有bug也不会影响到内核空间。所以在向CD/DVD写数据时,进程就使用像/dev/sg1这样的通用SCSI设备。至于读取数据,虽然很简单,但我们仍然使用内核中一个特制的sr光驱驱动来完成。

3.6.4 访问设备的多种方法

图3-3展示了从用户空间访问光驱的两种方法:sr和sg(图中忽略了在SCSI更下层的驱动)。进程A使用sr驱动来读数据,进程B使用sg驱动。然而,它们之间并不是并行访问的。

图3-3 光学设备图解

图中进程A从块设备读取数据,但是通常用户进程不使用这样的方式读取数据,至少不是直接读取。在块设备之上还有很多的层和访问入口,我们将在下一章介绍。

目录

  • 版权声明
  • 前言
  • 致谢
  • 第一版书评
  • 第1章 概述
  • 第2章 基础命令和目录结构
  • 第3章 设备管理
  • 第4章 硬盘和文件系统
  • 第5章 Linux内核的启动
  • 第6章 用户空间的启动
  • 第7章 系统配置:日志、系统时间、批处理任务和用户
  • 第8章 进程与资源利用详解
  • 第9章 网络与配置
  • 第10章 网络应用与服务
  • 第11章 shell脚本
  • 第12章 在网络上传输文件
  • 第13章 用户环境
  • 第14章 Linux桌面概览
  • 第15章 开发工具
  • 第16章 从C代码编译出软件
  • 第17章 在基础上搭建