1. cdev 结构体
(1)在 Linux 内核中,使用 cdev 结构体描述一个字符设备,定义如下:
struct cdev {
struct kobject kobj; /* 内嵌的 kobject 对象 */
struct module *owner; /* 所属模块 */
const struct file_operations *ops; /* 文件操作结构体 */
struct list_head list;
dev_t dev; /* 设备号 */
unsigned int count;
} __randomize_layout;
cdev 结构体的 dev_t 成员定义了设备号,为 32 位,其中 12 位为主设备号,20 位为次设备号。使用下列宏可以从 dev_t 获得主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和次设备号生成 dev_t:
MKDEV(int major, int minor)
cdev 结构体的另一个重要成员 file_operations 定义了字符设备驱动提供给虚拟文件系统的接口函数。
(2)Linux 内核提供了一组函数用于操作 cdev 结构体:
void cdev_init(struct cdev *, const struct file_operations *);
struct cdev *cdev_alloc(void);
int cdev_add(struct cdev *, dev_t, unsigned);
void cdev_del(struct cdev *);
cdev_init() 函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接,其源代码如下所示:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; /* 将传入的文件操作结构体指针赋值给 cdev 的 ops */
}
cdev_alloc() 函数用于动态申请一个 cdev 内存,其源代码如下所示:
struct cdev *cdev_alloc(void)
{
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj, &ktype_cdev_dynamic);
}
return p;
}
cdev_add() 函数和 cdev_del() 函数分别向系统添加和删除一个 cdev,完成字符设备的注册和注销。对 cdev_add() 的调用通常发生在字符设备驱动模块加载函数中,而对 cdev_del() 函数的调用则通常发生在字符设备驱动模块卸载函数中。
2. 分配和释放设备号
在调用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region() 或 alloc_chrdev_region() 函数向系统申请设备号,这两个函数的原型如下:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
register_chrdev_region() 函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数 dev 中。alloc_chrdev_region() 相比于 register_chrdev_region() 的优点在于它会自动避开设备号重复的冲突。
相应地,在调用 cdev_del() 函数从系统注销字符设备之后,unregister_chrdev_region() 应该被调用以释放原先申请的设备号,这个函数的原型如下:
void unregister_chrdev_region(dev_t from, unsigned count)
3. file_operations 结构体
file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()、write()、read()、close() 等系统调用时最终被内核调用。