字符设备驱动、平台设备驱动、设备驱动模型、sysfs的比较和关联
最后更新于:2022-04-01 06:57:39
**Linux驱动开发经验总结,绝对干货!**
**学习Linux设备驱动开发的过程中自然会遇到字符设备驱动、平台设备驱动、设备驱动模型和sysfs等相关概念和技术。对于初学者来说会非常困惑,甚至对Linux有一定基础的工程师而言,能够较好理解这些相关技术也相对不错了。要深刻理解其中的原理需要非常熟悉设备驱动相关的框架和模型代码。网络上有关这些技术的文章不少,但多是对其中的某一点进行阐述,很难找到对这些技术进行比较和关联的分析。对于开发者而言,能够熟悉某一点并分享出来已很难得,但对于专注传授技术和经验给学习者而言,横向比较关联各个驱动相关的知识点和纵向剖析Linux整个驱动软件层次是非常有必要的,也非常有意义。**
**本文依然是从需求的角度去理解以上知识点,存在即是合理,以上技术知识能够存在,即代表其有一定的作用。我们着重去理解每一个技术点的作用,并明确其在驱动开发中的角色。**
**一、设备驱动**
**Linux设备驱动分三种,包括字符设备驱动、块设备驱动和网络设备驱动。字符设备只能按字节流先后顺序访问设备内存,不能随机访问。鼠标、触摸屏、LCD等是字符设备的代表。块设备可以随机访问设备内存的任意地址,硬盘、SD卡、NAND FLASH是块设备的代表。网络设备指的是网卡一类使用socket套接字进行通信的设备。本文以字符设备为例讲述相关知识。**
**二、字符设备驱动**
**字符设备驱动框架请参考嵌入式企鹅圈的两篇文章:**
[** Linux字符设备驱动剖析**](http://blog.csdn.net/yueqian_scut/article/details/45938557)
[**Linux 设备文件的创建和mdev**](http://blog.csdn.net/yueqian_scut/article/details/46771595)
**1. 字符设备驱动纵向关系**
**从可以看出,应用层访问设备驱动非常简单,即是通过open接口来最终获得设备驱动的操作接口集struct file_opertions.而open接口传入的参数是/dev目录下的设备名。而从可以知道,设备名对应的设备文件节点inode会存储设备号,而驱动框架中的全局数组cdev_map则维护设备号和file_opertions的关系。即应用层到底层的关系主要是(忽略VFS这一层):**
**设备名-->设备号-->file_opertions**
**Open函数返回的局部fd和file_opertions的关系(忽略进程数据结构)如下:**
**fd-->file(当前进程数据结构成员)-> file_opertions**
**这样,通过fd即可以获得file_opertions,即可以通过read、write等接口来调用驱动的读操作函数和写操作函数、ioctl函数等。**
**2. 字符设备驱动的任务**
**1)字符设备驱动最本质的任务应该是提供file_opertions的各个open、read、write、ioctl等接口的实现。**
**另外从以上的描述中,为了让应用层能够调用到底层的file_opertions还涉及到以下任务:**
**2)申请设备号,并将设备号和file_opertions注册(cdev_add接口)到驱动框架中的cdev_map数组。这点应该在字符设备驱动中负责,涉及到其主动向系统报备自己的存在。**
**3)在/dev目录中创建设备文件,内容包括设备号。这一点是否由字符设备驱动来负责商榷。字符设备驱动位于内核层,如果由其负责这个任务,那么驱动就得知道它要创建的设备名。简单的字符驱动还好,如果是USB等可插拔的设备,驱动怎么知道自己要创建什么设备名呢?有人说可以写明一套规则。确实如此,但如果把这套规则放到应用层,由应用程序开发人员去明确这个规则(mdev正是这样做的),会不会更好?因为是应用程序直接编程访问这个设备名对应的设备驱动的。所以字符设备驱动不应该直接负责设备文件的创建。**
**3\. 谁来创建设备文件**
总得有人出来做吧,否则应用层怎么访问啊?**
一种方法就是用户在shell中使用mknod命令创建设备文件,同时传入设备名和设备号。这是人工的做法,很不科学。但它是一种演示的方法。
另外一种方法就是依赖设备模型来辅助创建设备文件。这也是设备模型的作用之一。
**4\. 字符设备驱动编程流程**
1)定义struct file_opertions my_fops并实现其中的各个接口,如open、read、write、ioctl等接口。
2)实现驱动的入口函数,如chardev_init
static int __init chardev_init(void){
alloc_chrdev_region(&devno,…);//申请设备号
my_cdev=cdev_alloc();
cdev_init(my_cdev,&my_fops);
cdev_add(my_fops,devno, 1);//注册设备号和file_opertions
}
3)module_init(chardev_init);//宏定义该初始化入口函数。卸载流程不做解释。
4)insmod加载这个module后,可以人工在shell命令行利用mknod创建设备文件。
5)应用层即可以用open来打开设备文件来进行访问了。
**5. 总结**
**可以看出,字符设备驱动的核心框架跟设备模型、平台设备驱动没有直接关系,不用他们也一样能够正常工作。**
**三、设备驱动模型**
**我们主要谈及设备驱动模型在linux驱动中的作用和角色,有关设备模型的原理和实现我们另文再述。**
**1\. 设备驱动模型的作用**
**1)设备驱动模型实现uevent机制,调用应用层的medv来自动创建设备文件。这在上面已经论述过。**
**2)设备驱动模型通过sysfs文件系统向用户层提供设备驱动视图,如下。**
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908a6ec58.jpg)
**上图只是可视化的一种表达,有助于大家去理解设备模型,类似于windows的设备管理程序,在嵌入式linux里面并没有相关应用通过图形的方式来展现这种关系。但是用户可以通过命令窗口利用ls命名逐级访问/sys文件夹来获得各种总线、设备、驱动的信息和关系。可以看出,在/sys顶级目录,有三个关键的子目录,就是设备类、设备和总线。**
**设备是具体的一个个设备,在/sys/devices/是创建了实际的文件节点。而其他目录,如设备类和总线以下的子目录中出现的设备都是用符号链接指向/sys/devices/目录下的文件。**
**设备类是对/sys/devices/下的各种设备进行归类,以体现一类设备的公共属性,如鼠标和触摸屏都是属于input设备类。**
**总线目录是总线、设备、驱动模型的核心目录。因为设备和驱动都是依附在某种总线上的,如USB、PCI和平台总线等。设备和驱动正是依靠总线的管理功能才能找到对方,如设备注册到总线时去寻找驱动,而驱动注册的时候去寻找其能够支持的设备。**
**最重要的是,如果没有设备模型,那应用层很难知晓驱动和设备的关系,因为字符设备驱动并没有提供这些信息,那对于设备驱动的管理者而言会非常麻烦。**
**事实上,内核中的总线class、设备device和驱动device_driver都不会将所有的信息暴露给用户层,例如这三个数据结构都有对应的private数据结构,它用于内核对上下级总线设备驱动的链表关系维护。如果暴露给用户层,那容易被用户层修改而使系统混乱。实际上,用户层只关心三者的视图关联,至于他们的关联在底层怎么实现则不需要关心。**
**3)设备驱动模型提供统一的电源管理机制。很明显,我们在字符设备驱动的file_operations接口中并没有看到电源管理方面的接口。而对于操作系统来说,电源功耗管理必不可少。电源管理其实不应该由应用开发人员来负责,而是应该由系统来负责,例如手机很久没有触摸了,那会进入休眠状态。这种状态的改变应该由系统来完成,而各种设备进入睡眠模式也应该由系统来完成。因此file_operations不提供电源管理的接口给应用程序是合理的。而设备模型作为系统管理的一种机制,由它来提供电源管理是非常合理的。**
如设备device数据结构有struct dev_pm_info power功耗属性参数,驱动device_driver数据结构有struct dev_pm_ops *pm功耗操作接口。
4)设备驱动模型提供各种对象实例的引用计数,防止对象被应用层误删。设备模型的所有数据结构均是继承kobject而来,而kobject就提供基础的计数功能。
5)设备驱动模型提供多一种方式给应用层,用户和内核可以通过sysfs进行交互,如通过修改/sys目录下设备的文件内容,即可以直接修改设备对应的参数。
总结,设备驱动模型侧重于内核对总线、设备和驱动的管理,并向应用层暴露这些管理的信息,而字符设备驱动则侧重于设备驱动的功能实现。
**2\. 设备驱动模型的核心接口**
bus_register(struct bus_type *bus) 注册总线
device_add(struct device *dev) 注册设备
driver_register(struct device_driver*drv) 注册驱动
class_create(owner, name) 创建设备类
等等
**3. 设备驱动模型和字符设备驱动区别**
**设备驱动模型侧重于内核对总线、设备和驱动的管理,并向应用层暴露这些管理的信息,而字符设备驱动则侧重于设备驱动的功能实现。**
**四、sysfs文件系统**
**1.sysfs文件系统和设备驱动模型的关系**
**Sysfs文件系统是设备驱动模型得以向用户暴露其管理信息的载体。它们之间的关系如下:**
**1)设备驱动模型的上下级关系(如子设备和所属父设备)通过sysfs文件系统的父目录和子目录来体现。**
**2)设备驱动模型的平级关系(如设备类管理的设备和具体的设备的关系)则通过sysfs文件系统的目录符号链接来实现。**
**3)设备驱动模型的属性(如设备的参数和设备名,设备号等)则通过sysfs文件系统的文件内容来记录实现。**
**4)设备驱动模型数据结构中的kobject对应于sysfs文件系统中的目录,而数据结构中的struct attribute成员则对应于sysfs文件系统中的文件。对应的意思是指继续与kobject的device、device_driver和bus等在向系统注册的过程中会调用sysfs的create_dir接口来创建对应的目录,而含有struct attribute成员属性的device、device_driver和bus等在向系统注册的过程中则会调用sysfs的sysfs_create_file接口来创建文件。**
**2.sysfs核心接口**
**sysfs_create_file(struct kobject * kobj,const struct attribute * attr)创建属性文件**
**sysfs_create_dir(struct kobject * kobj)创建目录**
**int sysfs_open_file(struct inode *inode,struct file *file)打开sysfs文件系统格式的文件**
**sysfs_read_file(struct file *file, char__user *buf, size_t count, loff_t *ppos) 读操作**
**sysfs_write_file(struct file *file, constchar __user *buf, size_t count, loff_t *ppos) 写操作**
**3\. sysfs文件系统与属性文件读写**
**sysfs_read_file是sysfs文件系统的读写入口,但是驱动需要向系统提供真正的读写操作,也即是struct sysfs_ops数据结构中的show和store接口。**
**Sysfs是基于内存的文件系统,掉电即消失,sysfs所有的操作接口均是对内存中的内核数据结构进行访问操作。假如用户用cat命令去读取一个属性文件(如dev)的内容,那么会产生以下流程:**
**1)fd=open(“dev”)->vfs_open(“dev”)->sysfs_open(“dev”)获取该文件的句柄**
**2)read()->vfs_read()->sysfs_read_file()->sysfs_ops->show()该show接口即是设备在注册时产生属性文件,并向系统提供该文件的读接口。而读接口的实现中自然是对该属性参数的读访问。**
**/sys挂载了sysfs文件系统,因此所有对/sys目录下的文件或者目录的操作都会通过sysfs文件的接口进行访问。**
**五、平台设备驱动**
**平台设备驱动中的“平台”指的是平台总线,即platform_bus_type,是linux众多总线中的一种,如USB总线、PCI总线、I2C总线等等。只不过平台总线是一种虚拟的总线,专门用来管理SOC上的控制器(如看门狗、LCD、RTC等等),它们都是CPU的总线上直接取址访问设备的。而USB、PCI等设备都有通过特定的时序来访问SOC芯片以外的设备。平台设备驱动体现的关系是设备驱动模型上的一个子集,将平台视为一种总线的概念,那两者的关系就会容易理解。**
**1. 平台设备驱动和设备驱动模型的关系**
**1)平台设备驱动接口在设备驱动模型视图上创建了相关的平台设备类(/sys/class/platform_bus)、平台总线(/sys/bus/platform)、平台设备(/sys/devices/).**
**2)平台设备(platform_device)和平台设备驱动(platform_driver)均注册到平台总线上,即在/sys/bus/platform/目录下创建相应的设备和驱动目录。**
**3)平台总线负责匹配注册到其上面的设备和驱动,匹配成功后回调用驱动的probe接口。**
**4)平台设备驱动利用设备驱动模型接口来辅助创建对应的设备文件(位于/dev/目录下)。**
**相关的接口包括:**
**platform_device_register(structplatform_device *pdev) 注册平台设备**
**platform_driver_register(structplatform_driver *drv) 注册平台设备驱动**
**两个接口的实现里面都会对平台驱动和设备进行匹配,匹配成功会调用驱动的probe接口。**
**2\. 平台设备驱动和字符设备驱动的关系**
**我们假设这个平台设备是字符设备。**
**平台设备驱动和字符设备驱动的关系始于驱动的probe接口,即在probe接口中实现字符设备驱动所要完成的任务,即通过alloc_chrdev_region申请设备号和通过cdev_add注册驱动的struct file_opertions.另外为了自动创建应用层访问的设备文件,还要调用class_create和device_create接口在平台设备类下创建对应的设备类和设备,并发出uevent事件,调用mdev来创建设备文件。**
**3\. 平台设备驱动的开发流程**
**1)将字符设备驱动的char_init函数的实现搬到platform_driver的probe接口中。**
**2)在char_init中调用platform_device_register和platform_driver_register分别注册设备和驱动。其实,对于移植好的系统,platform_device_register是在linux启动的过程中完成的。因此char_init一般只有platform_driver_register注册驱动。**
**详细的平台设备驱动的实现原理和开发流程另文再述。本次的重点是为了阐述字符设备驱动、设备驱动模型、sysfs和平台设备驱动之间的关系。**
更多原创技术分享敬请关注微信公众号:嵌入式企鹅圈
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-13_5695f8f8d24b2.jpg)
从需求的角度去理解Linux系列:总线、设备和驱动
最后更新于:2022-04-01 06:57:37
**一、软件、面向对象、软件框架**
软件是为了解决现实问题而产生的,面向对象的软件思维是解决普遍现实问题的一种有效的抽象方法,而软件框架指的是用面向对象的思维去解决某种特定领域的问题而专门设计的一套行之有效的解决方案。
一般地,JAVA/C++编程反映面向对象的软件思维,而像Android Framework、Windows MFC和Linux的QT则代表应用层的软件框架。前述应用框架要解决的问题包括应用消息处理、UI控件显示和处理、资源管理等等。软件框架带来的好处就是对于解决某个领域问题,框架会帮你完成80%的开发工作量,而你只需要完成20%的开发工作量。
Linux平台上的各个子系统,如设备驱动模型、input子系统、I2C总线、frame buffer驱动等等都属于软件框架,它是针对特定的硬件体系需求以面向对象的思维去设计的一种软件解决方案,而且已经经过长时间的多平台验证。严格意义上,将子系统归入软件抽象组件会更加贴切,而软件框架表现为一组抽象组件及其组件实例之间的交互。软件框架和软件组件的特点都是解决特点领域问题,可以高度重用设计。
Linux系统以C语言开发为主,C语言在教科书上会被认为是过程语言。事实上,面向对象只是一种软件思维,并不局限于某种语言,只不过C++/JAVA在娘胎(编译器)里就已经得到支持,而C语言通过struct数据结构和函数指针一样可以出色地完成面向对象抽象的工作。Linux系统绝对是利用C语言进行面向对象编程的开山鼻祖,处处洋溢着软件艺术的光辉!
**二、理解好软件需求是学习好软件框架的前提**
对于学习着来说,软件需求(即软件要解决的问题)和软件框架都已经存在。但学习者往往只关注软件框架,因为学习的终极目标也是为了掌握软件框架并使用它来解决自己的问题。对于一般的知识传播者来说(例如学校老师、机构培训师;教科书或者网络文献),往往也是着重于解读软件框架的组成和原理。
事实上,对于一个代码量有几万甚至几十万行代码量的软件框架,一开始接触就学习原理和代码并不是好事。这种做法很像是试图从软件框架的学习理解中得出软件需求,有太多的未知就接触源码,那理解过程会非常痛苦,往往会感到非常迷惑。
我认为,深入地理解好需求,再去理解软件框架会事半功倍。
甚至,当达到一定的水平后,知道了需求,完全可以去猜测软件框架的实现。
**三、Linux系统的软件需求**
对于软件需求,最容易让人联想到的是一种具体的业务需求,如12306购票业务等等。Linux是一种操作系统,操作系统的软件需求是什么?操作系统是为了给应用层提供良好的接口而进行总线设备驱动管理、内存管理、文件管理、进程管理等等。总线设备驱动管理就是我们今天要谈的主题。Linux平台有各种子系统、各种总线、各种驱动,Linux系统对它们的管理就是软件框架的组成。我们要理解好Linux已有的框架,就要清晰地知晓其解决的问题,也就是其管理了哪些硬件设备,这些硬件设备的特点是什么,这些设备的访问方式是什么。
可以说,深入地理解硬件体系是理解好Linux总线设备驱动框架的前提!从面向对象的角度,我们要弄清楚,物理意义上的硬件是什么,而对应的软件对象是如何表述的。 以下阐述会重点讲述软件需求,作为以后分析框架的基础。
**四、总线、驱动、设备**
> 1.总线
总线代表着同类设备需要共同遵守的工作时序,不同的总线对于物理电平的要求是不一样的,对于每个比特的电平维持宽度也是不一样,而总线上传递的命令也会有自己的格式约束。如I2C总线、USB总线、PCI总线等等。以I2C总线为例,在同一组I2C总线上连接着不同的I2C设备。
> 2.设备
设备代表真实的、具体的物理器件,在软件上用器件的独特的参数属性来代表该器件。如I2C总线上连接的I2C从设备都有一个标识自己的设备地址,由这个设备地址来确定主设备发过来的命令是否该由它来响应。
>3.驱动
驱动代表着操作设备的方式和流程。对于应用来说,应用程序open打开设备后,接着就read访问这个设备,驱动就是如何实现这个访问的具体的过程。驱动主要包括两部分,第一是通过对SOC的控制寄存器进行编程,按总线要求输出时序和命令,成功地与外围设备进行交互;第二是对第一步中得到的数据进行处理,并向应用层提供特定格式的数据。
a.不同总线的设备的驱动过程是不一样的,这个很容易理解,USB鼠标的驱动和I2C EEPROM的读时序肯定是不一样的,访问时序的产生和控制也是驱动的一部分。
b.同种总线不同设备类型的设备驱动也是不一样的。如I2C电容屏设备,对于读read来说就是在datasheet规定的地址上去读触摸点的X和Y坐标,而I2C EEPROM的读操作是读取存储的内容,两种设备的datasheet是不一样的,驱动自然是不一样的。
c.同种总线的同类设备的设备驱动也可能是不一样的。例如对于触摸屏,TSC2003只支持单点触控,而FT5X06支持多点触摸。在获取触控坐标时,前者只需要获得一个点的数据就返回,而后者则需要先获得当前有几个点的数据,然后再把所有点的坐标都读出来。
在驱动的操作中,一般都会用到GPIO和中断等硬件资源,如上图的SDA和SCL会连接到SOC芯片的具体的两个GPIO引脚,而I2C读写时一般都采用中断控制的方式(查询读写是否完成比较低效,浪费CPU)。如果我们在驱动中直接针对具体的引脚来编程,那这个驱动的平台可移植性就比较差,因为不同的产品设计可能引脚不一样。所以,为了提高驱动的可移植性,Linux把驱动要用到的GPIO和中断等资源剥离给设备去管理。即在设备里面包含其自己的设备属性,还包括了其连接到SOC所用到的资源。而驱动重点关注操作的流程和方法。
>4.再谈总线
第1点中谈到的总线只是物理意义上的表述,总线就是在行业中制定出标准,明确规定时序的格式。我们在第3点中谈到,在软件层面上,时序的产生和控制由驱动负责。那我们要思考在软件层面上,总线的职责是什么?
总线在软件层面主要是负责管理设备和驱动。 a.设备要让系统感知自己的存在,设备需要向总线注册自己;同样地,驱动要让系统感知自己的存在,也需要向总线注册自己。设备和总线在初始化时必须要明确自己是哪种总线的,I2C设备和驱动不能向USB总线注册吧。
b.多个设备和多个驱动都注册到同一个总线上,那设备怎么找到最适合自己的驱动呢,或者说驱动怎么找到其所支持的设备呢?这个也是由总线负责,总线就像是一个红娘,负责在设备和驱动中牵线。设备会向总线提出自己对驱动的条件(最简单的也是最精确的就是指定对方的名字了),而驱动也会向总线告知自己能够支持的设备的条件(一般是型号ID等,最简单的也可以是设备的名字)。那设备在注册的时候,总线就会遍历注册在它上面的驱动,找到最适合这个设备的驱动,然后填入设备的结构成员中;驱动注册的时候,总线也会遍历注册在其之上的设备,找到其支持的设备(可以是多个,驱动和设备的关系是1:N),并将设备填入驱动的支持列表中。我们称总线这个牵线的行为是match。牵好线之后,设备和驱动之间的交互红娘可不管了。
c.总线在匹配设备和驱动之后驱动要考虑一个这样的问题,设备对应的软件数据结构代表着静态的信息,真实的物理设备此时是否正常还不一定,因此驱动需要探测这个设备是否正常。我们称这个行为为probe,至于如何探测,那是驱动才知道干的事情,总线只管吩咐得了。所以我们可以猜测在总线的管理代码中会有这样的逻辑:
if(match(device, driver) == OK)
driver->probe();
>5.再谈驱动
假设设备正常,探测成功,这时就代表应用程序可以通过驱动来访问操作这个设备了。事实上是这样吗?仔细想想还少了什么东西。应用层通过什么来访问操作这个设备?想起来吗?我们公众号“嵌入式企鹅圈”的第一篇文章《Linux字符设备驱动剖析》中曾清晰地分析了Linux字符设备驱动的开发和访问过程,在开篇即提到应用程序如何访问设备:
int fd = open(“设备文件名”);
read(fd, buf, len);
write(fd, buf, len);
在这个应用程序中会涉及驱动两个问题,一是设备文件名从何而来,二是应用层的open、read和write对应驱动哪些接口,是如何对应的。这些都是驱动要解决的问题。
a.总线匹配设备和驱动之后,驱动探测到设备正常,这时驱动是处于做好准备让应用层来差遣了,但是设备文件名如果没有创建,应用程序也不知从何入手。所以在驱动的probe探测成功之后,立即创建设备文件是最合适的时机。其通过sysfs文件系统、uevent事件通知机制和后台应用服务mdev程序配合能够成功地在/dev目录创建对应的设备文件。
b.驱动要提供应用层open、read、write、ioctl等操作的对应接口,而且这些接口要向系统报备(注册)自己,否则系统也不知道怎么调用驱动,因为在上面的描述中从始至终都是设备、驱动和总线三个东西在唱戏,它们跟系统,严格意义是跟Linux的虚拟文件系统和设备文件系统还没建立起关系来。即驱动要包括以下步骤:
B1.设备要提供struct file_operation结构定义的接口:
struct file_operations {
int (*open) (struct inode *, struct file *);
int (*ioctl) (struct inode *, struct file *, ...);
ssize_t (*read) (struct file *, char __user *,...);
ssize_t (*write) (struct file *, const char __user *, ...);
…}
这些接口将会对应到应用层的设备访问操作。在这些接口中,其会根据第3点中提到的需求去完成自己的操作任务。
B2.应用层正常的访问流程是:应用层操作->虚拟文件系统操作->具体文件系统操作->具体设备驱动的操作。虚拟文件系统VFS系统已经存在,具体文件系统操作对于字符设备来说非常简单,我们姑且认为是字符设备文件系统devfs,此时字符设备驱动要做的是将自己的struct file_operations向devfs注册,对应字符设备驱动是cdev_add函数。详细的分析过程可以参考《Linux字符设备驱动剖析》。
所以我们可以想象在驱动driver的结构体中有一个probe接口,驱动要实现这个接口,而这个probe接口要完成的工作包括:
Driver->probe()
I.探测设备是否正常
II.cdev_add(struct file_operations)注册操作接口
III. device_create()创建设备文件
>6.继续谈驱动
做好以上准备即已万事俱备的时候,等着应用程序来访问操作了。通过《Linux字符设备驱动剖析》中open的整个过程,到最后会调用到具体驱动的open,接下来我们就要阐述一下设备驱动的struct file_operations中的接口都要做什么。我们挑几个主要的来讲讲,其余可以自己想象。
a.open一般会进行驱动的初始化,可能包括硬件的初始化和软件的初始化。我们在第3点谈驱动的时候,曾说明为了让驱动更具移植性,会将驱动driver过程中使用到的具体GPIO和IRQ中断等资源列入设备device的属性内容。这时device数据结构中断的GPIO和IRQ的标识都来源于SOC datasheet的物理地址定义。我们都知道Linux在运行过程中会使用到SOC的MMU内存管理单元来管理自己的内存,会将内存分为两部分,内核空间(3G-4G)和用户空间(0-3G),这两块地址空间都是虚拟线性地址空间,即程序编译链接之后对应的地址空间,虚拟地址空间需要通过MMU和页表来映射到实际的物理内存空间才能最终访问到物理内存和物理IO等资源。而驱动操作硬件都处在内核空间,在open函数中主要包括以下操作:
a1.通过系统提供的资源获取接口获取到GPIO和IRQ等资源
a2.通过ioremap接口将GPIO和IRQ从物理地址空间映射到3G-4G中的虚拟地址空间
a3.根据具体的控制规格设置GPIO和IRQ相关的寄存器。
以上初始化的动作可能会出现在驱动probe探测的代码中,那open的接口可以什么都不做。
b. read:驱动的open如果成功,那整个访问流程已经成功一大半了,因为open的流程足够漫长和复杂。而read只是从用户空间的fd文件句柄找到所属进程的file文件结构,然后即可找出file_operations->read,其即是驱动的read接口。那就按着外网设备的规格和总线的时候进行操作,达到read设备的目的。Write也一样。
c. ioctl一般是对设备进行参数设置。
隆重推荐本人以下原创文章,有助读者系统、全面地理解嵌入式Linux的系统架构和驱动开发!
从需求的角度去理解Linux系列相关博文:
1. [Linux字符设备驱动剖析](http://blog.csdn.net/yueqian_scut/article/details/45938557)
2\. [Linux设备文件的创建和mdev](http://blog.csdn.net/yueqian_scut/article/details/46771595)
3. [符设备驱动字、平台设备驱动、设备驱动模型、sysfs的关系](http://blog.csdn.net/yueqian_scut/article/details/47049021)
4.[ ](http://blog.csdn.net/yueqian_scut/article/details/46694229)[Linux模块化机制和module_init](http://blog.csdn.net/yueqian_scut/article/details/46694229)
5. [Linux中断完全分析](http://blog.csdn.net/yueqian_scut/article/details/46862235)
6. [ Linux input子系统分析之一:软件层次](http://blog.csdn.net/yueqian_scut/article/details/47903853)
7. [全网络对Linux input子系统最清晰、详尽的分析](http://blog.csdn.net/yueqian_scut/article/details/48026955)
8\. 陆续推出Framebuffer、I2C、MTD等子系统的分析
更多原创技术分享敬请关注微信公众号:嵌入式企鹅圈
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-13_5695f8f8d24b2.jpg)
Linux 设备文件的创建和mdev
最后更新于:2022-04-01 06:57:34
> 引子
本文是嵌入式企鹅圈开篇--《linux字符设备驱动剖析》的姐妹篇,在上述文章里面我们详细描述了字符设备驱动框架涉及的驱动注册、通过设备文件来访问驱动等知识,并明确通过device_create接口并结合mdev来创建设备文件,但没有展开这个知识点。本文将从代码级去理解Linux设备类和设备文件的创建过程。通过这两篇文章,我们将可以对linux字符设备驱动的机制和脉络有全面的认识。
以下程序分析没有缩进,编辑了好几次都不行,耐心点才能跟踪完整个代码:-)
> 一、设备类相关知识
设备类是虚拟的,并没有直接对应的物理实物,只是为了更好地管理同一类设备导出到用户空间而产生的目录和文件。整个过程涉及到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的子成员。
本文无意对sysfs和linux设备驱动模型进行展开,以后再另写文章进行分析。
> 二、两种创建设备文件的方式
在设备驱动中cdev_add将struct file_operations和设备号注册到系统后,为了能够自动产生驱动对应的设备文件,需要调用class_create和device_create,并通过uevent机制调用mdev(嵌入式linux由busybox提供)来调用mknod创建设备文件。当然也可以不调用这两个接口,那就手工通过命令行mknod来创建设备文件。
> 三、设备类和设备相关数据结构
~~~
## 1include/linux/kobject.h
struct kobject {
const char *name;//名称
struct list_head entry;//kobject链表
struct kobject *parent;//即所属kset的kobject
struct kset *kset;//所属kset
struct kobj_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.
## 2include/linux/device.h
~~~
struct class {//设备类
const char *name; //设备类名称
struct module *owner;//创建设备类的module
struct class_attribute *class_attrs;//设备类属性
struct device_attribute *dev_attrs;//设备属性
struct kobject *dev_kobj;//kobject再sysfs中代表一个目录
….
struct class_private *p;//设备类得以注册到系统的连接件
};
~~~
## 3drivers/base/base.h
~~~
struct class_private {
//该设备类同样是一个kset,包含下面的class_devices;同时在class_subsys填充父kset
struct kset class_subsys;
struct klist class_devices;//设备类包含的设备(kobject)
…
struct class *class;//指向设备类数据结构,即要创建的本级目录信息
};
~~~
## 4include/linux/device.h
~~~
struct device {//设备
struct device *parent;//sysfs/devices/中的父设备
struct device_private *p;//设备得以注册到系统的连接件
struct kobject kobj;//设备目录
const char *init_name;//设备名称
struct bus_type *bus;//设备所属总线
struct device_driver *driver; //设备使用的驱动
struct klist_node knode_class;//连接到设备类的klist
struct class *class;//所属设备类
const struct attribute_group **groups;
…
}
~~~
## 5drivers/base/base.h
~~~
struct device_private {
struct klist klist_children;//连接子设备
struct klist_node knode_parent;//加入到父设备链表
struct klist_node knode_driver;//加入到驱动的设备链表
struct klist_node knode_bus;//加入到总线的链表
struct device *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);
struct class_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);
//还会增加总线相关的一些属性环境变量等等。
#if defined(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.上述设备属性文件
~~~
static struct 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){{
return print_dev_t(buf, dev->devt);//即返回设备的设备号
}
~~~
6.devices设备目录响应uevent事件的操作
~~~
static const 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/目录下创建了设备文件。
更多原创技术分享敬请关注微信公众号:嵌入式企鹅圈
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-13_5695f8f8d24b2.jpg)
Linux模块化机制和module_init
最后更新于:2022-04-01 06:57:32
> 引子:模块化机制优点
模块化机制(module)是Linux系统的一大创新,是Linux驱动开发和运行的基础(当然,module并不仅仅是支撑驱动)。其优点在于:
1.在系统运行动态加载模块,扩充内核的功能。不需要时可以卸载。
2\. 修改内核功能,不必重新全部编译整改内核,只需要编译相应模块即可。
3.模块目标代码一旦被加载重定位到内核,其作用域和静态链接的代码完全等价。
本文重点阐述Linux module加载的来龙去脉,其中的奥秘就在于对宏module_init的解读。
> 一、模块例子
hello_module.c代码如下:
~~~
#include /* Needed by all modules */
#include /* Needed for KERN_ALERT */
#include /*Needed for __init */
static int __init test_init(void){
printk(KERN_ALERT"Hello world!\n");
return 0;
}
static void __exit test_exit(void){
printk(KERN_ALERT"Goodbye world!\n");
}
module_init(test_init);
module_exit(test_exit);
~~~
> 二、模块编程要点
1.头文件 linux/module.h、linux/kernel.h、linux/init.h
2. 定义模块的初始化函数test_init(名字任意)和卸载函数test_exit(名字任意)。
3. 用宏module_init声明初始化函数,用宏module_exit声明卸载函数。
> 三、模块运行
模块代码有两种运行的方式:
1. 编译成可动态加载的module,并通过insmod来动态加载,接着进行初始化。
2. 静态编译链接进内核,在系统启动过程中进行初始化。
有些模块是必须要编译到内核,和内核一起运行的,从不卸载,如vfs、platform_bus等等。
> 四、静态链接和初始化
Make menuconfig时选择将模块编译到内核即为静态链接,或者直接在makefile文件中指定为obj-y +=hello_module.o
1module宏展开
头文件路径:include/linux/init.h
//静态编译链接时没有定义宏MODULE
~~~
#ifndef MODULE
typedef int (*initcall_t)(void);
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
#define device_initcall(fn) __define_initcall("6",fn,6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
~~~
所以:
~~~
module_init(test_init)展开为:
__initcall(test _init)->
device_initcall(test _init)->
__define_initcall("6", test _init,6)->
static initcall_t __initcall_test_init_6 __attribute__((__section__(".initcall6.init"))) = test_init;
~~~
即是定义了一个类型为initcall_t的函数指针变量__initcall_test_init_6,并赋值为test_init,该变量在链接时会链接到section(.initcall6.init).
2linux链接脚本
路径 arch/arm/kernel/vmlinux.ld.S
~~~
#include
SECTIONS{
…
INIT_CALLS
…
}
~~~
路径:include/ asm-generic/vmlinux.lds.h
~~~
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
INITCALLS \
VMLINUX_SYMBOL(__initcall_end) = .;
#define INITCALLS \
….
*(.initcall6.init) \
…
~~~
可见__initcall_test_init_6将会链接到section(.initcall6.init).
3初始化
在linux启动的第三个阶段kernel_init的函数里会调用:
路径init/main.c
~~~
Kernel_init
do_basic_setup
do_initcalls
static void __init do_initcalls(void){
initcall_t *fn;
for (fn = __early_initcall_end; fn
do_one_initcall(*fn);
}
~~~
即取出函数指针__initcall_test_init_6的值并进行调用,即执行test_init。
> 五、动态链接加载和初始化
Make menuconfig时选择将模块编译成模块即为动态链接,或者直接在makefile文件中指定为obj-m +=hello_module.o
编译成模块的命令是:
make –C $KERNEL_PATH M=$MODULE_PATH modules
即使用linux根目录下的makefile,执行该makefile下的modules伪目标,对当前模块进行编译。编译的结果是可重定位文件,insmod加载时才完成最终的链接动作。
1Module编译选项
Linux根目录下的makefile定义了modules伪目标会用到的编译选项。
//即定义宏MODULE,-D是GCC定义宏的语法。
MODFLAGS = -DMODULE
//GCC编译模块代码时会用到该选项,即定义宏MODULE。这与在头文件中用#define MODULE是一样的。
CFLAGS_MODULE = $(MODFLAGS)
2Module_init宏展开
头文件路径:include/linux/init.h
~~~
#ifndef MODULE /*编译成module时定义了宏MODULE*/
#else /* MODULE obj-m*/
typedef int (*initcall_t)(void);
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
~~~
__inittest仅仅是为了检测定义的函数是否符合initcall_t类型,如果不是__inittest类型在编译时将会报错。所以真正的宏定义是:
~~~
#define module_init(initfn)
int init_module(void) __attribute__((alias(#initfn)));
~~~
alias属性是GCC的特有属性,将定义init_module为函数initfn的别名。所以module_init(test_init)的作用就是定义一个变量名init_module,其地址和test_init是一样的。
3Hello_module.mod.c
编译成module的模块都会自动产生一个*.mod.c的文件,Hello_module.mod.c的内容如下:
~~~
struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
~~~
即定义了一个类型为module的全局变量__this_module,其成员init即为init_module,也即是test_init.并且该变量会链接到section(".gnu.linkonce.this_module").
4动态加载
insmod是busybox提供的用户层命令:
路径busybox/modutils/ insmod.c
~~~
insmod_main
bb_init_module
init_module
路径busybox/modutils/modutils.c:
# define init_module(mod, len, opts) .\
syscall(__NR_init_module, mod, len, opts)
~~~
该系统调用对应内核层的sys_init_module函数。
路径:kernel/module.c
SYSCALL_DEFINE3(init_module,…)
//加载模块的ko文件,并解释各个section,重定位
mod = load_module(umod, len, uargs);
//查找section(".gnu.linkonce.this_module")
modindex = find_sec(hdr, sechdrs, secstrings,
".gnu.linkonce.this_module");
//找到Hello_module.mod.c定义的module数据结构
mod = (void *)sechdrs[modindex].sh_addr;
if (mod->init != NULL)
ret = do_one_initcall(mod->init); //调用test_init.
模块的传参、符号导出、模块依赖等机制以后再另文描述
更多原创技术分享敬请关注微信公众号:嵌入式企鹅圈
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-13_5695f8f8d24b2.jpg)
Linux字符设备驱动剖析
最后更新于:2022-04-01 06:57:30
**一、先看看设备应用程序**
1.很简单,open设备文件,read、write、ioctl,最后close退出。如下:
~~~
intmain(int argc ,char *argv[]){
unsigned char val[1] = 1;
int fd =open("/dev/LED",O_RDWR);//打开设备
write(fd,val,1);//写入设备,这里代表LED全亮
close(fd);//关闭设备
return 0;
}
~~~
**二、/dev目录与文件系统**
2./dev是根文件系统下的一个目录文件,/代表根目录,其挂载的是根文件系统的yaffs格式,通过读取/根目录这个文件,就能分析list出其包含的各个目录,其中就包括dev这个子目录。即在/根目录(也是一个文件,其真实存在于flash介质)中有一项这样的数据:
文件属性 文件偏移 文件大小 文件名称 等等
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的内容来挂载,其内容如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
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
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908546ad5.jpg)
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);
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_569390855fc6c.jpg)
break;
case S_IFREG://file
case S_IFDIR://DIR
case S_IFLNK:
//dentry填入inode信息,这时对应的dentry和inode都已经存在于内存中。
d_instantiate(dentry, inode);
8\. 可见,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);
fd = get_unused_fd_flags(flags);
struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);
//path_init返回时nd->dentry即为搜索路径文件名的起点
path_init(dfd, pathname, LOOKUP_PARENT, &nd);
//link_path_walk一步步建立打开路径的各个目录的dentry和inode
link_path_walk(pathname, &nd);
do_last(&nd, &path, open_flag, acc_mode, mode, pathname);
//先处理..父目录和.当前目录
//通过inode节点创建file
filp = nameidata_to_filp(nd);
__dentry_open()
//inode->i_fop=&def_chr_fops
f->f_op =fops_get(inode->i_fop);
if (!open && f->f_op)
open = f->f_op->open;
if (open) {
//调用def_chr_fops->open
error = open(inode, f);
其中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:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
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设备驱动初始化和设备驱动注册
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
* cdev_init是初始化cdev结构体,并将led_fops填入该结构。
* cdev_add
~~~
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);
}
~~~
* cdev_map是一个全家指针变量,类型如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
* 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);返回。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
Fd_install(fd, f)即是在当前进程中将存有led_ops的file指针填入进程的file数组中,下标是fd。最后将fd返回给用户空间。而用户空间只要传入fd即可找到对应的file数据结构。
**七、设备操作**
15\. 这里以设备写为例,主要是控制led的亮和灭。
write(fd,val,1)系统调用后对应sys_write,其对应所有的文件写,包括目录、一般文件和设备文件,一般文件有位置偏移的概念,即读写之后,当前位置会发生变化,所以如要跳着读写,就需要fseek。对于字符设备文件,没有位置的概念。所以我们重点跟踪vfs_write的过程。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
1)fget_light在当前进程中通过fd来获得file指针
2)vfs_write
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-11_5693908537870.png)
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的代码分析请留意后续文章。本文涉及的vfs虚拟文件系统知识(如vfs框架、dentry,inode数据结构等内容)也由后续文章详细讲述。
更多原创技术分享敬请关注微信公众号:嵌入式企鹅圈
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-01-13_5695f8f8d24b2.jpg)
前言
最后更新于:2022-04-01 06:57:28
> 原文出处:[移动开发专栏文章](http://blog.csdn.net/column/details/emlinux.html)
> 作者:[吴跃前](http://blog.csdn.net/yueqian_scut)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
#嵌入式Linux内核驱动情景分析
> 深入剖析嵌入式Linux系统的内核和驱动开发技术,包括Linux设备驱动框架、软件层次、虚拟文件系统、内存管理、系统调用、功耗管理、具体文件系统、进程间通信等操作系统相关技术以及各种总线子系统框架和开发技术。