这段时间算是把linux下的字符设备给基本吃透了,这边walfred会根据自己的观点,分解拆卸linux下字符设备并将其整理出来。
预备知识
这边提到的linux字符设备驱动是基于linux动态加载模块的思想,所以请务必知道linux模块的应用,可参考内核模块编程入门程序及标准Makefile文件。
1Linux字符设备驱动描述图
说明:
1.1这里我向大多数介绍linux字符设备驱动的书籍一样,将字符设备、linux字符设备驱动、用户程序分别独立开来。
1.2通过上图可以简单的看出linux字符设备驱动这一块主要是有cdev这个玩意主导了一切呀,所以下文会讲到cdev以及cdev关联的file_operations结构体。
2拆分字符设备驱动
2.1两个结构体cdev和file_operations
2.1.1关于cdev结构体
在字符设备驱动中,cdev结构体顾名思义(char device)可以知道其就是来描述一个字符设备的,看下cdev结构体的定义:
linux-2.6.32/include/linux/cdev.h
struct cdev {
struct kobject kobj;// kobj是一个嵌入在该结构中的内核对象。
struct module *owner;// owner指向提供驱动程序的模块
const struct file_operations *ops;// ops是一组文件操作,实现了与硬件通信的操作。
struct list_head list;// list用来实现一个链表,其中包含所有表示该设备的设备特殊文件的inode.
dev_t dev;// 指定了设备号
unsigned int count;// count表示与该设备关联的从设备的数目
};
看到了这个cdev结构体还真不简单,里面包含这么一些东西,关于ops在cdev结构体定义中已经有解释,所以下面将是重点描述下file_operations结构体。
2.1.2关于file_operations结构体
file_operation就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
在系统内部,I/O设备的存取操作通过特定的入口点来进行,而这组特定的入口点恰恰是由设备驱动程序提供的。通常这组设备驱动程序接口是由结构file_operations结构体向系统说明的,它定义在include/linux/fs.h中。
linux-2.6.32/include/linux/fs.h
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 *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *, struct dentry *, 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 **);
};
传统上, 一个 file_operation 结构或者其一个指针称为 fops( 或者它的一些变体).。结构中的每个成员必须指向驱动中的函数, 这些函数实现一个特别的操作, 或者对于不支持的操作留置为 NULL. 当指定为NULL 指针时内核的确切的行为是每个函数不同的。
在你通读 file_operations 方法的列表时, 你会注意到不少参数包含字串 __user. 这种注解是一种文档形式, 注意, 一个指针是一个不能被直接解引用的用户空间地址. 对于正常的编译, __user 没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用。
struct file_operations是一个字符设备把驱动的操作和设备号联系在一起的纽带,是一系列指针的集合,每个被打开的文件都对应于一系列的操作,这就是file_operations,用来执行一系列的系统调用。
在常见的字符设备驱动程序中,我们一般这样做, 它的 file_operations 结构是如下初始化的:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.ioctl = scull_ioctl,,
.open = scull_open,
.release = scull_release,
};
这种做法可以将我们自己定义的接口函数和file_operations结构体中定义的函数指针关联起来,说白了我们就是这样将file_operations结构体初始化的。
2.2 dev_t dev指定设备号
在cdev结构体中,我们对const struct file_operations *ops和dev_t dev关爱有加,其中我们在上面已经详细的说了file_operations,现在就重点说下dev_t dev,因为这玩意一旦指定分配了,在用户空间创建的字符设备必须也要使用相同的设备,然后进行对应起来,用户程序才会打开字符设备。
2.2.1dev_t类型
在内核中,dev_t类型(定义在<linux/types.h>中)用来保存设备编号——包括主设备号和次设备号。在内核2.6.0之后,dev_t是一个32位的数,其中12位表示主设备号,20位表示次设备号。
获得dev_t的主、次设备号:
MAJOR(dev_t dev);
MINOR(dev_t dev);
生成(转换成)dev_t类型:
MKDEV(int major, int minor);
分配和释放设备号
2.2.2建立一个字符设备之前,驱动程序首先要做的事情就是获得设备编号。其这主要函数在<linux/fs.h>中声明:
//指定设备编号
int register_chrdev_region(dev_t first, unsigned int count,char*name);
//动态生成设备编号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
unsigned int count, char *name);
//释放设备编号
void unregister_chrdev_region(dev_t first, unsigned int count);
3 Linux字符设备模板
3.1 驱动加载和卸载函数
加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中实现设备号的释放和cdev的注销。
工程师习惯定义一个与设备相关的结构体,通常会涉及私有数据、cdev、信号量等信息(这个比较重要可以将与一个设备相关信息通过”面向对象”的方式包装起来)。
设备结构体
Struct xxx_dev_t {
Struct cdev cdev;
…..
私有数据;
}
加载函数
static int __init xxx_init(void)
{
1. 申请设备号
2.cdev_init(&xxx_dev.dev,&xxx_fops);
3.cdev_add;
}
卸载函数
static int __exit xxx_exit(void)
{
1. 释放设备号
2. 注销设备;
}
3.2 定义字符设备file_operations结构体成员函数
read();
write();
ioctl();
3.3 file_operations结构体关联
Struct file_operations xxx_ops={
.owner=THIS_MODULE,
.read=xxx_read,
.write=xxx_write,
…….
};
结语:关于字符设备驱动,walfred就暂时分析到这边了,总体来说字符设备还是蛮简单的,其实只要掌握我画的那图,就基本ok了。可能我在上面分析时系统对cdev的操作没有详细的讲到,但这也无妨,希望读者通过通过我的分析能够理解Linux字符设备驱动程序的基本框架。