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