The Linux Kernel Module Programming Guide
- Peter Jay Salzman, Michael Burian, Ori Pomerantz, Bob Mottram, Jim Huang
- 译 断水客(WaterCutter)
6 字符设备驱动
include/linux/fs.h 中定义了结构体 file_operations
,这个结构体包含指向再设备上执行各种操作的系列函数。结构体的每一字段都对应着驱动中定义的处理请求的函数的地址。
所谓“每一字段对应驱动中…的函数的地址”,即是说
file_operations
中包含一系列的函数指针,指向模块中具体的函数实现。可以把这个结构体理解为设备的操作清单,编写驱动时只需要根据实际需要实现清单中的部分接口就行了。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
struct file *file_out, loff_t pos_out,
loff_t len, unsigned int remap_flags);
int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;
某些操作不会在驱动中实现(implemeted by a driver)。例如声卡驱动不需要实现从目录结构中读取的接口,那么这个驱动提供的 file_operations
结构体中的相关指针就可以设为 NULL
。
GCC 扩展(gcc extension)支持便捷的结构体初始化方式(即内核中常用的乱序初始化),用法形如:
struct file_operations fops = {
read: device_read,
write: device_write,
open: device_open,
release: device_release
};
或者使用 C99 风格的 designed initilizers 初始化结构体。
file_operations
中包含的用于实现read、write等系统调用的函数,通常被称为 fops
。
从 3.14 版内核开始, read、write、seek等操作fops
就已经有线程安全的(thread-safe)特定锁(specific lock)保护了,这使得文件位置更新(file position update)是互斥的(mutual exclusion)。所以我们在实现这些操作的时候不需要类似目的的锁(unnecessary locking)。
在计算机中,文件位置更新是指将文件指针移动到文件中的特定位置。
此外,从 5.6 版开始,开发者向内核引入了 proc_ops
结构体,在注册 proc handlers 时不在使用 file_operations
结构体。
在计算机操作系统中,进程处理程序(proc handlers)是一种用于处理进程中断和异常的机制。主要作用是保证进程的正常运行和安全性。当进程发生中断或异常时,进程处理程序可以采取适当的措施来处理这些事件,例如重新启动进程、恢复进程状态、记录日志等。
6.2 File 结构体
每个设备在内核中都由一个 struct file
结构体表示,这个结构体定义在文件 include/linux/fs.h.中。
这个结构体不是用户程序常用的 glibc 中定义的 FILE
。另外这个结构体的命名有些误导作用,它指的是抽象的打开的文件,而非用 inode
指代的磁盘文件。
struct file
的指针(instance)通常被称为 filp
。
驱动基本不会使用include/linux/fs.h. 中定义的各类接口直接覆写(fill) struct file
,只会调用 struct file
中包含的各结构体。
6.3 注册设备
如前所述,用户一般是通过 /dev
目录下的设备文件(device files)访问字符设备的。
主设备号标明驱动处理哪个设备文件,次设备号只用于有多个设备时,驱动分辨正在使用的设备(which device is operating on)。
向系统中添加一个设备意味着将这个设备注册到内核中。在模块初始化的时候会通过调用定义在 include/linux/fs.h
中的 register_chrdev()
函数为设备分配一个主设备号,其原型如下:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
major
是主设备号,name
是可以在文件 /proc/devices
中看到的设备名,*fops
指向驱动中 file_operations 表的指针。函数返回负数表明设备注册失败。值得一提的是,这个函数不涉及次设备号,因为只有驱动才使用这个属性,内核并不关心次设备号。
现在问题来了,如何才能获取一个没被使用的主设备号呢?最简单的方式是查看 Documentation/admin-guide/devices.txt 然后选一个没有被占用的设备号。但这不是一个好办法,因为这个方法无法操作的互斥性,不能保证后续不会有其他设备使用同样的设备号。正确的答案是向内核请求一个动态的主设备号(ask the kernel to assign you a dynamic major number)。
向函数 register_chrdev()
传参数 0 ,它的返回值就是 dynamic major number
。这个办法的弊端在于,因为不确定设备注册时会获得哪个动态设备号,也就不能提前创建设备文件。又三种解决方案:
- 驱动打印输出主设备号,然后手动创建设备文件
- 新注册的设别会显示在
/proc/devices
文件中,可以通过读这个文件获取主设备号,然后手动/脚本创建设备文件 - 驱动在成功注册设备后,通过
device_create()
函数创建设备文件,并在cleanup_module()
函数中调用函数device_destroy()
。
不过,register_chrdev()
函数会占用一些和主设备号相关的次设备号,推荐使用 cdev interface 注册字符设备以减少对次设备号的浪费。
使用 cdev interface 注册字符设备分两步走。
第一步,调用 register_chrdev_region()
或者 alloc_chrdev_region()
注册一系列设备号(register a range of device numbers)。
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()
。
第二步,使用下面的方法为字符设备初始化结构体 struct cdev
,并将它和第一步注册的 device number 关联起来(associate it with the device numbers)。
struct cdev *my_dev = cdev_alloc();
my_cdev->ops = &my_fops;
上面是 cdev
单独存在的情况,更常规的情况是,设备驱动的 fops
中包含这个结构体,那就要用到 cdev_init()
函数了,原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
完成初始化后,可用 cdev_add()
函数将字符设备添加到系统中,函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
上述各种用法,可以在第 9 章中找到使用范例。