精通Linux(第2版) 第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
命令一次读取或者写入数据的大小。对于海量数据,你可以在数字后设置b
和k
来分别代表512字节和1024字节。如:bs=1k
和bs=1024
一样。ibs=size,obs=size
:代表输入和输出块大小。如果输入输出块大小相同,你可以使用bs
选项,如果不相同的话,可以使用ibs
和obs
分别指定。count=num
:代表复制块的总数。在处理大文件或者无限数据流(/dev/zero)的时候,你可能会需要在某个地方停止dd
复制,不然的话将会消耗大量硬盘空间和CPU时间。这时你可以使用count
和skip
选项从大文件或设备中复制一小部分数据。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 feed
或reset
)。像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系统,但是更简单。内核根据需要创建设备文件,并且在新设备可用时通知udevd
。udevd
在收到通知后并不创建设备文件,而是进行设备初始化以及发送消息通知。此外还在/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
,当前规则返回false
,udevd
继续解析下一规则。
这是什么意思呢?这两条规则的目的是找出硬盘设备的序列号,如果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