深入理解Linux驱动程序设计(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-29 03:09:00

点击下载

作者:吴国伟、姚琳、毕成龙

出版社:清华大学出版社

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

深入理解Linux驱动程序设计

深入理解Linux驱动程序设计试读:

内容简介

本书基于Linux内核3.8.13源代码及相关实例向读者系统而详尽地介绍和分析了Linux设备驱动程序开发框架、原理和方法。全书共分13章,内容包括字符设备、块设备、网络设备、MMC/SD驱动、USB驱动、总线驱动及Flash驱动的开发机制和实例。

本书各章均首先概要介绍各模块的实现原理,随后列举各模块中的关键数据结构,再结合源代码及实例分析介绍,让读者更全面地了解Linux驱动开发。

本书内容丰富,概念和原理讲解细致、深入浅出。其中,有关代码的部分都标有注释以详细介绍功能,书中的设计和分析也配以编程实例帮助理解。

本书适合作为高年级本科生、研究生和从事嵌入式系统开发设计的工程技术人员。

作者简介

吴国伟大连理工大学软件学院教授,博士生导师。长期讲授“操作系统”、“嵌入式操作系统”方面的课程,著有畅销图书《嵌入式操作系统应用开发》、《Linux内核分析与高级编程》和《嵌入式系统原理与设计》。

前言PREFACE

Linux从1991年发布第一个版本到现在的3.19.3版,经过无数开发者的共同努力,内核日趋完善。Linux作为一种开源、跨平台的操作系统,受到了越来越多开发者的青睐。

随着物联网和人工智能的发展,Linux将更多地应用在嵌入式设备中,这对Linux内核中的驱动设计和实现也提出了更高的要求。现有的介绍Linux设备驱动开发的图书中,有的偏重于内核各模块的结构和原理的阐述,难以理解和掌握;有的侧重Linux内核的部分特征及应用,缺少对Linux架构整体的介绍及系统原理的分析。基于这样的现状,编写此书供广大Linux爱好者参考。

本书结合Linux内核中各模块的原理及设备驱动实例,详细地介绍了Linux设备驱动开发的方法与实践。全书共分为13章,首先介绍了Linux操作系统的发展,然后针对Linux内核3.8.13全面介绍了Linux设备驱动开发,分析了各模块的Linux实现并给出了驱动开发实例。在介绍了Lnux内核机制的基础上,着重论述块设备、网络设备、MMC/SD驱动、USB驱动、总线驱动及Flash驱动的开发。

全书各章均首先概要介绍各模块的实现原理,随后列举各模块中的关键数据结构,再结合源代码及实例分析介绍,让读者更全面地了解Linux驱动开发。

本书编写过程中参考了众多Linux开发者的研究成果和相关书籍,参考文献中无法一一列出,在此向他们致以谢意。书中实际案例,是诸多课程的研究生们在Linux3.8.13版本下调试通过,在此一并表示感谢。本书的出版也离不开清华大学出版社的支持,对此表示衷心的感谢!

由于时间仓促和作者水平有限,书中难免出现遗漏与不当之处,敬请广大读者批评指正。如有任何问题,请发邮件至wgwdut@dlut.edu.cn。编者2015年4月

第1章 Linux内核组成和机制

1.1 Linux内核版本与发展

1.1.1 Linux操作系统的诞生

在Linux操作系统诞生之前,微软公司的MS-DOS主宰着操作系统的市场,其价格十分昂贵,而UNIX操作系统的经销商为了获取高额利润,也将价格抬高,在一段时间内市场上没有廉价的操作系统,二者都严格保护着自己的源代码,使得计算机爱好者无法对操作系统进行探究。此时,MINIX操作系统出现,并且有一本详细介绍其实现原理的书,Linux的创造者Linus Torvalds正是通过MINIX系统了解到操作系统的原理,但是Linus不满足MINIX系统的性能,并酝酿开发一款新的免费操作系统。自1991年4月至同年的9月,Linus开发并发布了Linux 0.01。随后世界各地的爱好者纷纷下载测试,并将反馈和改进的代码发回,Linus则根据反馈进一步修改系统。很快,10月5日0.02版就出现了,0.03版也在几周内出现12月发布了0.10版。这时的Linux仅仅支持AT硬盘,无法登录(直接启动到bash)。Linux0.11带来多语言键盘、软驱、VGA等一系列更新,接下来版本号从0.12直接跳到了0.95,0.96。代码通过芬兰的FTP站点传播到世界各地,世界各地的开发者下载使用并建立FTP镜像,至此,Linux操作系统已经具备了雏形。

Linux操作系统的开发还在继续,阵营很快从百人扩大到千人,继而是几十万人,无数开发者通过调制解调器联系在一起,在世界各地贡献代码和补丁,就这样看似一盘散沙的分布式开发模式写出了稳定的内核,Linux操作系统颠覆了微软推崇的代码专有时代。在此之后,Linus使用GNU通用公共许可证(GPL)将Linux重新授权以保证爱好者可以完全自由地复制、学习和修改源代码。由于Linux内核刚好填补了GNU缺少开源内核的空缺,二者共同构成了一个完整的GNU系统。经过1992年和1993年的改进,Linux系统先后支持包括TCP/IP网络及图形窗口系统在内的许多重要功能。1994年3月14日,Linux1.0版本正式发布,标志着Linux操作系统正式诞生。

1.1.2 Linux内核版本的变迁

Linux内核版本一般划分为3个阶段,第1个阶段是0.01~1.0,第2个阶段是1.0.x~1.2.x,第3个阶段是1.2.x之后的版本。

1991年2月,Linus初步编写的Linux功能就是两个进程分别显示AAA和BBB,同年9月,Linus发布的Linux0.01版本的代码量约10 000行;到1993年参与编写修改Linux内核代码的程序员扩大到100人左右,核心组由5人组成;在1994年3月发布的Linux1.0,代码量达到了17万行,并且是按照完全免费的协议发布,随后正式使用GPL协议。此后Linux的开发进入了一个良性循环,核心小组收集程序员提供的改进的源代码,逐步完善Linux的功能。到1995年Linux操作系统已经可以运行在Intel、Sun SPARC等处理器上,用户量也超过了50万;1996年6月,Linux2.0内核发布,其代码量达到40万行,并可以支持多个处理器,Linux正式进入实用阶段,全球用户约350万。

2011年7月21日,Linus宣布Linux内核3.0正式发布,虽然在版本号的数字上有了划时代的改变,但实际上Linux3.0更像是Linux2.6.40版本的新名称,对于内核开发并没有表现出里程碑似的特征。在Linux2.6.x的基础上,Linux3.0正式版仍然做出了一些重要的特性改变,在此列出部分特性:(1)btrfs文件系统自动碎片整理、性能改进和检查;(2)支持sendmmsg( )函数调用,UDP发送性能提升20%,接口发送性能提升30%;(3)支持应用缓存清理(CleanCache);(4)支持柏克莱封包过滤器(Berkeley Packet Filter)实时过滤,配合libpcap/tcpdump提升包过滤规则的运行效率;(5)支持无线广域网(WLAN)唤醒;(6)支持非特殊授权的ICMP_ECHO函数;(7)支持高精度计时器Alarm-timers;(8)支持setns( )syscall,更好地命名空间管理;(9)支持微软Kinect体感设备;(10)支持AMD Llano APU处理器;(11)支持Intel iwlwifi 105/135无线网卡;(12)支持Intel C600 SAS控制器;(13)支持雷凌Ralink RT5370无线网卡;(14)支持多种Realtek RTL81xx系列网卡;(15)大量新驱动;(16)大量bug修正和改进。

1.2 Linux内核编译

1.2.1 获取内核源码

获取内核源码的方式有许多,可以直接从Linux内核官方网站(www.kernel.org)下载完全源码的压缩包,或者获取增量补丁形式的压缩包;利用Git获取源码,这也是当前比较流行的方式,Git是Linus及他领导的内核开发者共同开发的一个控制和管理Linux内核源码的系统。当前有许多在线资源提供了可靠有效的Git介绍,读者可根据自己的兴趣自行学习。在这里我们介绍通过登录Linux内核官方网站直接获取全部Linux内核源码的方式。

在官方网站中,可以看到两种类型的压缩形式gzip和bzip2。gzip形式压缩的文件后缀为tar.gz,以Linux内核3.8.13为例,如果要解压这种类型的文件需要执行: $tar-xvzf Linux-3.8.13.tar.gz

bzip2形式压缩文件后缀为tar.bz2,这种类型的文件解压执行命令的参数与前者存在不同,具体操作为: $tar-xvjf Linux-3.8.13.tar.bz2

补丁作为一种在Linux内核开发和管理中常用的形式,为Linux内核开发者提供了便利,可以通过补丁的形式发布对代码的修改,也可以下载补丁接收其他人的修改,补丁相当于代码版本之间的纽带。如果想更新内核代码,不再需要下载全部压缩代码,只需要在旧版本上打上一个增量补丁即可。

增量补丁的获取方式与源码的获取方式相同,在获得补丁之后,需要从内核源码树开始应用增量,具体使用方式如下: $patch-p1<../patch-3.8.13

1.2.2 内核源码树

Linux作为一种单内核模式的系统,运行其中的所有程序都有着紧密的联系,同时它们之间的调用关系十分密切。因此在完成Linux内核分析前,需要熟悉源代码的目录结构。Linux内核源代码由许多目录组成,而且大多数目录包含许多子目录,其具体的结构如表1.1所示。表1.1 Linux内核源码目录结构目 录描 述存放特定体系结构的源代码,内核中与具体CPU和体系arch结构相关的代码都放在此目录的子目录下,有关的.h文件在include/asm下Cryto API,存放比较流行的分组加密和哈希函数的源crypto代码Documentatio有关Linux内核的文档ndrivers驱动程序,包括各种块设备和字符设备的驱动VFS和各种文件系统,每个子目录分别对应一个特定的fs文件系统,还有一些用于虚拟文件系统(VFS)的公用源程序内核头文件,包含所有的.h文件,其每个子目录都对应include着一种体系结构,还有一些公用的子目录如sound、net、Linux等Linux内核的引导和初始化代码,如do_mounts.c、initinitramfs.c、main.c,version.c等文件Linux进程间通信代码,如mqueue.c、sem.c、shm.c等ipc文件Linux中进程管理和调度等核心子系统,如fork.c、kernelsched.c、time.c、wait.c等文件lib通用的Linux内核函数库,如对错误信息的处理内存管理及虚拟内存管理的代码,如memory.c、slab.c、mmshmem.c等文件网络相关代码,如网络协议、网卡驱动程序,其子目录net对应的网络协议如IEEE802、IPv4等scripts内核编译时需要的脚本,系统配置的一些命令文件securityLinux安全模块sound语音子系统

Linux内核源代码虽然很庞大,但是对于每一个具体的内核而言并不是所有的.c和.h文件都会被使用,在编译时会根据系统的配置有选择地使用。

1.2.3 编译内核

内核编译并没有想象中的那么难,每个版本的内核都会为用户提供编译工具,使编译内核变的容易。要完成内核的编译,首先在编译前对内核进行相关的配置。

由于Linux内核包含的功能非常丰富,例如对不同平台的支持等,每个用户可以根据自己的需要,完成内核编译的个性化配置。这些可以配置的选项以CONFIG_FEATURE形式表示,CONFIG是所有选项的前缀,例如对称多处理器(SMP)的配置选项为CONFIG_SMP,如果设置了该选项,SMP启用,否则,SMP不被启动。CONFIG_SMP配置项只有两个选项,要么yes(启用)要么no(不启用),而有一些配置项存在3个选项,除yes和no外,还有module选项。module意味着该配置项被选定,但编译时此功能的实现代码以模块的形式生成。在3选1的情况下,yes选项把代码编译进主内核镜像中,而不是作为一个模块。通常,驱动程序都是用3选1(Y/M/N)的配置项。

Linux提供了不同的工具来简化内核的配置,下面介绍三种配置命令,注意在执行命令前务必切换到root用户,并在内核源码目录中执行。(1)基于文本命令行方式: $make config(2)基于ncurse库编制的图形界面方式: $make menuconfig(3)基于gtk+的图形界面方式: $make gconfig

以上三种方式都可以完成内核的配置,并且生成用于存储配置信息的.config文件,该文件存放在内核源码的根目录下,可以直接进行修改,并且可以很容易查找和修改选项。

还可以使用make defconfig命令对内核进行配置,该命令会根据体系结构创建一个配置,这个命令非常适合初次接触内核编译的人使用,因为它可以保证硬件所配置的选项是启用的。

在完成修改内核配置,或者用已有的文件配置新的内核源码时,还需要验证和更新配置,具体命令如下: $make oldconfig

实际上,配置选项CONFIG_IKCONFIG_PROC把完整的压缩过的内核配置文件存放在/proc/config.gz下,也就是说,如果想编译一个新的内核源码,而且不修改当前的内核配置,可以直接将该压缩文件的内容复制到新的内核源码文件下,操作命令为: $zcat /proc/config.gz>.config $make oldconfig

至此,内核编译前的配置基本完成。此外,编译前还要准备一些编译所需的工具,包括gcc、make等以及相关的库。 #apt-get install build-essential libncurse-dev kernel-package fakeroot initramfs-tools module-init-tools

下载了Linux-3.8.13后,将其解压并存放在/usr/src目录下,开始进行编译。 #make mrproper(保证旧文件不再使用) #ake menuconfig #make(编译) #make install(安装内核) #make modules_install(安装模块)

由于make的时间较长,内核提供了一个并行的编译方式:make-jn,这里n是并行的作业数量。在实际操作中,通常一个处理器衍生出一个或者两个作业,并行的方式大大提升了编译的效率。

Linux内核编译部分基本完成,可以尝试完成内核的修改和编译了。

1.3 Linux内核组成

Linux内核主要有5部分组成:进程调度(SCHED)、进程间通信(IPC)、内存管理(MM)、虚拟文件系统(VFS)及网络接口(NET)。本节简要介绍各部分的主要功能及联系。从图1.1中可以发现,这5部分彼此之间是相互依赖缺一不可的。图1.1 内核组成结构依赖关系1.进程调度

进程调度控制系统中的众多进程对CPU的访问,并且处于系统的中心位置。可以清晰地从图1.1中发现,因为系统内的每个子系统都需要挂起或恢复进程,进程调度为其他子系统提供了相关的服务。Linux内核使用的是较为简单的基于优先级的进程调度算法来完成进程的选择。2.内存管理

内存管理实现的功能是控制多个进程安全地共享主内存区域。Linux内存管理支持虚拟内存,即在计算机中运行的程序,其代码、数据、堆栈的总量可能超过实际内存大小,操作系统只把当前使用的程序块保留在内存中,其余程序块保留在磁盘中。当CPU提供内存管理单元时,Linux内存管理完成为每个进程进行虚拟内存到物理内存的转换。内存管理从逻辑上分为硬件无关部分和硬件相关部分,硬件无关部分提供了进程的映射和逻辑内存的兑换;硬件相关部分为内存管理硬件提供虚拟接口。3.虚拟文件系统

虚拟文件系统(VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口,VFS提供了数十种不同的文件系统。虚拟文件系统独立于各个具体的文件系统,是对各个文件系统的一个抽象,它使用超级块super block存放文件系统的相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息。图1.2形象地描述了Linux的文件系统。图1.2 Linux文件系统4.网络接口

网络接口提供了对各种网络标准的存取和网络硬件的支持。网络接口可以分为两类,网络协议和网络设备驱动程序。网络协议负责实现每一种可能的网络传输协议;网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。5.进程间通信

进程间通信提供完成进程之间通信的功能,而Linux内核支持多种通信机制,包括信号量、共享内存及管道等,这种机制可协助多进程多资源的互斥访问、进程间同步和消息传递。

1.4 Linux内核机制

1.4.1 内核启动过程

内核启动是非常复杂的过程,如图1.3所示是内核启动过程的流程图。图1.3 Linux内核启动流程图

总体而言,内核的启动可以概括为6个步骤。(1)实模式的入口函数_start( ):在header.S中,这里会进入众所周知的main函数,它复制bootloader的各个参数,执行基本硬件设置,解析命令行参数。(2)保护模式的入口函数startup_32( ):在compressed/header_32.S中,这里会解压bzImage内核映像,加载vmlinux内核文件。(3)内核入口函数startup_32( ):在kernel/header_32.S中,这就是所谓的进程0,它会进入体系结构无关的start_kernel( )函数,即Linux内核启动函数。start_kernel( )会做大量的内核初始化操作,解析内核启动的命令行参数,并启动一个内核线程来完成内核模块初始化的过程,然后进入空闲循环。(4)内核模块初始化的入口函数kernel_init( ):在init/main.c中,这里会启动内核模块、创建基于内存的rootfs、加载initramfs文件或cpio-initrd,并启动一个内核线程来运行其中的/init脚本,完成真正的根文件系统的挂载。(5)根文件系统挂载脚本/init:这里会挂载根文件系统、运行/sbin/init,从而启动进程1。(6)init进程的系统初始化过程:执行相关脚本,以完成系统初始化,例如设置键盘、字体、装载模块、设置网络等,最后运行登录程序,出现登录界面。

1.4.2 模块机制

模块是Linux内核中非常重要的机制,它为Linux内核向外提供了一个插口,其全称为动态可加载内核模块(Loadable Kernel Module)。Lhnux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic kernel)。单内核的最大优点是效率高,因为所有的内容都集成在一起,其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。

模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。

模块从创建到使用需要经过源代码、Makefile文件、编译模块、加载模块和最后的卸载模块等步骤。下面介绍模块的加载与卸载。

在Linux中,对模块进行加载有两种方法,一种是手动加载,另一种是自动加载。前者使用insmod或modprobe命令实现,后者通过内核线程kmod来实现,而kmod通过调用modрrobe来实现模块的加载。在这里以insmod为例分析模块的加载过程。(1)insmod首先通过系统调用query_nodule( )遍历模块链表来获得系统中的所有符号及其在内存中的物理地址,然后利用得到的符号表修正模块中引用到的外部符号,在此过程中记录下该模块所要用到的模块。由于是直接用这些符号在内存中的物理地址进行更正,所以模块内核空间的地址引用是正确的,如果该模块还有一些符号的地址未知,则该模块不能被加载。(2)insmod填写模块module_ref表。其中dep指向本模块所使用的模块,即在修正本模块使用到的外部符号过程中标记过的模块,这些模块在内存中的物理地址也是通过系统调用query_module得到的。由于别的模块可能要使用本模块提供的服务,所以还需要提供本模块的输出符号表,此部分工作也由insmod完成。(3)insmod发出系统调用create_module( )。由该系统调用为模块分析足够的内存空间,并初始化位于该空间起始处的struct module结构体,然后insmod通过系统调用init_module( )让系统完成余下工作。

相比于模块的加载,模块的卸载比较简单。同样模块的卸载也有两种方式,一种是用户使用rmmod命令完成卸载,另一种通过kerneld或kmod自动卸载。以rmmod为例,在系统找到欲卸载的模块后,根据其引用链表检查是否有别的模块要使用本模块,若有则打印出错信息并停止卸载;否则调用delete_module( )函数完成卸载。该函数将调用free_module函数完成4项工作:释放系统分配给模块的资源;修改模块所依赖的所有模块的引用链表,将该模块从链表中删除;将该模块从系统的模块链表中删除;释放分配给该模块的核心内存。

第2章 Linux内核设备管理方式

2.1 devfs设备文件系统

devfs设备文件系统于Linux2.4内核引入,在使用之初许多工程师给予了高度的评价,它的出现使得设备驱动程序能自主地管理设备文件。它挂载于Linux文件系统中的/dev目录下。

devfs具有如下优点:(1)可以通过程序在设备初始化时,在/dev目录下创建设备文件,卸载设备时将它删除。(2)设备驱动程序可以指定设备名、所有者和权限位,用户空间程序可以修改所有者和权限位。(3)不再需要为设备驱动程序分配主设备号及处理次设备号,在程序中可以直接给注册函数传递0主设备号以动态获得可用的主设备号,并在devfs_register( )中指定次设备号。

当然,devfs也具有较大的缺陷,例如:(1)不确定的设备映射,有时同一个设备映射的设备文件可能不同,例如U盘可能对应sda可能对应sdb。(2)没有足够的主/辅设备号,当设备过多的时候,显然这是一个问题。(3)/dev目录下文件太多而且不能表示当前系统上的实际设备。(4)命名不够灵活,内核完成命名,用户无法指定。(5)不存在的节点被打开时自动加载驱动程序。

通过以上分析可知,devfs的出现帮助用户较为方便地进行设备操作,但是由于设计上的问题,它也存在很多缺陷,随着Linux的不断发展,已经不能适应新的需求。

2.2 sysfs文件系统

在Linux 2.6内核以后,新的文件系统sysfs以替代devfs,完成它不支持的需求,一直延续使用至今。sysfs挂载于/sys目录下,跟devfs一样,它也是一个虚拟文件系统,用来对系统的引入了一个设备进行管理,它把实际连接到系统上的设备和总线组织成一个分级的文件,用户空间的程序同样可以利用这些信息以实现和内核的交互,该文件系统是当前系统上实际设备树的一个直观反映。

它将系统中的设备组织成层次结构,可解决智能电源管理和设备分类的需求。它向用户模式程序提供详细的内核数据结构信息,提供接口给用户空间对驱动和设备进行配置,实现与用户空间的通信,用户甚至可以通过echo、cat等命令来直接操作设备的属性文件,完成设备配置。例如通过在终端输入命令echo 1>/sys/devices/platform/leds-qpio/leds\:LED1/brightness,就可以实现点亮设备的LED1的功能。

sysfs挂载于/sys目录下,目录下各子目录的名称及功能如下:(1)block:系统中发现的每个块设备在该目录下对应一个目录。(2)bus:内核中注册的每条总线在该目录下对应一个目录,例如ide、pci、scsi和usb,等等。(3)bus/某一总线/devices:该总线下所有设备在该目录下对应一个目录。(4)bus/某一总线/drivers:该总线下所有驱动在该目录下对应一个目录。(5)classes:将设备按照功能进行分类,例如net目录下包含所有的网络设备。(6)dev:包含block和char两个文件,内含部分块设备和字符设备符号链接。(7)devices:包含系统所有设备,并根据设备挂接总线类型组织成层次结构。(8)firmwares:系统中的固件信息。(9)fs:描述系统中存在的文件系统。(10)hypervisor:新版本内核中的空目录。(11)kernel:系统内核的配置参数。(12)module:系统中所有模块的信息。(13)power:系统中电源选项。

sys下面的目录和文件反映了整台机器的系统状况。其中10个目录代表了10种完全不同的设备类型,这些目录只是给我们提供了如何去看整个设备模型的不同视角。真正的设备信息放在devices子目录下,Linux系统中的所有设备都可以在这个目录里找到。如图2.1所示,bus下对应驱动和设备,classes下有设备的不同分类,分类下也对应各种设备,实际上它们都是devices目录下设备文件的符号链接。

图2.1 设备文件对应关系

2.3 udev设备文件系统

udev是另一种设备管理工具,最初在Linux 2.6中使用。它能够根据系统中设备的状况动态更新设备文件,包括设备文件的创建、删除,等等,是实现系统热插拔功能的主要工具。用户空间的工具udev利用了sysfs提供的信息来实现所有devfs的功能,但不同的是udev运行在用户空间,devfs运行在内核空间,而且udev不存在devfs那些先天的缺陷。

udev分为3个子计划,如图2.2所示,namedev为设备命名子系统,为发现的设备提供默认的命名规则,libsysfs是访问sysfs文件系统并从中获取设备信息的标准接口,udev是namedev和libsysfs连接的桥梁,利用获取的信息完成设备节点文件的动态创建和删除策略。图2.2 udev具体工作流程

udev结合sysfs才能完整地实现设备的热插拔功能,同时解决devfs中存在的部分缺陷,例如无法自主命名,/dev下不对应实际设备等。

udev完全工作在用户态(userspace)下,利用设备加入或移除时内核所发送的hotplug事件(event)来工作。关于设备的详细信息由内核输出(export)到位于/sys的sysfs文件系统中。所有设备的命名策略、权限控制和事件处理都是在用户态下完成的。与此相反,devfs是作为内核的一部分工作的。

一般Linux系统使用创建静态设备的方法,因此在/dev目录下创建了大量的设备节点(有时会有数千个节点),而不管对应的硬件设备实际上是否存在。这通常是由makedev脚本完成,这个脚本包含许多调用mknod程序的命令,为这个世界上可能存在的每个设备创建相应的主设备号和次设备号。而使用udev方式,只有被内核检测到的设备才为其创建设备节点。

2.4 主要数据结构

2.4.1 kobject

kobject是采用面向对象的思想而设计的特殊数据结构,既然是面向对象,它就有父类节点,可以呈层次结构排列的对象。

kobject主要包括对象引用计数、维护对象链表、对象上锁和在用户空间的表示4种功能。在用户空间的表示,主要是通过sysfs实现,每个在内核注册的kobject对象对应于sysfs文件系统中的一个目录,这些功能主要通过它对应结构体所包含的对象来完成。

内核中由struct kobject表示,使所有设备在底层都具有统一的接口,提供基本的对象管理。它是组成设备的基本结构,类似于C++中的基类,kobject把一些基本的对象和操作封装起来,避免了重复实现,C中不支持继承,故需要在子类中定义kobject对象,从而实现基类的作用。

kobject定义于文件linux/kobject.h中,可创建呈层次结构排列的对象。在内核中注册的每个kobject对象,对应于sysfs文件系统中的一个目录。以下为kobject结构体的源代码: struct kobject { const char * name; struct list_head entry; struct kobject * parent; struct kset * kset; struct kobj_type * ktype; struct sysfs_dirent * sd; struct kref kref; unsigned int state_initialized:1; unsigned int state_in_sysfs:1; unsigned int state_add_uevent_sent:1; unsigned int state_remove_uevent_sent:1; unsigned int uevent_suppress:1; };

其中(1)name表示真正存放名字的数组。(2)kref表示其他结构体,entry与kset一起联合使用。(3)parent指向kobject的父对象。(4)kset和ktype指针分别指向kobject所在的集合和符合的类型。(5)sd指针指向sysfs_dirent结构体,代表sysfs中的kobject。

而kobject的主要函数包括:kobject_init( );//kobject初始化函数kobject_set //设置指定kobject的名称name( );kobject_get( );//将kobject对象的引用计数加1//将kobject对象的引用计数减1,如果引用计数降为kobject_put( );0,//则调用kobject release( )释放该kobject对象kobject_registe//kobject注册函数r( );kobject_unregi//kobject注销函数,调用kobject put( )减少该对象的引ster( )用计数,//如果引用计数降为0,则释放kobject对象

2.4.2 ktype

kobject结构体中定义了一个很关键的对象struct kobj_ype * ktype。 kobj_type用来表示该对象的类型,是具有相同操作的kojbect的集合。它负责管理这一类kojbect在sysfs下的操作,主要是show和store函数的实现。它的结构体定义在include/linux/kobject.h文件中,主要对象如下:struct kobj_type{void( * release)(struct kobject //释放kobject占用的资源* );struct sysfs_ops * sysfs_ops;//指针指向sysfs操作表struct attribute * * default_attrs;//一个sysfs文件系统默认属性列表};

其中,release函数用于释放kobject占用的资源,当kref代表的对象引用计数为0时,release函数被调用来释放资源,对象引用计数的增加和减少通过kobject_get( )和kobject_put( )函数来完成。sysfs操作表包含两个函数show( )和store( ),对应文件的读和写,当用户态读取属性时,show( )被调用,而store( )函数用于存储用户空间传入的属性值。struct attribute为默认的属性列表,对应sysfs文件系统中的一个属性文件。

2.4.3 kset

简单地说,kset就是kobject的集合,kset就像一个容器来包容kobject这些子集。kset中的所有kobject可以拥有相同的ktype,也可以各自属于自己的ktype。kset中也有属于自己的kobj ect,不过该kobj ect属于自动处理的,可以忽略它核心代码处理的过程。

kset最重要的是建立上层和下层的关联性,kobject也会利用它分辨自己属于哪一个类型,然后在/sys下建立自己所属的kset,并把kobj_type指定成该kset下的kobj_type。

kset是kobject对象的集合体,可看做一个容器,将相关kobject对象置于同一位置,其结构体定义于中。需要注意的是,具有相同ktype的kobject可以分组到不同的kset中。kset数据结构如下所示: struct kset { struct list_head list; spinlock_t list_lock; struct kobject kobj; const struct kset_uevent_ops * uevent_ops ; };

其中(1)list变量连接该集合kset中所有的kobject对象。(2)kobj代表了该集合的基类。(3)uevent_ops指向一个用于处理集合中kobject对象的各操作的结构体。

kset的主要函数包括:kset_init( )//完成指定kset的初始化kset_add( )//把一个kset加到层次结构中//和kset_put( )分别增加和减少kset对象的引用计数(其kset_get( )实就是内嵌kobject的引用计数)kset_register( //完成kset的注册)kset_unregist//完成kset的注销er( )

2.4.4 三者关系

kobject结构为如图2.3的kobject类型部分,kset结构为如图2.3的kset类型部分,一个kobject加入一个kset,主要是kobject结构体中的相关字段记录了对应的kset信息,流程为:①记录了kobject所对应kset,指向的是kset所包含的kobject的地址;②记录kobject所对应的kset的kset指针;③记录kobject的类型;④记录了kset所有的kobject的链子,这个链子是一个双向链表,每当有一个kobject加入到当前的kset,就会调用list_add_tail( )函数,把要加入kset的kobject连入链表的结尾,最终形成一个链表。整个过程如图2.3所示。图2.3 kobject、ktype、kset之间的关系

当有另外一个kobject要加入当前的kset,其中的①②③步跟第一个加入当前kset的kobject是一样的,即设置要加入的kobject的成员,使之指向当前的kset对应数据,第④步需要把kobject添加到kset的list的尾部。图2.4表示了新的kobject b加入到kset A。图2.4 将新kobject加入到已有的kset

2.5 热插拔设备管理机制

2.5.1 热插拔事件流程

在Linux环境中,热插拔设备管理机制并非是设备驱动单独进行的一项工作。可以将其概括为所处系统、设备驱动、设备模型、通信机制等几方面工作相结合处理的工作机制。下面从设备插入系统产生热插拔事件来介绍相应设备工作流程:(1)内核检测到新硬件插入,然后分别通知hotplug和udev。前者用来装入相应的内核模块(例如usb_storage),后者用来在/dev中创建相应的设备节点(例如/dev/sdal)。(2)udev创建了相应的设备节点之后,会将这一消息通知hal的守护程序hald。udev还必须保证新创建的设备节点可以被普通用户访问。(3)hotplug装入了相应的内核模块并把这一消息通知给hald。(4)hald在收到hotplug和udev发出的消息之后,认为新硬件已经正式被系统认可。此时它会通过一系列精心编写的规则文件(xxx-policy.fdi),把发现新硬件的消息通过netlink发送出去,同时还会调用update-fstab或fstab-sync来更新/etc/fstab,为相应的设备节点创建适合的挂载点。(5)卷管理器会监听netlink中发现新硬件的消息。根据所插入硬件(区分U盘和数码相机等)的不同,卷管理器会先将相应的设备节点挂载到hald创建的挂载点上,然后再打开不同的应用程序。如果是在光驱中插入光盘,过程比较简单。因为光驱本身就是一个固定的硬件,无须hotplug和udev的协助。(6)hald会自己监视光驱,并且将光盘托架开合的消息通过netlink发出去。(7)卷管理器负责检查光驱中的盘片内容,进行挂载,并调用合适的应用程序。要注意,hald的工作是从上游得到硬件就绪的消息,然后将这个消息转发到netlink中。尽管它会调用程序来更新fstab,实际上自己并不执行挂载的工作。

按照以上流程进行操作后,系统将完成从设备热插拔响应到添加等操作。设备插入情况如图2.5所示。插入情况因为涉及首次添加设备等情况,所以操作流程较复杂。图2.5 设备插入在各模块间处理流程

设备拔出情况如图2.6所示。因为在插入过程中已经将部分工作完成,为了使拔出后的下一次插入时设备正常运行,不需要对驱动等信息进行处理,所以操作流程相比于设备插入简单。图2.6 设备拔出在各模块间处理流程

在进行这一系列相应操作之后,将会关联设备驱动。具体分为以下两种情况:● 若驱动直接编译进内核或在启动时加载,则无需在udev中加载

驱动模块,在bus_probe_device( )中会为其找到相应的驱动。● 若驱动需要动态加载,则先有device或先有driver均有可能。内

核层面,在手动加入的驱动register函数中,找到相应device进行

关联。用户层面,udev(目前的情况是这样,以前也有其他方

式,例如/sbin/hotplug等)中,动态加载相应脚本程序。

2.5.2 涉及的模块

热插拔事件中主要涉及3个模块hotplug、netlink及udev。udev在本章2.3节已经介绍,在此对另外两个模块进行介绍。1.hotplug

hotplug包和内核里的hotplug模块不是一回事,2.6版内核里的pci_hotplug.ko是一个内核模块,而hotplug包是用来处理内核产生的hotplug事件。这个软件包还在引导时检测现存的硬件,并在运行的内核中加载相关模块。

不但有热插拔,还有冷插拔(cold pluging)。热插拔在内核启动之后发生,而冷插拔发生在内核启动的过程中。

/etc/hotplug/ *.rc这些脚本用于冷插拔,检测和激活在系统启动时已经存在的硬件。它们被hotplug初始化脚本调用。*.rc脚本会尝试恢复系统引导时丢失的热插拔事件,举例来说,内核没有挂载根文件系统。

/etc/hotplug/ *.agent这些脚本将被hotplug调用,以响应内核产生的各种不同的热插拔事件,导致插入相应的内核模块和调用用户预定义的脚本。

/sbin/hotplug内核默认情况下,将在内核态的某些事情发生变化时(例如硬件的插入和拔出)调用此脚本。

发送热插拔事件的子系统(subsystem)包括总线驱动(USB、PCI等)和一些设备的抽象层(网络接口、磁盘分区等)。它们通过/sbin/hotplug的第一个参数来识别。

对于设备驱动来说,需要在代码里设置MODULE_DEVICE_TABLE,指向驱动程序感兴趣的设备ID列表。2.netlink

netlink是一种特殊的socket,它是Linx特有的,目前多用于在最新的Linux内核中使用netlink进行内核与用户之间的通信,例如路由(NETLINK_ROUTE)、防火墙(NETLINK_FIREWALL)等。

netlink是一种在内核与用户应用进行双向数据传输的有效方式。用户态应用使用标准的socket API即可。内核态需使用专门的内核API调用。

netlink是一种异步通信机制。在内核与用户态应用之间传递的消息保存在socket缓存队列中,发送消息只是把消息保存在接受者的socket的接受队列,而不需要等待接受者收到消息。

netlink的内核部分可以采用模块的方式实现,使用netlink的应用部分和内核部分没有编译时依赖,但系统调用时有依赖,新的系统调用必须静态链接到内核中,使用新系统调用的应用在编译时需要依赖内核。

2.5.3 关键驱动函数

当系统配置发生变化时,例如添加kset到系统,或移动kobject,一个通知会从内核空间发送到用户空间,这就是热插拔事件。Linux中采用kset_uevent_ops函数来对热插拔事件进行相关响应。

对热插拔事件的实际控制,是由保存在kset_uevent_ops结构中的函数完成的,它们分别是filter、name、uevent函数。 struct kset_uevent_ops { int( * const filter)(struct kset *kset, struct kobject *kobj); const char *( * const name)(struct kset *kset, struct kobject *kobj); int( * const uevent)(struct kset *kset, struct kobject *kobj, struct kobj_uevent_env *env); };

当kobject和kset状态变化时3个函数被调用。(1)filter:决定是否将事件传递到用户空间。如果filter返回0,不传递事件。(2)name:负责将相应的字符串传递用户空间的热插拔处理程序。(3)uevent:将用户空间需要的参数添加到环境变量中。返回值正常是0,若返回非0,则终止热插拔事件的产生。

第3章 Linux驱动开发基础

3.1 同步机制

共享内存的应用程序必须特别留意保护共享资源,防止共享资源被并发访问。内核也不例外。共享资源之所以要防止并发访问,是因为多个执行程序同时访问和操作数据,就有可能发生各线程之间相互覆盖共享数据的情况,造成被访问数据处于不一致的状态。并发访问共享数据是造成系统不稳定的隐患,而且这种错误难以跟踪和调试。

要做到对共享资源的恰当保护很困难。在Linux还未支持对称多处理器的时候,避免并发访问数据的方法相对比较简单。在单一处理器的时候,只要在中断发生,或在内核代码显式地请求重新调度执行另一个任务的时候,数据才可能被并发访问。

从Linux 2.0开始,内核开始支持对称多处理器,并且对它的支持不断地加强和完善。支持多处理器意味着内核代码可以同时运行在两个或更多的处理器上。因此,如果不加以保护,运行在两个不同处理器上的内核代码完全可能在同一时刻并发访问共享数据。随着2.6版内核的出现,Linux内核已发展成抢占式内核,这意味着调度程序可以在任何时刻抢占正在运行的内核代码,重新调度执行其他的进程。

并发指的是多个执行单元同时被并行执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问很容易导致竞态。多个程序对共享资源的并发访问是Linux设备驱动中必须解决的问题之一。

在Linux中,主要的竞态发生在如下几种情况:(1)对称多处理器(SMP)的多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。(2)单CPU内进程与抢占它的进程。(3)中断(硬中断、软中断、Tasklet、底半部)与进程之间。

总之,只要并发的多个执行单元存在对共享资源的访问,竞态就有可能发生。如果中断处理程序访问进程正在访问的资源,则竞态也会发生。多个中断之间本身也可能引起并发而导致竞态(中断被更高优先级的中断打断)。

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。访问共享资源的代码区域称为临界区,临界区需要以某种互斥机制加以保护。

所谓临界区就是访问和操作共享数据的代码段,为了避免在临界区中发生并发访问,编程者必须保证这些代码原子地执行。也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令,如果两个执行线程有可能处于同一个临界区中,那么程序就包含一个bug。如果这种情况发生,就称之为竞争条件,避免并发和防止竞争条件称为同步。

3.1.1 内核同步机制分类

Linux提供了多种解决竞态问题的同步机制,例如中断屏蔽、原子操作、自旋锁、信号量和大内核锁等。具体分类如图3.1所示,这些同步机制根据自身的特点适用于不同的应用场景。下面将详细阐述各同步机制。图3.1 内核同步机制分类1.中断屏蔽

1)定义

中断屏蔽是在单CPU范围内避免竞态的一种简单方法,在进入临界区之前屏蔽系统的中断。CPU一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序抢占,防止某些竞态条件的发生。总之,中断屏蔽将使得中断与进程间的并发不再发生。由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免。

2)中断屏蔽的使用方法local_irq_disable( )//屏蔽中断…critical section//临界区…local_irq_enable( )//打开中断

3)中断屏蔽的特点

由于Linux系统的异步IO,进程调度等很多重要操作都依赖于中断,在屏蔽中断期间所有的中断程序都无法得到处理,因此长时间的屏蔽是很危险的,有可能造成数据丢失甚至系统崩溃,这就要求在屏蔽中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。

中断屏蔽只能禁止本CPU内的中断,因此并不能解决多CPU引发的竞态,所以单独使用中断屏蔽并不是一个值得推荐的方法,它一般和自旋锁配合使用。2.原子操作

1)定义

所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就是说,它是最小的执行单位,不可能有比它更小的执行单位。

Linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分成两类,分别是整型原子操作和位原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。原子操作都需要硬件的支持,因此很多函数都与CPU架构密切相关。它们都使用汇编语言实现,因为C语言不能实现这样的操作。

针对整数的原子操作只能对atomic_t类型的数据进行处理。在这里之所以引入了一个特殊数据类型,而没有直接使用C语言的int类型,主要有以下几点原因:首先,让原子函数只接受atomic_t类型的操作数,以确保原子操作只与这种特殊类型数据一起使用,同时,这也保证了该类型的数据不会传递给其他任何非原子函数;其次,使用atomic_t类型确保编译器不对相应的值进行访问优化,使得原子操作最终接收到正确的内存地址;最后,在不同体系结构上实现原子操作时,使用atomic_t可以屏蔽其间的差异。

位原子操作是对普通指针进行的操作,所以不像整型原子操作对应atomic_t类型,这里没有特殊的数据类型。它是与体系结构相关的,对普通的内存地址进行操作。

2)原子操作使用情况线程1线程2atomic increment i(7-…>8)…atomic increment i(8->9)

两个原子操作绝对不可能并发地访问同一个变量,这样也就绝对不可能引发竞争。

3)原子操作API函数(1)整型原子操作//该函数对原子类型的变量进行原子读atomic_read(atomic_t * v);操作//它返回原子类型的变量v的值atomic_set(atomic_t * v, int i);//该函数设置原子类型的变量v的值为ivoid atomic_add(int i, //该函数给原子类型的变量v增加值iatomic_t * v);atomic_sub(int i, atomic_t * //该函数从原子类型的变量v中减去iv);int atomic_sub_and_test(int //该函数从原子类型的变量v中减去i,并i, atomi_c_t * v);判//断结果是否为0,如果为0,返回真;否则返回假void atomic_inc(atomic_t * v);//该函数对原子类型变量v原子地增加1void atomic_dec(atomic_t * //该函数对原子类型的变量v原子地减1v);int //该函数对原子类型的变量v原子地减atomic_dec_and_test(atomic1,并判_t * v);//断结果是否为0,如果为0,返回真;否则返回假int //该函数对原子类型的变量v原子地增加atomic_inc_and_test(atomic_1,并判t * v);//断结果是否为0,如果为0,返回真;否则返回假int atomic_add_negative(int //该函数对原子类型的变量v原子地增加i, atomic_t * v);1//并判断结果是否为负数,如果是,返回真;否则返回假(2)位原子操作void set_bit(int nr, void * //原子地设置addr所指的第nr位addr);void clear_bit(int nr, void * //原子地清空所指对象的第nr位addr);void change_bit(nr, void * //原子地翻转addr所指的第nr位addr);int tes_bit(nr, void * addr);//原子地返回addr位所指的第nr位

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

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载