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