跳到主要内容

Linux 设备节点

概述

A device node, device file, or device special file is a type of special file used on many Unix-like operating systems, including Linux.

设备节点、设备文件或设备专用文件是一种在许多类 Unix 操作系统(包括 Linux)上使用的专用文件。

Device nodes facilitate transparent communication between user space applications and computer hardware.

设备节点促进用户空间应用程序和计算机硬件之间的透明通信。

By definition, device nodes correspond to resources that have already been allocated by the operating system Kernel. The resources are identified by a major number and a minor number, which are stored as part of the structure of a node. The assignment of these numbers is specific to different operating systems and computer platforms. Generally, the major number identifies the device driver and the minor number identifies a particular device (possibly out of many) that the driver controls, and is passed to the driver as an argument.

根据定义,设备节点对应于操作系统内核已经分配的资源。 资源由主要编号和次要编号标识,它们作为节点结构的一部分存储。 这些编号的分配特定于不同的操作系统和计算机平台。 通常,主要编号标识设备驱动程序,次要编号标识驱动程序控制的特定设备(可能是许多),并作为参数传递给驱动程序。

Like other special file types, device nodes are accessed using standard system calls and treated like regular files.

与其他特殊文件类型一样,设备节点使用标准系统调用进行访问,并被视为常规文件。


设备节点

Linux 下的设备通常分为三类:字符设备、块设备和网络设备。相对应的,驱动程序也分为三类,即字符设备驱动程序、块设备驱动程序和网络设备驱动程序。

  • 常见的字符设备有鼠标、键盘、串口、控制台等,它们的特点是只能顺序访问。
  • 常见的块设备有各种磁盘、硬盘、Flash、RAM 等,它们的特点是可以随机访问。
  • Linux 中的网络设备是比较特殊的,一个网络设备通常也称为一个网络接口(如 eth0),应用程序并非通过设备节点而是通过 socket 来访问网络设备。

其实在 Linux 系统中根本就不存在网络设备节点(通过 cat /proc/devices 查看只有 Character devices 和 Block devices)。网络接口没有像字符设备和块设备一样的设备号,只有一个唯一的名字,如 eth0、eth1 等,而这个名字也不需要与设备文件节点对应。

设备节点被创建在 /dev 下,是连接内核与用户层的枢纽,就是设备是接到对应哪种接口的哪个 ID 上。 相当于硬盘的 inode一样的东西,记录了硬件设备的位置和信息。在Linux中,所有设备都以文件的形式存放在 /dev 目录下,都是通过文件的方式进行访问,设备节点是 Linux 内核对设备的抽象,一个设备节点就是一个文件。应用程序通过一组标准化的调用执行访问设备,这些调用独立于任何特定的驱动程序。而驱动程序负责将这些标准调用映射到实际硬件的特有操作。

在 /dev 目录下除了字符设备和块设备节点之外,还通常还会存在 FIFO 管道、Socket、软/硬连接、目录等,不过这些东西没有主/次设备号。

主设备号和次设备号

在 Linux 系统中,对字符设备的访问是通过文件系统内的设备名称进行的,设备文件位于 /dev 目录,例如 /dev/device0。

当执行 ls -l 命令查看设备文件时,可以看到字符设备文件用字符 'c' 标识,块设备文件用字符 'b' 标识。对于设备文件,还会显示出相应设备的主设备号和次设备号。

例如,当前系统有四个 /dev/ttyUSB* 字符设备,主设备号为 188,次设备号依次为 0 至 3。

$ ls -l /dev/ttyUSB*
crw-rw---- 1 root dialout 188, 0 623 10:00 /dev/ttyUSB0
crw-rw---- 1 root dialout 188, 1 623 09:26 /dev/ttyUSB1
crw-rw---- 1 root dialout 188, 2 623 10:59 /dev/ttyUSB2
crw-rw---- 1 root dialout 188, 3 623 09:26 /dev/ttyUSB3

对于块设备,同样有主设备号和次设备号。

$ ls -l /dev/sda*
brw-rw---- 1 root disk 8, 0 622 08:51 /dev/sda
brw-rw---- 1 root disk 8, 1 622 08:51 /dev/sda1
brw-rw---- 1 root disk 8, 2 622 08:51 /dev/sda2

通常,主设备号标识设备对应的驱动程序。现在的 Linux 内核允许多个驱动程序共享主设备号,但大多数设备仍遵守「一个主设备号对应一个驱动程序」的原则。次设备号则由 Linux 内核使用,用于确定设备文件所指的设备。

设备节点,驱动,硬件设备是如何关联到一起的呢?

这是通过设备号实现的,包括主设备号和次设备号。当我们创建一个设备节点时需要指定主设备号和次设备号。应用程序通过名称访问设备,而设备号指定了对应的驱动程序和对应的设备。主设备号标识设备对应的驱动程序,次设备号由内核使用,用于确定设备节点所指设备。

  • 主设备号(major number):驱动程序在初始化时,会注册它的驱动及对应主设备号到系统中,这样当应用程序访问设备节点时,系统就知道它所访问的驱动程序了。你可以通过 /proc/devices 文件来查看系统设备的主设备号。
  • 次设备号(minor number):驱动程序遍历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。这样当应用程序访问设备节点时驱动程序就可以根据次设备号知道它说访问的设备了。
  • 设备节点(设备文件):Linux中设备节点是通过“mknod”命令来创建的。一个设备节点其实就是一个文件,Linux 中称为设备文件。有一点必要说明的是,在 Linux 中,所有的设备访问都是通过文件的方式,一般的数据文件称为普通文件,设备节点则称为设备文件。
  • 设备驱动(device driver):也称为设备驱动程序,或简称为驱动(driver),是一个允许高级(High level)计算机软件(computer software)与硬件(hardware)交互的程序,这种程序建立了一个硬件与硬件,或硬件与软件沟通的接口,经由主板上的总线(bus)或其它沟通子系统(subsystem)与硬件形成连接的机制,这样的机制使得硬件设备(device)上的数据交换成为可能。想象平时我们说的写驱动,例如点 led 灯的驱动,就是简单的 io 操作。

设备名称、设备节点和主次设备号

The Linux® kernel represents character and block devices as pairs of numbers <major>:<minor>.

Some major numbers are reserved for particular device drivers. Other device nodes are dynamically assigned to a device driver when Linux boots. For example, major number 94 is always the major number for DASD devices while the device driver for channel-attached tape devices has no fixed major number. A major number can also be shared by multiple device drivers. See /proc/devices to find out how major numbers are assigned on a running Linux instance.

The device driver uses the minor number <minor> to distinguish individual physical or logical devices. For example, the DASD device driver assigns four minor numbers to each DASD: one to the DASD as a whole and the other three for up to three partitions.

Device drivers assign device names to their devices, according to a device driver-specific naming scheme. Each device name is associated with a minor number.

User space programs access character and block devices through device nodes also referred to as device special files. When a device node is created, it is associated with a major and minor number.

SUSE Linux Enterprise Server 15 SP3 uses udev to create device nodes for you. Standard device nodes match the device name that is used by the kernel, but different or additional nodes might be created by special udev rules. See SUSE Linux Enterprise Server 15 SP3 Administration Guide and the udev man page for more details.

设备编号的内部表达

设备编码

在 Linux 内核中,dev_t 类型用来保存设备编号(包括主设备号和次设备号),其定义包含在头文件 <linux/types.h> 中。dev_t 是一个 32 位的无符号整型数,其中的 12 位用来表示主设备号,其余 20 位用来表示次设备号。

typedef u32 __kernel_dev_t;

typedef __kernel_dev_t dev_t;

从 dev_t 设备编号中获取主设备号和次设备号可以使用 <linux/kdev_t.h> 中定义的宏 MAJORMINOR,反过来可以使用宏 MKDEV 将主次设备号转换成 dev_t 类型数值。

#define MINORBITS       20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

静态申请设备编号

在建立一个字符设备之前,驱动程序需要获得一个或多个设备编号。如果提前确定所需要的设备编号,可以通过 register_chrdev_region() 函数注册设备编号:

#include <linux/fs.h>

int register_chrdev_region(dev_t first, unsigned int count, char *name);

函数说明:

参数描述
first要分配的设备编号范围的起始值
count所请求的连续设备编号的个数
name和该编号范围关联的设备名称,将出现在 /proc/devices 和 sysfs 中
返回值
0分配成功时返回 0
错误码错误时返回一个负的错误码,并且不能使用所请求的编号区域

动态申请设备编号

如果不知道设备将要使用的设备编号,可以通过 alloc_chrdev_region() 函数注册设备编号:

#include <linux/fs.h>

int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);

函数说明:

参数描述
dev用于输出参数,函数执行成功时,将保存已分配范围的第一个设备编号
firstminor要使用的被请求的第一个次设备号,通常是 0
count所请求的连续设备编号的个数
name和该编号范围关联的设备名称,将出现在 /proc/devices 和 sysfs 中

释放设备编号

在不使用申请的设备编号时,要释放这些设备编号,通常在模块的清除函数中释放申请的设备编号。可以通过 unregister_chrdev_region() 函数释放设备编号:

#include <linux/fs.h>

void unregister_chrdev_region(dev_t first, unsigned int count);

通过以上方法,为驱动程序的使用分配设备编号,但是在用户空间程序访问设备编号,驱动程序需要将设备编号和内部函数连接起来,而这些内部函数用来实现设备的操作。

动态分配主设备号

部分主设备号已经静态地分配给了常用设备,在内核的 Documentation/devices.txt 文件中可以查到设备清单,驱动程序申请设备编号可以由以下两个选择:

  1. 简单选定一个尚未被使用的主设备号;此方法适用于只有我们自己使用驱动程序的情况;
  2. 通过动态方式分配主设备号;此方法适用于驱动程序被广泛使用时的情况;此时选定的主设备号可能造成冲突和麻烦;

对于一个新的驱动程序,建议不要随便选择一个当前未使用的设备号作为主设备号,应该使用动态分配机制获取主设备号。驱动程序应始终使用 alloc_chrdev_region() 函数,而不是 register_chrdev_region() 函数。

动态分配主设备号的缺点:由于分配的主设备号不能保证始终一致,无法预先创建对应的设备节点。一旦分配了设备号,就可以从 /proc/devices 中读取到。

为了能够加载使用动态主设备号的设备驱动程序,可以使用脚本代替 insmod 命令加载程序,该脚本在调用 insmod 之后,读取到 /proc/devices 以获取新分配的主设备号,然后创建对应的设备文件。

反复创建和删除/dev节点有些不必要,如果只是装载和卸载单个驱动程序,则可在第一次创建设备文件之后仅使用 rmmod 和 insmod 两个命令;因为动态设备号并不是随机生成的,如果不受其他动态模块影响,可以获取到相同的动态主设备号;但是这个技巧不适用于同时有多个驱动程序存在的场合。

分配主设备号的最佳方式是:默认采用动态分配,同时保留在加载甚至是编译时指定主设备号的余地。scull 驱动程序的实现方式:使用一个全局变量 scull_major 保存所选择的设备号,一个全局变量 scull_minor 保存次设备号。scull_major 默认值为 SCULL_MAJOR,该宏定义在 scull.h 中,SCULL_MAJOR 默认值为 0,即采用动态分配设备号。这样既可以在编译前修改 SCULL_MAJOR 宏定义,也可以通过 insmod 命令行指定 scull_major 的值。

在 scull 驱动中获取主设备号的代码,如下所示:

if (scull_major) {
dev = MKDEV(scull_major, scull_minor);
result = register_chrdev_region(dev, scull_nr, "scull");
} else {
result = alloc_chrdev_region(&dev, scull_minor, scull_nr, "scull");
scull_major = MAJOR(dev);
}

if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
return result;
}

linux的设备节点

在linux的驱动学习过程中,经常会碰到设备节点这一概念,什么主设备号,次设备号,可能都是一知半解的,只知道要想用户进程与内核下的硬件进行通信需要建立一个设备节点。至于这个设备节点到底是怎样的一个存在,也许好多人并不清楚。

设备节点的作用

设备节点使得用户可以与内核进行硬件的沟通,读写设备以及其他的操作,在linux里面设备就像是普通文件一样的存在,访问一个设备就好像是访问一个文件一样。主设备号代表着一类设备,次设备号代表着同一类设备的不同个体,说到这里也许并不知道设备节点的存在形式

设备节点的存在形式

另外在linux里面还有一个概念,就是inode与block,也就是硬盘一面的块与节点,硬盘里面的inode就相当于一个文件或者文件夹,它记录下此文件下面的文件位置所在,文件的位置是以block大小对齐的,例如有些系统就是4K的大小,而inode的大小是有限的,所以就有了单个文件不能超过4G的说法。而在linux的驱动程序里面的节点在我个人的理解也可以看做是一个类似于硬盘的inode一样的东西,里面可以记录硬件设备的位置以及别的一些信息,在用户需要进行访问的时候就参照到设备节点所记录的信息进行设备的访问