1、设备驱动概述n设备由两部分组成,一个是被称为控制器的电器部分,另一个是机械部分。n一组寄存器组被赋予到各个控制器。I/O端口包含4组寄存器,即状态寄存器,控制寄存器,数据输入寄存器,数据输出寄存器。n状态寄存器拥有可以被CPU读取的(状态)位,用来 指示当前命令是否执行完毕,或者字节是否可以被读出或写入,以及任何错误提示。n控制寄存器则用于启动一条命令(指令)或者改变设备的(工作)模式。n数据输入寄存器用于获取输入的数据。n数据输出寄存器则向CPU发送结果。设备驱动概述n操作系统是通过各种驱动程序来驾驭硬件设备,它为用户屏蔽了各种各样的设备。n设备驱动程序是操作系统内核和机器硬件之间的接口,系
2、统调用是操作系统内核和应用程序之间的接口。n在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作.4设备驱动概述n驱动完成以下的功能:n对设备初始化和释放.n把数据从内核传送到硬件和从硬件读取数据.n读取应用用程序传送给设备文件的数据和回送应用用程序请求的数据.n检测和处理设备出现的错误.5设备驱动概述n无操作系统的设备驱动n有操作系统的设备驱动ApplicationDriverHardwareApplicationLib APISystem callEmbedded OSHardware不带操作系统软件结构 带操作系统软件结构Driver6Linux设备
3、驱动7Linux设备驱动n用户级的程序使用内核提供的标准系统调用来与内核通讯,这些系统调用有:open(),read(),write(),ioctl(),close()等等。nLinux的内核是映射到每一个进程的高1G空间。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。8Linux设备驱动nLinux内核使用“设备无关”的I/O子系统来为所有的设备服务。n每个设备都提供标准接口给内核,尽可能地隐藏了自己的特性。n用户程序使用一些基本的系统调用从设备读取数据并且将它们存入缓冲的例子。我们可以看到,每当
4、一个系统调用被使用时,内核就转到相应的设备驱动例程来操纵硬件。Linux设备驱动nLinux操作系统把设备纳入文件系统的范畴来管理。n每个设备在Linux系统上看起来都像一个文件,它们存放在/dev目录中,称为设备节点。n对文件操作的系统调用大都适用于设备文件。10Linux设备驱动nLinux下设备的属性n设备的类型:字符设备、块设备、网络设备n主设备号:标识设备对应的驱动程序。一般“一个主设备号对应一个驱动程序”n次设备号:每个驱动程序负责管理它所驱动的几个硬件实例,这些硬件实例则由次设备号来表示。同一驱动下的实例编号,用于确定设备文件所指的设备。n可通过ls l“设备文件名”命令查看设备
5、的主次设备号,以及设备的类型。11Linux设备驱动nLinux设备驱动程序是一组由内核中的相关子例程和数据组成的I/O设备软件接口。n每当用户程序要访问某个设备时,它就通过系统调用,让内核代替它调用相应的驱动例程。这就使得控制从用户进程转移到了驱动例程,当驱动例程完成后,控制又被返回至用户进程。12一些重要的数据结构n大部分驱动程序涉及三个重要的内核数据结构:n文件操作结构体n文件对象file结构体n索引节点inode结构体13一些重要的数据结构n文件操作结构体n结构体在头文件 linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。n结构体的每个域都对应着驱动
6、模块用来处理某个被请求的事务的函数的地址。struct struct module*owner;ssize_t(*read)(struct file*,char _user*,size_t,loff_t*);ssize_t(*write)(struct file*,const char _user*,size_t,loff_t*);。14一些重要的数据结构n重要的成员nStruct module*owner,指向拥有该结构体的模块的指针。内核使用该指针维护模块使用计数。n方法llseek用来修改文件的当前读写位置,把新位置作为返回值返回。loff_t是在LINUX中定义的长偏移量 n方法rea
7、d用来从设备中读取数据。非负返回值表示成功读取的直接数。n方法write向设备发送数据。n方法ioctl提供一种执行设备特定命令的方法。15一些重要的数据结构n重要的成员nunsigned int(*poll)(struct file*,struct poll_table_struct*);系统调用select和poll的后端实现,用这两个系统调用来查询 设备是否可读写,或是否处于某种状态。如果poll为空,则驱动设备会被认为即可读又可写,返回值是一个状态掩码。nint(*mmap)(struct file*,struct vm_area_struct*);将设备内存映射到进程地址空间16一些
8、重要的数据结构n重要的成员n驱动内核模块是不需要实现每个函数的。相对应的的项就为 NULL。nGcc的语法扩展,使得可以定义该结构体:struct fops=read:device_read,write:device_write,open:device_open,release:device_release;n没有显示声明的结构体成员都被gcc初始化为NULL。17一些重要的数据结构n重要的成员n标准C的标记化结构体的初始化方法:struct fops=.read=device_read,.write=device_write,.open=device_open,.release=device
9、_release;n推荐使用该方法,提高移植性,方法允许对结构体成员进行重新排列。没有显示声明的结构体成员同样都被gcc初始化为NULL。n指向结构体的指针通常命名为fops。18一些重要的数据结构n文件对象file结构体n文件对象file代表着一个打开的文件。进程通过文件描述符fd与已打开文件的file结构相联系。进程通过它对文件的线性逻辑空间进行操作。例如:file-f_op-read();nStruct file 在中定义。n指向结构体struct file的指针通常命名为filp,或者file。建议使用文件指针filp。19一些重要的数据结构n文件对象file结构体的成员nStruct
10、 *f_op;与文件相关的操作结构体指针。与文件相关的操作是在打开文件的时候确定下来的,也就是确定该指针的值。可在需要的时候,改变指针所指向的文件操作结构体。用C语言实现面向对象编程的方法重载。n其他成员可先忽略,后面具体实例分析。因为设备驱动模块并不自己直接填充结构体 file,只是使用file中的数据。20一些重要的数据结构n索引节点inode结构n文件打开,在内存建立副本后,由唯一的索引节点inode描述。n与file结构不同。nfile结构是进程使用的结构,进程每打开一个文件,就建立一个file结构。不同的进程打开同一个文件,建立不同的file结构。nInode结构是内核使用的结构,文
11、件在内存建立副本,就建立一个inode结构来描述。一个文件在内存里面只有一个inode结构对应。21一些重要的数据结构n索引节点inode结构nInode结构包含大量描述文件信息的成员变量。n但是对于描述设备文件的inode,跟设备驱动有关的成员只有两个。nDev_t i_rdev;包含真正的设备编号。nStruct cdev*i_cdev;指向cdev结构体的指针。cdev是表示字符设备的内核数据结构。n从inode中获得主设备号和次设备号的宏:nUnsigned int iminor(struct inode*inode);nUnsigned int imajor(struct inode
12、*inode);22Linux设备驱动n主设备号和次设备号的内部表达:nDev_t类型用于保存设备号,称为设备编号。/linux/types.h文件中定义。n目前设备编号dev_t是一个32位的整数,其中12位表示主设备号,20位表示次设备号。n通过设备编号获取主次设备号:nMAJOR(dev_t dev);nMINOR(dev_t dev);n通过主次设备号合成设备编号:nMKDEV(int major,int minor);nDev_t格式以后可能会发生变化,但只要使用这些宏,就可保证设备驱动程序的正确性。23分配和释放字符设备号n编写驱动程序要做的第一件事,为字符设备获取一个设备号。n事
13、先知道所需要的设备编号(主设备号)的情况:nint register_chrdev_region(dev_t first,unsigned count,const char*name)nfirst是要分配的起始设备编号值。first的次设备号通常设置为0。nCount 所请求的连续设备编号的个数。nName设备名称,指和该编号范围建立关系的设备。n分配成功返回0。24分配和释放字符设备号n动态分配设备编号(主要是主设备号)nint alloc_chrdev_region(dev_t*dev,unsigned baseminor,unsigned count,const char*name)nd
14、ev 是一个仅用于输出的参数,它在函数成功完成时保存已分配范围的第一个编号。nbaseminor 应当是请求的第一个要用的次设备号,它常常是 0.ncount 和 name 参数跟request_chrdev_region 的一样.25分配和释放字符设备号n不再使用时,释放这些设备编号。使用以下函数:nvoid unregister_chrdev_region(dev_t from,unsigned count)n在模块的卸载函数中调用该函数。26分配和释放字符设备号n新驱动程序,建议使用动态分配机制获取主设备号,也就是使用alloc_chrdev_region()。n动态分配导致无法预先创建
15、设备节点。n可在分配设备号后,从/proc/devices文件中获取。n为了加载后自动创建设备文件,可以通过编写内核模块加载脚本实现。27字符设备的注册n内核内部使用struct cdev结构表示字符设备。编写设备驱动的第二步就是注册该设备。n包含头文件。n获取一个独立的cdev结构:struct cdev*my_cdev=cdev_alloc();n调用cdev_init初始化cdev结构体void cdev_init(struct cdev*cdev,struct *fops);n初始化该设备的所有者字段:dev-cdev.owner=THIS_MODULE;n初始化该设备的可用操作集:d
16、ev-cdev.ops=&device_fops;28字符设备的注册n编写设备驱动的第二步就是注册该设备。ncdev 结构已建立和初始化,最后通过cdev_add函数把它告诉内核:int cdev_add(struct cdev*dev,dev_t num,unsigned int count);ndev 是要添加的设备的 cdev 结构,nnum 是这个设备对应的第一个设备编号,ncount 是应当关联到设备的设备号的数目.n卸载字符设备时,调用相反的动作函数:nvoid cdev_del(struct cdev*dev);29设备的注册n早期方法:n内核中仍有许多字符驱动不使用刚刚描述过的
17、cdev 接口。没有更新到 2.6 内核接口的老代码。n注册一个字符设备的早期方法:int register_chrdev(unsigned int major,const char*name,struct *fops);nmajor 是给定的主设备号。为0代表什么?nname 是驱动的名字(将出现在/proc/devices),nfops 是设备驱动的 结构。nregister_chrdev 将给设备分配 0-255 的次设备号,并且为每一个建立一个缺省的 cdev 结构。n从系统中卸载字符设备的函数:int unregister_chrdev(unsigned int major,cons
18、t char*name);30Open方法n编写字符设备驱动的第三步:定义设备驱动与文件系统的接口,结构体的函数定义。nopen 方法nint(*open)(struct inode*inode,struct file*filp);n驱动程序提供open 方法,让用户进程使用设备之前,进行一些初始化的工作。n检查设备特定的错误。n如果第一次打开设备,则初始化设备。n如果需要,更新 f_op 指针,更换操作方法集。n分配并填充要放进 filp-private_data 的任何数据结构。31Open方法n对于设备文件,inode 参数只有两个参数对设备驱动有用的。nDev_t i_rdev;包含真
19、正的设备编号。nStruct cdev*i_cdev;指向cdev结构体的指针。ni_cdev里面包含我们之前建立的 cdev 结构。但是有时候,我们需要的是包含 cdev 结构的描述设备的结构。n使用通过成员地址获取结构体地址的宏container_of,在 中定义:ncontainer_of(pointer,container_type,container_field);n这个宏使用一个指向 container_field 类型的成员的指针,它在一个 container_type 类型的结构中,宏通过分析他们关系,返回指向包含该成员的结构体指针.32Open方法n在 myscull_ope
20、n,这个宏用来找到适当的设备结构:dev=container_of(inode-i_cdev,struct scull_dev,cdev);n找到 myscull_dev 结构后,scull 在filp-private_data 中存储其指针,为以后存取使用.filp-private_data=dev;33release 方法nrelease 方法做open相反的工作n释放 open 分配给filp-private_data的内存空间。n在最后一次的关闭操作时,关闭设备。n不是每个 close 系统调用引起调用 release 方法。34Read和Write方法nRead的任务,就是从设备拷贝
21、数据到用户空间。nWrite的任务,则从用户空间拷贝数据到设备。ssize_t read(struct file*filp,char _user*buff,size_t count,loff_t*offp);ssize_t write(struct file*filp,const char _user*buff,size_t count,loff_t*offp);nfilp 是文件对象指针,ncount 是请求的传输数据大小.nbuff 参数对write来说是指向持有被写入数据的缓存,对read则是放入新数据的空缓存.noffp 是指向一个“long offset type”的指针,它指出用户
22、正在存取的文件位置.n返回值是“signed size type”类型;35Read和Write方法nread 和 write 方法的 buff 参数是用户空间指针,不能被内核代码直接解引用。_user字符串只是形式上的说明,表明是用户空间地址。n驱动必须能够存取用户空间缓存以完成它的工作。内核如何解决这个问题?n为安全起见,内核提供专用的函数来完成对用户空间的存取。这些专用函数在中声明。中声明。unsigned long copy_to_user(void _user*to,const void*from,unsigned long count);unsigned long copy_fro
23、m_user(void*to,const void _user*from,unsigned long count);n大多数读写函数都会调用这两个函数,用于跟应用程序空间交流信息。36Read和Write方法n典型的Read函数对参数的使用。37llseek函数nllseek函数用于对设备文件访问定位。n驱动接口loff_t(*llseek)(struct file*,loff_t,int);n库函数off_t lseek(int,off_t offset,int whence);参数 offset 的含义取决于参数 whence:n如果 whence 是 SEEK_SET,文件偏移量将被设置
24、为 offset。n如果 whence 是 SEEK_CUR,文件偏移量将被设置为 cfo 加上 offset,offset 可以为正也可以为负。n如果 whence 是 SEEK_END,文件偏移量将被设置为文件长度加上 offset,offset 可以为正也可以为负。nSEEK_SET、SEEK_CUR 和 SEEK_END 是 System V 引入的,是 0、1 和 2。38ioctl n进行超出简单的数据传输之外的操作,进行各种硬件控制操作.ioctl 方法和用户空间版本不同的原型:int(*ioctl)(struct inode*inode,struct file*filp,uns
25、igned int cmd,unsigned long arg)n不管可选的参数arg是否由用户给定为一个整数或一个指针,它都以一个unsigned long的形式传递。n返回值返回值POSIX 标准规定:如果使用了不合适的 ioctl 命令号,应当返回-ENOTTY。这个错误码被 C 库解释为“不合适的设备 ioctl。-EINVAL也是相当普遍的。39结构化设备驱动程序 n设备结构体 n把与某设备相关的所有内容定义为一个设备结构体n其中包括设备驱动涉及的硬件资源、全局软件资源、控制(自旋锁、互斥锁、等待队列、定时器等)n在涉及设备的操作时,就仅仅操作这个结构体40Linux设备驱动的并发控
26、制41设备驱动的并发控制 n在驱动程序中,当多个线程同时访问相同的资源时,可能会引发“竞态”,必须对共享资源进行并发控制。n并发和竞态广泛存在。n并发控制的目的:使得线程访问共享资源的操作是原子操作。n原子操作:在执行过程中不会被别的代码路径所中断的操作。n驱动程序中的全局变量是一种典型的共享资源。42n考虑一个非常简单的共享资源的例子:一个全局整型变量和一个简单的临界区,其中的操作仅仅是将整型变量的值增加1:i+n该操作可以转化成下面三条机器指令序列:得到当前变量i的值并拷贝到一个寄存器中将寄存器中的值加1把i的新值写回到内存中 原子操作43原子操作内核任务内核任务1 1 内核任务内核任务2
27、 2获得i(1)-增加 i(1-2)-写回 i(2)-获得 i(2)增加 i(2-3)写回 i(3)内核任务内核任务1 1 内核任务内核任务2 2获得 i(1)-增加 i(1-2)-获得 i(1)-增加 i(1-2)-写回 i(2)写回 i(2)-可能的实际执行结果:期望的结果44Linux内核的并发控制 n在内核空间的内核任务需要考虑同步n内核空间中的共享数据对内核中的所有任务可见,所以当在内核中访问数据时,就必须考虑是否会有其他内核任务并发访问的可能、是否会产生竞争条件、是否需要对数据同步。45 确定保护对象确定保护对象 n找出哪些数据需要保护是关键所在n内核任务的局部数据仅仅被它本身访问
28、,显然不需要保护。n如果数据只会被特定的进程访问,也不需加锁 n大多数内核数据结构都需要加锁:若有其它内核任务可以访问这些数据,那么就给这些数据加上某种形式的锁;若任何其它东西能看到它,那么就要锁住它。Linux内核的并发控制46Linux内核的并发控制n并发控制的机制n中断屏蔽,原子数操作,自旋锁和信号量都是解决并发问题的机制。n中断屏蔽很少被单独使用,原子操作只能针对整数来进行。因此自旋锁和信号量应用最为广泛。47中断屏蔽n单CPU系统中,避免竟态的一种简单方式n保证正在执行的内核执行路径不被中断处理程序所抢占,防止竟态条件的发生。Local_irq_disable()/关中断Critic
29、al section /临界区Local_irq_enable()/开中断n中断对内核非常重要,长时间屏蔽中断非常危险!n只适合短时间的关闭n对SMP多CPU引发的竟态无效48n锁机制可以避免竞争状态正如门锁和门一样,门后的房间可想象成一个临界区。n在一段时间内,房间里只能有一个内核任务存在,当一个任务进入房间后,它会锁住身后的房门;当它结束对共享数据的操作后,就会走出房间,打开门锁。如果另一个任务在房门上锁时来了,那么它就必须等待房间内的任务出来并打开门锁后,才能进入房间。加锁机制 49n任何要访问临界资源的代码首先都需要占住相应的锁,这样该锁就能阻止来自其它内核任务的并发访问:任务任务 1
30、 1 试图锁定队列 成功:获得锁 访问队列 为队列解除锁 任务任务2 2 试图锁定队列失败:等待 等待 等待 成功:获得锁 访问队列 为队列解除锁加锁机制 50原子数操作n整型原子数操作n原子变量初始化atomic_t test=ATOMIC_INIT(i);n设置原子变量的值void atomic_set(atomic_t*v,int i)n获得原子变量的值atomic_read(v)n原子变量加void atomic_add(int i,atomic_t*v)n原子变量减void atomic_sub(int i,atomic_t*v)51原子数操作n整型原子数操作n原子变量的自增操作vo
31、id atomic_inc(atomic_t*v)n原子变量的自减操作void atomic_dec(atomic_t*v)n操作并测试(测试其是否为0,0为true,否为false)atomic_inc_and_test(atomic_t*v)atomic_dec_and_test(atomic_t*v)int atomic_sub_and_test(int i,atomic_t*v)n操作并返回(返回新值)int atomic_add_return(int i,atomic_t*v)int atomic_sub_return(int i,atomic_t*v)52原子数操作n原子位操作n设
32、置位void set_bit(int nr,volatile unsigned long*addr)n清除位void clear_bit(int nr,volatile unsigned long*addr)n改变位change_bit(nr,p)n测试位test_bit(int nr,const volatile unsigned long*p)n测试并操作位test_and_set_bit(nr,p)53自旋锁n自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。而对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。n自旋锁最多只能被一
33、个内核任务持有,若一个内核任务试图请求一个已被持有的自旋锁,那么这个任务就会一直进行忙循环,也就是旋转,等待锁重新可用。n自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。54自旋锁n自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话,最好使用信号量。55自旋锁n自旋锁防止在不同CPU上的执行单元对共享资源的同时访问,以及不同进程上下文互相抢占导致的对共享资源的非同步访问。n在单CPU且
34、不可抢占的内核下,自旋锁的所有操作都是空操作。n自旋锁不允许任务睡眠。56自旋锁n自旋锁的基本形式如下:spin_lock(&mr_lock);/*临界区*/spin_unlock(&mr_lock);57自旋锁n自旋锁原语要求包含文件是.锁的类型是 spinlock_t.n锁的两种初始化方法:nspinlock_t my_lock=SPIN_LOCK_UNLOCKED;nvoid spin_lock_init(spinlock_t*lock);n进入一个临界区前,必须获得需要的 lock。nvoid spin_lock(spinlock_t*lock);n自旋锁等待是不可中断的。一旦你调用s
35、pin_lock,将自旋直到锁变为可用。n释放一个锁:nvoid spin_unlock(spinlock_t*lock);58自旋锁n关中断的自旋锁nSpin_lock_irq()nSpin_unlock_irq()nSpin_lock_irqsave()nSpin_unlock_irqrestore()59信号量nLinux中的信号量是一种睡眠锁。n如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。n当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。n信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;n信
36、号量的操作n信号量支持两个原子操作P()和V(),前者做测试操作,后者叫做增加操作。nLinux中分别叫做down()和up()。60信号量ndown()和up()。ndown()操作通过对信号量计数减1来请求获得一个信号量。n如果结果是0或大于0,信号量锁被获得,任务就可以进入临界区了。n如果结果是负数,任务会被放入等待队列,处理器执行其它任务。n相反,当临界区中的操作完成后,up()操作用来释放信号量,增加信号量的计数值。n如果在该信号量上的等待队列不为空,处于队列中等待的任务在被唤醒的同时会获得该信号量。61信号量62信号量63Linux信号量的实现n内核代码必须包含,才能使用信号量。n
37、相关的类型是 struct semaphore信号量的定义struct semaphore atomic_t count;int sleepers;wait_queue_head_t wait;64Linux信号量的实现n信号量的声明和初始化n直接创建一个信号量 struct semaphore*sem;n接着使用 sema_init 来初始化这个信号量:void sema_init(struct semaphore*sem,int val);n互斥模式的信号量声明,内核提供宏定义.nDECLARE_MUTEX(name);信号量初始化为 1nDECLARE_MUTEX_LOCKED(name
38、);信号量初始化为065Linux信号量的实现n动态分配的互斥信号量声明nvoid init_MUTEX(struct semaphore*sem);信号量初始化为 1nvoid init_MUTEX_LOCKED(struct semaphore*sem);信号量初始化为066Linux信号量的实现n信号量的P操作nvoid down(struct semaphore*sem);down减小信号量的值,并根据信号量的值决定是否等待。不可中断的等待。nint down_interruptible(struct semaphore*sem);操作是可中断的。nint down_trylock(s
39、truct semaphore*sem);信号量在调用时不可用,down_trylock 立刻返回一个非零值.67Linux信号量的实现n信号量的V操作nvoid up(struct semaphore*sem);通过down操作进入临界区的进程,再退出的时候都需要调用一个up操作,释放信号量。68Linux信号量的实现n信号量基本使用形式为:static DECLARE_MUTEX(mr_sem);/声明互斥信号量if(down_interruptible(&mr_sem)/*可被中断的睡眠,当信号来到,睡眠的任务被唤醒*/*临界区*/up(&mr_sem);n操作配套使用69Linux 设
40、备驱动调试 70内核调试选项n内核开发者在内核自身中构建了多个调试特性。这些特性会产生额外的输出并降低性能,Linux发行版的内核为了提高性能,去除这些调试特性。n用来开发的内核应当激活的调试配置选项,是在“kernel hacking”菜单中。71通过打印调试 nPrintknprintk 通过附加不同的消息优先级在消息上,对消息的严重程度进行分类。在 定义了8个loglevel。nDEFAULT_MESSAGE_LOGLEVEL为默认级别(printk.c)n当消息优先级小于console_loglevel,信息才能显示出来。而console_loglevel的初值为DEFAULT_CON
41、SOLE_LOGLEVEL。n通过对/proc/sys/kernel/printk的访问来改变console_loglevel的值。该文件包含四个数字:当前的loglevel、默认loglevel、最小允许的loglevel、引导时的默认loglevel。echo 1 /proc/sys/kernel/printk echo 8 /proc/sys/kernel/printk72通过打印调试n打开和关闭消息n通过封装printk函数,快速打开调试信息或者关闭调试信息。#define PDEBUG(fmt,args.)printk(KERN_DEBUG“myscull:fmt,#args)n通过
42、在Makefile里面定义调试开关变量去决定调试信息是否打开。73通过查询调试n获取相关信息的最好方法:在需要的时候才去查询系统信息,而不是持续不断地产生数据。n/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用他向外界导出信息。n/proc下面的每个文件都绑定于一个内核函数,用户读取其中的文件时,该函数动态的生成文件的内容。例如/proc/devices74通过查询调试n包含包含 n在驱动中定义跟proc文件绑定的内核函数read_proc,在函数里面定义要输出的信息。n在初始化函数中调用creat_proc_read_entry函数将/proc入口文件和read_proc函数联
43、系起来。n卸载模块时调用remove_proc_entry撤销proc入口。75通过查询调试nread_proc函数int(*read_proc)(char*page,char*start,off_t offset,int count,int*eof,void*data);npage 是输出数据的缓存内存页。进程读取/proc文件时,内核会分配一个内存页,read_proc将数据通过这个内存页返回到用户空间。nstart 是这个函数用来说有关的数据写在页中哪里neof,当没有数据可返回时,驱动设置这个参数。nData是提供给驱动的专用数据指针。76通过查询调试nint sprintf(char
44、*buf,const char*fmt,.)n将数据打包成字符流的形式。n内核很多象printk函数一样,通过库函数的形式提供给内核开发者的函数,以满足内核开发中的一些简单的需要。nvoid*memset(void*s,char c,size_t count);nvoid*memcpy(void*dest,const void*src,size_t count);77通过查询调试ncreat_proc_read_entry函数struct proc_dir_entry*create_proc_read_entry(const char*name,mode_t mode,struct proc_
45、dir_entry*base,read_proc_t*read_proc,void*data);nname 是要创建的proc文件名,nmod 是文件的访问掩码(缺省0),nbase 指出要创建的文件的目录;如果 base 是 NULL,文件在/proc 根下创建。n read_proc 是实现文件内容的 read_proc 函数,ndata 被内核忽略(传递给 read_proc).78查看Oops信息n大多数bug通常是因为废弃了一个NULL指针或者使用了错误的指针值。这类bug导致的结果通常是一条oops消息。n一条oops消息能够显示发生故障时CPU的状态,以及CPU寄存器的内容和其他
46、看似难以理解的信息。79查看Oops信息n例如访问一个NULL指针。因为NULL不是一个可访问的指针值,所以会引发一个错误,内核会简单地将其转换为oops消息并显示。然后其调用进程会被杀死。Unable to handle kernel NULL pointer dereference at virtual address 00000000 printing eip:d083a064 Oops:0002#1 SMP CPU:0 EIP:0060:Not tainted EFLAGS:00010246(2.6.6)EIP is at oops_example _write+0 x4/0 x10
47、oops_example eax:00000000 ebx:00000000 ecx:00000000 edx:00000000 80通过监视调试nstrace命令可以显示由用户空间程序所发出的所有系统调用。n还以符号形式显示调用的参数和返回值。当一个系统调用失败,错误的符号值(例如,ENOMEM)和对应的字串(Out of memory)都显示.nstrace 有很多命令行选项;n-t 来显示每个调用执行的时间,n-T 来显示调用中花费的时间,n-e 来限制被跟踪调用的类型,n-o 来重定向输出到一个文件.缺省地,strace 打印调用信息到 stderr.81Linux 的内存分配 82k
48、malloc函数void*kmalloc(size_t size,int flags);n所分配到的内存在物理内存中连续且保持原有的数据(不清零)。nsize是要分配的块的大小。Linux 创建一系列内存对象slab,每个slab内的内存块大小是固定。处理分配请求时,就直接在包含有足够大内存块的slab中分配一个整块给请求者。n内核只能分配一些预定义的、固定大小的字节数组。kmalloc 能够处理的最小内存块是 32 或 64 字节(体系结构依赖),而内存块大小的上限随着体系和内核配置而变化。n不应分配大于 128 KB的内存。若需多于几个 KB的内存块,最好使用其他方法。83kmalloc函
49、数void*kmalloc(size_t size,int flags);nFlags 分配标志,表示如何分配空间。所有标志都定义在 nGFP_KERNEL 内存分配内存分配是代表运行在内核内核空间的进程执行的,并且在空闲内存较少时把当前进程转入休眠以等待一个页面。n GFP_ATOMIC 内核通常会为原子性的分配预留一些空闲页。当当前进程不能被置为睡眠时,应使用 GFP_ATOMIC,这样kmalloc 甚至能够使用最后一个空闲页。如果连这最后一个空闲页也不存在,则分配返回失败。常用来从中断处理和进程上下文之外的其他代码中分配内存,从不睡眠。nGFP_DMA 要求分配可用于DMA的内存。84
50、后备高速缓存后备高速缓存 n内核为驱动程序常常需要反复分配许多相同大小内存块的情况,增加了一些特殊的内存池,称为后备高速缓存(lookaside cache)。设备驱动程序通常不会涉及后备高速缓存,但是也有例外:在 Linux 2.6 中 USB 和 SCSI 驱动。85后备高速缓存后备高速缓存n创建一个新的后备高速缓存 kmem_cache_create nname:name和这个后备高速缓存相关联;通常设置为被缓存的结构类型的名字,不能包含空格。n参数size:每个内存区域的大小。noffset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0 表示缺省值。nflags:控制分配方