内核中每个字符设备都对应一个cdev结构的变量,在kernel/include/linux/cdev.h中有它的定义及操作cdev结构体的一些函数: - struct cdev {
- struct kobject kobj; // 每个 cdev 都是一个 kobject
- struct module *owner; // 指向实现驱动的模块
- const struct file_operations *ops; // 操纵这个字符设备文件的方法
- struct list_head list; // 与 cdev 对应的字符设备文件的 inode->i_devices 的链表头
- dev_t dev; // 起始设备编号
- unsigned int count; // 设备范围号大小
- };
- void cdev_init(struct cdev *, const struct file_operations *);
- struct cdev *cdev_alloc(void);
- void cdev_put(struct cdev *p);
- int cdev_add(struct cdev *, dev_t, unsigned);
- void cdev_del(struct cdev *);
复制代码cdev结构体的结构成员dev_t定义了一个32位的设备号,其中高12位为主设备号,低20位为次设备号。尽管在很多情况下我们无需关心设备号,但是每一个设备文件的设备号都是真实存在的。使用以下宏可以获得主次设备号: - MAJOR(dev_ t dev)
- MINOR(dev_ t dev)
复制代码使用以下宏可以通过主次设备号生成dev_t: - MKDEV(int major, int minor)
复制代码从cdev.h中的程序清单可以看出,有五个函数用于操作cdev。cdev_init()和*cdev_alloc函数分别用于静态和动态初始化cdev,使用cdev_init()函数静态初始化的示例如下: - struct cdev my_cdev;
- cdev_init(&my_cdev, &fops);
- my_cdev.owner = THIS_MODULE;
- 使用*cdev_alloc()函数动态初始化的示例如下:
- struct cdev *my_cdev = cdev_alloc();
- my_cdev->ops = &fops;
- my_cdev->owner = THIS_MODULE;
复制代码可见,这二者的区别在于,使用动态初始化时需要手动指定cdev->ops,这是由二者的函数原型决定的。在kernel/fs/char_dev.c中可以看到其函数源码如下: - 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;
- }
- 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_init函数相对*cdev_alloc函数,多赋了cdev->ops。这就是动态初始化cdev需要手动指定cdev->ops的真正原因。 cdev_add()函数和cdev_del()函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对 cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而对cdev_del()函数的调用则通常发生在字符设备驱动模块卸载函数中。 在调用cdev_add()函数向系统注册字符设备之前,应首先调用register_chrdev_region()或alloc_chrdev_region()函数向系统申请设备号,register_chrdev_region()函数用于已知起始设备的设备号的情况;而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况。函数调用成功之后,会把得到的设备号放入第一个参数dev中。alloc_chrdev_region()与register_chrdev_region()对比的优点在于它会自动避开设备号重复的冲突。相反地,在调用cdev_del()函数从系统注销字符设备之 后,unregister_chrdev_region()应该被调用以释放原先申请的设备号。 当设备驱动程序成功调用了cdev_add之后,就意味着一个字符设备对象已经加入到了系统,在需要的时候,系统就可以找到它。对用户态的程序而言,cdev_add调用之后,就已经可以通过文件系统的接口呼叫到我们的驱动程序了。典型的文件系统接口如open(),close(),read(),write()等,他们真正操作的是在驱动程序中封装的file_operations结构体。 在linux/fs.h中可以看到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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
- ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
- int (*readdir) (struct file *, void *, filldir_t);
- unsigned int (*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 *);
- int (*open) (struct inode *, struct file *);
- int (*flush) (struct file *, fl_owner_t id);
- int (*release) (struct inode *, struct file *);
- int (*fsync) (struct file *, int datasync);
- int (*aio_fsync) (struct kiocb *, 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 **);
- long (*fallocate)(struct file *file, int mode, loff_t offset,
- loff_t len);
- };
复制代码下面对file_operations结构体中的主要成员进行讲解。 *owner指向拥有该结构的模块的指针,避免正在操作时被卸载,一般为初始化为THIS_MODULES。 *llseek函数指针用来修改文件当前的读写位置,返回新位置。在出错时,返回一个负值。 *read函数指针用来从设备中同步读取数据。读取成功返回读取的字节数,失败则返回一个负值。 *write函数指针用来向设备发送数据。成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。 *aio_read函数指针初始化一个异步的读取操作,为NULL时全部通过read处理。 *aio_write函数指针用来初始化一个异步的写入操作。 *readdir函数指针仅用于读取目录,对于设备文件,该字段为 NULL。 *poll函数指针返回一个位掩码,用来指出非阻塞的读取或写入是否可能。把pool设置为NULL,设备会被认为即可读也可写。 *unlocked_ioctl函数指针提供一种执行设备特殊命令的方法。不设置入口点时返回-ENOTTY。注意,自linux3.0以后,ioctl函数将不复存在,使用unlocked_ioctl函数替代不失为一个比较好的方法。 *mmap指针函数用于请求将设备内存映射到进程地址空间。如果没有声明,将访问-ENODEV。 *open指针函数用于打开设备。如果为空,设备的打开操作永远成功,但系统不会通知驱动程序。 *flush指针函数用于在进程关闭设备文件描述符副本时,执行并等待,若设置为NULL,内核将忽略用户应用程序的请求。 *release函数指针在file结构释放时将被调用。 *fsync函数指针用于刷新待处理的数据,如果驱动程序没有实现,fsync调用将返回-EINVAL。 *aio_fsync函数对应异步fsync。 *fasync函数指针用于通知设备FASYNC标志发生变化,如果设备不支持异步通知,该字段可以为NULL。 *lock用于实现文件锁,设备驱动通常不去实现此lock *sendpage实现sendfile调用的另一部分,内核调用将其数据发送到对应文件,每次一个数据页,设备驱动通常将其设置为NULL。 *get_unmapped_area在进程地址空间找到一个合适的位置,以便将底层设备中的内存段映射到该位置。大部分驱动可将其设置为NULL。 *check_flags允许模块检查传递给fcntl(F_SETEL…)调用的标志。 模块的加载和卸载函数以及file_operations结构体中的成员函数构成了字符设备驱动的主体,驱动需要实现的各种功能都会封装在file_operations结构体的成员函数中,后面我们将通过实例讲述。
|