Linux内核模块开发 第 6 章

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。这个办法的弊端在于,因为不确定设备注册时会获得哪个动态设备号,也就不能提前创建设备文件。又三种解决方案:

  1. 驱动打印输出主设备号,然后手动创建设备文件
  2. 新注册的设别会显示在 /proc/devices 文件中,可以通过读这个文件获取主设备号,然后手动/脚本创建设备文件
  3. 驱动在成功注册设备后,通过 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 章中找到使用范例。

猜你喜欢

转载自blog.csdn.net/qq_33904382/article/details/131025146