【文档说明】linux驱动基础知识讲解-精品课件.pptx,共(72)页,1.035 MB,由小橙橙上传
转载请保留链接:https://www.ichengzhen.cn/view-3056.html
以下为本文档部分文字说明:
Linux驱动学习总结汇报2019年11月12日内核模块Bootloder并发控制中断处理设备驱动的结构Linux内核重要子系统1.系统调用接口2.进程管理3.内存管理4.虚拟文件系统5.网络堆栈6.设备驱动最简单的嵌入式系统Bo
otloader参数系统内核根文件系统Flash从低地址到高地址方向MTK的Bootloader在嵌入式操作系统中,BootLoader是在操作系统内核运行之前运行。可以初始化硬件设备、建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,以便为最终调用操作系统
内核准备好正确的环境。MTK的bootloader有两部分组成:(1)第1部分bootloader,也就是MTK内部(in-house)的pre-loader,这部分依赖平台。(2)第2部分bootloader,也就是LittleKernel,这部分依赖操作系统,负责引导linux操作系统
和Android框架。源码位置:\vendor\mediatek\proprietary\bootable\bootloaderMTK的Bootloader正常启动的主要工作如下:(1)设备上电后,BootROM开始运行。(2)BootROM初始化软件堆栈(softwarest
ack)、通信端口和可引导存储设备(比如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从存储器中加载引导镜像(bootimage),包括linux内核和ramd
isk(Android呢?)(8)LK跳转到linux内核并执行。MTK的Bootloaderpre-loaders中涉及的硬件部分(1)PLL模块1)PLL模块用于调整处理器和外部内存的频率。2)在PL
L模块初始化后,处理器和外部内存的频率可由26MHZ/26MHZ增加到1GHZ/192MHZ。(2)UART模块1)UART模块用于调试或是META(MobileEngineeringTestingArchitecture)模式下的握手。2)默认情况下,UART4初始化波特率为
9216000bps和用于调试信息的输出,UART1初始化为115200bps和作为UARTMETA端口。但也可以使用UART1作为调试或是UARTMETA端口。(3)计时器(timer)模块这是个基本的模块,用来计算硬件模块所需要
的延时或是超时时间。(4)内存模块1)Pre-loader由bootROM加载和在芯片组内部的SRAM中执行,因为外部的DRAM还没有初始化。2)为了准备软件整个可执行环境,pre-loader采用内置的内存设置来初始化DRAM(DRAMisinitializeduponpre
-loaderbuilt-inmemorysettigns)。这样,LK就能够被加载到DRAM中并执行。(5)GPIO模块(6)PMIC模块为了提供一些基本的硬件功能,比如控制外设电源,pre-loader初始化上层模块(uppermodules)。(7)RTC模块1)当通过pow
er按键开机后,pre-loader拉高RTC的PWBB来保持设备一直有电(keepthedevicealive)和继续引导LK。2)RTC闹钟(alarm)有可能是设备开机的启动源,对于这种情况,设备部需要按power按键就可自
动启动。(8)USB模块当USB线插入时,它初始化来和外部工具通信,比如用于升级系统的下载工具或是META模式触发器的META工具。(9)NAND模块(10)MSDC模块Pre-loader可以从NANDf
lash或是EMMC中加载LK,这两者只能选择其中一种来启动。LK中涉及的硬件部分LK是第2个loader,它由pre-loader引导并执行。从根本上来说(basically),pre-loader
已经初始化了相关的硬件模块,而不需要在LK中重新配置这些模块了。但一些模块在LK中被重新复位来配置硬件寄存器,这样可创造一个干净的环境。比如计时器模块,在LK中,计时器重新复位清零硬件计数来对计时进行复位。所有在LK中需要初始化的列在下面:(1)计时器
模块通过复位硬件寄存器来复位计时。(2)串口模块LK采用串口模块来配置它的输入/输出系统,在这个模块初始化后,我们可以使用LK提供的“printf(…)”等函数来使用串口功能。(3)I2C模块(4)P
WM模块(5)PMIC模块(6)RTC模块和计时器模块一样,在U-Boot中,I2C/PMIC/RTC重新复位寄存器来复位这些模块。(7)LED模块通过这poweroffcharging个模块,设备能够通知用户当前的充电状态。(8)充电
模块这个模块负责关机充电(poweroffcharging)、低电压充电(lowercharginginthesystem)。(9)LCD模块使用这个模块,设备能够显示logo或是任何通知的消息。(10)NAND模块因为
U-Boot也需要从flash读取镜像(比如内核或是ramdisk),所以有必要在U-Boot中初始化NAND相关的功能。(11)MSDC模块支持MSDC启动一些重要的数据结构1.大部分驱动程序涉及三个重要的内核数据结构:•文件操作file_operations结构体•文
件对象file结构体•索引节点inode结构体Linux设备驱动Linux下设备的属性设备的类型:字符设备、块设备、网络设备主设备号:标识设备对应的驱动程序。一般“一个主设备号对应一个驱动程序”次设备号:每个驱动程序负责管理它所驱动的几个硬件实例,这些硬件实例则由次设备号来表示。同一驱
动下的实例编号,用于确定设备文件所指的设备。可通过ls–l“设备文件名”命令查看设备的主次设备号,以及设备的类型。18分配和释放字符设备号1.编写驱动程序要做的第一件事,为字符设备获取一个设备号。2.事先知道所需要的设备编号(主设备号)的情况:•intregister
_chrdev_region(dev_tfirst,unsignedcount,constchar*name)•first是要分配的起始设备编号值。first的次设备号通常设置为0。•Count所请求的连续设备编号的个数。•Name设备名称,指和该编号范围建立关系的设备。•分配成功返回0。19分
配和释放字符设备号1.动态分配设备编号(主要是主设备号)•intalloc_chrdev_region(dev_t*dev,unsignedbaseminor,unsignedcount,constchar*name)•dev是一个仅用于输出的参数,它在函数成功完成
时保存已分配范围的第一个编号。•baseminor应当是请求的第一个要用的次设备号,它常常是0.•count和name参数跟request_chrdev_region的一样.20分配和释放字符设备号1.不再使用时,释放这些
设备编号。使用以下函数:•voidunregister_chrdev_region(dev_tfrom,unsignedcount)•在模块的卸载函数中调用该函数。21字符设备的注册1.内核内部使用structcdev
结构表示字符设备。编写设备驱动的第二步就是注册该设备。•包含<linux/cdev.h>头文件。•获取一个独立的cdev结构:structcdev*my_cdev=cdev_alloc();•调用cdev_init初始化cdev结构体void
cdev_init(structcdev*cdev,structfile_operations*fops);•初始化该设备的所有者字段:dev->cdev.owner=THIS_MODULE;•初始化
该设备的可用操作集:dev->cdev.ops=&device_fops;22字符设备的注册1.编写设备驱动的第二步就是注册该设备。•cdev结构已建立和初始化,最后通过cdev_add函数把它告诉内核:intcdev_ad
d(structcdev*dev,dev_tnum,unsignedintcount);•dev是要添加的设备的cdev结构,•num是这个设备对应的第一个设备编号,•count是应当关联到设备的设备号的数目.•卸载字符设备时,调用相反的动作函数:•voidcdev_del(stru
ctcdev*dev);23Linux设备驱动的并发控制24设备驱动的并发控制1.在驱动程序中,当多个线程同时访问相同的资源时,可能会引发“竞态”,必须对共享资源进行并发控制。2.并发和竞态广泛存在。3.并发控制的目的:使得线程访问共享资源的操作是原子操作。1.原子
操作:在执行过程中不会被别的代码路径所中断的操作。1.驱动程序中的全局变量是一种典型的共享资源。251.考虑一个非常简单的共享资源的例子:一个全局整型变量和一个简单的临界区,其中的操作仅仅是将整型变量的值增加1:i++1.该操作可以转化成下面三条机器指令
序列:①得到当前变量i的值并拷贝到一个寄存器中②将寄存器中的值加1③把i的新值写回到内存中原子操作26Linux内核的并发控制1.在内核空间的内核任务需要考虑同步•内核空间中的共享数据对内核中的所有任务可见,所以当在内核中访问数据时,就必须考虑是否会有其他内核任务并发访问的可能、
是否会产生竞争条件、是否需要对数据同步。27•确定保护对象•找出哪些数据需要保护是关键所在•内核任务的局部数据仅仅被它本身访问,显然不需要保护。•如果数据只会被特定的进程访问,也不需加锁•大多数内核数据结构都需要加锁:若有其
它内核任务可以访问这些数据,那么就给这些数据加上某种形式的锁;若任何其它东西能看到它,那么就要锁住它。Linux内核的并发控制28Linux内核的并发控制1.并发控制的机制•中断屏蔽,原子数操作,自旋
锁和信号量都是解决并发问题的机制。•中断屏蔽很少被单独使用,原子操作只能针对整数来进行。因此自旋锁和信号量应用最为广泛。291.锁机制可以避免竞争状态正如门锁和门一样,门后的房间可想象成一个临界区。2.在一段时
间内,房间里只能有一个内核任务存在,当一个任务进入房间后,它会锁住身后的房门;当它结束对共享数据的操作后,就会走出房间,打开门锁。如果另一个任务在房门上锁时来了,那么它就必须等待房间内的任务出来并打开门锁后,才能进入房间。加锁机制301.任何要访问临界资源的代码首
先都需要占住相应的锁,这样该锁就能阻止来自其它内核任务的并发访问:任务1试图锁定队列成功:获得锁访问队列„为队列解除锁„任务2试图锁定队列失败:等待„等待„等待„成功:获得锁访问队列„为队列解除锁加锁机制31原
子数操作1.整型原子数操作•原子变量初始化atomic_ttest=ATOMIC_INIT(i);•设置原子变量的值voidatomic_set(atomic_t*v,inti)•获得原子变量的值atomic_read(v)•原子变量加voi
datomic_add(inti,atomic_t*v)•原子变量减voidatomic_sub(inti,atomic_t*v)32原子数操作1.整型原子数操作•原子变量的自增操作voidatomic_inc(atomic_t*v)•原子变量的自减操作voidatomi
c_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)33原子数操作1.原子位操作•设置位voidset_bit(in
tnr,volatileunsignedlong*addr)•清除位voidclear_bit(intnr,volatileunsignedlong*addr)•改变位change_bit(nr,p)•测试位test_bi
t(intnr,constvolatileunsignedlong*p)•测试并操作位34自旋锁1.自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分。而对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁。2.自旋锁最多只能被一个内核任
务持有,若一个内核任务试图请求一个已被持有的自旋锁,那么这个任务就会一直进行忙循环,也就是旋转,等待锁重新可用。3.自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。35自旋锁1.自旋锁的初衷就是:在
短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话,最好使用信号量。36自旋锁1.自旋锁防止在不同CP
U上的执行单元对共享资源的同时访问,以及不同进程上下文互相抢占导致的对共享资源的非同步访问。2.在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。3.自旋锁不允许任务睡眠。37自旋锁1.自旋锁的基本形式如下:sp
in_lock(&mr_lock);/*临界区*/spin_unlock(&mr_lock);38自旋锁1.自旋锁原语要求包含文件是<linux/spinlock.h>.锁的类型是spinlock_t.2.锁的两种初始化方法:•spinlock_tmy_lock=SPIN_LOC
K_UNLOCKED;•voidspin_lock_init(spinlock_t*lock);3.进入一个临界区前,必须获得需要的lock。•voidspin_lock(spinlock_t*lock);•自旋锁等待是不可中断
的。一旦你调用spin_lock,将自旋直到锁变为可用。4.释放一个锁:•voidspin_unlock(spinlock_t*lock);39自旋锁1.关中断的自旋锁•Spin_lock_irq()•Spin_unlock_irq()•Spin_lock_irqsave()•Spin_u
nlock_irqrestore()40信号量1.Linux中的信号量是一种睡眠锁。•如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。•当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个
信号量。•信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;2.信号量的操作•信号量支持两个原子操作P()和V(),前者做测试操作,后者叫做增加操作。•Linux中分别叫做down()和up()。41信号量S:=S-1S大于等于0当前进程封锁
,PCB连到队列末尾,放弃CPUdown(s)w(s)NOYES42信号量S:=S+1S大于0up(s)R(s)NOYES释放信号量队列上的第一个进程,当前进程继续进行43Linux信号量的实现1.内核代码必须包含<asm/s
emaphore.h>,才能使用信号量。2.相关的类型是structsemaphore信号量的定义structsemaphore{atomic_tcount;intsleepers;wait_queue_head_twait;}4
4Linux信号量的实现1.信号量的声明和初始化•直接创建一个信号量structsemaphore*sem;•接着使用sema_init来初始化这个信号量:voidsema_init(structsemaphore*sem,intval);1.互斥模式的信号量声明
,内核提供宏定义.•DECLARE_MUTEX(name);信号量初始化为1•DECLARE_MUTEX_LOCKED(name);信号量初始化为045自旋锁忙等待,无调度开销;进程抢占被禁止;锁定期
间不能休眠;信号量拿不到就切换进程,有调度开销;锁定期间可以休眠;46Linux的中断处理47为什么会有中断1.中断最初是为克服对I/O接口控制采用程序查询所带来的处理器低效率而产生的。•处理器速度一般比外设快很多•用轮询的方式来查询设备的状态,CPU效率不高,CPU和外设不能并行工作
。•中断机制让CPU启动设备后,就去处理其他任务,只有当外设真正完成数据传输的准备,请求CPU服务的时候,CPU才转过来处理外设的请求。48中断和异常1.外部中断:外部设备所发出的I/O请求。1.随着计算机系统结构的不断改进以及应用技术的日益提高,中断的适用范围也随
之扩大,出现了所谓的内部中断(或叫异常)。2.异常:为解决机器运行时所出现的某些随机事件及编程方便而出现的。49I/O中断处理1.为了保证系统对外部的响应,一个中断处理程序必须被尽快的完成。因此,把所有的操作都放在中断处理程序中并不合适2.Linu
x中把紧随中断要执行的操作分为三类•紧急的(critical)一般关中断运行。诸如对PIC应答中断,对PIC或是硬件控制器重新编程,或者修改由设备和处理器同时访问的数据•非紧急的(noncritical)如修改那些只有处理器才会访问的数据
结构(例如按下一个键后读扫描码),这些也要很快完成,因此由中断处理程序立即执行,不过一般在开中断的情况下50I/O中断处理1.Linux中把紧随中断要执行的操作分为三类•非紧急可延迟的(noncriticaldeferrable)•这些操作可以被延迟较长的时间间隔而不影响内核操
作,有兴趣的进程将会等待数据。内核用下半部分这样一个机制来在一个更为合适的时机用独立的函数来执行这些操作。•如把缓冲区内容拷贝到某个进程的地址空间(例如把键盘缓冲区内容发送到终端处理程序进程)。51注册中断服务例程1.中断号是一个宝贵且常常有限的资源。内核维护一个中断
号的注册表。2.要使用中断,就要进行中断号的申请,也就是IRQ(InterruptReQuirement)。3.只有当设备需要中断的时候才申请占用一个IRQ,或者是在申请IRQ时采用共享中断的方式,让更多的设备使用中断。52
注册中断服务例程1.在<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);1.request_irq的返回值是0指示申请成功,为负值时表示错误码。函数返回-EBUSY表示已
经有另一个驱动占用了所要申请的中断线。53注册中断服务例程1.request_irq的参数说明:•unsignedintirq,要申请的中断号。•irqreturn_t(*handler)(int,void*,structpt_regs*),要安装的中断处理函数
指针。•constchar*dev_name,用在/proc/interrupts中显示中断的拥有者。54注册中断服务例程1.request_irq的参数说明:•unsignedlongflags,与中断管理相关的位掩码选项。•
Flags的每个位有不同含义•SA_INTERRUPT当该位被设置时,表示这是一个“快速”中断。快速中断处理例程运行时,屏蔽中断。•SA_SHIRQ这个位表示中断可以在设备间共享。•void*dev_id这个指针用于共享的中断号。做为驱动程序的私有数据区(可用来识别那个设备产生的中
断)。不使用共享中断线方式时,可设置为NULL。55实现中断处理例程1.中断处理例程特别之处:•在中断时间内运行,不能向用户空间发送或者接收数据。•不能做任何导致休眠的操作。•不能调用schedule函数。•无论快速还是
慢速中断处理例程,都应该设计成执行时间尽可能短。56实现中断处理例程1.中断处理函数的参数和返回值irqreturn_t(*handler)(intirq,void*dev_id,structpt_regs*regs)•Irq中断号•Dev_id驱动程序可用的数据区,通常可传递指向描述设备的
数据结构指针。•structpt_regs*regs,保存了处理器进入中断代码之前的cpu寄存器的值。一般驱动可不要。57实现中断处理例程1.启动和禁用中断•驱动禁止特定中断线的中断:•#include<asm/irq.h>.•vo
iddisable_irq(intirq);•voidenable_irq(intirq);•禁止所有中断•voidlocal_irq_save(unsignedlongflags);•local_irq_save在当前处理器上禁止
中断递交,在保存当前中断状态到flags。•voidlocal_irq_disable(void);•local_irq_disable关闭本地中断递交而不保存状态;58实现中断处理例程1.打开中断:•voidlocal_irq_restore(unsignedlongflags);恢复由loc
al_irq_save存储于flags的状态,而local_irq_enable无条件打开中断.•voidlocal_irq_enable(void);59顶半部和底半部1.中断处理的一个主要问题是如何在处理中进行长时间的任务。响应一次设备中断需要完成一定数量的工作,但是中断处理需要很快完成并
且不使中断阻塞太长。2.Linux把中断处理例程分两部分:•顶部分:实际响应中断的例程。•底部分:被顶部分调用,通过开中断的方式进行。两种机制实现:•Tasklet•工作队列workqueue60顶半部和底半部1.顶半部•顶半部的功能是“登记中断”,当一个
中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的底半部执行队列中去。•顶半部执行的速度就会很快,可以服务更多的中断请求。2.底半部•仅有“登记中断”是远远不够的,因为中断的事件可能很复杂。Linux引入了一个底半部,来完成中断事件的绝大多
数使命。•底半部和顶半部最大的不同是底半部是可中断的,而顶半部是不可中断的,底半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!•底半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行
时机,不在中断服务上下文中执行。611.软中断和tasklet的关系如下图:小任务机制tasklet62小任务机制tasklet1.ksoftirqd是一个后台运行的内核线程,它会周期的遍历软中断的向量列表,如果发现哪个软中断向量被挂起了(pend),就执行对应的
处理函数。2.tasklet所对应的处理函数就是tasklet_action,这个处理函数在系统启动时初始化软中断时,就在软中断向量表中注册。631.小任务以数据结构的形式存在:structtasklet_struct{s
tructtasklet_struct*next;unsignedlongstate;atomic_tcount;void(*func)(unsignedlong);unsignedlongdata;};2.每个结构一个函数指针fu
nc,指向自定义的函数。这就是我们要执行的小任务函数。小任务机制tasklet641.tasklet的接口•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)。小任务机制tasklet65工作队列1.工作队列类似taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:•tasklet
在软件中断上下文中运行,所以tasklet代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行,有更多的灵活性,且能够休眠。•tasklet只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。•内核代码可以请求工作队列函数被延后一个给定的时间间隔。
•tasklet执行的很快,短时期,并且在原子态,而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。66工作队列1.structworkqueue_struct类型在workqueue.h中定义。一个工作队列必须明确的在使用前创建,宏为:st
ructworkqueue_struct*create_workqueue(constchar*name);structworkqueue_struct*create_singlethread_workqueue(con
stchar*name);•每个工作队列有一个或多个专用的进程("内核线程"),这些进程运行提交给这个队列的函数。若使用create_workqueue,就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在
很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用create_singlethread_workqueue来创建工作队列。67工作队列1.当用完一个工作队列,可以去掉它,使用:voiddestroy_workqueue(structworkqueu
e_struct*queue);Linux内核延时:延时是对机器时钟中断的运用忙等待Voidmdelay(unsignedlongnsecs)#definemdelay(n)(\(__builtin_constant_p(n)&&(n)<=MAX_UDELAY_MS)?udelay((n)
*1000):\({unsignedlong__ms=(n);while(__ms--)udelay(1000);}))睡着延时Voidmsleep(unsignedintmillisecs)/***msleep-sleepsafelyevenwithwaitqueueinterru
ptions*msecs:Timeinmillisecondstosleepfor*/voidmsleep(unsignedintmsecs){unsignedlongtimeout=msecs_to_jiffies(msecs)+1;while(timeout)t
imeout=schedule_timeout_uninterruptible(timeout);}设备驱动模型架构的思考设备驱动模型有三部分组成:设备、驱动、总线。驱动只管驱动,设备只管设备,总线负责匹配设备和驱动,驱动则以标准途径拿到板级信息。问题:总线在匹配过程中是先去匹
配设备还是先匹配总线?