版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
会计学1Linux设备驱动开发设备驱动概述设备由两部分组成,一个是被称为控制器的电器部分,另一个是机械部分。一组寄存器组被赋予到各个控制器。I/O端口包含4组寄存器,即状态寄存器,控制寄存器,数据输入寄存器,数据输出寄存器。状态寄存器拥有可以被CPU读取的(状态)位,用来指示当前命令是否执行完毕,或者字节是否可以被读出或写入,以及任何错误提示。控制寄存器则用于启动一条命令(指令)或者改变设备的(工作)模式。数据输入寄存器用于获取输入的数据。数据输出寄存器则向CPU发送结果。第1页/共133页设备驱动概述操作系统是通过各种驱动程序来驾驭硬件设备,它为用户屏蔽了各种各样的设备。设备驱动程序是操作系统内核和机器硬件之间的接口,系统调用是操作系统内核和应用程序之间的接口。在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作.第2页/共133页4设备驱动概述驱动完成以下的功能:对设备初始化和释放.把数据从内核传送到硬件和从硬件读取数据.读取应用程序传送给设备文件的数据和回送应用程序请求的数据.检测和处理设备出现的错误.第3页/共133页5设备驱动概述无操作系统的设备驱动有操作系统的设备驱动ApplicationDriverHardwareApplicationLibAPISystemcallEmbeddedOSHardware不带操作系统软件结构带操作系统软件结构Driver第4页/共133页6Linux设备驱动第5页/共133页7Linux设备驱动用户级的程序使用内核提供的标准系统调用来与内核通讯,这些系统调用有:open(),read(),write(),ioctl(),close()等等。
Linux的内核是映射到每一个进程的高1G空间。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。
第6页/共133页8Linux设备驱动Linux内核使用“设备无关”的I/O子系统来为所有的设备服务。每个设备都提供标准接口给内核,尽可能地隐藏了自己的特性。用户程序使用一些基本的系统调用从设备读取数据并且将它们存入缓冲的例子。我们可以看到,每当一个系统调用被使用时,内核就转到相应的设备驱动例程来操纵硬件。
第7页/共133页Linux设备驱动Linux操作系统把设备纳入文件系统的范畴来管理。每个设备在Linux系统上看起来都像一个文件,它们存放在/dev目录中,称为"设备节点"。对文件操作的系统调用大都适用于设备文件。
第8页/共133页10Linux设备驱动Linux下设备的属性设备的类型:字符设备、块设备、网络设备主设备号:标识设备对应的驱动程序。一般“一个主设备号对应一个驱动程序”次设备号:每个驱动程序负责管理它所驱动的几个硬件实例,这些硬件实例则由次设备号来表示。同一驱动下的实例编号,用于确定设备文件所指的设备。可通过ls–l“设备文件名”命令查看设备的主次设备号,以及设备的类型。第9页/共133页11Linux设备驱动Linux设备驱动程序是一组由内核中的相关子例程和数据组成的I/O设备软件接口。每当用户程序要访问某个设备时,它就通过系统调用,让内核代替它调用相应的驱动例程。这就使得控制从用户进程转移到了驱动例程,当驱动例程完成后,控制又被返回至用户进程。第10页/共133页12一些重要的数据结构大部分驱动程序涉及三个重要的内核数据结构:文件操作file_operations结构体文件对象file结构体索引节点inode结构体第11页/共133页13一些重要的数据结构文件操作结构体file_operations结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。结构体的每个域都对应着驱动模块用来处理某个被请求的事务的函数的地址。structfile_operations{ structmodule*owner; ssize_t(*read)(structfile*,char__user*,size_t,loff_t*); ssize_t(*write)(structfile*,constchar__user*,size_t,loff_t*);
。。。
}第12页/共133页14一些重要的数据结构file_operations重要的成员Structmodule*owner
,指向拥有该结构体的模块的指针。内核使用该指针维护模块使用计数。方法llseek用来修改文件的当前读写位置,把新位置作为返回值返回。loff_t是在LINUX中定义的长偏移量方法read用来从设备中读取数据。非负返回值表示成功读取的直接数。方法write向设备发送数据。方法ioctl提供一种执行设备特定命令的方法。第13页/共133页15一些重要的数据结构file_operations重要的成员unsignedint(*poll)(structfile*,structpoll_table_struct*);
系统调用select和poll的后端实现,用这两个系统调用来查询设备是否可读写,或是否处于某种状态。如果poll为空,则驱动设备会被认为即可读又可写,返回值是一个状态掩码。int(*mmap)(structfile*,structvm_area_struct*);
将设备内存映射到进程地址空间
第14页/共133页16一些重要的数据结构file_operations重要的成员驱动内核模块是不需要实现每个函数的。相对应的file_operations的项就为NULL。Gcc的语法扩展,使得可以定义该结构体:
structfile_operationsfops={ read:device_read, write:device_write, open:device_open, release:device_release};没有显示声明的结构体成员都被gcc初始化为NULL。第15页/共133页17一些重要的数据结构file_operations重要的成员标准C的标记化结构体的初始化方法:
structfile_operationsfops={ .read=device_read, .write=device_write, .open=device_open, .release=device_release };
推荐使用该方法,提高移植性,方法允许对结构体成员进行重新排列。没有显示声明的结构体成员同样都被gcc初始化为NULL。指向结构体file_operations的指针通常命名为fops。第16页/共133页18一些重要的数据结构文件对象file结构体
文件对象file代表着一个打开的文件。进程通过文件描述符fd与已打开文件的file结构相联系。进程通过它对文件的线性逻辑空间进行操作。例如:file->f_op->read();Structfile在<linux/fs.h>中定义。指向结构体structfile的指针通常命名为filp,或者file。建议使用文件指针filp。第17页/共133页19一些重要的数据结构文件对象file结构体的成员Structfile_operations*f_op;
与文件相关的操作结构体指针。与文件相关的操作是在打开文件的时候确定下来的,也就是确定该指针的值。可在需要的时候,改变指针所指向的文件操作结构体。用C语言实现面向对象编程的方法重载。其他成员可先忽略,后面具体实例分析。因为设备驱动模块并不自己直接填充结构体file,只是使用file中的数据。第18页/共133页20一些重要的数据结构索引节点inode结构文件打开,在内存建立副本后,由唯一的索引节点inode描述。与file结构不同。file结构是进程使用的结构,进程每打开一个文件,就建立一个file结构。不同的进程打开同一个文件,建立不同的file结构。Inode结构是内核使用的结构,文件在内存建立副本,就建立一个inode结构来描述。一个文件在内存里面只有一个inode结构对应。第19页/共133页21一些重要的数据结构索引节点inode结构Inode结构包含大量描述文件信息的成员变量。但是对于描述设备文件的inode,跟设备驱动有关的成员只有两个。Dev_ti_rdev;包含真正的设备编号。Structcdev*i_cdev;指向cdev结构体的指针。cdev是表示字符设备的内核数据结构。从inode中获得主设备号和次设备号的宏:Unsignedintiminor(structinode*inode);Unsignedintimajor(structinode*inode);第20页/共133页22Linux设备驱动主设备号和次设备号的内部表达:Dev_t类型用于保存设备号,称为设备编号。/linux/types.h文件中定义。目前设备编号dev_t是一个32位的整数,其中12位表示主设备号,20位表示次设备号。通过设备编号获取主次设备号:MAJOR(dev_tdev);MINOR(dev_tdev);通过主次设备号合成设备编号:MKDEV(intmajor,intminor);Dev_t格式以后可能会发生变化,但只要使用这些宏,就可保证设备驱动程序的正确性。第21页/共133页23分配和释放字符设备号编写驱动程序要做的第一件事,为字符设备获取一个设备号。事先知道所需要的设备编号(主设备号)的情况:intregister_chrdev_region(dev_tfirst,unsignedcount,constchar*name)first是要分配的起始设备编号值。first的次设备号通常设置为0。Count所请求的连续设备编号的个数。Name设备名称,指和该编号范围建立关系的设备。分配成功返回0。第22页/共133页24分配和释放字符设备号动态分配设备编号(主要是主设备号)intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name)dev是一个仅用于输出的参数,它在函数成功完成时保存已分配范围的第一个编号。baseminor应当是请求的第一个要用的次设备号,它常常是0.count和name参数跟request_chrdev_region的一样.第23页/共133页25分配和释放字符设备号不再使用时,释放这些设备编号。使用以下函数:voidunregister_chrdev_region(dev_tfrom,unsignedcount)在模块的卸载函数中调用该函数。第24页/共133页26分配和释放字符设备号新驱动程序,建议使用动态分配机制获取主设备号,也就是使用alloc_chrdev_region()。动态分配导致无法预先创建设备节点。可在分配设备号后,从/proc/devices文件中获取。为了加载后自动创建设备文件,可以通过编写内核模块加载脚本实现。第25页/共133页27字符设备的注册内核内部使用structcdev结构表示字符设备。编写设备驱动的第二步就是注册该设备。包含<linux/cdev.h>头文件。获取一个独立的cdev结构:structcdev*my_cdev=cdev_alloc();调用cdev_init初始化cdev结构体voidcdev_init(structcdev*cdev,structfile_operations*fops);初始化该设备的所有者字段:dev->cdev.owner=THIS_MODULE;初始化该设备的可用操作集:dev->cdev.ops=&device_fops;第26页/共133页28字符设备的注册编写设备驱动的第二步就是注册该设备。cdev结构已建立和初始化,最后通过cdev_add函数把它告诉内核:
intcdev_add(structcdev*dev,dev_tnum,unsignedintcount);dev是要添加的设备的cdev结构,num是这个设备对应的第一个设备编号,count是应当关联到设备的设备号的数目.卸载字符设备时,调用相反的动作函数:voidcdev_del(structcdev*dev);第27页/共133页29设备的注册早期方法:内核中仍有许多字符驱动不使用刚刚描述过的cdev接口。没有更新到2.6内核接口的老代码。注册一个字符设备的早期方法: intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops);major是给定的主设备号。为0代表什么?name是驱动的名字(将出现在/proc/devices),fops是设备驱动的file_operations结构。register_chrdev将给设备分配0-255的次设备号,并且为每一个建立一个缺省的cdev结构。从系统中卸载字符设备的函数: intunregister_chrdev(unsignedintmajor,constchar*name);第28页/共133页30Open方法编写字符设备驱动的第三步:定义设备驱动与文件系统的接口,file_operation结构体的函数定义。open方法int(*open)(structinode*inode,structfile*filp);驱动程序提供open方法,让用户进程使用设备之前,进行一些初始化的工作。检查设备特定的错误。如果第一次打开设备,则初始化设备。如果需要,更新f_op指针,更换操作方法集。分配并填充要放进filp->private_data的任何数据结构。第29页/共133页31Open方法对于设备文件,inode参数只有两个参数对设备驱动有用的。Dev_ti_rdev;包含真正的设备编号。Structcdev*i_cdev;指向cdev结构体的指针。i_cdev里面包含我们之前建立的cdev结构。但是有时候,我们需要的是包含cdev结构的描述设备的结构。使用通过成员地址获取结构体地址的宏container_of,在<linux/kernel.h>中定义:container_of(pointer,container_type,container_field);这个宏使用一个指向container_field类型的成员的指针,它在一个container_type类型的结构中,宏通过分析他们关系,返回指向包含该成员的结构体指针.第30页/共133页32Open方法在myscull_open,这个宏用来找到适当的设备结构:dev=container_of(inode->i_cdev,structscull_dev,cdev);找到myscull_dev结构后,scull在filp->private_data中存储其指针,为以后存取使用.
filp->private_data=dev;
第31页/共133页33release方法release方法做open相反的工作释放open分配给filp->private_data的内存空间。在最后一次的关闭操作时,关闭设备。不是每个close系统调用引起调用release方法。第32页/共133页34Read和Write方法Read的任务,就是从设备拷贝数据到用户空间。Write的任务,则从用户空间拷贝数据到设备。ssize_tread(structfile*filp,char__user*buff,size_tcount,loff_t*offp);ssize_twrite(structfile*filp,constchar__user*buff,size_tcount,loff_t*offp);filp是文件对象指针,count
是请求的传输数据大小.buff
参数对write来说是指向持有被写入数据的缓存,对read则是放入新数据的空缓存.offp
是指向一个“longoffsettype”的指针,它指出用户正在存取的文件位置.返回值是“signedsizetype”类型;第33页/共133页35Read和Write方法read和write方法的buff参数是用户空间指针,不能被内核代码直接解引用。__user字符串只是形式上的说明,表明是用户空间地址。驱动必须能够存取用户空间缓存以完成它的工作。内核如何解决这个问题?为安全起见,内核提供专用的函数来完成对用户空间的存取。这些专用函数在<asm/uaccess.h>中声明。
unsignedlongcopy_to_user(void__user*to,constvoid*from,unsignedlongcount); unsignedlongcopy_from_user(void*to,constvoid__user*from,unsignedlongcount);大多数读写函数都会调用这两个函数,用于跟应用程序空间交流信息。第34页/共133页36Read和Write方法典型的Read函数对参数的使用。第35页/共133页37llseek函数llseek函数用于对设备文件访问定位。驱动接口loff_t(*llseek)(structfile*,loff_t,int);库函数off_tlseek(intfiledes,off_toffset,intwhence);
参数offset的含义取决于参数whence:如果whence是SEEK_SET,文件偏移量将被设置为offset。如果whence是SEEK_CUR,文件偏移量将被设置为cfo加上offset,offset可以为正也可以为负。如果whence是SEEK_END,文件偏移量将被设置为文件长度加上offset,offset可以为正也可以为负。SEEK_SET、SEEK_CUR和SEEK_END是SystemV引入的,是0、1和2。第36页/共133页38ioctl
进行超出简单的数据传输之外的操作,进行各种硬件控制操作.ioctl方法和用户空间版本不同的原型:
int(*ioctl)(structinode*inode,structfile*filp,
unsignedintcmd,unsignedlongarg)不管可选的参数arg是否由用户给定为一个整数或一个指针,它都以一个unsignedlong的形式传递。返回值
POSIX标准规定:如果使用了不合适的ioctl命令号,应当返回-ENOTTY。这个错误码被C库解释为“不合适的设备ioctl。
-EINVAL也是相当普遍的。第37页/共133页39结构化设备驱动程序设备结构体把与某设备相关的所有内容定义为一个设备结构体其中包括设备驱动涉及的硬件资源、全局软件资源、控制(自旋锁、互斥锁、等待队列、定时器等)在涉及设备的操作时,就仅仅操作这个结构体第38页/共133页40Linux设备驱动的并发控制第39页/共133页41设备驱动的并发控制在驱动程序中,当多个线程同时访问相同的资源时,可能会引发“竞态”,必须对共享资源进行并发控制。并发和竞态广泛存在。并发控制的目的: 使得线程访问共享资源的操作是原子操作。原子操作: 在执行过程中不会被别的代码路径所中断的操作。驱动程序中的全局变量是一种典型的共享资源。第40页/共133页42考虑一个非常简单的共享资源的例子:一个全局整型变量和一个简单的临界区,其中的操作仅仅是将整型变量的值增加1:
i++
该操作可以转化成下面三条机器指令序列:得到当前变量i的值并拷贝到一个寄存器中将寄存器中的值加1把i的新值写回到内存中
原子操作第41页/共133页43原子操作内核任务1内核任务2获得i(1)
增加
i(1->2)
写回
i(2)
获得
i(2)
增加
i(2->3)
写回
i(3)内核任务1内核任务2获得
i(1)增加i(1->2)
获得i(1)
增加
i(1->2)
写回i(2)写回i(2)可能的实际执行结果:期望的结果第42页/共133页44Linux内核的并发控制在内核空间的内核任务需要考虑同步内核空间中的共享数据对内核中的所有任务可见,所以当在内核中访问数据时,就必须考虑是否会有其他内核任务并发访问的可能、是否会产生竞争条件、是否需要对数据同步。第43页/共133页45确定保护对象
找出哪些数据需要保护是关键所在内核任务的局部数据仅仅被它本身访问,显然不需要保护。如果数据只会被特定的进程访问,也不需加锁
大多数内核数据结构都需要加锁:若有其它内核任务可以访问这些数据,那么就给这些数据加上某种形式的锁;若任何其它东西能看到它,那么就要锁住它。
Linux内核的并发控制第44页/共133页46Linux内核的并发控制并发控制的机制中断屏蔽,原子数操作,自旋锁和信号量都是解决并发问题的机制。中断屏蔽很少被单独使用,原子操作只能针对整数来进行。因此自旋锁和信号量应用最为广泛。
第45页/共133页47中断屏蔽单CPU系统中,避免竟态的一种简单方式保证正在执行的内核执行路径不被中断处理程序所抢占,防止竟态条件的发生。Local_irq_disable() //关中断…Criticalsection //临界区…Local_irq_enable() //开中断中断对内核非常重要,长时间屏蔽中断非常危险!只适合短时间的关闭对SMP多CPU引发的竟态无效第46页/共133页48锁机制可以避免竞争状态正如门锁和门一样,门后的房间可想象成一个临界区。在一段时间内,房间里只能有一个内核任务存在,当一个任务进入房间后,它会锁住身后的房门;当它结束对共享数据的操作后,就会走出房间,打开门锁。如果另一个任务在房门上锁时来了,那么它就必须等待房间内的任务出来并打开门锁后,才能进入房间。
加锁机制第47页/共133页49任何要访问临界资源的代码首先都需要占住相应的锁,这样该锁就能阻止来自其它内核任务的并发访问:
任务1
试图锁定队列成功:获得锁访问队列…
为队列解除锁…
任务2试图锁定队列失败:等待…
等待…
等待…
成功:获得锁
访问队列…为队列解除锁加锁机制第48页/共133页50原子数操作整型原子数操作原子变量初始化
atomic_ttest=ATOMIC_INIT(i);设置原子变量的值
voidatomic_set(atomic_t*v,inti)获得原子变量的值
atomic_read(v)原子变量加
voidatomic_add(inti,atomic_t*v)原子变量减
voidatomic_sub(inti,atomic_t*v)第49页/共133页51原子数操作整型原子数操作原子变量的自增操作
voidatomic_inc(atomic_t*v)原子变量的自减操作
voidatomic_dec(atomic_t*v)操作并测试(测试其是否为0,0为true,否为false)atomic_inc_and_test(atomic_t*v)atomic_dec_and_test(atomic_t*v)intatomic_sub_and_test(inti,atomic_t*v)操作并返回(返回新值)intatomic_add_return(inti,atomic_t*v)intatomic_sub_return(inti,atomic_t*v)第50页/共133页52原子数操作原子位操作设置位
voidset_bit(intnr,volatileunsignedlong*addr)清除位
voidclear_bit(intnr,volatileunsignedlong*addr)改变位
change_bit(nr,p)测试位
test_bit(intnr,constvolatileunsignedlong*p)测试并操作位
test_and_set_bit(nr,p)第51页/共133页53自旋锁自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。而对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。自旋锁最多只能被一个内核任务持有,若一个内核任务试图请求一个已被持有的自旋锁,那么这个任务就会一直进行忙循环,也就是旋转,等待锁重新可用。
自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。第52页/共133页54自旋锁自旋锁的初衷就是:
在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话,最好使用信号量。
第53页/共133页55自旋锁自旋锁防止在不同CPU上的执行单元对共享资源的同时访问,以及不同进程上下文互相抢占导致的对共享资源的非同步访问。在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
自旋锁不允许任务睡眠。第54页/共133页56自旋锁自旋锁的基本形式如下:
spin_lock(&mr_lock); /*临界区*/ spin_unlock(&mr_lock);第55页/共133页57自旋锁自旋锁原语要求包含文件是<linux/spinlock.h>.锁的类型是spinlock_t.锁的两种初始化方法:spinlock_tmy_lock=SPIN_LOCK_UNLOCKED;voidspin_lock_init(spinlock_t*lock);进入一个临界区前,必须获得需要的lock。voidspin_lock(spinlock_t*lock);自旋锁等待是不可中断的。一旦你调用spin_lock,将自旋直到锁变为可用。释放一个锁:voidspin_unlock(spinlock_t*lock);第56页/共133页58自旋锁关中断的自旋锁Spin_lock_irq()Spin_unlock_irq()Spin_lock_irqsave()Spin_unlock_irqrestore()第57页/共133页59信号量Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;信号量的操作信号量支持两个原子操作P()和V(),前者做测试操作,后者叫做增加操作。Linux中分别叫做down()和up()。
第58页/共133页60信号量down()和up()。down()操作通过对信号量计数减1来请求获得一个信号量。如果结果是0或大于0,信号量锁被获得,任务就可以进入临界区了。如果结果是负数,任务会被放入等待队列,处理器执行其它任务。相反,当临界区中的操作完成后,up()操作用来释放信号量,增加信号量的计数值。如果在该信号量上的等待队列不为空,处于队列中等待的任务在被唤醒的同时会获得该信号量。第59页/共133页61信号量第60页/共133页62信号量第61页/共133页63Linux信号量的实现内核代码必须包含<asm/semaphore.h>,才能使用信号量。相关的类型是structsemaphore信号量的定义structsemaphore{
atomic_tcount;
intsleepers;
wait_queue_head_twait;
}
第62页/共133页64Linux信号量的实现信号量的声明和初始化直接创建一个信号量structsemaphore*sem;接着使用sema_init来初始化这个信号量:
voidsema_init(structsemaphore*sem,intval);互斥模式的信号量声明,内核提供宏定义.DECLARE_MUTEX(name);
信号量初始化为1DECLARE_MUTEX_LOCKED(name);
信号量初始化为0第63页/共133页65Linux信号量的实现动态分配的互斥信号量声明voidinit_MUTEX(structsemaphore*sem);
信号量初始化为1voidinit_MUTEX_LOCKED(structsemaphore*sem);
信号量初始化为0第64页/共133页66Linux信号量的实现信号量的P操作voiddown(structsemaphore*sem); down减小信号量的值,并根据信号量的值决定是否等待。不可中断的等待。intdown_interruptible(structsemaphore*sem);
操作是可中断的。intdown_trylock(structsemaphore*sem);
信号量在调用时不可用,down_trylock立刻返回一个非零值.第65页/共133页67Linux信号量的实现信号量的V操作voidup(structsemaphore*sem);
通过down操作进入临界区的进程,再退出的时候都需要调用一个up操作,释放信号量。第66页/共133页68Linux信号量的实现信号量基本使用形式为:staticDECLARE_MUTEX(mr_sem);//声明互斥信号量…if(down_interruptible(&mr_sem))/*可被中断的睡眠,当信号来到,睡眠的任务被唤醒*//*临界区…*/up(&mr_sem);操作配套使用第67页/共133页69Linux
设备驱动调试
第68页/共133页70内核调试选项内核开发者在内核自身中构建了多个调试特性。这些特性会产生额外的输出并降低性能,Linux发行版的内核为了提高性能,去除这些调试特性。用来开发的内核应当激活的调试配置选项,是在“kernelhacking”
菜单中。第69页/共133页71通过打印调试Printkprintk通过附加不同的消息优先级在消息上,对消息的严重程度进行分类。在<linux/kernel.h>定义了8个loglevel。DEFAULT_MESSAGE_LOGLEVEL为默认级别(printk.c)当消息优先级小于console_loglevel,信息才能显示出来。而console_loglevel的初值为DEFAULT_CONSOLE_LOGLEVEL。通过对/proc/sys/kernel/printk的访问来改变console_loglevel的值。该文件包含四个数字:当前的loglevel、默认loglevel、最小允许的loglevel、引导时的默认loglevel。echo1>/proc/sys/kernel/printkecho8>/proc/sys/kernel/printk第70页/共133页72通过打印调试打开和关闭消息通过封装printk函数,快速打开调试信息或者关闭调试信息。
#definePDEBUG(fmt,args...)\ printk(KERN_DEBUG“myscull:"fmt,##args)通过在Makefile里面定义调试开关变量去决定调试信息是否打开。第71页/共133页73通过查询调试获取相关信息的最好方法:在需要的时候才去查询系统信息,而不是持续不断地产生数据。/proc文件系统是一种特殊的、由软件创建的文件系统,内核使用他向外界导出信息。/proc下面的每个文件都绑定于一个内核函数,用户读取其中的文件时,该函数动态的生成文件的内容。例如/proc/devices第72页/共133页74通过查询调试包含<linux/proc_fs.h>在驱动中定义跟proc文件绑定的内核函数read_proc,在函数里面定义要输出的信息。在初始化函数中调用creat_proc_read_entry函数将/proc入口文件和read_proc函数联系起来。卸载模块时调用remove_proc_entry撤销proc入口。第73页/共133页75通过查询调试read_proc函数int(*read_proc)(char*page,char**start,off_toffset,intcount,int*eof,void*data);page是输出数据的缓存内存页。进程读取/proc文件时,内核会分配一个内存页,read_proc将数据通过这个内存页返回到用户空间。start是这个函数用来说有关的数据写在页中哪里eof,当没有数据可返回时,驱动设置这个参数。Data是提供给驱动的专用数据指针。第74页/共133页76通过查询调试intsprintf(char*buf,constchar*fmt,...)将数据打包成字符流的形式。内核很多象printk函数一样,通过库函数的形式提供给内核开发者的函数,以满足内核开发中的一些简单的需要。void*memset(void*s,charc,size_tcount);void*memcpy(void*dest,constvoid*src,size_tcount);
第75页/共133页77通过查询调试creat_proc_read_entry函数
structproc_dir_entry*create_proc_read_entry(constchar*name,mode_tmode,structproc_dir_entry*base,read_proc_t*read_proc,void*data);name是要创建的proc文件名,mod是文件的访问掩码(缺省0),base指出要创建的文件的目录;如果base是NULL,文件在/proc根下创建。
read_proc是实现文件内容的read_proc函数,data被内核忽略(传递给read_proc).第76页/共133页78查看Oops信息大多数bug通常是因为废弃了一个NULL指针或者使用了错误的指针值。这类bug导致的结果通常是一条oops消息。一条oops消息能够显示发生故障时CPU的状态,以及CPU寄存器的内容和其他看似难以理解的信息。第77页/共133页79查看Oops信息例如访问一个NULL指针。因为NULL不是一个可访问的指针值,所以会引发一个错误,内核会简单地将其转换为oops消息并显示。然后其调用进程会被杀死。
UnabletohandlekernelNULLpointerdereferenceatvirtualaddress00000000
printingeip:d083a064Oops:0002[#1]SMPCPU:0EIP:0060:[]NottaintedEFLAGS:00010246(2.6.6)
EIPisatoops_example_write+0x4/0x10[oops_example]
eax:00000000ebx:00000000ecx:00000000edx:00000000
…
第78页/共133页80通过监视调试strace命令可以显示由用户空间程序所发出的所有系统调用。还以符号形式显示调用的参数和返回值。当一个系统调用失败,错误的符号值(例如,ENOMEM)和对应的字串(Outofmemory)都显示.strace有很多命令行选项;-t来显示每个调用执行的时间,-T来显示调用中花费的时间,-e来限制被跟踪调用的类型,-o来重定向输出到一个文件.缺省地,strace打印调用信息到stderr.第79页/共133页81Linux
的内存分配
第80页/共133页82kmalloc函数void*kmalloc(size_tsize,intflags);所分配到的内存在物理内存中连续且保持原有的数据(不清零)。size是要分配的块的大小。Linux创建一系列内存对象slab,每个slab内的内存块大小是固定。处理分配请求时,就直接在包含有足够大内存块的slab中分配一个整块给请求者。内核只能分配一些预定义的、固定大小的字节数组。kmalloc能够处理的最小内存块是32或64字节(体系结构依赖),而内存块大小的上限随着体系和内核配置而变化。不应分配大于128KB的内存。若需多于几个KB的内存块,最好使用其他方法。
第81页/共133页83kmalloc函数void*kmalloc(size_tsize,intflags);Flags分配标志,表示如何分配空间。所有标志都定义在<linux/gfp.h>GFP_KERNEL
内存分配是代表运行在内核空间的进程执行的,并且在空闲内存较少时把当前进程转入休眠以等待一个页面。
GFP_ATOMIC
内核通常会为原子性的分配预留一些空闲页。当当前进程不能被置为睡眠时,应使用GFP_ATOMIC,这样kmalloc甚至能够使用最后一个空闲页。如果连这最后一个空闲页也不存在,则分配返回失败。常用来从中断处理和进程上下文之外的其他代码中分配内存,从不睡眠。GFP_DMA
要求分配可用于DMA的内存。第82页/共133页84后备高速缓存
内核为驱动程序常常需要反复分配许多相同大小内存块的情况,增加了一些特殊的内存池,称为后备高速缓存(lookasidecache)。设备驱动程序通常不会涉及后备高速缓存,但是也有例外:在Linux2.6中USB和SCSI驱动。第83页/共133页85后备高速缓存创建一个新的后备高速缓存kmem_cache_createname:name和这个后备高速缓存相关联;通常设置为被缓存的结构类型的名字,不能包含空格。参数size:每个内存区域的大小。offset:页内第一个对象的偏移量;用来确保被分配对象的特殊对齐,0表示缺省值。flags:控制分配方式的位掩码:SLAB_NO_REAP
保护缓存在系统查找内存时不被削减,不推荐。SLAB_HWCACHE_ALIGN
所有数据对象跟高速缓存行对齐,平台依赖,可能浪费内存。SLAB_CACHE_DMA
每个数据对象在DMA内存区段分配.第84页/共133页86后备高速缓存创建一个新的后备高速缓存kmem_cache_create参数constructor和destructor是可选函数,用来初始化新分配的对象和在内存被作为整体释放给系统之前“清理”对象。第85页/共133页87后备高速缓存kmem_cache_alloc从已创建的后备高速缓存中分配对象:
void*kmem_cache_alloc(kmem_cache_t*cache,intflags);flags和kmalloc的flags相同使用kmem_cache_free释放一个对象:
voidkmem_cache_free(kmem_cache_t*cache,constvoid*obj);当驱动用完这个后备高速缓存(通常在当模块被卸载时),释放缓存: intkmem_cache_destroy(kmem_cache_t*cache);第86页/共133页88get_free_page相关函数如果一个模块需要分配大块的内存,最好使用面向页的分配技术。
__get_free_page(unsignedintflags);
返回一个指向新页的指针,未清零该页get_zeroed_page(unsignedintflags);
类似于__get_free_page,但用零填充该页__get_free_pages(unsignedintflags,unsignedintorder);
分配是若干(物理连续的)页面并返回指向该内存区域的第一个字节的指针,该内存区域未清零flags与kmalloc的用法相同order是请求或释放的页数以2为底的对数。若其值过大(没有这么大的连续区可用),则分配失败
第87页/共133页89get_free_page相关函数当程序不需要页面时,用下列函数之一来释放
voidfree_page(unsignedlongaddr);
voidfree_pages(unsignedlongaddr,unsignedlongorder);第88页/共133页90Linux
的中断处理
第89页/共133页91为什么会有中断中断最初是为克服对I/O接口控制采用程序查询所带来的处理器低效率而产生的。处理器速度一般比外设快很多用轮询的方式来查询设备的状态,CPU效率不高,CPU和外设不能并行工作。中断机制让CPU启动设备后,就去处理其他任务,只有当外设真正完成数据传输的准备,请求CPU服务的时候,CPU才转过来处理外设的请求。第90页/共133页92中断和异常外部中断: 外部设备所发出的I/O请求。随着计算机系统结构的不断改进以及应用技术的日益提高,中断的适用范围也随之扩大,出现了所谓的内部中断(或叫异常)。异常: 为解决机器运行时所出现的某些随机事件及编程方便而出现的。第91页/共133页93I/O中断处理为了保证系统对外部的响应,一个中断处理程序必须被尽快的完成。因此,把所有的操作都放在中断处理程序中并不合适Linux中把紧随中断要执行的操作分为三类紧急的(critical)
一般关中断运行。诸如对PIC应答中断,对PIC或是硬件控制器重新编程,或者修改由设备和处理器同时访问的数据非紧急的(noncritical)
如修改那些只有处理器才会访问的数据结构(例如按下一个键后读扫描码),这些也要很快完成,因此由中断处理程序立即执行,不过一般在开中断的情况下第92页/共133页94I/O中断处理Linux中把紧随中断要执行的操作分为三类非紧急可延迟的(noncriticaldeferrable)这些操作可以被延迟较长的时间间隔而不影响内核操作,有兴趣的进程将会等待数据。内核用下半部分这样一个机制来在一个更为合适的时机用独立的函数来执行这些操作。如把缓冲区内容拷贝到某个进程的地址空间(例如把键盘缓冲区内容发送到终端处理程序进程)。第93页/共133页95S3c2410的中断中断发生时,需要知道中断的来源。芯片的引线有限,很难提供很多条中断请求引线。使用中断控制器管理中断请求线。S3c2410将中断控制器集成在CPU芯片中,“复用”GPIO端口引脚,具有作为中断请求线的功能。SRCPND寄存器32位中的每一位对应着一个中断源,每一位被设置为1,则相应的中断源产生中断请求并且等待中断被服务。第94页/共133页96注册中断服务例程
中断号是一个宝贵且常常有限的资源。内核维护一个中断号的注册表。要使用中断,就要进行中断号的申请,也就是IRQ(InterruptReQuirement)。只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,让更多的设备使用中断。第95页/共133页97注册中断服务例程
在<linux/interrupt.h>实现中断注册接口: intrequest_irq(unsignedintirq, irqreturn_t(*handler)(int,void*,structpt_regs*), unsignedlongflags, constchar*dev_name, void*dev_id ); voidfree_irq(unsignedintirq,void*dev_id);request_irq的返回值是0指示申请成功,为负值时表示错误码。函数返回-EBUSY表示已经有另一个驱动占用了所要申请的中断线。第96页/共133页98注册中断服务例程
request_irq的参数说明:unsignedintirq,要申请的中断号。irqreturn_t(*handler)(int,void*,structpt_regs*),要安装的中断处理函数指针。constchar*dev_name,
用在/proc/interrupts中显示中断的拥有者。第97页/共133页99注册中断服务例程
request_irq的参数说明:unsignedlongflags,与中断管理相关的位掩码选项。Flags的每个位有不同含义SA_INTERRUPT当该位被设置时,表示这是一个“快速”中断。快速中断处理例程运行时,屏蔽中断。SA_SHIRQ这个位表示中断可以在设备间共享。void*dev_id
这个指针用于共享的中断号。做为驱动程序的私有数据区(可用来识别那个设备产生的中断)。不使用共享中断线方式时,可设置为NULL。第98页/共133页100proc文件系统中的中断信息/proc/interrupts反映系统的中断信息第一列是IRQ号给出每个中断线发生中断的次数。给出处理中断的可编程中断控制器。给出在该中断号上注册中断处理例程的设备名称。第99页/共133页101实现中断处理例程中断处理例程特别之处:在中断时间内运行,不能向用户空间发送或者接收数据。不能做任何导致休眠的操作。不能调用schedule函数。无论快速还是慢速中断处理例程,都应该设计成执行时间尽可能短。第100页/共133页102实现中断处理例程中断处理函数的参数和返回值irqreturn_t(*handler)(intirq,void*dev_id,structpt_regs*regs)Irq中断号Dev_id驱动程序可用的数据区,通常可传递指向描述设备的数据结构指针。structpt_regs*regs,保存了处理器进入中断代码之前的cpu寄存器的值。一般驱动可不要。第101页/共133页103实现中断处理例程启动和禁用中断驱动禁止特定中断线的中断:#include<asm/irq.h>.voiddisable_irq(intirq);voidenable_irq(intirq);禁止所有中断voidlocal_irq_save(unsignedlongflags);local_irq_save在当前处理器上禁止中断递交,在保存当前中断状态到flags。voidlocal_irq_disable(void);local_irq_disable关闭本地中断递交而不保存状态;第102页/共133页104实现中断处理例程打开中断:voidlocal_irq_restore(unsignedlongflags);
恢复由local_irq_save存储于flags的状态,而local_irq_enable无条件打开中断.voidlocal_irq_enable(void);第103页/共133页105顶半部和底半步中断处理的一个主要问题是如何在处理中进行长时间的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理需要很快完成并且不使中断阻塞太长。Linux把中断处理例程分两部分:顶部分:实际响应中断的例程。底部分:被顶部分调用,通过开中断的方式进行。两种机制实现:Tasklet工作队列workqueue第104页/共133页106顶半部和底半部顶半部顶半部的功能是“登记中断”,当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的底半部执行队列中去。顶半部执行的速度就会很快,可以服务更多的中断请求。底半部仅有“登记中断”是远远不够的,因为中断的事件可能很复杂。Linux引入了一个底半部,来完成中断事件的绝大多数使命。底半部和顶半部最大的不同是底半部是可中断的,而顶半部是不可中断的,底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!底半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。
第105页/共133页107小任务机制tasklet内核在BH机制的基础上进行了扩展,实现“软中断请求”(softirq)机制。利用软中断代替bottomhalfhandler的处理。tasklet机制正是利用软中断来完成对驱动bottomhalf的处理。tasklet会让内核选择某个合适的时间来执行给定的小任务。tasklet所对应的处理函数就是tasklet_action,这个处理函数在系统启动时初始化软中断时,就在软中断向量表中注册。Tasklet_action遍历一个全局的tasklet_vec链表。链表中的元素为tasklet_struct结构体。第106页/共133页108软中断和tasklet的关系如下图:小任务机制tasklet第107页/共133页109小任务机制taskletksoftirqd是一个后台运行的内核线程,它会周期的遍历软中断的向量列表,如果发现哪个软中断向量被挂起了(pend),就执行对应的处理函数。tasklet所对应的处理函数就是tasklet_action,这个处理函数在系统启动时初始化软中断时,就在软中断向量表中注册。第108页/共133页110小任务以数据结构的形式存在:structtasklet_struct{ structtasklet_struct*next; unsignedlongstate; atomic_tcount; void(*func)(unsignedlong); unsignedlongdata;};每个结构一个函数指针func,指向自定义的函数。这就是我们要执行的小任务函数。小任务机制tasklet第109页/共133页111tasklet的接口DECLARE_TASKLET(name,function,data)此接口初始化一个tasklet;name是tasklet的名字,function是执行tasklet的函数;data是unsignedlong类型的function参数。
staticinlinevoidtasklet_schedule(structtasklet_struct*t)调度执行指定的tasklet。将定义后的tasklet挂接到cpu的tasklet_vec链表。而且会引起一个软tasklet的软中断,既把tasklet对应的中断向量挂起(pend)。小任务机制tasklet第110页/共133页112voidtasklet_disable(structtasklet_struct*t);这个函数禁止给定的tasklet.tasklet,但仍然可以被tasklet_schedule调度,但是它的执行被延后直到这个tasklet被再次激活。voidtasklet_enable(structtasklet_struct*t);激活一个之前被禁止的tasklet.如果这个tasklet已经被调度,它会很快运行.
一个对tasklet_enable的调用必须匹配每个对tasklet_disable的调用,因为内核跟踪每个tasklet的"禁止次数".小任务机制tasklet第111页/共133页113voidtasklet_hi_schedule(structtasklet_struct*t);调度tasklet在更高优先级执行.当软中断处理运行时,它在其他软中断之前处理高优先级tasklet。voidtasklet_kill(structtasklet_struct*t);这个函数确保了这个tasklet没被再次调度来运行;它常常被调用当一个设备正被关闭或者模块卸载时.如果这个tasklet被调度来运行,这个函数等待直到它已执行.
小任务机制tasklet第112页/共133页114工作队列
工作队列类似taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:tasklet在软件中断上下文中运行,所以tasklet代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行,有更多的灵活性,且能够休眠。tasklet只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。内核代码可以请求工作队列函数被延后一个给定的时间间隔。tasklet执行的很快,短时期,并且在原子态,而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。第113页/共133页115工作队列structworkqueue_struct类型在workqueue.h中定义。一个工作队列必须明确的在使用前创建,宏为: structworkqueue_struct*create_workqueue(constchar*name); structworkqueue_struct*create_singlethread_workqueue(constchar*name);每个工作队列有一个或多个专用的进程("内核线程"),这些进程运行提交给这个队列的函数。若使用create_workqueue,就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用create_singlethread_workqueue来创建工作队列。第114页/共133页116工作队列当用完一个工作队列,可以去掉它,使用:voiddestroy_workqueue(structworkqueue_struct*queue);
第115页/共133页117Linux内核中定义了一个timer_list结构,我们在驱动程序中可以利用定时器structtimer_list{
structlist_headlist;
unsignedlongexpires;//定时器到期时间
unsignedlongdata;//作为参数被传入定时器处理函数
void(*function)(unsignedlong);
};第116页/共133页118定时器timer的API函数:增加定时器
voidadd_timer(structtimer_list*timer);删除定时器 intdel_timer(structtimer_list*timer);修改定时器的expire intmod_timer(structtimer_list*timer,unsignedlongexpires);第117页/共133页119定时器使用定时器的一般流程为:
(1)timer、编写function;
(2)为timer的expires、data、function赋值;
(3)调用add_timer将timer加入列表;
(4)在定时器到期时,function被执行;
(5)在程序中涉及timer控制的地方适当地调用del_timer、mod_timer删除timer或修改timer的expires。第118页/共133页120矩阵式键盘原理矩阵式键盘一般适用于按键数量较多的场合,它由行线和列线组成,按键位于行、列的交叉点上。如图所示,一个4×4的行、列结构可以构成一个有16个按键的键盘。第119页/共133页121矩阵式键盘原理按键设置在行、列交叉点上,行、列分别连接到按键开关的两端。行线通过上拉电阻接到十5V上。平时无按键动作时,行线处于高电平状态;而当有健按下时,行线电平状态将由通过此按键的列线电平决定:列线电平如果为低,行线电平为低;列线电平如果为高,则行线电平亦为高。这一点是识别矩阵式键盘是否被按下的关键所在。第120页/共133页122矩阵式键盘原理矩阵键盘按键的识别方法分两步进行:①识别键盘哪一行的键被按下。让所有列线均为低电平,检查各行线电平是否为低。如果有行线为低,则说明该行有键被按下,否则说明无键被按下。②如果某行有键被按下,识别键盘哪一列的键被按下
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 现代办公事务处理任务十一拟写洽谈接待计划
- 一背肌二胸肌三膈四腹肌五盆部肌第三节躯干
- 土壤三普分析培训
- 《咨询行业研究》课件
- 团队凝聚力培训
- MBA领导力培训课程
- 《女子体育卫生》课件
- 《电工基本知识》课件
- 《光电发射器件》课件
- 妊娠糖尿病护理知识
- 《内蒙古自治区国土空间规划(2021-2035年)》
- JGJ48-2014 商店建筑设计规范
- 酒店开业庆典活动合同
- 2024新外研版初一上英语单词默写表
- 病人发生心脏骤停应急预案演练脚本
- 高考化学一轮总复习:环境保护与绿色化学
- 山东省各地2023-2024学年高一数学第二学期期末统考试题含解析
- 2024届高考英语作文复习专项读后续写:拯救小猫任务单素材
- 雷雨-剧本原文-高中语文雷雨剧本原文
- 某电机修造厂35kv终端变电所设计
- 2.1《迎接蚕宝宝的到来》教学设计(新课标)
评论
0/150
提交评论