1、Linux 驱动学习总结汇报,2016年11月12日,内核模块Bootloder并发控制中断处理设备驱动的结构,Linux内核重要子系统,系统调用接口进程管理内存管理虚拟文件系统网络堆栈设备驱动,最简单的嵌入式系统,MTK的Bootloader,在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统内核准备好正确的环境。MTK的bootloader有两部分组成:(1)第1部分bootloader,也就是MTK内部(in-house)的pre-loader,这部分依赖平台。(2)
2、第2部分bootloader,也就是Little Kernel,这部分依赖操作系统,负责引导linux操作系统和Android框架。源码位置: vendormediatekproprietarybootablebootloader,MTK的Bootloader,正常启动的主要工作如下:(1)设备上电后,Boot ROM开始运行。(2)BootROM初始化软件堆栈(software stack)、通信端口和可引导存储设备(比如NAND/EMMC)。(3)BootROM从存储器中加载pre-loader到内部SRAM(ISRAM)中,因为这时候还没有初始化外部的DRAM。(4)BootROM跳转到
3、pre-loader的入口处并执行。(5)Pre-loader初始化DRAM和加载LK到RAM中。(6)Pre-loader跳转到LK中并执行,然后LK做一些初始化,比如显示的初始化等。(7)LK从存储器中加载引导镜像(boot image),包括linux内核和ramdisk(Android呢?)(8)LK跳转到linux内核并执行。,MTK的Bootloader,pre-loaders中涉及的硬件部分,(1)PLL模块1)PLL模块用于调整处理器和外部内存的频率。2)在PLL模块初始化后,处理器和外部内存的频率可由26MHZ/26MHZ增加到1GHZ/192MHZ。(2)UART模块1)U
4、ART模块用于调试或是META(Mobile Engineering Testing Architecture)模式下的握手。2)默认情况下,UART4初始化波特率为9216000bps和用于调试信息的输出,UART1初始化为115200bps和作为UART META端口。但也可以使用UART1作为调试或是UART META端口。(3)计时器(timer)模块这是个基本的模块,用来计算硬件模块所需要的延时或是超时时间。,(4)内存模块1)Pre-loader由boot ROM加载和在芯片组内部的SRAM中执行,因为外部的DRAM还没有初始化。2)为了准备软件整个可执行环境,pre-loader
5、采用内置的内存设置来初始化DRAM(DRAM is initialized upon pre-loader built-inmemory settigns)。这样,LK就能够被加载到DRAM中并执行。(5)GPIO模块(6)PMIC模块为了提供一些基本的硬件功能,比如控制外设电源,pre-loader初始化上层模块(upper modules)。(7)RTC模块1)当通过power按键开机后,pre-loader拉高RTC的PWBB来保持设备一直有电(keep the device alive)和继续引导LK。2)RTC闹钟(alarm)有可能是设备开机的启动源,对于这种情况,设备部需要按po
6、wer按键就可自动启动。,(8)USB模块当USB线插入时,它初始化来和外部工具通信,比如用于升级系统的下载工具或是META模式触发器的META工具。(9)NAND模块(10) MSDC模块Pre-loader可以从NAND flash或是EMMC中加载LK,这两者只能选择其中一种来启动。,LK中涉及的硬件部分,LK是第2个loader,它由pre-loader引导并执行。从根本上来说(basically),pre-loader已经初始化了相关的硬件模块,而不需要在LK中重新配置这些模块了。但一些模块在LK中被重新复位来配置硬件寄存器,这样可创造一个干净的环境。比如计时器模块,在LK中,计时器
7、重新复位清零硬件计数来对计时进行复位。所有在LK中需要初始化的列在下面:(1)计时器模块通过复位硬件寄存器来复位计时。(2)串口模块LK采用串口模块来配置它的输入/输出系统,在这个模块初始化后,我们可以使用LK提供的“printf()”等函数来使用串口功能。,(3)I2C模块(4)PWM模块(5)PMIC模块(6)RTC模块和计时器模块一样,在U-Boot中,I2C/PMIC/RTC重新复位寄存器来复位这些模块。(7)LED模块通过这power off charging个模块,设备能够通知用户当前的充电状态。(8)充电模块这个模块负责关机充电(power off charging)、低电压充电
8、(lower charging in the system)。,(9)LCD模块使用这个模块,设备能够显示logo或是任何通知的消息。(10) NAND模块因为U-Boot也需要从flash读取镜像(比如内核或是ramdisk),所以有必要在U-Boot中初始化NAND相关的功能。(11) MSDC模块支持MSDC启动,一些重要的数据结构,大部分驱动程序涉及三个重要的内核数据结构:文件操作file_operations结构体文件对象file结构体索引节点inode结构体,Linux设备驱动,Linux下设备的属性设备的类型:字符设备、块设备、网络设备主设备号:标识设备对应的驱动程序。一般“一个
9、主设备号对应一个驱动程序”次设备号:每个驱动程序负责管理它所驱动的几个硬件实例,这些硬件实例则由次设备号来表示。同一驱动下的实例编号,用于确定设备文件所指的设备。可通过ls l “设备文件名”命令查看设备的主次设备号,以及设备的类型。,18,分配和释放字符设备号,编写驱动程序要做的第一件事,为字符设备获取一个设备号。事先知道所需要的设备编号(主设备号)的情况:int register_chrdev_region(dev_t first, unsigned count, const char *name)first是要分配的起始设备编号值。 first的次设备号通常设置为0。Count 所请求的
10、连续设备编号的个数。Name设备名称,指和该编号范围建立关系的设备。分配成功返回0。,19,分配和释放字符设备号,动态分配设备编号(主要是主设备号)int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name)dev 是一个仅用于输出的参数, 它在函数成功完成时保存已分配范围的第一个编号。baseminor 应当是请求的第一个要用的次设备号,它常常是 0. count 和 name 参数跟request_chrdev_region 的一样.,20,分配和释放字符设备号,不再使用时,
11、释放这些设备编号。使用以下函数:void unregister_chrdev_region(dev_t from, unsigned count)在模块的卸载函数中调用该函数。,21,字符设备的注册,内核内部使用struct cdev结构表示字符设备。编写设备驱动的第二步就是注册该设备。包含头文件。获取一个独立的cdev结构:struct cdev *my_cdev = cdev_alloc();调用cdev_init初始化cdev结构体void cdev_init(struct cdev *cdev, struct file_operations *fops);初始化该设备的所有者字段:de
12、v-cdev.owner = THIS_MODULE;初始化该设备的可用操作集:dev-cdev.ops = ,22,字符设备的注册,编写设备驱动的第二步就是注册该设备。cdev 结构已建立和初始化, 最后通过cdev_add函数把它告诉内核:int cdev_add(struct cdev *dev, dev_t num, unsigned int count);dev 是要添加的设备的 cdev 结构, num 是这个设备对应的第一个设备编号,count 是应当关联到设备的设备号的数目. 卸载字符设备时,调用相反的动作函数:void cdev_del(struct cdev *dev);,
13、23,Linux设备驱动的并发控制,24,设备驱动的并发控制,在驱动程序中,当多个线程同时访问相同的资源时,可能会引发“竞态”,必须对共享资源进行并发控制。并发和竞态广泛存在。并发控制的目的:使得线程访问共享资源的操作是原子操作。原子操作:在执行过程中不会被别的代码路径所中断的操作。 驱动程序中的全局变量是一种典型的共享资源。,25,考虑一个非常简单的共享资源的例子:一个全局整型变量和一个简单的临界区,其中的操作仅仅是将整型变量的值增加1: i+ 该操作可以转化成下面三条机器指令序列:得到当前变量i的值并拷贝到一个寄存器中将寄存器中的值加1把i的新值写回到内存中,原子操作,26,Linux内核
14、的并发控制,在内核空间的内核任务需要考虑同步内核空间中的共享数据对内核中的所有任务可见,所以当在内核中访问数据时,就必须考虑是否会有其他内核任务并发访问的可能、是否会产生竞争条件、是否需要对数据同步。,27,确定保护对象 找出哪些数据需要保护是关键所在内核任务的局部数据仅仅被它本身访问,显然不需要保护。如果数据只会被特定的进程访问,也不需加锁 大多数内核数据结构都需要加锁:若有其它内核任务可以访问这些数据,那么就给这些数据加上某种形式的锁;若任何其它东西能看到它,那么就要锁住它。,Linux内核的并发控制,28,Linux内核的并发控制,并发控制的机制中断屏蔽,原子数操作,自旋锁和信号量都是解
15、决并发问题的机制。中断屏蔽很少被单独使用,原子操作只能针对整数来进行。因此自旋锁和信号量应用最为广泛。,29,锁机制可以避免竞争状态正如门锁和门一样,门后的房间可想象成一个临界区。在一段时间内,房间里只能有一个内核任务存在,当一个任务进入房间后,它会锁住身后的房门;当它结束对共享数据的操作后,就会走出房间,打开门锁。如果另一个任务在房门上锁时来了,那么它就必须等待房间内的任务出来并打开门锁后,才能进入房间。,加锁机制,30,任何要访问临界资源的代码首先都需要占住相应的锁,这样该锁就能阻止来自其它内核任务的并发访问:,加锁机制,31,原子数操作,整型原子数操作原子变量初始化atomic_t te
16、st = ATOMIC_INIT(i);设置原子变量的值void atomic_set(atomic_t *v, int i)获得原子变量的值atomic_read(v)原子变量加void atomic_add(int i, atomic_t *v)原子变量减void atomic_sub(int i, atomic_t *v),32,原子数操作,整型原子数操作原子变量的自增操作void atomic_inc(atomic_t *v)原子变量的自减操作void atomic_dec(atomic_t *v)操作并测试 (测试其是否为0,0为true,否为false)atomic_inc_and
17、_test(atomic_t *v)atomic_dec_and_test(atomic_t *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),33,原子数操作,原子位操作设置位void set_bit(int nr, volatile unsigned long * addr)清除位void clear_bit(int nr, volatile unsigned
18、long * addr)改变位change_bit(nr,p)测试位test_bit(int nr, const volatile unsigned long * p)测试并操作位test_and_set_bit(nr,p),34,自旋锁,自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。而对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。 自旋锁最多只能被一个内核任务持有,若一个内核任务试图请求一个已被持有的自旋锁,那么这个任务就会一直进行忙循环,也就是旋转,等待锁重新可用。 自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,
19、因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。,35,自旋锁,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。,36,自旋锁,自旋锁防止在不同CPU上的执行单元对共享资源的同时访问,以及不同进程上下文互相抢占导致的对共享资源的非同步访问。在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。 自旋锁不允许任务睡眠。,37,自旋锁,自旋锁的基本形式如下:spin_lock(,38,自旋锁,自旋锁原语要求包含文件是 .
20、 锁的类型是 spinlock_t.锁的两种初始化方法: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);,39,自旋锁,关中断的自旋锁Spin_lock_irq( )Spin_unlock_irq( )Spin_lock
21、_irqsave ( )Spin_unlock_irqrestore ( ),40,信号量,Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;信号量的操作信号量支持两个原子操作P()和V(),前者做测试操作,后者叫做增加操作。Linux中分别叫做down()和up()。,41,信号量,42,信号量,43,Linux信号量的实现,内核代码必须包含 ,才能使用信号量。相关的类型是
22、struct semaphore,信号量的定义,struct semaphore atomic_t count; int sleepers; wait_queue_head_t wait; ,44,Linux信号量的实现,信号量的声明和初始化直接创建一个信号量 struct semaphore * sem;接着使用 sema_init 来初始化这个信号量:void sema_init(struct semaphore *sem, int val);互斥模式的信号量声明,内核提供宏定义.DECLARE_MUTEX(name);信号量初始化为 1DECLARE_MUTEX_LOCKED(name)
23、;信号量初始化为0,45,自旋锁忙等待,无调度开销;进程抢占被禁止;锁定期间不能休眠;信号量拿不到就切换进程,有调度开销;锁定期间可以休眠;,46,Linux 的中断处理,47,为什么会有中断,中断最初是为克服对I/O接口控制采用程序查询所带来的处理器低效率而产生的。处理器速度一般比外设快很多用轮询的方式来查询设备的状态,CPU效率不高,CPU和外设不能并行工作。中断机制让CPU启动设备后,就去处理其他任务,只有当外设真正完成数据传输的准备,请求CPU服务的时候,CPU才转过来处理外设的请求。,48,中断和异常,外部中断:外部设备所发出的I/O请求。随着计算机系统结构的不断改进以及应用技术的日
24、益提高,中断的适用范围也随之扩大,出现了所谓的内部中断(或叫异常)。异常:为解决机器运行时所出现的某些随机事件及编程方便而出现的。,49,I/O中断处理,为了保证系统对外部的响应,一个中断处理程序必须被尽快的完成。因此,把所有的操作都放在中断处理程序中并不合适Linux中把紧随中断要执行的操作分为三类紧急的(critical)一般关中断运行。诸如对PIC应答中断,对PIC或是硬件控制器重新编程,或者修改由设备和处理器同时访问的数据非紧急的(noncritical)如修改那些只有处理器才会访问的数据结构(例如按下一个键后读扫描码),这些也要很快完成,因此由中断处理程序立即执行,不过一般在开中断的
25、情况下,50,I/O中断处理,Linux中把紧随中断要执行的操作分为三类非紧急可延迟的(noncritical deferrable)这些操作可以被延迟较长的时间间隔而不影响内核操作,有兴趣的进程将会等待数据。内核用下半部分这样一个机制来在一个更为合适的时机用独立的函数来执行这些操作。如把缓冲区内容拷贝到某个进程的地址空间(例如把键盘缓冲区内容发送到终端处理程序进程)。,51,注册中断服务例程,中断号是一个宝贵且常常有限的资源。内核维护一个中断号的注册表。要使用中断,就要进行中断号的申请,也就是IRQ(Interrupt ReQuirement)。只有当设备需要中断的时候才申请占用一个IRQ,
26、或者是在申请IRQ时采用共享中断的方式,让更多的设备使用中断。,52,注册中断服务例程,在 实现中断注册接口:int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *),unsigned long flags,const char *dev_name, void *dev_id);void free_irq(unsigned int irq, void *dev_id);request_irq 的返回值是 0 指示申请成功,为负值时表示错误码。函数返回 -EBUSY 表示已经有另
27、一个驱动占用了所要申请的中断线。,53,注册中断服务例程,request_irq的参数说明:unsigned int irq, 要申请的中断号。irqreturn_t (*handler)(int, void *, struct pt_regs *),要安装的中断处理函数指针。const char *dev_name, 用在 /proc/interrupts 中显示中断的拥有者。,54,注册中断服务例程,request_irq的参数说明:unsigned long flags,与中断管理相关的位掩码选项。Flags的每个位有不同含义SA_INTERRUPT 当该位被设置时, 表示这是一个“快速
28、”中断。快速中断处理例程运行时,屏蔽中断。SA_SHIRQ 这个位表示中断可以在设备间共享。void *dev_id这个指针用于共享的中断号。做为驱动程序的私有数据区(可用来识别那个设备产生的中断)。不使用共享中断线方式时,可设置为NULL。,55,实现中断处理例程,中断处理例程特别之处:在中断时间内运行,不能向用户空间发送或者接收数据。不能做任何导致休眠的操作。不能调用schedule函数。无论快速还是慢速中断处理例程,都应该设计成执行时间尽可能短。,56,实现中断处理例程,中断处理函数的参数和返回值irqreturn_t (*handler)(int irq, void *dev_id,
29、struct pt_regs *regs)Irq 中断号Dev_id 驱动程序可用的数据区,通常可传递指向描述设备的数据结构指针。struct pt_regs *regs,保存了处理器进入中断代码之前的cpu寄存器的值。一般驱动可不要。,57,实现中断处理例程,启动和禁用中断驱动禁止特定中断线的中断:#include .void disable_irq(int irq);void enable_irq(int irq);禁止所有中断void local_irq_save(unsigned long flags);local_irq_save 在当前处理器上禁止中断递交, 在保存当前中断状态到
30、flags。void local_irq_disable(void);local_irq_disable 关闭本地中断递交而不保存状态;,58,实现中断处理例程,打开中断:void local_irq_restore(unsigned long flags);恢复由 local_irq_save 存储于 flags 的状态, 而 local_irq_enable 无条件打开中断.void local_irq_enable(void);,59,顶半部和底半部,中断处理的一个主要问题是如何在处理中进行长时间的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理需要很快完成并且不使中断阻塞太长
31、。Linux把中断处理例程分两部分:顶部分:实际响应中断的例程。底部分:被顶部分调用,通过开中断的方式进行。两种机制实现:Tasklet工作队列work queue,60,顶半部和底半部,顶半部顶半部的功能是“登记中断”,当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的底半部执行队列中去。顶半部执行的速度就会很快,可以服务更多的中断请求。底半部仅有“登记中断”是远远不够的,因为中断的事件可能很复杂。Linux引入了一个底半部,来完成中断事件的绝大多数使命。底半部和顶半部最大的不同是底半部是可中断的,而顶半部是不可中断的,底半部几乎做了中断处理程序所有的事情,而且可以被新
32、的中断打断!底半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。,61,软中断和 tasklet 的关系如下图:,小任务机制tasklet,62,小任务机制tasklet,ksoftirqd是一个后台运行的内核线程,它会周期的遍历软中断的向量列表,如果发现哪个软中断向量被挂起了( pend ),就执行对应的处理函数。tasklet 所对应的处理函数就是tasklet_action,这个处理函数在系统启动时初始化软中断时,就在软中断向量表中注册。,63,小任务以数据结构的形式存在:struct tasklet_structstruct ta
33、sklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;每个结构一个函数指针func,指向自定义的函数。这就是我们要执行的小任务函数。,小任务机制tasklet,64,tasklet 的接口DECLARE_TASKLET(name,function,data) 此接口初始化一个 tasklet ;name 是 tasklet 的名字, function 是执行 tasklet 的函数; data 是 unsigned long 类型的 functio
34、n 参数。 static inline void tasklet_schedule(struct tasklet_struct *t) 调度执行指定的tasklet。将定义后的 tasklet 挂接到 cpu 的 tasklet_vec 链表。而且会引起一个软 tasklet 的软中断 , 既把 tasklet 对应的中断向量挂起 (pend) 。,小任务机制tasklet,65,工作队列,工作队列类似 taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:tasklet 在软件中断上下文中运行,所以 tasklet 代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行
35、,有更多的灵活性,且能够休眠。tasklet 只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。内核代码可以请求工作队列函数被延后一个给定的时间间隔。tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。,66,工作队列,struct workqueue_struct 类型在 workqueue.h中定义。一个工作队列必须明确的在使用前创建,宏为:struct workqueue_struct *create_workqueue(const char *name);struct workqueue_struct *
36、create_singlethread_workqueue(const char *name); 每个工作队列有一个或多个专用的进程(内核线程), 这些进程运行提交给这个队列的函数。 若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。,67,工作队列,当用完一个工作队列,可以去掉它,使用: void destroy_workqueue(struct workqueue_struct *queue);,L
37、inux内核延时: 延时是对机器时钟中断的运用忙等待Void mdelay(unsigned long nsecs)#define mdelay(n) (_builtin_constant_p(n) ),睡着延时Void msleep(unsigned int millisecs)/* * msleep - sleep safely even with waitqueue interruptions * msecs: Time in milliseconds to sleep for */void msleep(unsigned int msecs)unsigned long timeout = msecs_to_jiffies(msecs) + 1;while (timeout)timeout = schedule_timeout_uninterruptible(timeout);,设备驱动模型架构的思考,设备驱动模型有三部分组成:设备、驱动、总线。驱动只管驱动,设备只管设备,总线负责匹配设备和驱动,驱动则以标准途径拿到板级信息。问题:总线在匹配过程中是先去匹配设备还是先匹配总线?,