精华内容
下载资源
问答
  • Linux内核设备模型与总线 - 内核版本 Linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样 - 源代码下载路径: ...

    Linux内核设备模型与总线

    -         内核版本 Linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样

    -         源代码下载路径: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2


     

    1.      Linux内核设备模型分析

    1)     kobject对象的设计思想

    -         对于没有接触过JAVA、Python等高级面向对象编程语言工程师,第一次看到struct  kobject对象,都会对它的作用感到困惑,不知道为什么这么多Linux内核对象结构体中,都要有一个看起来没什么用的struct  kobject。

    -         对于接触过JAVA、Python等面向对象编程语言的工程师,对object对象肯定不陌生。在JAVA、Python中,object对象是所有对象的基类,所有的对象最终都会继承到object对象。关于JAVA中object对象及其相关方法的描述,可以参阅JDK在线文档

    -         在Linux内核设备模型中,也是借鉴了JAVA、Python中的做法,让struct kobject对象作为所有内核设备的基类,kobject是一个最高层次的抽象基类,这样Linux内核才能够通过一个继承树体系,管理到系统里的每一个设备。

    -         在Linux内核中,虽然kobject对象可以作为所有设备对象的基类,但是类比于JAVA、Python,我们一般不直接使用kobject这种最高抽象层次的基类作为实际需要开发的设备程序的的直接基类,原因见图1。kobject类比于物质在自然界继承体系里的概念,物质是一个抽象的概念,所有的生物和非生物都继承自物质,即他们和物质都是IS-A的关系。狗 IS-A 物质,水IS-A 物质。但是我们真正研究狗的时候,一般是从其犬科动物或者动物等比较具体的基类开始研究,研究它动物属性,研究它发出叫声的特性,很少研究狗的物质属性(除非是唯物主义哲学家),但是狗确实是物质。

    -         同理,在研究一般具体的Linux设备驱动,如视频设备V4L2驱动的代码,一般都从其上层基类struct  video_device研究起,或者再抽血一些,研究struct  video_device 的基类struct  cdev, 很少直接使用最上层的kobject基类。但是V4L2设备驱动,确实 IS-A kobject。


    Figure 1 自然界继承体系,所有对象都继承自物质

    2)     kobject对象的特性

    -         kobjet对象的声明在与相关的API在include/linux/kobject.h文件中。

    -         kobject 对象有对象引用计数(kref),父子object索引(parent)等基本属性。

    -         kobject提供了sysfs文件系统相关的节点描述,属性与函数,使得Linux系统可以通过特殊的sysfs文件系统,以树形继承的关系来访问Linux内核中的每个具体的kobject对象。实际上,kobject最初的设计目的就是为了在sysfs中模仿windows的设备管理器,提供一个类似树形的,可以管理额访问系统所有设备的接口。

    -         kobject对象还提供了Linux系统设备中hotplug热插拔相关的事件与函数方法,使得内核设备可以支持热插拔机制。

    -         每个继承了object的Linux设备对象,在调用者获得kobject基类对象实例之后,可以通过container_of()函数,一层一层转换,最终获得具体子类对象。有了kobject,就可以实现通过基类来访问子类对象的机制

    3)     Linux内核中继承kobject的主要基础类设备模型

    -         Kobject类似于JAVA中的object类,一般不作为内核设备对象的直接基类。但是类似于JDK中有不少对象直接继承自object对象,JDK中直接继承自object的对象,一般都是最为基础类对象,提供给开发者使用。同样,Linux内核中也有不少继承自kobject的基础类对象,Linux内核驱动开发者可以使用这些直接继承自kobject的基础类设备,来构建实际的Linux设备驱动。

    -         kset是一个集合容器,用于管理一类object子类对象的集合,继承自某个基类kobject的所有基类的kobject对象都可以用一个kset容器来管理。

    -         Linux内核在继承自kobject的重要基础类对象如图2所示。



    Figure 2  Linux内核中直接继承kobject的重要基础类对象

    -         device类对象一般用于Linux各种总线设备(platform虚拟平台总线、USB总线等)的基类,下一节详细介绍。

    -         module对象是用于模块管理接口(就是上一篇文章中单继承与接口一节的接口),接口本身也是一个对象(JAVA中的interface也是对象,继承object),实现了module接口的对象,可以通过模块的方式,动态装载、卸载代码块。

    -         class对象是用于设备分类管理的相关接口,通过class接口可以将内核中各种设备类型的信息导出到用户态。

    -         cdev对象就是典型的字符设备基类对象,所有的字符设备最终都会继承到cdev对象。cdev对象同时制定了字符设备标准的访问接口函数方法。

    -         总之,拥有了以上这些基础类,内核开发者就能开发自己特殊的设备驱动,并且通过这些类和接口,将驱动程序集成到Linux内核中。


    3). Linux内核中是怎么管理维护这些继承类对象

    -         之前的内容讲了一堆面向对象的概念,描述了一堆与Linux设备驱动有关的对象之间的关系,可Linux内核毕竟是C语言写的,内核中如何维护这些基础类对象呢?

    -         在Linux内核drivers/base/ 目录下,有很多重要的代码,目录命名为base/,可见基础类对象这个 名词还是有来源依据的,Linux设备驱动里的对象基本都是继承自drivers/base/里面的对象。

    -         drivers/base/base.h中声明了base的一些私有对象属性,以及API,其中很多API如devices_init(),buses_init(),classes_init(),platform_bus_init()等初始化函数都会在Linux内核init函数的driver_init()中被调用,因此可见,Linux内核在启动时通过base.h中的这些初始化*_init()函数使得Linux内核中整个驱动系统相关的基础类组件对象都能进入工作状态。

    -         查看devices_init()函数实现代码如下,我们发现实际上,内核在初始化devices的时候,使用kobject对象创建了dev_kobj作为所有device子类对象的基类。并且创建了相应的devices_kset来管理这些子类。

    int __init devices_init(void)
    {
    	devices_kset = kset_create_and_add("devices", &device_uevent_ops, NULL);
    	if (!devices_kset)
    		return -ENOMEM;
    	dev_kobj = kobject_create_and_add("dev", NULL);
    	if (!dev_kobj)
    		goto dev_kobj_err;
    	sysfs_dev_block_kobj = kobject_create_and_add("block", dev_kobj);
    	if (!sysfs_dev_block_kobj)
    		goto block_kobj_err;
    	sysfs_dev_char_kobj = kobject_create_and_add("char", dev_kobj);
    	if (!sysfs_dev_char_kobj)
    		goto char_kobj_err;
    
    	return 0;
    
     char_kobj_err:
    	kobject_put(sysfs_dev_block_kobj);
     block_kobj_err:
    	kobject_put(dev_kobj);
     dev_kobj_err:
    	kset_unregister(devices_kset);
    	return -ENOMEM;
    }



    -         最后,我们实现device的子类对象,并通过device_register()函数将其注册的时候(Linux内核还有很多类似的register函数,例如register_chrdev_region,注册函数意图大同小异,都是让基类能够获取注册后的子类对象),用户就可以通过基类访问注册后的子类对象了。

    -         在device_register()函数中,我们看到注册的device子类会和基类的kset容器以及关联起来,最终系统可以通过基类device对象实例dev_kobj所关联的kset容器来访问deivce的子类对象(一般会通过container_of()函数获取子类对象)。

    -         实际上,Linux内核中,cdev,device等基础类对象,都会在初始化时init()相关的全局变量,Linux内核需要维护这些全局变量以及相关的容器(kset、数组、list都可以看成容器)。

    -         由此可见,面向对象思想里面,继承,多态,虚函数的实现并不神秘,还是通过精巧的设计,用全部变量加容易的方法来管理这些关系。由于有这些由Linux内核维护的全局变量和相关容器,所以在开发设备驱动模块子类时,需要通过注册函数(register)才能让基类能够关联到子类的对象。当有了面向对象的观点,我们可以在更高层次理解Linux内核这些对象的关系,从而设计并改进我们的系统。




    2.      Linux内核总线、设备与驱动

    1). 计算机系统总线模型

    - Linux内核总线的思想来源于如图3所示的计算机系统总线模型。

    - 计算机系统总线控制着外部设备与计算机CPU的通信,任意CPU N都可以通过总线访问到任意外部设备。

    - 一般情况下,外部设备数量都会大于CPU的数量,有了总线,无需为每个外部设备都配备一个CPU。只有外部设备需要CPU来访问读取处理数据,发送控制信号时,一个空闲的CPU才会通过总线控制器与某个外部设备建立通信连接。

    - 一旦CPU处理完某个外部设备的数据之后,CPU可以通过总线控制器,断开和某个外部设备的连接,去处理其它外部设备的访问需求。

    - 总之,计算机系统中的总线模型为数量较多的外部设备提供了一种共享数量较少的CPU的控制访问机制。



    Figure 3 计算机系统中的总线

    2). Linux内核中的与总线

    - Linux内核之后有各种各样的软件总线,系统中所支持的所有总线型驱动设备在/sys/bus/ 目录下可以看到。主要的总线型设备驱动有USB、platform、I2C、SPI、SCSI、mmc等。

    - Linux内核中所有总线接口的的基类以及相关的API都是在include/linux/device.h中声明。其中一个总线接口包括三个核心的基类对象:struct bus_type、struct device 与struct device_driver。这些基类对象与Linux内核中实际的USB、platform、I2C总线接口的继承关系如图4所示。



    Figure 4 Linux内核中,各种总线设备与总线接口基类的对应关系

     

    - Linux 内核总线驱动实际上是模仿计算机系统总线的机制,在一个具体类型的总线上(例如I2C、USB、platform)多数的device设备共享少数的device_driver提供了一种管理机制

    - 通过总线,将一个设备驱动中,逻辑功能部分(device_driver)与硬件具体资源bsp相关的部分(device)分隔开来,使得同一种类型的多数设备实例(device),能够共享同一个驱动程序逻辑代码(device_driver)。

    - struct  bus_type 对象与struct device 对象、structdevice_driver对象构成了一个设备实例化管理接口,对象之间的行为模式如图5所示,类似于抽象共产,将每个device与device_driver装配起来,构造出真正的设备实例。

    - struct bus_type对象在 match()函数方法中,通过对比新发现的device 与 device_driver 的 Id(Id可以是name也可以是dts的描述节点或者实际总线自己定义的匹配Id号都可以匹配),为新加入的device设备找到合适的Id匹配的device_driver,然后调用device_driver的probe()函数方法,进行构造实例化,最终产生实例化的设备驱动并且作为节点挂载在/dev目录下。

    - struct bus_type对象的 remove()函数则处理设备卸载析构的行为

    - bus_type对象与device对象、device_driver对象除了用于实例化的函数方法,还有suspend()、resume()、shutdown()等函数方法,用于实现设备的休眠、唤醒等电源管理相关的功能。

    - struct bus_type对象的uevent()函数方法提供了热插拔事件的相关通信机制,通过该函数接口,可以给用户态发送热插拔相关的异步事件。



    Figure 5 bus_type 与 device、device_driver对象的行为模式

    3).platform总线与设备简介

    - 在Linux 内核中,USB、I2C、SPI等总线都是实际存在的总线,都有对应的相关的外部硬件电路以及相关的标准化通信协议最终以电信号为载体与实际的I2C、USB等实际硬件设备通信。

    - USB、I2C等总线设备,可以通过真正的硬件热插拔,从而触发具体的bus_type总线进行driver与device匹配,最终在/dev/*目录下构造实例化设备驱动。

    - 而struct platform_bus_type对象在Linux内核中代表一个虚拟的平台设备总线。即在系统硬件中,不存在与platform_bus_type对应的硬件电路,在SOC中也不存在对应的总线控制器(USB、I2C等模组在SOC芯片中都有相关的硬件控制器)。

    - 虽然struct platform_bus_type不存在真正的总线,但是我们在处理各种杂七杂八的驱动时(比如LED、智能卡、杂项设备、framebuffer等),也有把device_driver的驱动实现逻辑代码和device硬件bsp相关的代码分离出来的需求, 这样使得同样类型但是占用不同端口或者资源(比如 LED1、 LED2都是LED设备,但是一般会占用不同的GPIO口)的device能够共享同一份device_driver的逻辑代码,不需要为每一个LED设备都写一份驱动(维护量无比巨大)。

    - 因而Linux内核采用 struct platform_bus_type、structplatform_device 与struct platform_driver三个对象继承了总线设备相关的基类对象,模仿系统总线的行为模式,通过struct platform_bus_type来管理 structplatform_driver与struct platform_device的设备匹配与设备构造实例化

    - 虽然platform虚拟平台总线不像usb、I2C等总线接口有真正的硬件设备插拔事件。但是struct platform_driver与struct platform_device对象都实现了module接口,可以编译成module进行insmod/rmmod等动态装载于卸载。那么struct platform_driver与struct platform_device对象在在作为module动态地装载与卸载时,相当于模拟了总线的热插拔事件,那么可以通过insmod/rmmod模拟总线设备的热插拔,来触发struct platform_bus_type对象进行driver与device匹配,并在/dev/*目录下构造实例化真正的设备驱动。

    展开全文
  • Linux内核驱动中面向对象的基本规则和实现方法 - 内核版本 Linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样 - 源代码下载路径: ...

    Linux内核驱动中面向对象的基本规则和实现方法


    -         内核版本 Linux Kernel 2.6.34, 与 Robert.Love的《Linux Kernel Development》(第三版)所讲述的内核版本一样

    -         源代码下载路径: https://www.kernel.org/pub/linux/kernel/v2.6/linux-2.6.34.tar.bz2

    1. 引言——为什么要把面向对象的概念引入Linux内核


    1). 管理大型软件的复杂度

    - 众所周知,现代的软件项目变得越来越复杂,Linux内核更是世界上最大最复杂的软件工程。引用自《代码大全》中的观点,面向对象的设计思想是一种管理软件系统复杂性的手段。


    - 人的精力有限,一个程序员在同一时间,只专注于软件系统的一小部分,才能最大的发挥工作效率。构建软件就像设计大型建筑,一个时间点上,我们聚焦在建筑结构蓝图设计时,就不要过分地将注意力分散的建筑内的电线布局,第N层的排水管道如何施工这些细节问题上,而是应该聚焦在整体设计上。


    - 面向对象思想就是一种在代码编写之上的软件系统结构设计的思想。面向对象的语言用于描述系统框架结构是怎么样的,需要什么模块,模块之间的关系如何,如何遵守开闭准则,方便后期的维护和开发等一些设计意图层次上的问题。


    - 面向对象的设计思想不太关心xxx函数实现如何初始化,要注册什么结构,要把xxx寄存器状态设置为0等流程细节的问题。这些应该是属于面向过程设计时的问题。


    - 面向过程与面向对象的思想用途不同,没有好坏之分。面向对象思想更倾向于程序之上的顶层设计与程序系统结构设计,然后真正要实现一个函数细节的时候,还是需要面向过程地分析细节如何实现,需要初始化哪些变量,注册哪些结构,设置哪些寄存器等面向过程的问题。


    - 面向对象的思想和语言无关,并不是C++或者JAVA 、Python等语言才有的。面向对象思想,是随着软件系统的复杂度越来越高,面对大规模软件系统设计的问题,而提出的一种管理大型软件系统设计的思想。只是在C语言出现时,计算机软硬件系统还在起步阶段,面向对象的思想尚未发展(可能Keep it simple and stupid 原则也是让C语言语法尽量保持简单的原因),因而C语言中缺乏面向对象相关的核心关键词语法的支持。而JAVA、Python等一些1990年代之后问世的语言,受到C++语言影响以及面向对象思想的逐渐流行,在语法层面就提供了面向对象的核心关键词支持,可以说在处理面向对象问题上具有先天优势。


    - 虽然C语言不支持很多面向对象的核心关键词,但是随着Linux内核,Ffmpeg,Nginx等大规模以C语言编写的开源软件项目的发展与推广,C语言遇到的软件复杂度增加以及系统设计与系统长期维护的问题,与JAVA、C++编程遇到的复杂度问题是想通的。并且,面向对象思想也是由于开发者们在开发过程中遇到瓶颈才提出来的,这些问题,不管是用C语言编程还是JAVA编程,都会客观存在。因而用C语言模拟JAVA等面向对象的语言,采用面向对象的思想进行系统顶层设计是很有必要的。


    - JAVA是一种类似C语言的命令式语言(用于区别Lisp等函数式编程语言),并且是设计良好的纯面向对象的语言。JAVA有不少关键词与C语言相同,并且JAVA标准制定委员会的很多成员也是C/C++标准制定委员会的。因而借鉴JAVA的面向对象思想,分析编写面向对象的C语言程序,是相当有帮助的。JAVA不同于C++, C++现在的定位是多种范式语言,细节太多,在面向对象特性的设计上也不够友好方便。所以我更倾向于借鉴JAVA的编程思想来分析编程面向对象的C语言代码。


    - 文章中很多面向对象思想的出处,都来自JAVA。作为Linux和C语言嵌入式系统相关的开发者,可能大部分缺乏JAVA的编程经验。所以希望读者抽空学习一下JAVA,做一些小练习,拓宽编程的知识面,这样更能够领会面向对象设计的精髓,还能提出一些自己的新的看法。我认为,JAVA实际上是在C语言基础上的的一种改进,很多关键词与C相同,但是又弥补了C语言的不少缺陷,并且专注在面向对象的设计上。


    - 本文也是希望起到抛砖引玉的作用,如有概念性问题,还请批评指正。

     

    2). 良好的沟通需要一套通用的语言规则

    - 将面向对象的概念引入基于Linux系统的C语言程序开发是很有必要的。虽然面向对象思想会带来很多新的概念术语(继承,多态等),很多做Linux驱动开发的工程师都是电子工程相关专业出身,这些概念可能刚接触会稍显晦涩,但是习惯性以面向对象思想分析大规模软件系统之后,能够帮你快速得掌握整个系统的结构以及原作者的设计意图,而不会陷入到一个个API的实现细节中,只见树木,不见森林。接手维护一个大型软件项目,首先要知道原作者的意图,即知道原作者为什么要这么编程,才能理清楚软件设计思路,进而修改扩展源代码。


    - 面向对象的术语,是一种通用的规则,大家都掌握该规则,然后按照规则上的术语沟通,就能够用简短的语言表述出程序的意图。例如面向对象思想中的术语——继承基类,实现多态函数,如果用面向过程思想描述就是——定义一个XXX结构体,再定义XX和YY函数,用XX和YY函数填充XXX结构体中的A函数指针和B函数指针,再在初始化函数中调用C函数注册这个XXX结构体。不同思想所用术语的繁简程度,高下立判。过长的语言描述,容易带来更多的误解,信息丢失,理解不全等沟通障碍,这时,专用术语的优势就体现出来了。


    - 面向对象思想往往和设计模式是分不开的。比如Linux内核中的通知链系统,用设计模式的术语来说,叫观察者模式,有学过该设计模式的读者,立马明白程序大概做了什么。但是如果不了解这一套语言沟通规则,就代码讲代码,可能又是一堆这样的过程描述性语言——定义一个xxx头,定义xx结构体,设置xx结构体的回调函数,回调函数输入参数是什么,返回什么,注册xx结构到xxx头……

     

    3). Linux内核源码是训练面向对象思维的实战场

    - 单纯地学习面向对象思想或者设计模式,如何不结合实际的代码来分析具体案例,过一段时间可能就会忘记书上讲了什么东西。


    - 平时编写小规模程序时候,只需要一个人,不需要面向对象的思想就能完成,因而觉得面向对象这些东西都是书上的理论,实际上又用不着,但是一旦遇到Linux内核源码这种复杂级别的程序,就不知如何下手,容易一脸懵逼。


    - Linux内核是世界上最大的软件工程项目之一,经过了20多年的发展和完善,内部子系统结构经过了不断重构(refracting)和反复迭代设计,其代码质量也是越来越高,这其中肯定从面向对象的编程思想中借鉴模仿了很多东西。当然阅读Linux内核源码时,由于历史原因,还是会有很多代码的命名,架构和设计风格不那么完美(例如videobuf的第一版)。所以阅读内核源码的时候,要学会其精华,同时也要抛弃一些不良的设计。


    - JAVA Python等面向对象语言有大量的开源框架,让我们从实战中体验面向对象思想和设计模式。C语言的框架类库可能没有JAVA那么丰富,但是Linux内核作为C语言的代表作品,其中的设计思想是很值得用面向对象的语言分析一遍的,尤其是设备驱动相关的代码,有很多层抽象才变成了用户态都喜爱的文件。


    - 其实抛开Linux内核中动态运行,维护系统运转的线程,其中大部分驱动代码都是静态存在于内存,等待被调用的。这样看来Linux内核中维持系统运转的进程就像JAVA虚拟机,而内核中等待被调用的代码,就像JDK的框架和类库,需要用户态去调用,也需要内核态驱动开发者利用框架进一步扩展。这样看来JDK与Linux内核设计思想也有就有了很大共通之处,用面向对象思想分析Linux内核设备驱动也就是一种高屋建瓴,了解各个子系统结构框架的通用思想。


    - 网络上有一些C语言面向对象思想编程的文章,但是只是零零散散地整理了一些观点,不够系统化,案例也过于简单。所以我希望结合Linux内核这个大型的实际软件系统,更加系统化地在C语言编程中描述和应用面向对象思想。


    - 综上所述,作为Linux系统C语言开发者,带着面向对象的思想,从不同的视角来学习Linux内核吧!

     

    2.抽象与封装

     

    -         熟悉结构化C语言编程的读者,对抽象与封装应该不陌生,在此简要带过,抽象和封装是面向对象编程思想的基础。

     

    -         抽象即抽出事物最本质的特征来从某个方面描述这个事物。例如,牧羊犬和藏獒,它们抽象出来都是狗,都有会“汪汪汪”叫,会吃骨头等狗所拥有的行为特征。对于不需要分辨其到底是牧羊犬还是藏獒的用户,只需要知道一个物体是狗,那么肯定会联想到它会“汪汪汪”地叫这个特点。

     

    -         在C语言数据结构中,我们所描述的ADT抽象数据类型,就是对各种数据对象模型的抽象,在此不多累述。


    -         封装,在C语言编程中,大部分时候用一个函数调用(API)将一个复杂过程的细节屏蔽起来,用户不需要了解细节,只需要调用该函数就能实现相应的行为。例如吃饭函数,将盛饭,动筷子,夹菜,张嘴,咀嚼,下咽等细节屏蔽起来,我们只需要调用吃饭函数,默认就实现了一遍这样的流程。

     

    -         面向对象思想中的封装使用更广泛,即一个对象类(C语言中用结构体代替),需要隐藏用户不需要也不应该知道的行为和属性。用户在访问对象时,不需要了解被封装的对象和属性,就能使用该对象类,同时对象类也应该通过权限设置,禁止用户过多地了解被封装的对象属性与行为。

     

    -         总之,抽象与封装的思想都是为了让用户不需要了解对象过多的细节,就能直接通过API来使用对象,从而达到模块化编程,,程序员分工合作,各自负责维护自己负责模块对象细节的作用。

     

    3. 继承与接口


    1). 基类、子类与继承关系

    - 在面向对象(OOP)程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Baseclass、Super class)。


    - 典型的继承关系如图1所示,动物是一个基类,猫、狗和老鼠都是动物的子类,子类拥有父类的特征,我们称子类继承了父类的特征。比如猫、狗和老鼠都继承了动物都需要进食获取能量,能够发出叫声特征等。


    - 继承描述的是一种IS-A的关系,例如C继承了B,那么我们称B是基类,C是子类,C IS B(例如:猫是动物),但是我们不能说B IS C(比如:动物是猫)。


    - 继承关系和多态函数结合起来,就很容易达到管理系统对象复杂度的用途。例如,一个对象的实例,无论这个对象实例是猫、狗还是老鼠,我们只需要知道它是动物就OK了,我们可以把猫和狗之间当做它们的基类对象——动物类的实例来访问,调用动物发出叫声的函数,经过叫声函数在猫和狗中的多态函数实现,如果对象是猫,则会发出“喵喵”的叫声,如果对象是狗,则发出“汪汪”的叫声。


    Figure 1 典型的继承关系

    2). Linux设备驱动中的继承关系实现

     

    - 在Linux 内核C代码中,class类都用struct结构体来模拟

     

    - Linux内核设备驱动中,字符设备模型是典型的基类,而video_device、 framebuffer、miscdevice都是字符设备,满足IS-A的关系,因而都继承了字符设备的特性(支持字符设备open, ioctl, read,write等典型的访问方法函数), 都是字符设备的子类。

     

    -         字符设备基类与video_device、 framebuffer、miscdevice等继承体系关系的UML描述图如何2所示。由于C语言并没有严格的继承关系语法支持,加上多级继承的缘故,所以实现这种继承关系需要一些C语言技巧,细节上需要仔细钻研代码,推敲。但是细节上的障碍并不阻碍我们从面向对象这种较高的层次来阅读和管理Linux设备驱动的代码,理解这种继承关系。

     

    -         用面向对象思想分析Linux内核,重点是理解代码模块之间的关系和设计思想、意图,至于如何处理继承关系的代码细节,实际上内核各个子系统框架已经通过精妙的C代码地处理过了,虽然不是严格的面向对象,但是思路上大致相同。

     

    -        字符设备对象struct  cdev在include/linux/cdev.h中声明, Linux内核中相当多的驱动类型都抽象成字符设备cdev(就如同猫、狗抽象成动物),cdev通过和文件系统的inode节点关联,对用户态而言,抽象成字符设备类型的文件(这也是为什么用户态看来,所有设备都是抽象的问题)。

     

    -        struct  file_operations是字符设备cdev最重要的组成部分,这个组件包含了cdev的核心函数方法(open/read/write/ioctl等),继承字符设备的子类都需要实现自己的struct  file_operations方法,以实现子类的多态函数

     

    -         由于字符设备cdev类的继承体系以及其struct  file_operations中函数的多态实现,所以用户态程序可以通过访问字符设备cdev的方法来访问framebuffer、video_device等各种具体的设备驱动(类比于访问动物的叫声函数的方法,来调用具体的动物,如猫、狗的叫声函数)。

     

    -         综合而言,继承关系最重要的优点就是,通过基类对象以及多态函数来访问具体子类对象实例

     


    Figure 2 Linux内核字符设备驱动之间的继承关系


    - Linux内核中用C语言实现继承关系的方法和技巧有以下几种:

     

    具体子类实现和基类的抽象差异性不大,继承体系只有一级继承,不需要做过多扩展时,在基类函数中加入空指针priv域即可。子类的私有特殊属性对象,可以放到空指针priv即可,子类相关的函数中,可以通过自定义的解析方法,强制转换,解析priv对象。

    struct base_dev {
    	int attr;
    	int (*func1)();
    	void *priv;
    }
    




    *具体子类实现和基类的抽象差异性较大,继承体系只有一级继承,需要扩展基类时,可以将基类对象嵌入到子类中,在访问到具体的子类时,通过内核特有的container_of()类型的函数,获取子类对象。


    例如图3所示video_device对象的声明:

    在得到基类对象cdev之后,通过 container_of(vdev, struct video_device, cdev) 可以在vdev中获取structvideo_device对象的实例,进而访问struct v4l2_file_operations中的相关多态文件操作函数。



    Figure 3  video_device对象通过嵌入cdev从而继承cdev类

     

    具体子类实现和基类的抽象差异性较大,继承体系只有多级(一般只有2), 需要一个抽象层来管理基类与子类的继承关系.

    例如; framebuffer对象,具体的例如vga16设备的framebuffer,是用struct fb_info来描述的。

    而在具体的vga16fb设备驱动子类中,在init加载时,都会调用register_framebuffer()函数将 vga16fb的struct fb_info描述对象注册到registered_fb[32]这个全局数组中(其实用链表更好,支持动态扩展,就可以超过32个fb对象的限制了),并且在会创建一个以FB_MAJOR为主设备号的次设备节点(创建节点,意味着有一个字符设备cdev对象实例化了)。

    而在framebuffer子类对象的初始化函数fbmem_init()中,已经创建了一个framebuffer的字符设备cdev的子类对象,并设置了FB_MAJOR的主节点号。

    这样在用户态通过 framebuffer的主设备节点字符设备cdev 的子类对象实例-- > cdev 注册的fb_fops 函数方法 -- > 数组registered_fb[32] 中的fb_info 子类对象实例 --> 调用fbops中的文件操作多态函数。

    通过这个调用路径,从cdev的抽象对象访问到fb_info的子类具体对象,从而实现了多级继承体系以及多态函数的调用。

     

    - 综上所述,Linux内核C语言编程,根据继承级数不同,实际对象和抽象对象差异不同,实现继承和多态的方法也存在多样化,但是宏观上的思路是一样的,用面向对象的语言来说,就是要继承基类,实现多态,让用户态程序能够以访问抽象字符设备文件的方法,访问具体驱动设备

     

    3). 不严格的基类与抽象基类

    - 在JAVA面向对象概念值,有抽象基类的概念, C++中有类似的虚基类的概念。抽象基类指基类对象的函数方法都是虚函数,并且抽象基类不能够直接实例化,必须被子类继承,然后实例化子类,由子类真正定义基类函数的实现。


    - C语言的struct结构体中,不能直接定义函数,实现函数体。只能够通过声明函数指针的方式将函数指针嵌入到结构体中,然后在定义结构体或者实例化时,才真正给指针赋值实际的函数地址。类似struct cdev 极其重要的组件struct file_operations的声明如图4所示。


    - 在面对对象编程中,基类与子类最重要的差别就是函数的多态实现上。比如基类动物的叫声函数,假如默认发出一种“哦哦”的声音,而子类猫的叫声函数通过多态覆盖基类的叫声函数,发出“喵喵”声,子类狗的叫声函数通过多态覆盖基类的叫声函数,发出“汪汪”声,从而实现了子类和基类的差异化。


    - 那么问题来了,在C语言中,定义或者实例化一个结构体对象,例如 struct cdev  mycdev;mycdev->ops->ioctl = myioctl;  那么mycdev到底是structcdev这个基类的实例,还是继承了struct cdev对象的cdev的子类的实例(虽然定义了struct  cdev对象,但是cdev的函数方法被覆盖重写了,这里就是歧义产生点,在面向对象思想中,子类才会在继承基类之后覆盖重写基类的函数)。



    Figure 4 struct cdev基类及其核心函数方法的声明

    - 为了沟通交流上的统一,消除理解歧义,这里需要做一些妥协折中,放宽松面向对象思想的规则限制,制定几条Linux内核C语言面向对象编程的自定义规则,才可以在没有语法关键字支持的条件下,模拟OOP编程,将OOP灵活应用到内核设备驱动模型的分析。关于基类与继承的几条折中妥协的规则如下:


    1. 在C语言面向对象编程中,因为结构体本身只能嵌入函数指针,所以不区分基类与抽象基类,我们用一个大概的基类概括这两种情况。


    2. 定义一个结构体对象实例,例如struct cdev  mycdev; 如果mycdev中的structfile_operations *ops中的函数方法是用户自己实现的,那么我们就认为mydev对象是cdev的子类。如果mydev的mycdev中的struct file_operations *ops中的函数方法全部是采用Linux内核提供的默认的实现函数,那么我们就认为mycdev就是cdev类的一个实例,是一个基类对象的实例。实际上Linux内核为很多内核基类对象都提供了默认的实现函数(我们可以称为基类函数的实现),在对象的构造函数中,我们会给对应的函数指针赋值。在实例化一个字符设备为抽象的字符设备文件时,我们都会创建inode节点,而在这个过程中,调用的init_special_inode()函数如图5所示。可见cdev对象创建过程中都采用了默认的def_chr_fops实例化基类函数cdev的structfile_operations  *ops的函数方法。



    Figure 5 实例化字符设备cdev的过程中,使用Linux内核默认的基类函数方法def_chr_fops实例化cdev

    3. 大部分情况下,定义cdev对象,实际上都是cdev的子类,因为cdev本身抽象层次太高,默认实现的函数方法也只提供了open的方法,open也只会最终调用实际字符设备驱动的open函数,并没有实现驱动的有效的功能。cdev就类似于动物这个基类,实际上还是抽象基类,世界上并没有一种真正叫做动物的对象实例,但是猫、狗才有真正的实例对象。从动物到猫和狗的对象,实际上还应该分出一些中间的子类,例如猫科动物,犬科动物,猫是猫科动物的子类,狗是犬科动物的子类。同理,framebuffer是继承cdev的子类,vga16fb才是真正的vga16图形显示器的驱动程序(要实例化成/dev/*下的节点)。


    4. 从基类到子类的继承关系,最重要的思想是从抽象的基类对象到子类的具体对象的一个逐渐具体化的过程。在管理软件系统复杂度时,通过这种抽象到具体的过程,应用开发者只需了解一些抽象概念,调用抽象的API。而具体对象的细节维护则交给子类的维护者管理。所以继承关系,重要的是看清楚抽象到具体的思维方法,C语言实现这种继承关系的细节是次要的。

    4). 单继承与接口

    - 在真正的面向对象编程语言中,C++支持多重继承而JAVA不支持多重继承,多重继承会使得对象继承关系变得复杂化,同时会埋有钻石问题的隐患(如图6所示)。

     


    Figure 6 多重继承中存在的钻石问题,如果哺乳动物和食肉动物实现了相同的函数方法,狗在多重继承遇到不同基类相同函数的时候,容易引发混乱

     

    - 在C语言面向对象编程的规则中,我建议模仿JAVA的单继承机制,另外用JAVA中接口实现机制(interface)代替可能在C++中的多继承机制

     

    - 接口在面向对象编程中描述了一种LIKE-A的关系。例如机器狗,它不是动物,它在事物分类里面应该是机器而不是真正的狗,机器狗与牧羊犬,哈巴狗有着本质的区别,牧羊犬可以作为狗的一种子类,是一种继承关系,但是机器狗就不可以,生物学家也不认为机器狗是真正的狗。但是机器狗可以和狗一样发出“汪汪汪”的叫声,所以我们可以说机器狗与狗是LIKE-A而不是IS-A的关系,机器狗和机器才是IS-A的关系。这样可以说,机器狗是机器,但是它实现了狗的“汪汪汪”的叫声接口(当然它不能继承真正狗的DNA)。继承关系与接口实现关系的差别如图7所示。


    Figure 7 单继承体系中,继承关系与接口实现的区别

    - Linux内核设备驱动中,也会遇到类似的多继承问题,例如三星framebuffer的驱动(s3c-fb.c),既是字符设备类型的驱动,又是虚拟平台总线(platform_driver)类型的驱动。所以需要制定面向对象的规则,管理类似多继承的问题。

     

    - 关于Linux内核C语言编程中,需要为多继承与接口相关的几条规则,来适应上述出现的相关问题:

    1. Linux内核C语言模拟JAVA的单继承机制,不支持多重继承,遇到同时具备cdev对象与platform_driver对象两种类型的驱动时,只继承其中一个对象,另一个则作为接口实现,以描述LIKE-A的概念。

     

    2. 对于类似s3c-fb.c这类驱动,我们认为它是framebuffer 与 cdev的子类,实现struct  platform_driver这个虚拟总线实例化接口,因为s3c-fb驱动核心的功能特性是framebuffer的显示缓存功能逻辑,而struct  platform_driver这个接口的相关函数,只是在动态实例化framebuffer设备节点的时候调用一次,并非framebuffer本质的特征(就像机器狗,本质特性是机器的特性)。所以我们说s3c-fb IS-A  framebuffer, s3c-fb LIKE-A struct  platform_driver。

     

    3. 由于驱动设备的复杂性,并不像自然界的事物容易看出继承关系。所以在研究内核设备驱动单继承关系的时候,不要拘泥于条条框框,要根据自己的研究目的来选择基类与继承关系。例如图6所示的,如果要研究狗的哺乳动物属性,在单继承条件下,我们可以认为狗继承了哺乳动物,实现了食肉动物的接口

     

    4. 在研究类似s3c-fb.c这类驱动时,如果关注重点在s3c-fb的显示缓冲等framebuffer特性上,我们就认为s3c-fb继承了framebuffer,实现了struct  platform_driver的虚拟总线实例化接口。如果我们关注点在s3c-fb如何识别fb设备动态识别实例化,如果通过sys文件系统进行电影管理的特性,那么我们可以认为s3c-fb继承了platform_driver类,实现了framebuffer的接口(尽管这种case比较少见)。


    5. 总之,在研究Linux内核设备驱动单继承与接口实现规则时,要主动权衡研究目的,根据需要选择继承的基类与实现的接口。但是字符设备驱动在大多数情况下,我们都认为XX驱动继承了字符设备cdev,实现了platform_driver的虚拟总线实例化接口。


    5). 通过基类访问子类的方法

    - 在面向对象编程中,通过继承关系,我们将子类对象赋值给基类对象的时候,可以通过基类对象,调用多态函数访问子类对象的实际函数。


    -在Linux内核设备驱动中,我们在用户态open一个字符设备,然后调用字符设备的read/write/ioctl函数,最终也会调用到内核态设备驱动程序相应的read/write/ioctl函数的实现,从而模拟了通过基类与多态函数的特性来访问子类的目的。


    -实际上,Linux内核会维护基类与子类cdev对象实例的链表,当用户态发起read/write/ioctl等字符设备系统调用函数时,read/write/ioctl等字符设备系统调用函数会通过/dev/*下的字符设备,设备节点号的方式(主节点号major,次节点号minor)从cdev链表的子类中,找到对应子类的cdev对象实例,然后判断是否为空并调用子类cdev->ops->read/write/ioctl等实际子类的多态函数实现,从而最终实现了通过访问基类的多态函数,最终访问到子类实际的多态函数,这个面向对象的特性。

     

    4.多态与实例化


    1). 多态

    - 在面向对象程序设计中,属于继承关系的基类classanimal 与子类 class dog 都实现了相同的函数方法bark()时,我们说子类的bark()覆盖了基类的bark()函数。如果一个 class animal 基类被实例化为一个class dog对象,那么调用animal.bark()时,实际上会调用dog.bark()。这样我们称父类与子类相同的函数方法bark()为多态函数。


    - 在C++中,多态是通过动态绑定实现的,程序在运行的时候,基类会通过虚函数表来查找子类的多态函数实现,当然这个过程都是系统运行库做的(run time),自己实现是相当复杂。


    - 在Linux内核中,模拟多态的方法要简单一些,实际上是在基类的函数方法中,通过获取子类对象,再嵌套调用子类对象的同名函数来实现的。图8为framebuffer设备驱动的多态函数read实现。实际上,framebuffer的cdev的struct  file_operations对象的read函数会调用子类struct  fb_info对象中同名fb_read函数(如果子类未实现该函数,则不调用),从而模拟了继承关系中基类同名函数通过多态的方法调用子类同名函数的行为。


    Figure 8 framebuffer driver 中read函数的多态实现

    2). 实例化

    - 在C语言面向对象编程时,如第3节继承与接口中在第3段不严格的基类与抽象基类,提出的一个关于实例化与继承的问题,定义一个变量struct  cdev  mycdev; mycdev到底代表cdev的子类还是cdev的实例,在该章节中,已有规则说明在何种场景下mydev代表cdev的子类。


    - 因而,在Linux内核中,关于对象实例化,还需要以下几条规则:

    1. 定义一个类似 struct  cdev  mycdev;这样的结构体变量mycdev,虽然mydev会占用内存空间,但是mydev并并不算实例化内核设备驱动,只有mydev真正在/dev/*目录下创建了设备节点,才是一个内核驱动设备的实例。


    2. 大部分情况下,内核设备是通过总线的接口(包括platform虚拟总线,也包括USB、SPI、I2C等真正的总线,只要是继承structbus_type基类对象的总线都可以)的probe()函数进行实例化的。例如三星的framebuffer驱动s3c-fb.c,就是通过实现struct platform_driver这个接口,在接口的probe()函数中,为识别到的framebuffer设备创建/dev/*下的节点,实现s3c-fb字符设备的实例化。


    3. 在不实现总线接口的字符设备中,定义一个struct cdev  mycdev;之后,在模块加载module_init()的时候,也是可以调用构造函数(初始化函数),创建/dev/*下的设备节点,从而完成实例化的。这种情况下,mycdev可以代表一个设备的实例,这种情况下模拟了面向对象设计模型中的单例模式,这也是可行的。

    5.聚合(组合)

    -         聚合在面向对象中代表一种HAS-A的关系,比如struct cdev对象HAS-A  struct file_operations对象,我们就认为structfile_operations对象与struct cdev对象是聚合关系。


    -         在面向对象中,聚合关系主要是为了区别于继承关系,例如V4L2子系统中,structvideo_device 有struct v4l2_file_operations对象,但是实际上structv4l2_file_operations中的函数都是struct file_operations中的同名函数,并且基类的struct file_operations中的同名函数最终会模拟多态函数的方式,调用到struct v4l2_file_operations中的函数。因而我们认为struct video_device对象中的struct v4l2_file_operations对象是继承自struct cdev对象,而不是聚合关系(HAS-A).但是struct video_device对象中的v4l2_std_id tvnorms对象,在struct  cdev基类对象中是不存在的,是structvideo_device对象特有的,struct video_device  HAS-A v4l2_std_idtvnorms, 因而我们认为v4l2_std_id tvnorms对象与struct video_device对象是聚合关系。



    -         由于C语言没有严格的面向对象关键词标准来支持,所以聚合和继承的区别,还是需要人为分类维护。如果子类与基类有同名函数,并且子类同名函数被基类同名函数所调用,那么同名函数所在的*_operations对象都认为是从基类继承过来的,子类所拥有的与基类无关的对象,我们才认为是子类的聚合。

    6. 模板与泛型


    -         Linux内核中为了简化复杂对象的定义,提供了很多#define宏来模仿面向对象中的模板和泛型机制。

    1). 模板

    - 典型的模板宏代码如Linux内核信号量的模板include/linux/semaphore.h,为简化信号量的定义与初始化,提供了模板函数。

    #define DECLARE_MUTEX(name)  \
        structsemaphore name = __SEMAPHORE_INITIALIZER(name, 1)


     

    2). 泛型

    - 典型的泛型宏代码如Linux内核网络部分的socket地址泛型(include/linux/net.h ),定义一个socket地址,socket地址的具体数据类型在实际定义的时候才由type参数确定

    #define DECLARE_SOCKADDR(type, dst,src)        \
        typedst = ({ __sockaddr_check_size(sizeof(*dst)); (type) src; })



    7. 开闭原则


    -         开闭原则是可复用的面向对象代码设计的基石。


    -         开闭原则是指,代码要对扩展开放,对修改关闭。


    -         Linux内核设计中,通过继承关系,以及用户态与内核态隔离,限定使用一组API实现用户态与内核态通信的机制,使得内核代码对Linux内核态的设备驱动扩展与开发是开放的,而对Linux用户态应用程序的各种可能的修改关闭。


    -         通过开闭原则,也实现了程序员的分工,Linux内核对内核维护的程序员扩展开放,对用户态应用开发程序员的修改关闭。整个内核设计思路是符合开闭原则的。


    8. 设计模式


    - 设计模式是在面向对象程序设计中总结出来的一些常用的代码复用规则。Linux内核设计中,大量地参考了经典的设计模式,这里仅仅举两个例子,读者在阅读各驱动源码时,需要自我总结一些设计模式。


    1). 观察者模式(订阅者/发布者)

    - 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。


    - Linux内核的通知链模型是典型的观察者模式,其介绍可以参阅本博客另一篇文章《Linux内核重点精要》中通知链模型的相关的介绍。


    2).桥接模式(handle/body)

    - 桥接模式的设计意图将抽象部分与它的实现部分分离,使它们都可以独立地变化。


    - Linux内核中使用的最重要的桥接模式,在于万物皆文件的思想。即将用户态的抽象字符设备文件,与实际的字符设备驱动实现分离,从而使得文件描述符和内核设备驱动可以分别在用户态和内核态独立变化,只需要在open的时候将抽象文件与实际的设备驱动关联起来即可。


    - 抽象字符设备文件与实际的内核设备驱动桥接模式的UML简化图如图9所示。


    Figure 9  Linux内核设备驱动模型中经典的桥接模式


    9. 参考文献

    -         《Linux内核设计与实现》—— Robert.Love

    -         《设计模式》——伽马等(四人组)

    -         《JAVA编程思想》——Bruce.Eckel

    -         《代码大全》——SteveMcConnell

    -         《C语言面向对象编程》 —— foruok的博客


    展开全文
  • Linux内核设备驱动模块自动加载机制

    万次阅读 2013-08-08 15:06:06
    摘要: 现在大多数硬件设备驱动都是作为模块出现的,Linux启动过程中会自动加载这些模块,本文通过内核源码简要说明这个过程。 1 驱动模块本身包含设备商、设备ID号等详细信息 如果想让内核启动过程中自动加载某个...

    摘要: 现在大多数硬件设备的驱动都是作为模块出现的,Linux启动过程中会自动加载这些模块,本文通过内核源码简要说明这个过程。

    1 驱动模块本身包含设备商、设备ID号等详细信息

    如果想让内核启动过程中自动加载某个模块该怎么做呢?最容易想到的方法就是到/etc/init.d/中添加一个启动脚本,然后在/etc/rcN.d/目录下创建一个符号链接,这个链接的名字以S开头,这内核启动时,就会自动运行这个脚本了,这样就可以在脚本中使用modprobe来实现自动加载。但是我们发现,内核中加载了许多硬件设备的驱动,而搜索/etc目录,却没有发现任何脚本负责加载这些硬件设备驱动程序的模块。那么这些模块又是如何被加载的呢?每一个设备都有Verdon ID, Device ID, SubVendor ID等信息。而每一个设备驱动程序,必须说明自己能够为哪些Verdon ID, DevieceID, SubVendor ID的设备提供服务。以PCI设备为例,它是通过一个pci_device_id的数据结构来实现这个功能的。例如:RTL8139的pci_device_id定义为:

    static struct pci_device_id rtl8139_pci_tbl[] = {
    {0x10ec, 0x8139, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
    {0x10ec, 0x8138, PCI_ANY_ID, PCI_ANY_ID, 0, 0, RTL8139 },
    ......
    }
    MODULE_DEVICE_TABLE (pci, rtl8139_pci_tbl);
    

    上面的信息说明,凡是Verdon ID为0x10EC, Device ID为0x8139, 0x8138的PCI设备(SubVendor ID和SubDeviceID为PCI_ANY_ID,表示不限制。),都可以使用这个驱动程序(8139too)。

    2 模块安装过程提取设备商、设备ID信息,并写入modules.alias文件

    在模块安装的时候,depmod会根据模块中的rtl8139_pci_tbl的信息,生成下面的信息,保存到/lib/modules/uname-r/modules.alias文件中,其内容如下:

    alias pci:v000010ECd00008138sv*sd*bc*sc*i* 8139too
    alias pci:v000010ECd00008139sv*sd*bc*sc*i* 8139too
    ......
    
    

    后面的000010EC说明其Vendor ID为10EC,d后面的00008138说明Device ID为8139,而sv,和sd为SubVendor ID和SubDevice ID,后面的星号表示任意匹配。另外在/lib/modules/uname-r/modules.dep文件中还保存这模块之间的依赖关系,其内容如下:

    (这里省去了路径信息。)
    8139too.ko:mii.ko
    

    3 内核启动过程中,总线枚举时把读取的设备ID等信息发送到udevd,udevd根据modules.alias文件找到匹配的驱动模块,加载之。

    在内核启动过程中,总线驱动程序会会总线协议进行总线枚举,并且为每一个设备建立一个设备对象。每一个总线对象有一个kset对象,每一个设备对象嵌入了一个kobject对象,kobject连接在kset对象上,这样总线和总线之间,总线和设备设备之间就组织成一颗树状结构。当总线驱动程序为扫描到的设备建立设备对象时,会初始化kobject对象,并把它连接到设备树中,同时会调用kobject_uevent()把这个(添加新设备的)事件,以及相关信息(包括设备的VendorID,DeviceID等信息。)通过netlink发送到用户态中。在用户态的udevd检测到这个事件,就可以根据这些信息,打开/lib/modules/uname-r/modules.alias文件,根据alias pci:v000010ECd00008138sv*sd*bc*sc*i* 8139too得知这个新扫描到的设备驱动模块为8139too。于是modprobe就知道要加载8139too这个模块了,同时modprobe根据 modules.dep文件发现,8139too依赖于mii.ko,如果mii.ko没有加载,modprobe就先加载mii.ko,接着再加载 8139too.ko。

    4 实验

    在你的shell中,运行:

    # ps aux | grep udevd
    
    root 25063 ...... /sbin/udevd --daemon
    我们得到udevd的进程ID为25063,现在结束这个进程:
    
    # kill -9 25063
    然后跟踪udevd,在shell中运行:
    # strace -f /sbin/udevd --daemon
    这时,我们看到udevd的输出如下:
    ......
    close(8) = 0
    munmap(0xb7f8c000, 4096) = 0
    select(7, [3 4 5 6], NULL, NULL, NULL
    我们发现udevd在这里被阻塞在select()函数中。
    select函数原型如下:
    
    int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    第一个参数:nfds表示最大的文件描述符号,这里为7(明明是6 ?)。
    第二个参数:readfds为读文件描述符集合,这里为3,4,5,6.
    第三个参数:writefds为写文件描述符集合,这里为NULL。
    第四个参数:exceptfds为异常文件描述符集合,这里为NULL。
    第五个参数:timeout指定超时时间,这里为NULL。
    select函数的作用是:如果readfds中的任何一个文件有数据可读,或者witefds中的任何一个文件可以写入,或者exceptfds中的任何一个文件出现异常时,就返回。否则阻塞当前进程,直到上诉条件满足,或者因阻塞时间超过了timeout指定的时间,当前进程被唤醒,select返回。
    
    所以,在这里udevd等待3,4,5,6这几个文件有数据可读,才会被唤醒。现在,到shell中运行:
    
    # ps aux | grep udevd
    root 27615 ...... strace -o /tmp/udevd.debug -f /sbin/udevd --daemon
    root 27617 ...... /sbin/udevd --daemon
    udevd的进程id为27617,现在我们来看看select等待的几个文件:
    
    # cd /proc/27615/fd
    # ls -l
    
    udevd的标准输入,标准输出,标准错误全部为/dev/null.
    0 -> /dev/null
    1 -> /dev/null
    2 -> /dev/null
    
    udevd在下面这几个文件上等待。
    3 -> /inotify
    4 -> socket:[331468]
    5 -> socket:[331469]
    6 -> pipe:[331470]
    7 -> pipe:[331470]
    由于不方便在运行中插入一块8139的网卡,因此现在我们以一个U盘来做试验,当你插入一个U盘后,你将会看到strace的输出,从它的输出可以看到 udevd在select返回后,调用了modprobe加载驱动模块,并调用了sys_mknod,在dev目录下建立了相应的节点。
    
    execve("/sbin/modprobe", ["/sbin/modprobe", "-Q", "usb:v05ACp1301d0100dc00dsc00dp00"...]
    ......
    mknod("/dev/sdb", S_IFBLK|0660, makedev(8, 16)) = 0
    ......
    这里modprobe的参数"usb:v05AC..."对应modules.alias中的某个模块。
    
    可以通过udevmonitor来查看内核通过netlink发送给udevd的消息,在shell中运行:
    
    # udevmonitor --env
    然后再插入U盘,就会看到相关的发送给udevd的消息。
    == 内核处理过程 ==:
    
    这里我们以PCI总线为例,来看看在这个过程中,内核是如何处理的。当PCI总线驱动程序扫描到一个新的设备时,会建立一个设备对象,然后调用 pci_bus_add_device()函数,这个函数最终会调用kobject_uevent()通过netlink向用户态的udevd发送消息。
    
    int pci_bus_add_device(struct pci_dev *dev)
    {
    int retval;
    retval = device_add(&dev->dev);
    
    ......
    
    return 0;
    }
    device_add()代码如下:
    
    int device_add(struct device *dev)
    {
    struct device *parent = NULL;
    
    dev = get_device(dev);
    
    ......
    
    error = bus_add_device(dev);
    if (error)
    goto BusError;
    kobject_uevent(&dev->kobj, KOBJ_ADD);
    ......
    }
    device_add()在准备好相关数据结构后,会调用kobject_uevent(),把这个消息发送到用户空间的udevd。
    
    int kobject_uevent(struct kobject *kobj, enum kobject_action action)
    {
    return kobject_uevent_env(kobj, action, NULL);
    }
    int kobject_uevent_env(struct kobject *kobj, enum kobject_action action, char *envp_ext[])
    {
    struct kobj_uevent_env *env;
    const char *action_string = kobject_actions[action];
    const char *devpath = NULL;
    const char *subsystem;
    struct kobject *top_kobj;
    struct kset *kset;
    struct kset_uevent_ops *uevent_ops;
    u64 seq;
    int i = 0;
    int retval = 0;
    
    ......
    
    /* default keys */
    retval = add_uevent_var(env, "ACTION=%s", action_string);
    if (retval)
    goto exit;
    retval = add_uevent_var(env, "DEVPATH=%s", devpath);
    if (retval)
    goto exit;
    retval = add_uevent_var(env, "SUBSYSTEM=%s", subsystem);
    if (retval)
    goto exit;
    
    /* keys passed in from the caller */
    if (envp_ext) {
    for (i = 0; envp_ext[i]; i++) {
    retval = add_uevent_var(env, envp_ext[i]);
    if (retval)
    goto exit;
    }
    }
    
    ......
    
    /* 通过netlink发送消息,这样用户态的udevd进程就会从select()函数返回,并做相应的处理。 */
    #if defined(CONFIG_NET)
    /* send netlink message */
    if (uevent_sock) {
    struct sk_buff *skb;
    size_t len;
    
    /* allocate message with the maximum possible size */
    len = strlen(action_string) + strlen(devpath) + 2;
    skb = alloc_skb(len + env->buflen, GFP_KERNEL);
    if (skb) {
    char *scratch;
    
    /* add header */
    scratch = skb_put(skb, len);
    sprintf(scratch, "%s@%s", action_string, devpath);
    
    /* copy keys to our continuous event payload buffer */
    for (i = 0; i < env->envp_idx; i++) {
    len = strlen(env->envp[i]) + 1;
    scratch = skb_put(skb, len);
    strcpy(scratch, env->envp[i]);
    }
    
    NETLINK_CB(skb).dst_group = 1;
    netlink_broadcast(uevent_sock, skb, 0, 1, GFP_KERNEL);
    }
    }
    #endif
    
    ......
    return retval;
    }
    
    

    5 思考

    现在我们知道/dev目录下的设备文件是由 udevd负责建立的,但是在内核启动过程中,需要mount一个根目录,通常我们的根目录是在硬盘上,比如:/dev/sda1,但是硬盘对应的驱动程序没有加载前,/dev/sda1是不存在的, 如果没有/dev/sda1,就不能通过mount /dev/sda1 /来挂载根目录。另一方面udevd是一个可执行文件,如果连硬盘驱动程序到没有加载,根目录都不存在,udevd就不能运行。如果udevd不能运行,那么就不会自动加载磁盘驱动程序,也就不能自动创建/dev/sda1。这不是死锁了吗?那么你的Linux是怎么启动的呢?

    展开全文
  • 尝试在树莓派安装的raspbian系统上进行linux字符设备驱动 1.更新安装kernel header源码 sudo apt-get update sudo apt-get install raspberrypi-kernel-headers 可以使用uname -r查看当前版本 2.编写模块源码...

    尝试在树莓派安装的raspbian系统上进行linux字符设备驱动

    1.更新安装kernel header源码

    sudo apt-get update
    sudo apt-get install raspberrypi-kernel-headers
    

    可以使用uname -r查看当前版本

    2.编写模块源码mydev1.c

    #include<linux/module.h>
    #include<linux/init.h>
    #include<linux/kernel.h>
    #include<linux/moduleparam.h>
    #include<linux/cdev.h>
    #include<linux/fs.h>
    #include<linux/wait.h>
    #include<linux/poll.h>
    #include<linux/sched.h>
    #include<linux/slab.h>
    
    #define BUFFER_MAX (10)
    #define OK 	   (0)
    #define ERROR	   (-1)
    
    struct cdev *gDev;
    struct file_operations *gFile;
    dev_t devNum;
    
    unsigned int subDevNum=1;
    int reg_major=232;
    int reg_minor=0;
    char *buffer;
    int flag=0;
    int testOpen(struct inode *p,struct file *f)
    {
    	printk(KERN_EMERG"testOpen OK\r\n");
    	return 0;
    }
    
    int testWrite(struct file *f,const char __user *u,size_t s,loff_t *l)
    {
    	printk(KERN_EMERG"testWrite OK\r\n");
    	return 0;
    }
    
    int testRead(struct file *f,char __user *u,size_t s,loff_t *l)
    {
    	printk(KERN_EMERG"testRead OK\r\n");
    	return 0;
    }
    
    int charDrvInit(void)
    {
    	devNum=MKDEV(reg_major,reg_minor);
    
    	if(OK == register_chrdev_region(devNum,subDevNum,"testchar1"))
            {
    	  printk(KERN_EMERG"register_chrdev_region OK\r\n");
    	}
    	else
    	{
    	  printk(KERN_EMERG"register_chrdev_region error\r\n");
    	  return ERROR;
    	}
    	printk(KERN_EMERG"devNum is %d\r\n",devNum);
    	gDev = kzalloc(sizeof(struct cdev),GFP_KERNEL);
    	gFile = kzalloc(sizeof(struct file_operations),GFP_KERNEL);
    
    	gFile->open=testOpen;    //当测试程序调用open函数时会进入testOpen
    	gFile->read=testRead;	//当测试程序调用read函数时会进入testRead
    	gFile->write=testWrite;	//当测试程序调用write函数时会进入testWrite
    	gFile->owner=THIS_MODULE;
    	cdev_init(gDev,gFile);
    	cdev_add(gDev,devNum,3);
    	return 0;
    }
    
    void __exit charDrvExit(void)
    {
      cdev_del(gDev);
      unregister_chrdev_region(devNum,subDevNum);
      return;
    }
    
    module_init(charDrvInit);//insmod **.ko
    module_exit(charDrvExit);//rmmod **.ko
    MODULE_LICENSE("GPL");
    

    创建编写Makefile文件
    sudo vim Makefile

    obj-m :=mydev1.o
    HEAD :=$(shell uname -r)
    KERNEL := /usr/src/linux-headers-$(HEAD)/
    all:
    	make -C $(KERNEL) M=$(shell pwd) modules
    clean:
    	make -C $(KERNEL) M=$(shell pwd) clean
    install:
    	ismod mydev1.ko
    unistall:
    	rmmod mydev1
    

    esc :wq 保存后退出vim

    编译内核模块

    make
    

    成功后可看到如下文件
    在这里插入图片描述
    清空一下dmesg消息

    sudo dmesg -C
    

    加载模块

    sudo insmod mydev1.ko   
    

    可以看到
    在这里插入图片描述
    查看内核缓存信息

    dmesg
    

    可以看到
    在这里插入图片描述

    3.编写测试程序源码testchar1.c

    #include <fcntl.h>
    #include <stdio.h>
    #include <string.h>
    #include <sys/select.h>
    
    #define DATA_NUM (10)
    int main(int argc,char *argv[])
    {
    	int fd,i;
    	int WD,RD;
    	fd_set fdset;
    	char buf[DATA_NUM]="111";
    	memset(buf,0,DATA_NUM);  //清空数组buf
    	fd=open("/dev/testchar1",O_RDWR);
    	   printf("%d\r\n",fd);
    	if(-1==fd)
    	{
    	  perror("open file error\r\n");
    	}
    	else
    	{
    	  printf("open successe\r\n");
    	}
    
    	WD=write(fd,buf,DATA_NUM);
    	RD=read(fd,buf,DATA_NUM);
    	printf("%d %d\r\n",WD,RD);
    	printf("%s\r\n",buf);
    	return 0;
    
    }
    

    编译代码

       sudo gcc -o testchar1 testchar1.c
    

    程序中fd=open("/dev/testchar1",O_RDWR); 所以还需建立/dev/testchar1 目录项及索引节点

    # c表示为字符设备(b表示块设备) 232为主设备号 0为次设备号  
    sudo mknod /dev/testchar1 c 232 0
    

    执行测试程序

    sudo ./testchar1
    

    在这里插入图片描述

    展开全文
  • 文章目录Linux字符型设备驱动之初体验前言框架字符型设备程序实现cdevkobjownerfile_operationsdev_t设备注册过程申请设备号注册设备register_device如何构建模块编译内核编译MakefileKconfig总结参考 前言 驱动总共...
  • Linux内核驱动程序

    千次阅读 2016-06-05 06:43:01
    本文译自Linux.orgDevynCJohnson的系列文章... 在上一篇文章Linux内核:源代码中我们探讨了源代码的组成结构以及各个部分的功能,今天我们来介绍一下Linux的驱动驱动是使内核能够与硬件或协议进行通信或控制的程
  • 本博实时更新《Linux设备驱动开发详解(第3版)》(即《Linux设备驱动开发详解:基于最新的Linux 4.0内核》)的最新进展。 目前已经完成稿件。 2015年8月9日,china-pub开始上线预售: ... 2015年8月20日,各路朋友报喜...
  • Preface  Linux内核对网络驱动程序使用统一的接口,并且对于网络设备采用面向对象的思想设计。  Linux内核采用分层结构... 在Linux内核中,对网络部分按照网络协议层、网络设备层、设备驱动功能层和网络媒介
  • 文章目录Linux platform设备驱动模型前言框架设备驱动的分离设备(device)驱动(driver)匹配(match)参考 前言 为什么要往平台设备驱动迁移?这里需要引入设备,总线,驱动这三个概念。上一篇字符型设备驱动的...
  • Linux内核设备驱动和总线的概念

    千次阅读 2011-04-27 10:40:00
    Linux2.6内核设备模型中有三个重要概念:设备驱动和总线。其中设备设备,总线是总线,驱动驱动,各负其责,严格区分,有各自的内核结构定义。Ø 设备通过struct device结构体定义,但通常将该结构体嵌入到...
  • 深入Linux设备驱动程序内核机制 你懂的 好书 今年新书 驱动
  •  读写设备文件也就是调用系统调用read()和write(),系统调用就是内核提供给应用程序的接口,应用程序对底层的操作大部分都是通过系统调用来完成。几乎所有的系统调用都涉及到内核和应用的数据交换,本节并非讲述...
  • 安卓编译与开发、Linux内核驱动

    万人学习 2015-08-17 17:42:48
    安卓编译与开发、Linux内核驱动视频教程,该课程内容包括一、源码编译:1、常见的ROM种类、谷歌的ROM、第三方的ROM2、区别3、RockChip的ROM、4、编译环境配置、源码下载、编译命令;二、源码开发:源码结构分析、...
  • 编写Java程序调用内核模块 功能为向内核模块虚拟设备写字符串,再从内核模块虚拟设备读出字符串长度。 编译加载内核模块 见《 Android内核开发 Linux C编程调用内核模块设备驱动
  • Linus Torvalds,世界上最伟大的程序员之一,Linux内核的创始人,Git的缔造者,仍然在没日没夜的合并补丁,升级内核。做技术,从来没有终南捷径,拼的就是坐冷板凳的傻劲。 这是一个连阅读都被碎片化的时代,在...
  • Linux设备驱动开发详解:基于最新的Linux 4.0内核
  • Linux设备驱动构建内核

    千次阅读 2013-06-05 08:44:06
    学习Linux设备驱动时第一步就是构建内核树,首先下载Linux内核,按Linux Device Dervier 3作者的说法,只要是linux-2.6.xxx的内核都可以,我的是linux-2.6.30.4。 具体步骤: (1) 下载内核源码包,并解压:[root@...
  • 提取安卓的Linux内核驱动

    千次阅读 2017-03-30 17:05:45
    水平有限,中间可能有各种错误,欢迎拍砖指正。 ... 大家最关心的一个问题是,平板电脑或手机要是安装Linux,驱动的问题怎么解决? 我们知道Linux的驱动是要加载在内核里的,...如果我们能提取安卓的Linux内核驱动
  • linux 内核PCI驱动总结记录

    千次阅读 2018-04-16 16:59:45
    最常用的总线,内核支持最好的总线。ISA裸金属总线,电子爱好者偏爱。 2. PCI的特点 是一种完整的规范,定义计算机计算机不同部分之间的通信。   获取、访问PCI设备。   对比ISA总线三个目标: 比ISA有更好的性能...
  • Linux内核模块编程-字符设备驱动

    千次阅读 2015-11-07 17:30:34
    设备驱动简介设备被大概的分为两类: 字符设备和块设备。 字符设备 提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,键盘、串口、调制解调器都是...
  • 1 Linux设备驱动概述及开发环境构建 1.1 设备驱动的作用 驱使硬件设备行动 1.2 无操作系统时的设备驱动 典型架构:一个无限循环中夹杂着对设备中断的检测或者对设备的轮询 1.3 有操作系统时的设备...
  • Linux内核TUN/TAP设备驱动

    万次阅读 2018-10-26 15:20:39
    Linux内核的TUN/TAP虚拟设备,不同于内核的其它设备,其发送和接收数据包都在网络协议栈内部完成,发送的数据包并不会离开协议栈进入到物理网络中,同样,也不会接收到从物理网络中进入协议栈的数据包。 用户空间的...
  • 深入Linux设备驱动程序内核机制

    千次阅读 2012-02-21 20:24:16
    深入Linux设备驱动程序内核机制 陈学松 著 ISBN978-7-121-15052-4 2012年1月出版 定价:98.00元 ...这是一本系统阐述Linux设备驱动程序技术内幕的专业书籍,它的侧重点不是...作者通过对Linux内核源码抽丝剥茧般的
  • Android下Linux内核网卡驱动移植

    千次阅读 2016-12-30 17:53:18
    1.下载最新的rtl8188cu的linux驱动:  下载页面:http://www.realtek.com/downloads  也可在这里下载:http://download.csdn.net/detail/hanbo622/9725813   2.解压缩下载的文件,把驱动文件rtl8192cu.zip(xxx是...
  • Linux内核驱动自动创建设备节点文件

    千次阅读 2016-03-06 08:28:51
    Linux下生成驱动设备节点文件的方法有3个:1、手动mknod;2、利用devfs;3、利用udev 在刚开始写Linux设备驱动程序的时候,很多时候都是利用mknod命令手动创建设备节点,实际上Linux内核为我们提供了一组函数,...
  • 浅谈 Linux 内核开发之网络设备驱动

    千次阅读 2017-12-12 20:49:21
    转载自 https://www.ibm.com/developerworks/cn/linux/l-cn-networkdriver/个人最近在学习网络设备驱动,本文从宏观上概括,略去了繁琐复杂的细节,易于初学者理解。这里Mark一下,和同样从事驱动开发的兄弟们进行...
  • 现在出现的问题就是,按照裸板上的配置步骤,让开发板在Linux内核模块程序中配置寄存器,insmod能够成功添加模块,但printk打印来看,某些寄存器(LCD寄存器)能够修改,某些寄存器不能修改,修改的方法是一样的。...
  • 驱动linux内核的关系 驱动程序提供的一组设备驱动接口函数Device Driver Interface给操作系统在linux中,这一组设备驱动接口函数一般包括open,close,read,write,ioctl等。 这一组函数是通过一个叫做file ...
  • Linux内核usb驱动框架——U盘挂载

    千次阅读 2012-10-02 23:09:12
    根据师兄的建议,下载了2.6.32的内核源码,在/usr/src/linux-2.6.32.27/drivers/usb/下面可以找到usb-skeleton.c文件,这个就是linux内核给我们提供的usb驱动框架,好好利用这个框架就可以写好linux的usb驱动啦~ ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 241,550
精华内容 96,620
关键字:

linux内核设备驱动

linux 订阅