嵌入式Linux驱动模板精讲与项目实践(txt+pdf+epub+mobi电子书下载)


发布时间:2021-08-03 12:02:16

点击下载

作者:林锡龙

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

嵌入式Linux驱动模板精讲与项目实践

嵌入式Linux驱动模板精讲与项目实践试读:

前言

一、行业背景

嵌入式Linux驱动开发涉及的知识点比较多,要求开发者掌握的技能也比较多,且内核知识点比较分散,对于初学者来说门槛比较高,而嵌入式Linux在各行各业中已被广泛应用,在物联网、通信行业、航空航天、消费电子、汽车电子等行业中急需掌握嵌入式Linux软件开发的研发人员。近年来,随着嵌入式应用越来越广泛,嵌入式Linux开发变得更加复杂,嵌入式Linux驱动开发已经成为嵌入式应用领域的一个重大课题。二、关于本书

本书大部分内容基于专业培训机构特训的讲稿。在专业培训提倡的嵌入式Linux驱动的模板式教学中取得了很好的效果,在此之上结合一线研发经验对驱动开发进行战略性指导,其中很多关键点是作者花了很长时间实实在在整理出来的,旨在揭晓嵌入式Linux驱动中的各种机制,达到知其然且知其所以然的目的。

本书各章内容说明如下。

第1章为驱动总论,是驱动开发在高层次上的介绍。

第2章介绍的内核编程及基础知识点是驱动开发的基础,对驱动开发涉及的Linux内核中的各个知识点进行介绍,旨在扫清对Linux内核阅读的障碍。

第3章介绍驱动模块开发,涉及各种模块移植中常见的设备方法,其中各个模板可以直接应用到实际研发中。

第4章结合Linux操作系统讲解互斥机制在各种情况下的使用,重点分析各种机制的使用注意事项。

第5章重点介绍Linux中断的上下部机制及Linux提供的各种接口,强调中断程序设计的基本理念及设计手段。

第6章介绍Linux驱动中字符设备驱动的高级设备方法。

第7章在Linux子系统的层面上介绍各种高级设备驱动。

第8章重点介绍如何使用虚拟文件系统,这些实用技巧是一线研发的经验总结。

第9章对设备驱动模型各个元素进行讲解,并介绍如何一步步手动建立设备驱动模型模板。

第10章手把手带领读者建立最基本的文件系统,并制作各种常见的文件系统,其中穿插介绍各种实用技巧及实际研发工具。

第11章介绍一线研发人员使用的开发调试技巧,重点强调实用。

第12章结合V字形开发模型在嵌入式Linux驱动开发中的具体实施进行总结。

第13章介绍嵌入式Linux设备驱动编程规范。三、本书特色● 一线研发实战经验积累,所有技巧及讲解使用的工具都可以直接

应用到实际开发工作中。● 采用模板的方式对分散的各种驱动知识点进行讲解,所有模板都

可以直接引用。● 对每个知识点都提供实际案例,从模块的原理介绍,到系统层次

的分析,图文并茂,力求分析透彻。● 提供了大量的驱动例程,这些例程可以快速应用在实际开发中,

读者可以直接运行调试。● 结合实际研发工作对开发过程中的思考进行总结,重在实用。四、作者介绍

本书由林锡龙编著,编著者在写作过程中查阅了大量开源工具及互联网上的资料,对各种资料的作者不能一一列举,在此表示谢意。

由于时间仓促,书中程序和图表较多,错误之处在所难免,请广大读者批评指正。作者电子邮箱:wlxl_1204@163.com。编著者第1章驱动总论1.1 总论1.1.1 驱动在内核中的比例

在Linux内核中,驱动程序的代码量占有相当大的比重。图1.1是一幅Linux内核代码量的统计图(单位:行数),对应的内核版本是2.6.29。从图中可以很明显地看到,在Linux内核中驱动程序(drivers)的代码超过了500万行,所占的比例最高。图1.1 Linux内核代码量统计图1.1.2 驱动开发需要具备的能力

目前,Linux软件工程师大致可分为两个层次。(1)Linux应用软件工程师。Linux应用软件工程师主要利用C库函数和Linux API进行应用软件的编写。(2)Linux固件工程师。Linux固件工程师主要进行Bootloader、Linux的移植及Linux设备驱动程序的设计。

一般而言,对固件工程师的要求要高于应用软件工程师,而其中的Linux设备驱动编程又是Linux程序设计中比较复杂的部分,究其原因,主要包括如下几个方面。(1)设备驱动属于Linux内核的部分,编写Linux设备驱动需要有一定的Linux操作系统内核基础。(2)编写Linux设备驱动需要对硬件原理有相当的了解,大多数情况下是针对一个特定的嵌入式硬件平台编写驱动的。(3)Linux设备驱动中广泛涉及多进程并发的同步、互斥等控制,容易出现bug。(4)由于Linux设备驱动属于内核的一部分,所以它的调试也相当复杂。

Linux设备驱动的开发要求比较高,要求开发人员掌握一定的硬件知识、Linux内核技能、操作系统并发概念和较高的软件编程驾驭能力,Linux设备驱动程序作为内核的一部分运行,像其他内核代码一样,如果出错将导致系统严重损伤。一个编写不当的驱动程序甚至会导致系统崩溃,导致文件系统破坏和数据丢失,所以对Linux设备驱动开发比对应用程序要求高得多。

Linux驱动的编写涉及如下主题。(1)内核模块、驱动程序的结构。(2)驱动程序中的并发控制。(3)驱动程序中的中断处理。(4)驱动程序中的定时器。(5)驱动程序中的I/O与内存访问。(6)驱动程序与用户程序的通信。

实际内容错综复杂,掌握起来也有难度,但从本质上来说,这些内容仅分为两类。(1)设备的访问。(2)对设备访问的控制。

前者是目的,而为了达到访问的目的,又需要借助并发控制等辅助手段。1.1.3 驱动开发重点关注的内容

初看起来Linux设备驱动开发涉及的内容很多,而需要实现驱动的设备又千差万别,其实质主要包括以下几点内容。(1)对驱动进行分类,先归纳为是哪种类型的驱动,归类正确再利用内核提供的子系统进行开发,这时往往会发现很多通用的事情内核已经做了。一个优秀的驱动工程师应该最大限度地利用内核的资源,因为内核已经实现的毕竟稳定性强、可移植性高。(2)找到内核提供的子系统后,接下来就是要制作该子系统对该类设备提供的表征,也就是描述该类驱动的结构体,然后定义这个结构体,把必要的数据进行初始化,最后调用该内核子系统提供的接口函数提交给内核管理。这是大部分驱动程序开发的战略流程。(3)明确子系统已经做了什么,需要在自己驱动中实现哪些内容,通常的做法是找一个接近的驱动程序进行修改,而不是一行一行地对代码进行编写。到内核中找接近的驱动例程是一个又快又好的方法。这些例程基本上都提供接口如何使用、调用流程等,借鉴已有例子可以避免低级错误。(4)以上都是与内核接口有关的,驱动另一个涉及的就是芯片手册,这个与做其他嵌入式软件一致,故对从单片机软件开发或者其他操作系统软件开发转过来做Linux驱动开发的人员来说,这部分是一个强项。(5)驱动的另外一个内容就是协议,包括各种嵌入式总线协议,从简单的SPI到复杂的PCI 或者USB等。协议的基本知识是需要掌握的,好在内核对各种常见协议都是以子系统的形式提供的,在子系统中做了大部分共性工作,所以大大降低了驱动开发的工作量。

综上所述,学好驱动开发,一个重要的方面就是对内核的学习,熟悉内核的组织和思维方式。1.2 驱动理论与思想

Linux对驱动的管理有自身的一套方法并提供相应的机制,但其具体实现可以由开发者自己决定。这一点可能比较抽象,但真正接触到实践之后,你会发现相同的一个功能,如点灯驱动,有很多实现方式——简单字符方式、cdev方式、mics实现、设备驱动模型等,对于设备的管理实现也有多种方法,只要你喜欢你可以写出很多种方式的驱动。正因为存在这么多种形式的驱动,初学者最初会比较茫然,进而觉得复杂,再后来就会感觉很灵活,然后感觉很多事情Linux已经做好了,你要做的就是熟悉一下它所提供的机制,然后正确调用其所提供的接口实现功能。

从2.4到2.6,Linux内核在可装载模块机制、设备模型、一些核心API等方面发生较大改变。特别是引入设备模型,设备模型是一个难点,将由后续专门章节介绍。本章作为驱动理论将在一个更高的角度来总结内核对各种驱动所提供的相似点。之所以先提及设备模型,个人觉得真正理解Linux的驱动应该以设备模型为主线,设备驱动模型的提出涉及驱动管理方式的变化,如从原来手动创建设备文件到自动创建设备文件这个变化Linux引入了Udev系统,再展开来说,从设备模型中引入各种子系统、总线等概念,对这些进一步深入可以发现Linux提供各种驱动的core,如I2C-core,input-core,rtc-core,serial_core等,如果打开阅读内核代码,你会发现在内核代码driver下对每种设备都进行了分类,而且每个文件夹下基本都有一个core文件,这些core文件已经对相应类型的驱动进行通用属性和行为的封装。市场上很多Linux驱动的书籍基本上都会对每种驱动以一个章节进行描述。在某种程度上讲,随着内核的发展,在驱动上尽量将各种类型的设备进行抽象,由内核里底层的代码,如总线驱动或此类设备共用的核心模块来实现共性的工作,从而简化设备驱动开发。

设备模型的引入既在驱动设备管理上进行了分类又在文件系统上进行了统一。“一切皆文件”是Linux的设计思想,可以在Linux下的sys虚拟文件系统中看到class文件夹下有各种类型设备的相应文件夹。系统对设备的管理系统Udev也是按照class下的文件进行设备文件创建的。当然这也只是众多主线之一,还有总线、设备、驱动等概念,提出这些概念及响应机制的目的是为了简化设备驱动的开发、管理与维护。Linux博大精深,且不断发展,新的特性不断涌现,知识点众多,为开发者和用户不断提供新的模块,我们必须抓住主线,逐个深入,长期积累。

现在不少嵌入式驱动开发者在项目中只采用基本的属性行为,很少或者基本用不上高级特性功能,采用IOCTL方式进行用户与驱动之间的对话式交互,通过编制上下对应的IOCTL命令即可完成绝大部分工作。在对驱动开发应聘者的面试中可以发现,很多应聘者对设备模型理解不多或者不清楚,这对深入理解新内核驱动显然是不够的。1.2.1 分类思想

Linux对各种各样的设备进行分类,一般分为字符型设备、块设备和网络设备3种。但在内核中对于一个具体的设备还有细分,而且整个内核是按照不同细分种类的设备来提供支持的。在系统运行之后查看一下sys/class,图1.2所示为显示sys/class目录情况。图1.2 sys/class目录

并且可以看到各种类型的驱动。对于具体驱动来说,Linux内核还保留了一些固定的主设备号,在内核代码include\linux/Major.h中定义了一些默认的类型主设备号。比如: #define MISC_MAJOR 10 /*混杂型设备*/ #define SCSI_CDROM_MAJOR 11 #define MUX_MAJOR 11 #define XT_DISK_MAJOR 13 #define INPUT_MAJOR 13 /*输入型设备*/

对于一个新的驱动来说,首先必须要确定一下要把它当哪类设备来处理,也就是归为哪类设备,然后再看内核在这类设备中提供的支持,最后调用这类设备的接口函数进行处理。当然,如果一个新驱动有接近类似的,那么还是在接近的驱动基础上进行修改。1.2.2 分层思想

对Linux的input、RTC、MTD、I2C、SPI、TTY、USB等诸多设备驱动进行分析,可以看到大致都是按照分层次来设计的。市面上很多介绍Linux驱动的书籍在章节编排上一般先介绍该类型的硬件知识,再介绍协议和相关操作,然后再对实际例子进行分析。实际上,协议和相关操作是归纳在内核相对应的core或者类似core相关的文件中的。这样对于一个具体设备来说并不需要对该部分再进行一次编写。这就是分层思想带来的好处。

Linux内核分层的框架设计用到了面向对象的设计思想。在设备驱动方面,往往为同类设备设计了一个框架,而框架中的核心层则实现了该设备通用的一些功能。如果具体设备不想使用核心层的函数则可以对其进行实现重载。图1.3所示为驱动核心层与实例之间的关系图。图1.3 驱动核心层与实例之间的关系图1.input子系统层次

input子系统整体系统框架如图1.4所示,整个系统是按一定层次进行划分和组织的。图1.4 input子系统整体系统框架图

再回到内核相应的路径文件夹drivers/input下,图1.5所示为看到的输入子系统相关文件。图1.5 Linux输入子系统相关文件

这些文件分为事件层、核心层和处理层。每个文件夹对应的是一种类型的输入设备,会产生事件报告到核心层,核心层决定调用哪些处理层对事件进行分发,最后发送到用户空间相应的设备文件下。图1.6所示为input子系统文件层次示意图。图1.6 input子系统文件层次示意图

这是在Linux-2.6.24版本下的标准内核,当然如果有厂家需要自己添加相应的驱动即可以在事件层中添加底层操作的一个文件夹,然后在处理层中添加一个相应的文件并添加到input系统中。2.RTC子系统层次

接着移到drivers/rtc目录下,该目录下的文件并没有像input下那样组织,但是按照前面提过的Linux很多地方采用面向对象的方法抽象共同点,然后在实例化的指导下我们可以看出,下面这些rtc-×××.c带有型号的文件都是实例化的驱动。图1.7所示为Linux中RTC实例化文件。图1.7 Linux中RTC实例化文件

在RTC系统中使用的是platform总线,platform总线在后面设备模型中会重点介绍。这里只要理解为设备模型中“总线,驱动,设备”中的一种总线即可,而这些实例化的rtc-×××.c文件就是模型中的驱动,需要实现的是platform_driver结构,设备则放在相应体系结构中对应的目录下,如arch/arm/plat-s3c24xx/下,需要实现的是platform_device。Linux已经提供platform总线,驱动是对应某个型号的芯片,基本上可以在drivers/rtc目录下找到,那么驱动开发实际上就是完成系统中各个器件相应的设备描述即可,这就是驱动开发所要做的,而且熟悉之后工作量不是很大。至此,设备模型中的三个要素已经具备,只要驱动和设备注册成功且匹配,实际芯片就能正常工作。

下面看一个采用Linux的RTC系统运行的例子,如图1.8所示。图1.8 RTC系统运行实例

这个class中的rtc是在哪儿实现的呢?看一下drivers/rtc下的class.c文件: static int __init rtc_init(void) { rtc_class = class_create(THIS_MODULE, "rtc"); … }

这是RTC系统核心层附属的class模块自动做的。也就是说,该子系统不但把RTC相关的共同操作提供了,而且还附带送上驱动模型这一套linux-2.6中的亮点特性。

接下来回到本节主题,图1.9所示为RTC系统的层次结构图。图1.9 RTC系统的层次结构图

当系统运行起来之后,再看一下由rtc-proc.c所创建的proc虚拟文件系统文件: [root@FriendlyARM driver]# pwd /proc/driver [root@FriendlyARM driver]# cat rtc rtc_time : 05:21:48 rtc_date : 2000-05-05 alrm_time : 00:00:00 alrm_date : 1970-01-01 alarm_IRQ : no alrm_pending : no 24hr : yes periodic_IRQ : no

还有由class.c所创建的sys虚拟文件系统文件: [root@FriendlyARM class]# pwd /sys/class [root@FriendlyARM class]# ls bdi input net scsi_generic vc firmware leds ppp scsi_host video4linux graphics mem pvrusb2 sound vtconsole hwmon misc rtc tty i2c-adapter mmc_host scsi_device ubi i2c-dev mtd scsi_disk usb_device [root@FriendlyARM class]# ls rtc rtc0

提示一下,该系统设备文件操作在RTC子系统核心rtc-dev.c文件中。 static const struct file_operations rtc_dev_fops = { .owner = THIS_MODULE, .llseek = no_llseek, .read = rtc_dev_read, .poll = rtc_dev_poll, .ioctl = rtc_dev_ioctl, .open = rtc_dev_open, .release = rtc_dev_release, .fasync = rtc_dev_fasync, };3.MTD子系统层次

相对而言,MTD会比较复杂,图1.10所示为Linux中MTD所处的层次。图1.10 Linux中MTD所处的层次

图1.11所示为MTD子系统框架图。图1.11 MTD子系统框架图

对于MTD子系统的优点,简单解释就是它实现了驱动设计者要去实现的很多功能。换句话说,有了MTD,使得你设计Nand Flash的驱动所要做的事情要少很多很多,因为大部分工作都由MTD做好了。

MTD比较复杂,在此只做简单介绍,后面有专门小节进行详细描述,这里可以按照前面介绍的方式对sys/class/mtd下的目录进行查看。 [root@FriendlyARM class]# ls mtd mtd0 mtd0ro mtd1 mtd1ro mtd2 mtd2ro [root@FriendlyARM class]# cat mtd/mtd0/name Bootloader [root@FriendlyARM class]# cat mtd/mtd0/size 4194304

在Linux中,一个类别的设备驱动被归结为一个子系统,如PCI子系统、input子系统、usb子系统、scsi子系统等。在内核文件结构中,drivers目录下第一层中的每个目录都算一个子系统,每个目录都代表一类设备。

每个子系统中都有一个subsys_initcall宏,例如: subsys_initcall(init_scsi); scsi子系统 subsys_initcall(input_init); input子系统 subsys_initcall(usb_init); usb子系统 subsys_initcall(misc_init); misc子系统

针对某个子系统内核使用subsys_initcall宏来指定初始化函数。使用subsys_initcall宏表示该部分代码比较核心,应该视为一个子系统,而不仅仅是一个内核模块。内核中将同类的驱动以子系统的方式进行管理,在子系统中抽象出共性部分,有些以core方式出现,如I2C-core,input-core等,并对外提供主要接口函数,而对于驱动开发者来说就要最大限度地使用内核提供的子系统,对数据结构进行初始化并按照子系统提供的接口函数注册到相应的子系统中,简化驱动开发。

关于子系统之间的消息传递,内核有自己的一套机制,称为内核通知链,内核通知链在后面章节介绍。1.2.3 分离思想

在内核大部分驱动中采用的是设备驱动模型,设备驱动模型中重要的三角关系就是总线、驱动和设备。其中,驱动和设备就是分离思想的体现,实现了驱动和资源的分离,如platform总线,在内核中支持了大部分驱动,抽象出来的就是设备的注册,往往需要做的接口就是Resource的传递。Resource是设备资源的体现,通过设备驱动模型传递到驱动中,采用设备和驱动都向总线注册的方式工作。

这种分离思想所带来的好处就是驱动开发者真正要做的就是对资源的使用,将设备对应的资源注册到模型中,具体驱动的实现都是类似的。具体设备和相应驱动可以理解为成员和行为的关系,这两者的关系在设计上采用的就是分离思想。

分离思想也为驱动设计带来很强的可移植性,同时也为驱动设计带来灵活性,另一方面也增加了系统的复杂度。在I2C驱动中采用适配器与从设备分离的设计方式是一个很好的说明。图1.12所示为I2C多控制器与多设备关系例图。图1.12 I2C多控制器与多设备关系例图

假如有3个CPU的I2C适配器A、B、C控制三个从设备a、b、c,如果直接控制,则必须要实现9个读写函数,即 A_ReadWrite_a();A_ReadWrite_b();A_ReadWrite_c(); B_ReadWrite_a();B_ReadWrite_b();B_ReadWrite_c(); C_ReadWrite_a();C_ReadWrite_b();C_ReadWrite_c();

可以看出,如果有N个适配器和M个从设备,那么将需要N×M个驱动,并且这N×M个驱动程序必然有很多重复代码,而且不利于驱动程序的移植。

采用分离的思想是将适配器和从设备分离,通过总线驱动的方式进行管理,适配器和从设备分别注册到系统的I2C-core中。每个适配器和从设备都有相应的描述,分离之后只需要N + M个驱动描述,而且适配器和从设备之间几乎没有耦合性,增加一个适配器或者从设备并不会影响其余驱动。1.2.4 总线思想

总线是设备模型中核心三角关系之一,也是联系驱动和设备的纽带。采用总线管理方式作为处理器与设备之间的通道,将所有设备都通过总线相连。对于没有相应总线的设备可以归到“platform”虚拟总线上,如CPU上的片上外设。

通过总线的管理,驱动和设备分别向某条总线注册,一旦两者匹配,设备就可以找到相应的驱动,这样对于多个设备来说,只要注册不同的设备device即可。

总线的核心作用是:总线相关代码屏蔽了大量底层琐碎的技术细节,为驱动程序员提供了一组使用友好的接口,简化了驱动程序开发工作。

关于总线,同样地利用sys文件系统可以观察到系统已经存在的总线,图1.13所示为系统总线情况。图1.13 系统总线情况

这种思想的好处是可以在不同体系的CPU中进行快速移植,而且驱动与具体设备分开,另外一个最重要的好处就是对设备的管理。本章小结

本章主要对驱动开发的情况在一定程度上进行描述,对Linux内核的一些思想进行总结,这些优秀的软件思想除了给我们学习使用内核、编写驱动提供很大帮助之外,很多优秀的设计思想也是软件开发中在总体设计上可以借鉴的。

本章内容初学起来比较抽象,可以在后面章节学习之后再回过头来再学习一遍,或者在做了一段时间驱动之后再回过头来学习,对比编写过的驱动进行理解。第2章内核编程及基础知识点2.1 内核线程

内核线程就是内核的分身,一个分身可以处理一件特定事情。这在处理异步事件,如异步IO时特别有用。内核线程的使用是廉价的,唯一使用的资源就是内核栈和上下文切换时保存寄存器的空间。支持多线程的内核叫做多线程内核(Multi-Threads Kernel)。

内核线程跟普通进程之间最大的区别就是只运行在内核态,不受用户态上下文的拖累。另外,内核线程没有独立的地址空间,mm(内存管理结构)指针被设置为NULL,它只在内核空间运行,从来不切换到用户空间中,并且和普通进程一样,可以被调度,也可以被抢占。

内核线程与普通进程之间的比较如下。(1)内核线程只运行在内核态,而普通进程既可以运行在内核态(运行在内核态,如进行系统调用时进入内核态),也可以运行在用户态。(2)因为内核线程只运行在内核态,所以它们只使用大于PAGE_OFFSET的线性地址空间。另一方面,不管是在用户态还是在内核态,普通进程都可以用4GB的线性地址空间。(3)内核线程由系统内核负责创建、调度和管理。内核可以直接调度同一进程包含的所有线程,在多处理器系统中能使这些线程并发执行,同时克服了不同进程的线程之间的不公平。

用户线程执行一个导致阻塞的系统调用时会导致整个进程阻塞,即使该进程的其他线程仍具备运行条件;另外,若操作系统是以进程为单位调度的,则分配给进程的时间片就由该进程的所有线程分享,在不同进程的线程之间会产生不公平现象。

内核线程一旦启动起来会一直运行,除非该线程主动调用do_exit函数,或者其他进程调用kthread_stop函数结束线程的运行。1.内核线程的编写

1)所需头文件 #include //wake_up_process() #include //kthread_create()、kthread_run() #include //IS_ERR()、PTR_ERR()

2)模板 句柄:struct task_struct * kThread = NULL;(1)线程函数(结合kernel_run使用) static int threadFun(void *data) { … while(!kthread_should_stop()) { Dosomething(); schedule_timeout(HZ); //让出CPU运行其他线程,并在指定的时间内重新被调度 } 资源释放 return 0; }(2)线程启动

kernel_thread是主要的创建线程实现函数,为最原始函数,最终调用do_fork函数。kernel_thread需要使用deamonize释放资源并挂到init下,还需要使用complete等待这一过程的完成。

模板: kernel_thread(fun, s, CLONE_KERNEL); int fun(void *arg) { wait_queue_t wait; deamonize("kbnepd %s", name); init_waitqueue_entry(&wait, current); while (!atomic_read(&s->killed)) { set_current_state(TASK_INTERRUPTIBLE); ... schedule(); } set_current_state(TASK_RUNNING); return 0; }

使用kthread_create不马上运行,需要wake_up_process。使用kthread_run函数实现线程的创建和启动,是kthread_create和wake_up_process的组合。 kThread = kthread_run(threadFun,"hello world",线程名称);

kthread_run函数是创建线程kthread_create()和激活线程wake_up_process()的封装,内核提供比较简洁的接口kthread_run,一步到位。(3)结束线程

在模块卸载时,可以结束线程的运行。使用下面的函数: int kthread_stop(struct task_struct *k);

模板: static void test_cleanup_module(void) { if(test_task){ kthread_stop(kThread); //发信号给task,通知其可以退出了 kThread = NULL; } } module_exit(test_cleanup_module);

在执行kthread_stop时,目标线程必须没有退出,否则会oops。原因很容易理解,当目标线程退出时,其对应的task结构也变得无效,kthread_stop引用该无效task结构就会出错。

为了避免这种情况的发生,需要确保线程没有退出,其方法如代码中所示。 thread_func() { while(!thread_could_stop()) { wait(); } } exit_code() { kthread_stop(_task); //发信号给 task,通知其可以退出了 }

这种退出机制很温和,一切尽在thread_func()的掌控之中,线程在退出时可以从容地释放资源。

3)注意事项(1)在调用kthread_stop函数时,线程函数不能已经运行结束。否则,kthread_stop函数会一直等待。(2)线程函数必须能让出CPU,以便能运行其他线程,同时线程函数也必须能重新被调度运行。在例子程序中,这是通过schedule_timeout()函数完成的。不能使用mdelay让出CPU,而应该使用schedule_timeout(),否则将导致系统无法响应。

可以使用msleep_interruptible让出CPU,在线程运行之后查看top可以看出,CPU的占用率很低,而使用schedule_timeout对CPU的占用率比较高。

4)性能测试

可以使用top命令来查看线程(包括内核线程)的CPU占用率。命令如下: top -p线程号

可以使用下面命令来查找线程号: ps aux|grep线程名

注:线程名由kthread_create函数的第三个参数指定。2.内核线程例子

创建内核线程,每隔1s打印一次。 #include #include #include #include #include #include #include #include #include #include #include #ifndef SLEEP_MILLI_SEC #define SLEEP_MILLI_SEC(nMilliSec)\ do { \ long timeout = (nMilliSec) * HZ / 1000; \ while(timeout > 0) \ { \ timeout = schedule_timeout(timeout); \ 延时不到,继续调度 } \ }while(0); #endif static struct task_struct * kThread = NULL; static int kThreadFun (void *data) { char *mydata = kmalloc(strlen(data)+1,GFP_KERNEL); memset(mydata,'\0',strlen(data)+1); strncpy(mydata,data,strlen(data)); while(!kthread_should_stop()) { SLEEP_MILLI_SEC(1000); printk("%s\n",mydata); } kfree(mydata); return 0; } static int __init init_kthread(void) { kThread = kthread_run(kThreadFun,"lxlong","kThread"); return 0; } static void __exit exit_kthread(void) { if(kThread) {       printk("stop kThread\n");       kthread_stop(kThread); } } module_init(init_kthread); module_exit(exit_kthread); MODULE_AUTHOR("lxlong"); MODULE_LICENSE("GPL");2.2 内核定时器

内核定时器是基于软中断的基础实现的,其作用是在未来某个时间点到达时执行一个相应的动作,该相应的动作就是调用定时器绑定的函数。通常很多情况下需要定时执行一个动作,或者周期性地执行一个操作,如定时查询某个状态、定时打印、喂看门狗等,这在实际系统中应用很广泛。

内核中维护定时器是通过一个定时器链表来实现的,一旦一个定时器被添加到系统中,即该定时器会被连接到定时器链表上,内核系统会不断查询该链表,检测在定时器链表上是否有时间到达的定时器,一旦发现定时器时间到达,即触发软中断去调用并执行该定时器绑定的函数。定时器在初始化时绑定的函数类似于定时器的中断服务程序。图2.1为内核定时器链表图。图2.1 内核定时器链表图

定时器包含文件: #include #include

内核对象表征结构体: struct timer_list timer;

初始化: init_timer(&timer); timer.data = times; 传给 function的参数 timer.expires = jiffies + HZ;   1个jiffies大约为 1ms,1HZ为 1s timer.function = timer_funtion; add_timer(&timer); 将定时器添加到系统中

绑定函数的实现: void timer_funtion(unsigned long para) { 传入的参数 unsigned long para为初始化时初始化的参数 timer.data = times; 若需要重复周期性地执行该定时器,即调用 mod_timer(&timer,时间); }

修改定时: mod_timer(&timer,jiffies + (HZ * 2));实际上是先杀死定时器,重新设置时间大小,再启动定时器

定时器的删除: del_timer(&timer);一般使用del_timer_sysnc()代替del_timer(),前者立即删除不执行,后者会继续执行到下一个到期

定时器过期后会被系统自动删除,除非调用mod_timer函数重新启动定时器。del_timer(&timer)在定时器没有过期的情况下才有意义。

泛滥使用定时器会导致系统效率下降,因为定时器是基于中断实现的,并且在中断中定时器去操作硬件要考虑与进入定时器中断前的操作硬件之间的互斥问题。【案例分析】定时器使用不当。

在驱动中使用定时器,加载驱动正常,运行正常。当卸载时出现: Rmmod Drv

系统崩溃出现以下错误: # [ 135.961105] Unable to handle kernel paging request for data at address 0xe30aade8 [ 135.968588] Faulting instruction address: 0xc0025d28 [ 135.973548] Oops: Kernel access of bad area, sig: 11 [#1] … [ 136.111687] Kernel panic - not syncing: Fatal exception in interrupt

分析加载驱动和卸载驱动过程:

加载: dev_t devno; devno = MKDEV(DRV_MAJOR,0); result = register_chrdev_region(devno,1,"Drv"); cdev_init(&Drv_dev_P->Drv_c_dev, &Drv_ops); result = cdev_add(&Drv_dev_P->Drv_c_dev, devno, 1); init_timer(&Drv _dev_P->timer); Drv _dev_P->timer.function = (void *)&dspDogFun Drv _dev_P->timer.expires = jiffies + HZ; mod_timer(&Drv _dev_P->timer,jiffies + 10); return 0;

卸载: cdev_del(&Drv _dev_P-> Drv _c_dev); unregister_chrdev(MKDEV(DRV_MAJOR,0),"DRV");

卸载过程为加载过程的相反过程,在加载时申请的资源应该在卸载中释放。从以上过程来看,定时器在加载时初始化,但在卸载时没有注销,所以在rmmod该驱动时会出现系统崩溃。将卸载改为: del_timer(&Drv _dev_P->timer); cdev_del(&Drv _dev_P->msp_c_dev); unregister_chrdev(MKDEV(DRV _MAJOR,0),"DRV");

编译之后加载,使用rmmod卸载成功。【小结】

包含文件: #include #include

对象:struct timer_list timer;

初始化: init_timer(&timer); timer.data = times; 传给 function的参数 timer.expires = jiffies + HZ;  1个jiffies大约为 1ms,1HZ为 1s timer.function = timer_funtion; ――>void timer_funtion(unsigned long para) add_timer(&timer);

修改定时: mod_timer(&timer,jiffies + (HZ * 2));实际上是先杀死定时器,重新设置时间大小,再启动定时器

定时器过期后会被系统自动删除。del_timer(&timer)在定时器没有过期的情况下才有意义。

按照毫秒或者微秒的量度: unsigned int jiffies_to_msecs(const unsigned long j); unsigned int jiffies_to_usecs(const unsigned long j); unsigned long msecs_to_jiffies(const unsigned int m); unsigned long usecs_to_jiffies(const unsigned int u);2.3 链表

包含头文件:#include

主要结构: struct list_head { struct list_head *next, *prev; };

使用时将list_head放在实际数据结构中,相当于list_head承载一个数据结构。如: struct student{ char name[100]; int num; struct list_head list; };

链表相关例子参考在所附光盘中。

主要接口函数: struct list_head student_list;

初始化链表: INIT_LIST_HEAD(&student_list);

在链表尾部添加成员元素: list_add(&(pstudent[i].list),&student_list);

扫描链表: struct list_head *pos; list_for_each(pos,&student_list)到链表尾部才结束。

取出链表元素: list_entry(pos,struct student,list);

Linux内核代码中已经提供了对链表的基本操作,在include/linux/list.h中包含链表初始化、插入、删除、搬移、合并和遍历等操作,在驱动中需要时直接使用即可。2.4 内存操作

Linux2.6所有的内存分配函数包含在头文件中,而原来的不存在。老版本内存分配函数包含在头文件中。

内核最下层申请函数为: get_free_pages() <----> free_pages()

常用内存分配函数如下。(1)__get_free_pages unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

__get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址。__get_free_pages在实现上只是封装了alloc_pages函数。(2)kmem_cache_alloc struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*, struct kmem_cache *, unsigned long), void (*dtor)(void*, struct kmem_cache *, unsigned long)) void *kmem_cache_alloc(struct kmem_cache *c, gfp_t flags)

kmem_cache_create/kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合。首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从该高速缓存区域中获取新的内存块。kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏定义,在默认的2.6.18内核版本中,该宏定义为5,于是一次最多能申请(1<<5)* 4KB,也就是128KB的连续物理内存。分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()。(3)kmalloc void *kmalloc(size_t size, gfp_t flags)

kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/kmalloc_size.h的内容来决定,在默认的2.6.18内核版本中,kmalloc一次最多能申请的大小为131072B,也就是128KB的连续物理内存。测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译则不能通过。(4)vmalloc void *vmalloc(unsigned long size)

前面几种内存分配方式都是物理连续的,能保证较短的平均访问时间。但是在某些场合,对内存区的请求不是很频繁,较长的内存访问时间也可以接受,这时就可以分配一段线性连续物理不连续的地址,所带来的好处是一次可以分配较大块的内存。vmalloc对一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。(5)dma_alloc_coherent void *dma_alloc_coherent(struct device *dev, size_t size,ma_addr_t *dma_handle, gfp_t gfp)

DMA是一种硬件机制,允许外围设备和主存之间直接传输I/O数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的吞吐量。在DMA操作中,涉及CPU高速缓存和对应的内存数据一致性问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很好地解决了这个问题,dma_alloc_coherent和__get_free_pages函数的实现差别不大,两者最终都调用_alloc_pages函数来分配内存,它们所分配内存的大小限制一样,另外两者分配的内存都可以用于DMA操作。(6)ioremap void * ioremap (unsigned long offset, unsigned long size)

ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的I/O空间。ioremap能映射的内存由原有的物理内存空间决定。(7)Boot Memory

如果要分配大量的连续物理内存,则上述分配函数都不能满足,只能用比较特殊的方式在Linux内核引导阶段来预留部分内存。

在内核引导时分配内存: void* alloc_bootmem(unsigned long size)

可以在Linux内核引导过程中绕过伙伴系统来分配大块内存。使用方法是在Linux内核引导时,调用mem_init函数之前用alloc_bootmem函数申请指定大小的内存。如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导出,然后就可以使用这块内存。这种内存分配方式的缺点是,申请内存的代码必须链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统看不到这部分内存,需要用户自行管理。测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块。

通过内核引导参数预留顶部内存:

在Linux内核引导时,传入参数“mem=size”保留顶部的内存空间。比如,系统有256MB内存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存。

几种分配函数的比较如表2.1所示。表2.1 几种分配函数的比较最函数大分配原理其他名内存_get_f4M适用于分配较大量的连续物理内ree_p直接对页框进行操作B存ageskmem_cach128适合需要频繁申请释放相同大小基于slab机制实现e_alloKB内存块时使用c基于kmallo128最常见的分配方式,需要小于页kmem_cache_alloc实cKB框大小的内存时可以使用现物理不连续,适合需要大内存,vmallo建立非连续物理内存但是对地址连续性没有要求的场c到虚拟地址的映射合dma_alloc_c基于__alloc_pages实4M适用于DMA操作ohere现Bntiorema实现已知物理地址到适用于物理地址已知的场合,如p虚拟地址的映射设备驱动alloc_在启动kernel时,预小于物理内存大小,内存管理要bootm留一段内存,内核看求较高em不见2.5 I/O端口

几乎每种外设都通过读/写设备上的寄存器来访问,外设寄存器也称“I/O端口”,通常包括控制寄存器、状态寄存器和数据寄存器三大类,而一个外设寄存器通常被连续编址,描述的就是CPU对外设访问的方式取决于CPU对外设I/O端口编址的方式。

ARM、M68K等只有一个物理空间,统一编址,外设I/O端口物理地址被映射到CPU单一物理地址空间,成为统一编址的一部分,即访问这种I/O地址无须专门外设的I/O指令。

硬件上的差异对软件来说是完全透明的,可以将内存映射方式的I/O端口和外设内存统一看作I/O内存资源。X86等为外设专门实现一个独立空间,称为I/O地址空间,独立编址,与内存地址空间分开,并有专门的I/O指令(如X86的IN和OUT指令)访问。

Linux在io.h头文件中声明了ioremap(),用来将I/O内存资源的物理地址映射到核心虚拟地址空间(3~4GB)中。相应的取消映射函数为iounmap()。2.6 内核相关宏1.__init

位置:/include/linux/init.h

定义:#define __init __attribute__ ((__section__ (".init.text")))

注释:这个标志符和函数声明放在一起,表示gcc编译器在编译时需要把这个函数放在.text.init Section中,而这个Section在内核完成初始化之后就会被释放掉。2.__initdata

位置:/include/linux/init.h

定义:#define __initdata __attribute__ ((__section__ (".init.data")))

注释:这个标志符和变量声明放在一起,表示gcc编译器在编译时需要把这个变量放在.data.init Section中,而这个Section在内核完

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载