第二章 字符设备驱动程序
本文 欢迎转载,
内核为了简化设备驱动程序员的工作, 从各异的设备中提取出了共性的特征, 将其化分为三大类: 字符设备
,块设备,网络设备。内核针对每一类设备都提供了驱动模型框架。2.1 应用程序与设备驱动程序互动实例
书上实现了一个调用设备驱动程序的实例, 包括驱动程序和应用程序。
2.2 struct file_operations
这个结构中实现的几乎全是函数指针, 除了owner外, 它表示当前struct file_operations对象所属的内核
模块, 几乎所有的设备驱动程序都会用THIS_MODULE宏给owner赋值。
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 *, 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 **);
};
#define THIS_MODULE(&__this_module)
__this_module是内核模块的编译工具链为当前模块产生的struct module类型对象, 所有THIS_MODULE实际上
是当前内核模块对象的指针, file_operations中的owner成员可以避免file_operations中的模块正在被调用
时, 其所属的模块被从系统中卸载掉。 如果是静态编译进内核的, THIS_MODULEU将被赋值为空指针。
???????????owner是如何被用来避免模块被卸载的
2.3 字符设备的内核抽象
字符设备抽象出一个具体的数据结构struct cdev;
struct cdev {
struct kobject okbj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
}
struct kobject kobj: 内嵌的内核对象, 其用途将在设备模型一章中讨论
struct module *owner: 字符设备驱动程序所在的内核模块对象指针.
const struct file_operations *ops: 在应用程序通过文件调入到内核态时, ops指针起桥梁作用.
struct list_head list: 用来将系统中的字符设备形成链表.
dev_t dev: 字符设备的设备号, 由主次设备号构成.
unsigned int count:同一主设备号的次设备号的个数, 表示当前驱动控制的实际同类设备的数量.
产生struct cdev的方式:
静态: struct cdev 动态: static struct cdev *p = kmalloc(sizeof(struct cdev), GFP_KERNEL);
Linux源码中还提供一个接口: cdev_alloc,专门用于动态分配, 而且还会进行必要的初始化。
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;
}
系统抽象出cdev仅对字符驱动框架结构设计的需要, 现实中往往比这复杂的多, cdev结构常常做过一种内嵌
的数据结构出现在实际设备的数据结构中。
初始化cdev的方式:
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;
}
2.4 设备号的构成与分配
设备号由主设备号与次设备号组成, 主设备号用来定位对应的设备驱动, 次设备号用来标识同一驱动所管理
的若干类同类设备。 dev_t类型变量标识一个设备号, 这是个32位的unsigned int;
dev_t的代20位用来表示次设备号, 高12位用来表示主设备号, 但为了兼容性, 应使用以下宏来对设备号操
作.
MAJOR MINOR MKDEV:
MAJOR从dev_t类型的设备号中提取主设备号,MINOR用来提取次设备号, MKDEV将主设备号和次设备号合成
dev_t类型的设备号
2.4.2 设备号的分配和管理
.register_chrdev_region
int register_chrdev_region(dev_t from, unsigned count, const char *name);
参数from表示设备号, count是连续设备编号的个数,表示驱动所管理同类设备的个数,name表示驱动名,
static struct char_device_struct *chrdevs[CHRDEV_MAJOR_HASH_SIZE];用来管理和分配设备号。
数组中的每一项都指向一个struct char_device_struct类型的指针。
register_chrdev_region函数的功能是将当前设备驱动程序要使用的设备号记录到chrdevs数组中, 有了这种
记录,系统就可以避免不同的设备驱动程序使用同一个设备号了。
这个过程在__register_chrdev_region中实现, 它首先分配struct char_device_struct 对象cd并初始化,
然后开始搜索chrdevs数据, 搜索主设备号。
.alloc_chrdev_region函数
这个函数也是调用__register_chrdev_region, 相对于register_chrdev_region, alloc_chrdev_region在
调用__register_chrdev_region时, 传入第一个参数为0, 这样在代码中就会走另一个逻辑, 使用一个for
循环从chrdevs数组的最后一项向前扫描, 如果发现第i项为null, 就把该项的索引值作为分配的主设备号返
回给驱动程序,并生成一个struct char_device_struct节点, 将其加入到chrdevs[i]对应的哈希链表中。
设备号做为一种系统资源, 在设备驱动被卸载时, 需要通过unregister_chrdev_region将设备号释放。
2.5 字符设备的注册
完成了设备的初始化阶段后, 就可以把它加入到系统中, 以供别的模块使用它。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}
参数p为要加入到系统的字符设备对象指针, dev为设备的设备号, count表示从设备号开始连续的设备数量。
cdev_add的核心功能通过kobj_map函数实现, 后者通过操作一个全局变量cdev_map来把设备*p加入到其中的
哈希链表中。
static struct kobj_map *cdev_map;
这是一个struct kobj_map指针类型的全局变量, 在系统启动其间由chrdev_init函数负责初始化。
struct kobj_map {
struct probe {
struct probe *next;
dev_t dev;
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
void *data;
} *probes[255];
struct mutex *lock;
};
通过要加入的主设备号来获得probes数组的索引值, 然后把一个类型为struct probe的节点对象加入到prob
es[i]所管理的链表中, 实现加入的方法和前边设备号管理相同, 通过主设备号来获得probes数组中的索引值
, 然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中。
2.6 设备节点的生成
设备文件是特殊的文件类型, 存在的意义就是沟通用户空间程序和内核空间程序。
mknod命令最终是通过调用mknod函数实现, 调用的参数有两个, 一个是设备文件名, 一个是设备号, 设备
文件名主要在用户空间使用, 内核空间刚使用inode来表示相应的文件, mknod命令是通过系统调用sys_mkno
d进入内核空间.
sys_mknod的最后一个参数dev, 是由用户空间mknod命令构造出的设备号, sys_mknod系统调用将通过/dev目
录上挂载的文件系统来为/dev/demodev生成一个新的inode, 设备号将被记录在这个新的inode对象上。
sys_mknod首先在根文件系统ext3的根目录"/"下寻找dev目录对应的inode, ext3文件系统会通过inode编号得
到inode结构在内存中的实际地址, 接下来会通过dev的inode结构中的i_op成员指针指向的ext3_dir_inode_
operations, 来调用该对象中的mknod方法, 导致会调用ext3_mknod被调用.
ext3_mknod主要作用是生成一个新的inode, ext3_mknod中会调用一个和设备驱动程序关系密切的init_speci
al_inode函数, 这个函数的主要功能是为新生成的inode初始化其中的i_fop和i_rdev成员, 设备文件节点的
inode中的i_rdev成员用来表示该inode所对应设备的设备号, 通过参数rdev为其赋值。
i_fop成员的初始化根据是字符设备还是块设备而有不同的赋值, 对于字符设备fop指向def_chr_fops, 后者
主要定义一个open操作, 相对于字符设备, 块设备的def_blk_fops定义则要复杂。
2.7 字符设备文件的打开操作
用户空间程序调用open, 将发起一个系统调用, 通过sys_open函数进入内核空间, 其中一系列掉用关系如下:
sys_open --- do_sys_open --- do _filp_open --- do_filp_open --- nameidata_to_filp ---
__dentry_open --- chrdev_open
do_sys_open首先通过get_unused_fd_flags为本次的open分配一个未使用过的文件描述符.
do_filp_open函数会首先查找设备文件所对应的inode, 之后会调用get_empty_filp, 为每个打开的文件分配
一个新的struct file类型内存空间。 内核用struct file对象来描述进程打开的每一个文件的视图, 即使打
开同一文件, 内核也会为之生成一个新的struct file对象, 用来表示当前操作的文件相关信息.
这个结构中与设备驱动程序关系最紧密的是f_op, f_flags, f_count和private_data成员.
f_op是指针struct file_operations, f_flags用于记录当前文件被open时所指定的打开模式, 将影响后续的
read/write等函数的行为模式.
f_count用于对struct file对象的使用计数, 当close一个文件时, 只有struct file对象中的f_count为0才
真正执行关闭private_data常被用来记录设备驱动程序自身定义的数据。
进程为文件操作维护一个文件描述符表(current->files->fdt), 设备文件打开, 会得到一个fd, 然后用fd作
为进行维护文件描述符表(struct file*类型数组)的索引值, 将新分本的struct file空间地址赋值给它.
在do_sys_open后半部分, 会调用__dentry_open将设备对应节点的inode中的i_fop赋值给filp->f_op(即是
def_chr_fops), 然后调用i_fop中的open, 于是chrdev_open(def_chr_fops)就被调用到, 该函数灰常重要.
static int chrdev_open(struct inode *inode, struct file *filp)
{
struct cdev *p;
struct cdev *new = NULL;
int ret = 0;
spin_lock(&cdev_lock);
p = inode->i_cdev;
if (!p) {
struct kobject *kobj;
int idx;
spin_unlock(&cdev_lock);
kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx);
if (!kobj)
return -ENXIO;
new = container_of(kobj, struct cdev, kobj);
spin_lock(&cdev_lock);
/* Check i_cdev again in case somebody beat us to it while
we dropped the lock. */
p = inode->i_cdev;
if (!p) {
inode->i_cdev = p = new;
list_add(&inode->i_devices, &p->list);
new = NULL;
} else if (!cdev_get(p))
ret = -ENXIO;
} else if (!cdev_get(p))
ret = -ENXIO;
spin_unlock(&cdev_lock);
cdev_put(new);
if (ret)
return ret;
ret = -ENXIO;
filp->f_op = fops_get(p->ops);
if (!filp->f_op)
goto out_cdev_put;
if (filp->f_op->open) {
ret = filp->f_op->open(inode,filp);
if (ret)
goto out_cdev_put;
}
return 0;
out_cdev_put:
cdev_put(p);
return ret;
}
函数首先通过kobj_lookup在cdev_map中用inode->i_rdev来查找设备号对应的设备new, 并将i_rdev对应到在
cdev_map中找到的设备对象cdev, 然后通过filp->f_op = new->ops, 这行代码将设备对象new中的ops指针
赋值给filp对象中的f_op成员, 此时展示了驱动程序中实现的struct file_operations 与filp关联起来。
接下来检测驱动是否实现了open, 如果实现就调用之.
!!!!!!此时驱动中的filp_operations替换掉了之前赋值的def_chr_ops, filp是每个进程单独拥有的文件描述实例
内核每次打开一个设备文件时, 都会产生一个文件描述符fd和一个新的struct file对象filp来跟踪对文件的
这一次操作, 在打开文件时, 内核会将filp和fd关系起来, 同时会将cdev中的ops赋值给filp->fop, 最后sys
_open纱线充调用将设备文件描述符fd返回给用户空间, 如此用户空间的read, write就会对应到驱动上实现的
函数了.
通过以上过程, 可以发现设备号在其中的重要作用, cdev_add把一个设备加入到系统时, 需要一个设备号来标
记对象在cdev_map中的位置信息, 当mknod生成设备文件节点时, 也需要在命令行中提供设备号信息, 内核会
将该设备号信息记录到设备文件节点对应的inode的i_rdev成员中, 当打开一个设备时, 系统会根据设备文件
对应的inode->i_rdev信息在cdev_map中寻找设备, 所以在这个过程中务必要保证文件节点的inode->i_rdev数
据和设备驱动程序使用的设备号完全一致.
sys_close流程
sys_close调用filp_close, 首先判断filp中的f_count成员是否为0, 如果设备驱动定义了flush函数, 那么在
release函数被调用前, 会先调用flush, 这为了确保在把文件关闭前缓存在系统中的数据被真正写回硬件中.
!!!!!!flush函数, 从来没有实现过.
函数最后调用fput, 如果f_count为0, 就调用release函数, 接下来是系统资源释放.
本文 欢迎转载,
2.7 本章小结
字符设备的基本结构, 比较简单了, 不写什么了.