深入理解Linux字符设备驱动
文章从上层应用访问字符设备驱动开始,一步步地深入分析Linux字符设备的软件层次、组成框架和交互、如何编写驱动、设备文件的创建和mdev原理,对Linux字符设备驱动有全面的讲解。本文整合之前发表的《Linux字符设备驱动剖析》和《 Linux 设备文件的创建和mdev》两篇文章,基于linux字符设备驱动的所有相关知识给读者一个完整的呈现。
一、从最简单的应用程序入手
1.很简单,open设备文件,read、write、ioctl,最后close退出。如下:
二、/dev目录与文件系统
2. /dev是根文件系统下的一个目录文件,/代表根目录,其挂载的是根文件系统的yaffs格式,通过读取/根目录这个文件,就能分析list出其包含的各个目录,其中就包括dev这个子目录。即在/根目录(也是一个文件,其真实存在于flash介质)中有一项这样的数据:
是否目录 偏移 大小 名称 -- --
1 0xYYYY 0Xmmm dev -- --
Ls/ 命令即会使用/挂载的yaffs文件系统来读取出根目录文件的内容,然后list出dev(是一个目录)。即这时还不需要去读取dev这个目录文件的内容。Cd dev即会分析dev挂载的文件系统的超级块的信息,superblock,而不再理会在flash中的dev目录文件的数据。
3. /dev在根文件系统构建的时候会挂载为tmpfs. Tmpfs是一个基于虚拟内存的文件系统,主要使用RAM和SWAP(Ramfs只是使用物理内存)。即以后读写dev这个目录的操作都转到tmpfs的操作,确切地讲都是针对RAM的操作,而不再是通过yaffs文件系统的读写函数去访问flash介质。Tmpfs基于RAM,所以在掉电后回消失。因此/dev目录下的设备文件都是每次linux启动后创建的。
挂载过程:/etc/init.d/rcS
Mount –a 会读取/etc/fstab的内容来挂载,其内容如下:
4. /dev/NULL和/dev/console是在制作根文件系统的时候静态创建的,其他设备文件都是系统加载根文件系统和各种驱动初始化过程中自动创建的,当然也可以通过命令行手动mknod设备文件。
三、设备文件的创建
5. /dev目录下的设备文件基本上都是通过mdev来动态创建的。mdev是一个用户态的应用程序,位于busybox工具箱中。其创建过程包括:
1) 驱动初始化或者总线匹配后会调用驱动的probe接口,该接口会调用device_create(设备类, 设备号, 设备名);在/sys/class/设备类目录生成唯一的设备属性文件(包括设备号和设备名等信息),并且发送uvent事件(KOBJ_ADD和环境变量,如路径等信息)到用户空间(通过socket方式)。
2) mdev是一个work_thread线程,收到事件后会分析出/sys/class/设备类的对应文件,最终调用mknod动态来创建设备文件,而这个设备文件内容主要是设备号(这个设备文件对应的inode会记录文件的属性是一个设备(其他属性还包括目录,一般文件,符号链接等))。应用程序open(device_name,…)最重要的一步就是通过文件系统接口来获得该设备文件的内容—设备号。
6. 如果初始化过程中没有调用device_create接口来创建设备文件,则需要手动通过命令行调用mknod接口来创建设备文件,方可在应用程序中访问。
7. mknod接口分析,通过系统调用后对应调用sys_mknod,其是vfs层的接口。
Sys_mknod(设备名, 设备号)
vfs通过逐一路径link_path_walk,分析出dev挂载了tmpfs,所以调用tmpfs->mknod
shmem_mknod(structinode *dir, struct dentry *dentry, int mode, dev_t dev)
inode = shmem_get_inode(dir->i_sb,dir, mode, dev, VM_NORESERVE);
inode = new_inode(sb);
switch (mode & S_IFMT) {
default:
inode->i_op =&shmem_special_inode_operations;
init_special_inode(inode,mode, dev);//以下是函数展开
break;
case S_IFREG://file
case S_IFDIR://DIR
case S_IFLNK://dentry填入inode信息,这时对应的dentry和inode都已经存在于内存中。
d_instantiate(dentry, inode);
可见,tmpfs的目录和文件都是像ramfs一样一般都存在于内存中。通过ls命令来获取目录的信息则由dentry数据结构的内容来获取,而文件的信息由inode数据结构的内容来提供。Inode包括设备文件的设备号i_rdev,文件属性(i_mode: S_ISCHR),inode操作集i_fop(对于设备文件来说就是如何open这个inode)。
四、open设备文件
9. open设备文件的最终目的是为了获取到该设备驱动的file_operations操作集,而该接口集是struct file的成员,open返回file数据结构指针:
struct file {
conststruct file_operations *f_op;
unsignedint f_flags;//可读,可写等
…
};
以下是led设备驱动的操作接口。open("/dev/LED",O_RDWR)就是为了获得led_fops。
static conststruct file_operations led_fops = {
.owner =THIS_MODULE,
.open =led_open,
.write = led_write,
};
10. 仔细看应用程序int fd =open("/dev/LED",O_RDWR),open的返回值是int,并不是file,其实是为了操作系统和安全考虑。fd位于应用层,而file位于内核层,它们都同属进程相关概念。在Linux中,同一个文件(对应于唯一的inode)可以被不同的进程打开多次,而每次打开都会获得file数据结构。而每个进程都会维护一个已经打开的file数组,fd就是对应file结构的数组下标。因此,file和fd在进程范围内是一一对应的关系。
11. open接口分析,通过系统调用后对应调用sys_open,其是vfs层的接口
Sys_open(/dev/led)
SYSCALL_DEFINE3(open,const char __user *, filename, int, flags, int, mode)
do_sys_open(AT_FDCWD,/dev/tty, flags, mode);
//path_init返回时nd->dentry即为搜索路径文件名的起点
//link_path_walk一步步建立打开路径的各个目录的dentry和inode
其中inode->i_fop在mknod的init_special_inode调用中被赋值为def_chr_fops。以下该变量的定义,因此, open(inode, f)即调用到chrdev_open。其可以看出是字符设备所对应的文件系统接口,我们姑且称其为字符设备文件系统。
conststruct file_operations def_chr_fops = {
.open = chrdev_open,
};
继续分析chrdev_open:
Kobj_lookup(cdev_map,inode->i_rdev, &idx)即是通过设备的设备号(inode->i_rdev)在cdev_map中查找设备对应的操作集file_operations.关于如何查找,我们在理解字符设备驱动如何注册自己的file_operations后再回头来分析这个问题。
五、字符设备驱动的注册
12. 字符设备对应cdev数据结构:
struct cdev {
struct kobject kobj; // 每个 cdev 都是一个 kobject
struct module*owner; // 指向实现驱动的模块
const structfile_operations *ops; // 操纵这个字符设备文件的方法
struct list_headlist; //对应的字符设备文件的inode->i_devices 的链表头
dev_t dev; // 起始设备编号
unsigned intcount; // 设备范围号大小
};
13. led设备驱动初始化和设备驱动注册
1) cdev_init是初始化cdev结构体,并将led_fops填入该结构。
2) cdev_add
3) cdev_map是一个全家指针变量,类型如下:
4) kobj_map使用hash散列表来存储cdev数据结构。通过注册设备的主设备号major来获得cdev_map->probes数组的索引值i(i = major % 255),然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中,probes[i]->data即是cdev数据结构,而probes[i]->dev和range代表字符设备号和范围。
六、再述open设备文件
14. 通过第五步的字符设备的注册过程,应该对Kobj_lookup查找led_ops是很容易理解的。至此,已经获得led设备驱动的led_ops。接着立刻调用file->f_ops->open即调用了led_open,在该函数中会对led用到的GPIO进行ioremap并设置GPIO方向、上下拉等硬件初始化。
15. 最后,chrdev_open一步步返回,最后到
do_sys_open的struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);返回。
Fd_install(fd, f)即是在当前进程中将存有led_ops的file指针填入进程的file数组中,下标是fd。最后将fd返回给用户空间。而用户空间只要传入fd即可找到对应的file数据结构。
七、设备操作
16. 这里以设备写为例,主要是控制led的亮和灭。
write(fd,val,1)系统调用后对应sys_write,其对应所有的文件写,包括目录、一般文件和设备文件,一般文件有位置偏移的概念,即读写之后,当前位置会发生变化,所以如要跳着读写,就需要fseek。对于字符设备文件,没有位置的概念。所以我们重点跟踪vfs_write的过程。
1) fget_light在当前进程中通过fd来获得file指针
2) vfs_write
3) 对于led设备,file->f_op->write即是led_write。
在该接口中实现对led设备的控制。
八、再论字符设备驱动的初始化
综上所述,字符设备的初始化包括两个主要环节:
1) 字符设备驱动的注册,即通过cdev_add向系统注册cdev数据结构,提供file_operations操作集和设备号等信息,最终file_operations存放在全局指针变量cdev_map指向的Hash表中,其可以通过设备号索引并遍历得到。
2) 通过device_create(设备类, 设备号, 设备名)在sys/class/设备类中创建设备属性文件并发送uevent事件,而mdev利用该信息自动调用mknod在/dev目录下创建对应的设备文件,以便应用程序访问。
那么如何通过通过device_create来创建设备文件呢,mdev的原理又是什么呢?我们接着分析。
九、设备类相关知识
设备类是虚拟的,并没有直接对应的物理实物,只是为了更好地管理同一类设备导出到用户空间而产生的目录和文件。整个过程涉及到sysfs文件系统,该文件系统是为了展示linux设备驱动模型而构建的文件系统,是基于ramfs,linux根目录中的/sysfs即挂载了sysfs文件系统。
Struct kobject数据结构是sysfs的基础,kobject在sysfs中代表一个目录,而linux的驱动(struct driver)、设备(struct device)、设备类(struct class)均是从kobject进行派生的,因此他们在sysfs中都对应于一个目录。而数据结构中附属的struct device_attribute、driver_attribute、class_attribute等属性数据结构在sysfs中则代表一个普通的文件。
Struct kset是struct kobject的容器,即Struct kset可以成为同一类struct kobject的父亲,而其自身也有kobject成员,因此其又可能和其他kobject成为上一级kset的子成员。
十、两种创建设备文件的方式
在设备驱动中cdev_add将struct file_operations和设备号注册到系统后,为了能够自动产生驱动对应的设备文件,需要调用class_create和device_create,并通过uevent机制调用mdev(嵌入式linux由busybox提供)来调用mknod创建设备文件。当然也可以不调用这两个接口,那就手工通过命令行mknod来创建设备文件。
十一、设备类和设备相关数据结构
1. include/linux/kobject.h
struct kobject {
constchar *name;//名称
structlist_head entry;//kobject链表
structkobject *parent;//即所属kset的kobject
structkset *kset;//所属kset
structkobj_type *ktype;//属性操作接口
…
};
struct kset {
struct list_head list;//管理同属于kset的kobject
struct kobject kobj;//可以成为上一级父kset的子目录
const struct kset_uevent_ops *uevent_ops;//uevent处理接口
};
假设Kobject A代表一个目录,kset B代表几个目录(包括A)的共同的父目录。
则A.kset=B; A.parent=B.kobj.
2.include/linux/device.h
struct class {//设备类
const char *name;//设备类名称
struct module *owner;//创建设备类的module
structclass_attribute *class_attrs;//设备类属性
struct device_attribute *dev_attrs;//设备属性
struct kobject *dev_kobj;//kobject再sysfs中代表一个目录
….
struct class_private *p;//设备类得以注册到系统的连接件
};
3.drivers/base/base.h
struct class_private {
//该设备类同样是一个kset ,包含下面的class_devices;同时在class_subsys填充父kset
struct kset class_subsys;
structklist class_devices;//设备类包含的设备(kobject)
…
structclass *class;//指向设备类数据结构,即要创建的本级目录信息
};
4.include/linux/device.h
structdevice {//设备
structdevice *parent;//sysfs/devices/中的父设备
structdevice_private *p;//设备得以注册到系统的连接件
structkobject kobj;//设备目录
constchar *init_name;//设备名称
structbus_type *bus;//设备所属总线
structdevice_driver *driver; //设备使用的驱动
structklist_node knode_class;//连接到设备类的klist
structclass *class;//所属设备类
conststruct attribute_group **groups;
…
}
5. drivers/base/base.h
struct device_private {
structklist klist_children;//连接子设备
structklist_node knode_parent;//加入到父设备链表
structklist_node knode_driver;//加入到驱动的设备链表
structklist_node knode_bus;//加入到总线的链表
structdevice *device;//对应设备结构
};
6. 解释
class_private是class的私有结构,class通过class_private注册到系统中;device_private是device的私有结构,device通过device_private注册到系统中。注册到系统中也是将相应的数据结构加入到系统已经存在的链表中,但是这些链接的细节并不希望暴露给用户,也没有必要暴露出来,所以才有private的结构。而class和device则通过sysfs向用户层提供信息。
十二、创建设备类目录文件
1. 在驱动通过cdev_add将struct file_operations接口集和设备注册到系统后,即利用class_create接口来创建设备类目录文件。
led_class = class_create(THIS_MODULE,"led_class");
__class_create(owner, name,&__key);
cls->name = name;//设备类名
cls->owner= owner;//所属module
retval =__class_register(cls, key);
structclass_private *cp;
//将类的名字led_class赋值给对应的kset
kobject_set_name(&cp->class_subsys.kobj,"%s", cls->name);
// 填充class_subsys所属的父kset:ket:sysfs/class.
cp->class_subsys.kobj.kset= class_kset;
//填充class属性操作接口
cp->class_subsys.kobj.ktype= &class_ktype;
cp->class = cls;//通过cp可以找到class
cls->p = cp;//通过class可以找到cp
//创建led_class设备类目录
kset_register(&cp->class_subsys);
//在led_class目录创建class属性文件
add_class_attrs(class_get(cls));
2. 继续展开kset_register
kset_register(&cp->class_subsys);
kobject_add_internal(&k->kobj);
// parent即class_kset.kobj, 即/sysfs/class对应的目录
parent =kobject_get(kobj->parent);
create_dir(kobj);
//创建一个led _class设备类目录
sysfs_create_dir(kobj);
该接口是sysfs文件系统接口,代表创建一个目录,不再展开。
3. 上述提到的class_kset 在class_init被创建
class_kset= kset_create_and_add("class", NULL, NULL);
第三个传参为NULL,代表默认在/sysfs/创建class目录。
十三、创建设备目录和设备属性文件
1.利用class_create接口来创建设备类目录文件后,再利用device_create接口来创建具体设备目录和设备属性文件。
led_device =device_create(led_class, NULL, led_devno, NULL, "led");
device_create_vargs
dev->devt = devt;//设备号
dev->class= class;//设备类led_class
dev->parent =parent;//父设备,这里是NULL
kobject_set_name_vargs(&dev->kobj,fmt, args)//设备名”led”
device_register(dev)注册设备
2. 继续展开device_register(dev)
device_initialize(dev);
dev->kobj.kset= devices_kset;//设备所属/sysfs/devices/
device_add(dev)
device_private_init(dev)//初始化device_private
dev_set_name(dev,"%s", dev->init_name);//赋值dev->kobject的名称
setup_parent(dev,parent);//建立device和父设备的kobject的联系
//kobject_add在/sysfs/devices/目录下创建设备目录led,kobject_add是和kset_register相似的接口,只不过前者针对kobject,后者针对kset。
kobject_add(&dev->kobj,dev->kobj.parent, NULL);
kobject_add_varg
kobj->parent= parent;
kobject_add_internal(kobj)
create_dir(kobj);//创建设备目录
//在刚创建的/sysfs/devices/led目录下创建uevent属性文件,名称是”uevent”
device_create_file(dev,&uevent_attr);
//在刚创建的/sysfs/devices/led目录下创建dev属性文件,名称是”dev”,该属性文件的内容就是设备号
device_create_file(dev,&devt_attr);
//在/sysfs/class/led_class/目录下建立led设备的符号连接,所以打开/sysfs/class/led_class/led/目录也能看到dev属性文件,读出设备号。
device_add_class_symlinks(dev);
//创建device属性文件,包括设备所属总线的属性和attribute_group属性
device_add_attrs()
bus_add_device(dev)//将设备加入总线
//触发uevent机制,并通过调用mdev来创建设备文件。
kobject_uevent(&dev->kobj,KOBJ_ADD);
//匹配设备和总线的驱动,匹配成功就调用驱动的probe接口,不再展开
bus_probe_device(dev);
3. 展开kobject_uevent(&dev->kobj, KOBJ_ADD);
kobject_uevent_env(kobj,action, NULL);
kset= top_kobj->kset;
uevent_ops = kset->uevent_ops; //即device_uevent_ops
//subsystem即设备所属的设备类的名称”led_class”
subsystem= uevent_ops->name(kset, kobj);
//devpath即/sysfs/devices/led/
devpath= kobject_get_path(kobj, GFP_KERNEL);
//添加各种环境变量
add_uevent_var(env,"ACTION=%s", action_string);
add_uevent_var(env,"DEVPATH=%s", devpath);
add_uevent_var(env,"SUBSYSTEM=%s", subsystem);
uevent_ops->uevent(kset,kobj, env);
add_uevent_var(env,"MAJOR=%u", MAJOR(dev->devt));
add_uevent_var(env,"MINOR=%u", MINOR(dev->devt));
add_uevent_var(env,"DEVNAME=%s", name);
add_uevent_var(env,"DEVTYPE=%s", dev->type->name);
//还会增加总线相关的一些属性环境变量等等。
#ifdefined(CONFIG_NET)//如果是PC的linux会通过socket的方式向应用层发送uevent事件消息,但在嵌入式linux中不启用该机制。
#endif
argv [0] = uevent_helper;//即/sbin/mdev
argv [1] = (char *)subsystem;//”led_class”
argv [2] = NULL;
add_uevent_var(env,"HOME=/");
add_uevent_var(env,
"PATH=/sbin:/bin:/usr/sbin:/usr/bin");
call_usermodehelper(argv[0], argv,
env->envp, UMH_WAIT_EXEC);
4. 上述提到的devices_kset在devices_init被创建
devices_kset= kset_create_and_add("devices", &device_uevent_ops, NULL);
第三个传参为NULL,代表默认在/sysfs/创建devices目录
5. 上述设备属性文件
staticstruct device_attribute devt_attr =
__ATTR(dev, S_IRUGO, show_dev, NULL);
static ssize_t show_dev(struct device*dev, struct device_attribute *attr,
char *buf){{
returnprint_dev_t(buf, dev->devt); //即返回设备的设备号
}
6.devices设备目录响应uevent事件的操作
staticconst struct kset_uevent_ops device_uevent_ops = {
.filter = dev_uevent_filter,
.name = dev_uevent_name,
.uevent = dev_uevent,
};
7.call_usermodehelper是从内核空间调用用户空间程序的接口。
8. 对于嵌入式系统来说,busybox采用的是mdev,在系统启动脚本rcS 中会使用命令
echo /sbin/mdev >/proc/sys/kernel/hotplug
uevent_helper[]数组即读入/proc/sys/kernel/hotplug文件的内容,即 “/sbin/mdev”
十四、创建设备文件
轮到mdev出场了,以上描述都是在sysfs文件系统中创建目录或者文件,而应用程序访问的设备文件则需要创建在/dev/目录下。该项工作由mdev完成。
Mdev的原理是解释/etc/mdev.conf文件定义的命名设备文件的规则,并在该规则下根据环境变量的要求来创建设备文件。Mdev.conf由用户层指定,因此更具灵活性。本文无意展开对mdev配置脚本的分析。
Busybox/util-linux/mdev.c
int mdev_main(int argc UNUSED_PARAM, char**argv)
xchdir("/dev");
if (argv[1] &&strcmp(argv[1], "-s")//系统启动时mdev –s才会执行这个分支
else
action= getenv("ACTION");
env_path= getenv("DEVPATH");
G.subsystem= getenv("SUBSYSTEM");
snprintf(temp, PATH_MAX,"/sys%s", env_path);//到/sysfs/devices/led目录
make_device(temp,/*delete:*/ 0);
strcpy(dev_maj_min,"/dev");
//读出dev属性文件,得到设备号
open_read_close(path,dev_maj_min + 1, 64);
….
mknod(node_name,rule->mode | type, makedev(major, minor))
最终我们会跟踪到mknod在/dev/目录下创建了设备文件。
我们追求:
1.从上电第一行代码、系统第一行代码、模块第一行代码、应用第一行代码,深入讲解嵌入式软件生命周期。
2 深刻理解硬件体系,以面向对象思维剖析各种总线和驱动框架。
3 聚焦软件层次设计和框架设计
4 知其然,知其所以然
更多的嵌入式linux和android、物联网、汽车自动驾驶等领域原创技术分享请关注微信公众号: