携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情
一、Linux 设备和分类
Linux 系统中的设备可以分为字符设备、块设备和网络设备这 3 类。
- 字符设备:字符设备是能够像字节流一样被访问的设备,当对字符设备发出读写请求,相应的
I/O操作立即发生。Linux系统中很多设备都是字符设备,如字符终端、串口、键盘、鼠标等。在嵌入式Linux开发中,接触最多的就是字符设备以及驱动。 - 块设备:块设备是
Linux系统中进行I/O操作时必须以块为单位进行访问的设备,块设备能够安装文件系统。块设备驱动会利用一块系统内存作为缓冲区,因此对块设备发出读写访问,并不一定立即产生硬件I/O操作。Linux系统中常见的块设备有如硬盘、软驱等等。 - 网络设备:网络设备既可以是网卡这样的硬件设备,也可以是一个纯软件设备如回环设备。网络设备由
Linux的网络子系统驱动,负责数据包的发送和接收,而不是面向流设备,因此在Linux系统文件系统中网络设备没有节点。对网络设备的访问是通过socket调用产生,而不是普通的文件操作如open/close和read/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。
设备分为字符设备、块设备和网络设备,而网络设备没有设备节点,所以设备文件基本上就分为字符设备文件和块设备文件两类,在设备节点属性中,分别以 c 和 b 来表示,即 c表示字符设备节点文件,b 表示块设备节点文件。
当程序打开一个设备文件时,内核就可以获取对应设备的设备类型、主设备号和次设备号等信息,内核也就知道了程序需要操作使用哪个设备驱动程序。在程序随后对这个文件的操作都会调用相应的驱动程序的函数,同时把从设备号传递给驱动程序。
2.2、设备编号
设备编号由主设备号和从设备号构成。在 Linux 内核中,使用 dev_t 类型来保存设备编号。在 Linux 内核中,dev_t 是一个 32 位数,高 12 位是主设备号,低 20 位是次设备号。
主设备号标识设备对应的驱动程序,告诉 Linux 内核使用哪个驱动程序驱动该设备。如果多个设备使用同一个驱动程序,则它们拥有相同的主设备号。例如/dev/ttyS0~3 这 4 个设备,拥有相同的主设备号 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 是第一个次设备号,通常是 0,count 是获得的编号数量,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;
};
kobj 是Linux内核设备模型的基本结构,cdev 可以被设备模型管理;
owner 表示所属对象,一般设置为 THIS_MODULE;
ops 是与设备相关联的操作方法;
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 用于指定设备的操作方法,在此结构中定义与设备相关的各种操作方法。假定一个设备需要实现除打开关闭之外,还需实现 read、write 以及 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_add 将 cdev 添加到系统中了。不过在调用 cdev_add 之前,还需设置 cdev 的 owner 成员,一般设置为 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 领域,使用的内核版本相对稳定,在满足次设备号的限制条件下,还是可以使用的,并且能够简化驱动编写