Linux 设备和分类

868 阅读8分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情 


一、Linux 设备和分类

Linux 系统中的设备可以分为字符设备、块设备和网络设备这 3 类。

  • 字符设备:字符设备是能够像字节流一样被访问的设备,当对字符设备发出读写请求,相应的 I/O 操作立即发生。Linux 系统中很多设备都是字符设备,如字符终端、串口、键盘、鼠标等。在嵌入式 Linux 开发中,接触最多的就是字符设备以及驱动。
  • 块设备:块设备是 Linux 系统中进行 I/O 操作时必须以块为单位进行访问的设备,块设备能够安装文件系统。块设备驱动会利用一块系统内存作为缓冲区,因此对块设备发出读写访问,并不一定立即产生硬件 I/O 操作。Linux 系统中常见的块设备有如硬盘、软驱等等。
  • 网络设备:网络设备既可以是网卡这样的硬件设备,也可以是一个纯软件设备如回环设备。网络设备由 Linux 的网络子系统驱动,负责数据包的发送和接收,而不是面向流设备,因此在Linux系统文件系统中网络设备没有节点。对网络设备的访问是通过socket调用产生,而不是普通的文件操作如 open/closeread/write 等。

二、设备节点和设备号

2.1、设备节点

设备(包括硬件设备)在 Linux 系统下,表现为设备节点,也称设备文件。设备文件是一种特殊的文件,它们存储在文件系统中(通常在/dev 目录下),但它们仅占用文件目录项而不涉及存储数据。事实上,它们仅仅记录了其所属的设备类别、主设备号和从设备号等设备相关信息。

来看两个典型的设备文件的详细信息:

Linux:~$ ls -l /dev/ttyS0 /dev/sda1
brw-rw---- 1 root disk 8, 1 2011-01-07 17:48 /dev/sda1
crw-rw---- 1 root dialout 4, 64 2011-01-07 17:48 /dev/ttyS0

/dev/ttyS0 的信息为例,对其中几项进行说明 在这里插入图片描述

/dev/ttyS0 是设备节点名称,c 表示该设备是字符设备,主设备号为 4,从设备号为 64,设备节点对应于系统的串口 0。 设备分为字符设备、块设备和网络设备,而网络设备没有设备节点,所以设备文件基本上就分为字符设备文件和块设备文件两类,在设备节点属性中,分别以 cb 来表示,即 c表示字符设备节点文件,b 表示块设备节点文件。 当程序打开一个设备文件时,内核就可以获取对应设备的设备类型、主设备号和次设备号等信息,内核也就知道了程序需要操作使用哪个设备驱动程序。在程序随后对这个文件的操作都会调用相应的驱动程序的函数,同时把从设备号传递给驱动程序。

2.2、设备编号

设备编号由主设备号和从设备号构成。在 Linux 内核中,使用 dev_t 类型来保存设备编号。在 Linux 内核中,dev_t 是一个 32 位数,高 12 位是主设备号,低 20 位是次设备号。 主设备号标识设备对应的驱动程序,告诉 Linux 内核使用哪个驱动程序驱动该设备。如果多个设备使用同一个驱动程序,则它们拥有相同的主设备号。例如/dev/ttyS0~34 个设备,拥有相同的主设备号 4,说明它们使用同一份驱动。

主设备号由系统来维护,尽管 Linux 可以容纳大量的设备,但是在使用主设备号的时候,注意一定不要使用系统已经使用的主设备号。一般来说,231~239 这几个设备号是系统没有分配的,用户可以自行安排使用。当前运行系统占用了哪些主设备号,可通过查看/proc/devices 文件得到。

从设备号也称次设备号,用于确定该设备文件所指定的设备。如果一个设备驱动可以驱动一组相似的设备,此时就需要依赖于次设备号对这些外设进行区分。 获取一个设备的设备编号,应当使用<linux/kdev_t.h>中定义的宏,而不应当对设备号的位数和表述结构做任何假设,因为这样会导致不兼容以前的内核,或者未来版本设备号结构和表述方式发生变化。例如获取一个设备 dev 的主次设备号,可用:

MAJOR(dev_t dev);
MINOR(dev_t dev);

如果已知一个设备的主次设备号,要转换成 dev_t 类型的设备编号,则应当使用:

MKDEV(int major, int minor);

2.3、获取和释放设备编号

在建立一个设备节点之前,驱动程序首先应当为这个设备获得一个可用的设备号,注销设备需要释放所占用的设备号。设备号的生命周期是从设备注册到设备注销,在此期间,所占用的设备号不能被其它驱动使用。Linux 内核支持静态获取和动态获取设备号,下面以字符设备为例讲述设备号的获取与释放。

(1)静态获取主设备号 静态设备号的方式适用于下列情况: 1) 该驱动只在特定系统运行,且系统设备号使用情况明确; 2) 系统应用所要求;如为了快速启动等。

如果要从系统获得几个或者几个既定的主设备号,可用 register_chrdev_region 函数来获取。该函数在<linux/fs.h>中声明,函数定义如下:

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

这个函数可以向系统注册 1 个或者多个主设备号,first 是起始编号,count 是主设备号的数量,name 则是设备名称。注册成功返回 0,否则返回错误码。

(2)动态获取主设备号 如果事先不知道设备的设备号,或者一个驱动可能在多个系统上运行,为了避免出现设备号冲突,必须采用动态设备号。调用 alloc_chrdev_region 函数可以从系统获得一个或者多个主设备号。alloc_chrdev_region 函数在<linux/fs.h>中定义:

alloc_chrdev_region 函数可以从系统动态获得一个或者多个主设备号。dev 用于保存已经获得的编号范围的第一个值,firstminor 是第一个次设备号,通常是 0count 是获得的编号数量,name 是设备名称。 动态获取得到的设备号,一定要用一个全局变量保存下来,以便卸载使用,否则该设备号将不能被释放。

alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name);
 ret = alloc_chrdev_region(&devno, minor, 1, "char_cdev"); /* 从系统获取主设备号 */
 major = MAJOR(devno); /* 保存获得的主设备号 */
 if (ret < 0) {
	printk(KERN_ERR "cannot get major %d \n", major);
 return -1;
}

一个设备号一旦被系统分配,就会出现在/proc/devices 文件中。为了使用方便,除非特殊情况,请尽量采用动态分配设备号。

(3)释放设备号

在设备注销的时候必须释放占用的主设备号,调用 unregister_chrdev_region 可以释放设备号。函数原型:

void unregister_chrdev_region(dev_t from, unsigned count);

三、设备的注册和注销

内核用 cdev 数据结构来描述字符设备,cdev<linux/cdev.h>中定义:

struct cdev {
	struct kobject kobj;
	struct module *owner;
	const struct file_operations *ops;
	struct list_head list;
	dev_t dev;
	unsigned int count;
};

kobjLinux内核设备模型的基本结构,cdev 可以被设备模型管理; owner 表示所属对象,一般设置为 THIS_MODULEops 是与设备相关联的操作方法; dev 是内核中设备的设备号。 使用 cdev 大体步骤是先分配 cdev 结构,然后初始化,最后往系统添加,如果不再需要,可以从系统中删除。

(1)分配 cdev 结构 在注册设备之前,必须分配并注册一个或者多个 cdev 结构,可用 cdev_alloc 实现,如:

struct cdev *char_cdev = cdev_alloc(); /* 分配 char_cdev 结构 */

(2)初始化 cdev 结构 初始化 cdev 结构通过调用 cdev_init()实现,cdev_init()函数原型:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

参数 fops 用于指定设备的操作方法,在此结构中定义与设备相关的各种操作方法。假定一个设备需要实现除打开关闭之外,还需实现 readwrite 以及 ioctl 方法:

struct file_operations char_old_fops = {
 .owner = THIS_MODULE,
 .read = char_old_read,
 .write = char_old_write,
 .open = char_old_open,
 .release = char_old_release,
 .ioctl = char_old_ioctl
};

定义好 fops 后,cdev 初始化很简单:

cdev_init(char_cdev, &char_cdev_fops); /* 初始化 char_cdev 结构 */

(3)往系统添加一个 cdev

分配到 cdev 结构并初始化后,就可以通过调用 cdev_addcdev 添加到系统中了。不过在调用 cdev_add 之前,还需设置 cdevowner 成员,一般设置为 THIS_MODULE,设置完毕通过 cdev_add 添加:

	char_cdev->owner = THIS_MODULE;
	if (cdev_add(char_cdev, devno, 1) != 0) { /* 增加 char_cdev 到系统中 */
	printk(KERN_ERR "add cdev error!\n");
	goto error1;
 }

必须检查 cdev_add 的返回值,因为 cdev_add 不一定保证成功,添加成功返回 0,失败返回返回错误码。

(4)删除 cdev 将一个 cdev 结构从系统删除,调用 cdev_del()就可以了,如:

cdev_del(char_cdev); /* 移除字符设备 */

内核的字符驱动注册接口函数 register_chardev()和对应的注销函数 unregister_chrdev()

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
void unregister_chrdev(unsigned int major, const char *name);

这两个函数封装实际上是对 cdev 的使用方法进行了封装,只是同一个主设备号允许的次设备号最多为 256 个,并且能一次性完成设备号和设备的注册与注销。尽管在很多文献里面都不建议再使用对函数,担心将来版本不再支持这对函数,但是实际上在嵌入式 Linux 领域,使用的内核版本相对稳定,在满足次设备号的限制条件下,还是可以使用的,并且能够简化驱动编写