Scott's Blog

学则不固, 知则不惑

0%

Linux 是如何工作的-管理设备

关于 Linux 的设备文件系统、系统设备路径与设备名、udev, SCSI 等内容。

Linux 设备

设备文件系统(Device files)

操作系统负责帮我们管理设备,将其抽象成类似文件的形式放在磁盘上,所以当往设备中输出内容,就像我们往一个文件中写内容一样,这些设备文件存放于 /dev 中, 执行 ls /dev 可以看到这些设备。

Linux启动的时候,会动态的在/dev目录下创建好各种设备的设备文件节点(也就是说,系统启动后/dev目录下就有了各种设备的设备文件,直接就可使用了)。

除此之外,他还可以在设备卸载后自动的删除/dev下对应的设备文件节点(这对于一些热插拔设备很有用,插上的时候自动创建,拔掉的时候又自动删除)。

在我们编写设备驱动的时候,不必再去为设备指定主设备号,在设备注册时用0来动态的获取可用的主设备号,然后在驱动中来实现创建和销毁设备文件(一般在驱动模块加载和卸载函数中来实现)。

1
2
3
4
5
$ 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 分别代表 block, character, pipe, and socket。

block 设备:Programs access data from a block device in fixed chunks,block device’s total size is fixed and easy to index, processes have quick random access to any block in the device with the help of the kernel.

character 设备:work with data streams,only character, don’t have a size. Like printer; kernel cannot back up and reexamine the data stream after it has passed data to a device or process.

Pipe device: like character device, work with IO process instead of kernel.

Socket device: special-purpose interfaces that are frequently used for interprocess communication. They’re often found outside of the /dev directory.

系统设备路径

/dev 下面的名字所能告诉你的信息很有限,而且内核根据设备被发现的顺序来给设备命名,所以一旦计算机重启,这些设备的名字有可能会不一样。

Linux 根据设备的属性,提供了一个统一的视图来访问所有的设备,即: /sys/devices

/dev 下的文件让用户进程可以与设备交互。/sys/devices 用来查看和管理设备。

sys 目录下还有一些分类,比如 /sys/block 下面都是块设备。

在 /dev 目录下,找到设备的路径上有点不容易,不过可以通过 udevadm 命令查看,我们将会在之后介绍更多关于这个命令的知识。

dd 和 设备

dd 程序在与块设备与字符设备时非常方便,但它也是一个非常强大的命令,需要特别小心的使用。

1
2
# 从块数据中复制固定大小的数据
dd if=/dev/zero of=new_file bs=1024 count=1

参数解释:

  • if, 输入文件,默认是标准输入
  • of, 输出文件,默认是标准输出
  • bs, 块大小,可以使用 b 或者 k
  • count=num,块的个数,当使用大文件或者文件流,需要用 count 或下面的 skip 来控制大小
  • skip=num, 另外一个控制大小的选项

设备名

找到设备名

找到设备名有时候很麻烦,这里提供了集中方式:

  1. 使用 udevadm
  2. 检查 /sys 目录
  3. 通过 journalctl -k 命令打印系统内核的消息、日志,猜测设备名
  4. 对于存储设备,可以通过 mount 命令查看
  5. 通过 cat /proc/devices 命令查看块设备和字符设备的驱动。

其中只有第一种方式是最可靠的,建议主要还是用第一种方式,如果你的系统中实在是没有 udevadm 可以使用,可以再参考其他的方式。

下面的内容会例举出 Linux 中最常见的一些设备,以及它们的命名。

存储设备 /dev/sd*

存储设备的命名一般以 sd 开头,即 SCSI disk 的简写, SCSI 的意思是:Small Computer System Interface。SCSI 是最早被开发用来在存储设备和外围设备之间进行通讯。

lsscsi 命令可以用来检查设备中的存储设备,它的输出如下:

其中 1 表示设备在系统中的位置;2 表示设备描述; 3 表示去哪里找到这个设备;

虚拟存储 /dev/xvd*, /dev/vd*

有的磁盘设备是为了虚拟机优化而存在的,例如 AWS 实例,Virtual-Box.

Non-Volatile Memory Devices: /dev/nvme*

有一些系统现在使用 Non-Volatile memory,即 NVMe 接口与某些类型的固态存储通信。

可以用 nvme list 命令列出这些设备。

Device Mapper: /dev/dm-*, /dev/mapper/*

Device Mapper 并不是一个文件系统(File System),而是 Linux 内核映射块设备的一种技术框架。提供的一种从逻辑设备(虚拟设备)到物理设备的映射框架机制,在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略。

使用了这种映射的名字一般以 /dev/dm- 或者 /dev/mapper 命名。

CD 和 DVD: /dev/sr*

一般命名为 /dev/sr0, /dev/sr1,只读。如果是可以写的,可能会命名为:/dev/sg0.

PATA Hard Disks: /dev/hd*

Older type of storage bus. /dev/hda, /dev/hdb, /dev/hdc, and /dev/hdd , 如果你发现你的 SATA 驱动被识别成上面的格式,则代表它工作在钱荣模式下,这会影响性能,你可以查看 BIOS 设置是否正确。

终端:/dev/tty*, /dev/pts/*, and /dev/tty

终端是一种设备,它在用户进程和 I/O 设备间传输字符。而且大部分终端属于伪设备,它不是一个真的硬件。

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

显示模式和虚拟终端:

Linux 主要有两种显示模式,文本和图形。

Linux 系统传统上是在文本模式下启动的,现在分发的大部分的系统则会隐藏文本模式。

这里的控制台或终端都是计算机产生早期的遗留下来的概念。为了充分使用计算机提供的计算资源,早期很多计算机会连接若干终端控制台,这些终端控制台从硬件上构造很简单,只包括键盘和显示器,不执行计算的任务,只简单的把用户的输入发送到主计算机去处理,然后再把计算结果返回给用户。

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

串行接口(Serial port),主要用于串行式逐位数据传输。按电气标准及协议来分包括RS-232-C、RS-422、RS485、USB等。 RS-232-C、RS-422与RS-485标准只对接口的电气特性做出规定,不涉及接外挂程式、电缆或协议。USB是近几年发展起来的新型接口标准,主要应用于高速数据传输领域。

可以使用 screen 来连接 usb 串口设备。

参考命令行界面、终端、Shell、TTY 的区别

并行端口:/dev/lp0 and /dev/lp1

计算机上数据以并行方式传递的端口,也就是说至少应该有两条连接线用于传递数据。 与只使用一根线传递数据(这里没有包括用于接地、控制等的连接线)的串行端口相比,并口在相同的数据传送速率下,并口可以更快地传输数据。所以在21世纪之前,在需要较大传输速度的地方,例如打印机,并口得到广泛使用。但是随着速度迅速提高,并且上导线之间数据同步成为一个很难处理的难题,导致并口在速度竞赛中逐渐被淘汰。USB等改进的串口逐渐代替了并口。

音频设备:/dev/snd/*, /dev/dsp, /dev/audio, and More

Linux 有两种音频设备,ALSA 和 OSS。分别代表了 Advanced Linux Sound Architecture 和 Open Sound System.

The ALSA devices are in the /dev/snd directory, but it’s difficult to work with them directly. Linux systems that use ALSA support OSS backward-compatible devices if the OSS kernel support is currently loaded.

创建设备文件

在绝大多数情况下,你不需要自己创建设备文件,它们会被 devtmpfs 和 udev 自动创建。但我们至少可以学习一下如何做。

mknod 命令可以用来创建一个设备。你必须知道设备的名字,以及它的主要、次要版本号,比如:

1
2
# b 8 1, 指定了 block 设备,major 版本为8,minor 版本为1
mknod /dev/dda1 b 8 1

对于linux 来说管理如此多的设备以及版本是非常复杂的,特别是当linux系统在升级的时候,后来出现了 devfs,一个内核空间中的 /dev 的实现,它包括了所有现在内核支持的设备,可以指定设备号、所有者、用户空间等信息,devfs 运行在内核环境中,并有不少缺点:可能出现主/辅设备号不够,命名不灵活,不能指定设备名称等问题。

所以后人又开发了 udev 和 devtmpfs.

udev

关于 udev 的相关介绍,可以参考这篇文章: Linux 文件系统与设备文件系统 (一)—— udev 设备文件系统

devtmpfs

The devtmpfs filesystem was developed in response to the problem of device availability during boot.

udevd 操作和配置

在 GNU/Linux 系统中,虽然设备的底层支持是在内核层面处理的,但是,它们相关的事件管理是在用户空间中通过 udev 来管理的。确切地说是由 udevd 守护进程来完成的。

Udevd 守护进程所作的操作如下:

  1. 内核通过内部的网络连接向 udevd 发送一个名为 uevent 的通知事件
  2. udevd 从 uevent 中加载所有的属性
  3. udevd 从 rule 中提取、过滤或者是更新相关内容

可以使用 udevadm monitor --property 得到一个 uevent 的内容:

1
2
3
4
5
6
7
8
9
10
11
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

udev 规则是定义在一个以 .rules 为扩展名的文件中。那些文件主要放在两个位置:/usr/lib/udev/rules.d,这个目录用于存放系统安装的规则;/etc/udev/rules.d/ 这个目录是保留给自定义规则的。

定义那些规则的文件的命名惯例是使用一个数字作为前缀(比如,50-udev-default.rules),并且以它们在目录中的词汇顺序进行处理的。在 /etc/udev/rules.d 中安装的文件,会覆盖安装在系统默认路径中的同名文件。

下面是 udevd 规则的行为逻辑:

  1. udevd reads rules from start to finish of a rules file.
  2. After reading a rule and possibly executing its action, udevd continues reading the current rules file for more applicable rules.
  3. There are directives (such as GOTO) to skip over parts of rules files if necessary. These are usually placed at the top of a rules file to skip over the entire file if it’s irrelevant to a particular device that udevd is configuring.

我们来看一下一个 /dev/sda 下面的符号链接,它被定义在 lib/udev/rules.d/60-persistent-storage.rules 当中,这个文件中有几行:

1
2
3
4
5
# 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 子系统,这些规则会与 AKA 硬盘以及 optical media 设备匹配,一般是通过文本的模式来匹配,比如 "sd*[!0-9]|sr*".

ATA 是一个广为使用的 IDE 和 EIDE设备相关的标准,意思是高级技术附件规格。ATA的硬盘分为 PATA 和 SATA,其中 P 代表的是并行、S代表的串行。目前 SATA 使用广泛,它的速度比 PATA 快太多,而且体积小、散热也更好。

更多关于编写 udev 的规则,可参考这篇文章:> 在 Linux 中如何编写基本的 udev 规则

udev管理工具:udevadm

Udevadm 程序是 udev 的一个管理工具。你可以重新加载 udev 的规则,触发事件,最重要的一个特性是,你可以查找和探索系统的设备,或者是监控内核发出的 uevents 事件。

我们首先来看看怎么使用 udevadm 检查系统设备。

1
udevadm info --query=all --name=/dev/sda 

下面是这条命令在我电脑上的输出:

1
2
3
4
5
6
7
8
9
10
root@scott-pc:/mnt/c/Users/Scott# udevadm info --query=all --name=/dev/sda
P: /devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0004:00/VMBUS:00/fd1d2cbd-ce7c-535c-966b-eb5f811c95f0/host0/target0:0:0/0:0:0:0/block/sda
N: sda
L: 0
E: DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0004:00/VMBUS:00/fd1d2cbd-ce7c-535c-966b-eb5f811c95f0/host0/target0:0:0/0:0:0:0/block/sda
E: DEVNAME=/dev/sda
E: DEVTYPE=disk
E: MAJOR=8
E: MINOR=0
E: SUBSYSTEM=block

其中每一行代表了一个设备的属性,在上面的例子中:

  • P: 最开始的那行,是 sysfs 设备路径
  • N: 设备节点,也就是给 /dev 下面文件的名字
  • S: 表明了设备节点的符号链接
  • E: 从 udevd 规则中找到的附加的信息

使用 udevadm 监控设备

你可以使用 udevadm monitor 来监控电脑上的设备,下面是它的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
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--

这里有重复的输出,因为默认会输出来自内核的消息,以及 udevd 处理的消息,可以指定 --kernel 选项只查看内核的消息,--udev 查看 udevd处理的消息,以及--property 则可以让你查看整个 uevent, 包括属性。

你也可以通过 subsystem 来 filter 这些消息,比如你只想要看到 SCSI 子系统的 kernel 的消息,可执行下面的指令:

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

关于更多 Udevadm 的使用,可以查看 udevadm(8) 的操作手册。udev 本身还有很多其他的知识,例如有一个守护进程叫做 udisksd, 它会监听事件,当有新的磁盘接入的时候,会自动向进程发送消息。

深入理解:SCSI 和 Linux 内核

这一小节我们会探讨 Linux 内核对于 SCSI 的支持,以增加对 Linux 内核架构的理解。如果是为了学习如何使用磁盘,你无需学习本小节中的内容。

我们先讲一个小背景,传统的SCSi硬件配置是一个主机适配器通过 SCSI总线连接了一系列的设备,如下图所示:

其中的电脑通过 SCSI Host Adapter 来与其他的 Disk 通信,每一个 Disk 都有唯一的 ID,对于每一个单独的 Disk,他们之间可以通过 SCSI 命令进行点对点的通信。

新版本的 SCSI,也就是 Serial Attached SCSI (SAS)提供了更好的性能,但在大部分机器上,也许找不到任何真正意义上的 SCSI 设备。

SATA 磁盘在你的系统上,也已 SCSI 设备的形式出现,但是它们还是有一点不同的,因为它们中大部分都是通过 libata 库 中的翻译层通信。

这些说明了什么呢?假设我们查看系统上的设备,输出如下:

1
2
3
4
5
6
7
8
$ 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(Logical Unit Number)逻辑快号。

上面的例子中,可以看到有四个适配器(scsi0-3),每一个都有一个总线(号码都从0开始),每一个总线上,也只有一个设备。USB 读卡器在 2:0:0 上有四个逻辑单元,通过LUN 编号可以很容易看出这一点,即内核给每个逻辑单元都分配了一个 device file.

Figure 3-2 是内核中的驱动与接口的结构:

Figure 3-2
  • 图中上面的层负责处理设备集的操作,比如 sd(SCSI Disk) 在这层;它知道如何翻译来自内核块设备接口的命令,将它变成磁盘特定的、SCSI 协议的指令
  • 中间层,连接顶层和底层,传输 SCSI 消息,track 所有的SCSI总线,以及插入到系统上的设备
  • 执行特定的硬件操作。

The top and bottom layers contain many different drivers, but it’s important to remember that, for any given device file on your system, the kernel (nearly always) uses one top-layer driver and one lower-layer driver. For the disk at /dev/sda in our example, the kernel uses the sd top-layer driver and the ATA bridge lower-layer driver.

USB 存储 and SCSI

为了使 SCSI 子系统与常见的 USB 存储硬件通信,如图 3-2 所示,内核需要的不仅仅是一个较低层的 SCSI 驱动程序。 以 /dev/sdf 为代表的 USB 闪存驱动器可以理解 SCSI 命令,但要真正与驱动器通信,内核需要知道如何通过 USB 系统进行通信。

从抽象结构上来看,USB 和 SCSI 很像,它们都有 device classes, buses, host controller。 所以 Linux 在内核中一个 USB Subsystem.

就像 SCSI 子系统在其组件之间传递 SCSI 命令一样,USB 子系统在其组件之间传递 USB 消息。 甚至还有一个类似于 lsscsi 的 lsusb 命令。

这里有趣的在于,USB 子系统在一头使用的是 USB 命令,而另外一头则使用的是 SCSI 命令。

在结构上,USB 子系统和 SCSI 子系统是分开的,因为两个子系统不应该共享同一个驱动,为了让它们可以通信,在 SCSI 子系统中,还有一个 lower-layer 的 SCSI bridge.

SCSI 和 ATA

SATA 硬盘和其他的CD/DVD设备都使用了 SATA 接口。为了连接内核中 SATA 专用的驱动与 SCSI 子系统,内核利用了一个 bridge driver, 内核使用了一个桥接驱动程序,就像 USB 驱动器一样,但具有不同的机制和额外的复杂性。 光驱使用 ATAPI,一种以 ATA 协议编码的 SCSI 命令版本。 但是,硬盘不使用 ATAPI,也不编码任何 SCSI 命令!

Linux 内核使用名为 libata 的库的一部分来协调 SATA(和 ATA)驱动器与 SCSI 子系统。

通用 SCSI 设备

当用户空间进程与 SCSI 子系统通信时,它通常通过块设备层和/或位于 SCSI 设备类驱动程序(如 sd 或 sr)之上的另一个其他内核服务来进行。 换句话说,大多数用户进程从不需要知道关于 SCSI 设备或其命令的任何信息。

但是,用户进程可以绕过设备类驱动程序,直接向设备提供 SCSI 协议命令。 例如可以在 lsscsi 命令后添加 -g 选项:

1
2
3
4
5
6
7
8
$ 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

为什么要使用通用设备? 答案与内核中代码的复杂性有关。 随着任务变得越来越复杂,最好将它们排除在内核之外。 考虑 CD/DVD 写入和读取。 读取光盘是一个相当简单的操作,并且有一个专门的内核驱动程序。

然而,写入光盘比读取要困难得多,并且没有关键的系统服务依赖于写入的动作。 没有理由用这个活动来增加内核空间。 因此,要在 Linux 中写入光盘,您需要运行一个与通用 SCSI 设备(例如 /dev/sg1)对话的用户空间程序。 这个程序可能比内核驱动程序效率低一点,但它更容易构建和维护。

单个设备的多种访问方法

图 3-3 显示了 Linux SCSI 子系统从用户空间访问光驱的两个方法(sr 和 sg)(省略了 SCSI 底层以下的任何驱动程序)。

进程 A 使用 sr 驱动程序从驱动器读取,进程 B 使用 sg 驱动程序写入驱动器。 但是,像这样的进程通常不会同时运行以访问同一设备。

3-3

在图 3-3 中,进程 A 从块设备中读取数据。 但是用户进程真的以这种方式读取数据吗? 通常,答案是否定的。这种访问不是直接的,因为在块设备之上还有更多的层,甚至更多的硬盘访问点,我们将在下一章中学习。