如果你曾经学过或使用过Linux字符设备驱动,那么你能回答以下问题吗?
1.什么是Linux字符设备驱动?
2.编写一个Linux字符设备驱动需要哪些知识?
3.用户空间是如何调用到Linux字符设备驱动的?
4.编写Linux字符设备驱动的步骤?
我想如果你能把这些问题回答清楚,你就能写Linux字符设备驱动,对Linux字符设备驱动的原理机制有比较深刻的理解了。下面就让我们带着这些问题去找答案!
1.什么是Linux字符设备驱动?
我们都知道Linux中设备大致可以分为3类:字符设备,块设备,网络设备。字符设备是指那些只能按顺序一个一个字节存储的设备(如:键盘,鼠标,串口,控制台等),有些地方,高级的字符设备也可以从指定的位置一次读取一块数据。每个字符设备都有一个设备号(设备号由主/次设备号构成),而且在Linux系统中把字符设备当作普通文件来管理,因此每个字符设备会在/dev下生存对应的字符设备文件(又称设备节点)。每个设备文件又有自己的设备文件操作函数组结构(struct file_operations)里面往往包含有:open,close,read,write等文件操作方法。
2.编写Linux字符设备驱动的步骤?
之所以先说编写Linux字符设备驱动的步骤,是因为有过驱动编写或学习经历的人都对这个过程耳熟能详,我们大概都知道这个过程,使用哪些API。
步骤:
·申请设备号:动态/静态申请主设备号
·定义cdev结构并初始化:cdev_init(struct cdev* ,const struct file_operations* )
·注册该cdev结构:cdev_add(struct cdev* cdev, dev_t dev, unsigned cont)
对于申请设备号,提供了如下API:
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)
对于cdev结构,可以动态申请/也可以静态定义:
struct cdev *cdev_alloc(void)
初始化驱动程序结构:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
需要构造一个file_operations结构。
对于注册驱动程序:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
需要给定cdev结构,起始设备号及设备号范围。
3.编写一个Linux字符设备驱动需要哪些知识?
2.1 对字符设备硬件的了解(有哪些功能,如何使用这些功能?归根结底就是如何配置设备里的这些寄存器)
2.2 熟悉Linux内核的字符设备驱动框架
如果你对以上两点都掌握清楚了的话,编写字符设备驱动自然不在话下。如果你能更深入了解这些框架的内部实现机制,则能更好的编写驱动。
以下将对内核中字符设备驱动中涉及的概念及数据结构,重要函数进行分析:(以Linux2.6.30内核为例,其他版本的Linux2.6内核实现的机制原理是一样的)
kobj_map:内核对象的映射结构
struct kobj_map {
struct probe { 设备的探测结构
struct probe *next; 指向主设备号相同或%255相同设备探测结构
dev_t dev; 设备号(主+次)
unsigned long range; 设备号的范围
struct module *owner; 实现该驱动程序模块的指针
kobj_probe_t *get; 获取该驱动程序模块的函数指针
int (*lock)(dev_t, void *); 增强驱动程序引用计数器函数指针
void *data; 指向拥有该设备号范围的设备驱动程序结构指针(cdev)
} *probes[255]; 含有255项探测结构指针数组
struct mutex *lock; 映射结构的锁
};
在/fs/char_dev.c中静态定义了该结构的全局指针:
static struct kobj_map *cdev_map;
对于kobj_map结构在Linux内核中有如下调用过程:
void __init chrdev_init(void) /* 字符设备初始化 */
cdev_map = kobj_map_init(base_probe, &chrdevs_lock);
其结构如下图所示:
cdev:字符设备驱动程序结构
字符设备驱动本身需要一个结构来描述,在Linux2.6.x内核中使用struct cdev结构来描述。
char_dev_struct:设备号管理结构
下面就对上面所涉及的核心API及数据结构的调用进行分析,进一步弄清其内部的机制:
register_chrdev_region:
alloc_chrdev_region:
__register_chrdev_region:设备号分配的核心函数
第一步:动态分配设备号管理结构
第二步:填充该设备号管理结构
第三步:将该设备号管理结构指针装入全局设备号管理结构指针数组中
该步分成两种情况:该主设备相同的插槽中已经填入过char_dev_struct结构指针和该插槽至今为空。如果至今为空,直接插入即可;如果已经有主设备号相同或%255相同的char_dev_struct指针插入过,需进行一些列设备号范围的检查
最终都会将当前的char_dev_struct指针填入插槽中:
cdev_init:初始化字符设备驱动程序结构
cdev_add:注册字符设备驱动
kobj_map:注册字符设备驱动的核心函数
具体图示如下:
3.用户空间是如何调用到Linux字符设备驱动的?
经过以上的分析,已经具备编写一个简单字符设备驱动的能力。但是,我们在用户空间的open,read,write,ioctl等操作方法是如何映射到驱动中file_operations实现的方法的呢?那就跟我进行如下分析:
如果不使用自动创建设备节点的话,我们正常加载驱动程序后的第一步就是创建设备文件:
mknod /dev/xxx c[b] major minor
第二步便是对该设备文件进行各种文件操作:
open/read/write/ioctl/...
先分析第一步:创建设备文件
sys_mknod
vfs_mknod: 为设备节点创建一个目录条目对象,调用当前文件系统的mknod方法,如果文件系统为ext2,对应的方法为ext2_mknod()。
ext2_mknod
ext2_new_inode (dir, mode); 首先为设备节点创建新的索引节点结构对象
init_special_inode(inode, inode->i_mode, rdev); 对索引节点进行初始化
if(S_ISCHR(mode)){ 如果是字符设备
inode->i_fop = &def_chr_fops; 字符设备操作函数组用全局默认操作函数集
inode->i_rdev = rdev; 将字符设备的设备号赋值给索引节点对象的i_rdev自段
}
执行open/read/write/ioctl文件操作:
open: 用户空间
sys_open:系统调用
filp_open
do_filp_open
dentry_open
首先为设备文件申请未使用的文件句柄号,然后动态的申请一个struct file结构与其关联起来,最终最struct file进行初始化,将设备文件索引节点的文件操作函数组结构指针i_fop赋值给struct file.f_op重要struct file.f_op = &def_chr_fops。def_chr_fops中只含open方法chrdev_open
chrdev_open会根据设备文件索引节点的i_rdev从内核对象映射结构数组指针cdev_map查到i_rdev对应的字符设备驱动(struct cdev)并将设备文件操作函数组的指针赋给struct file.f_op,最后再调用struct file.f_op.open从而驱动的open方法被调用。其他设备方法的操作过程类似。