platform设备驱动
LINUX2.6的设备驱动模型中,关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。
基于这个背景,LINUX发明了一种虚拟总线,称为platform_bus_type,相应的设备称为platform_device,而驱动成为platform_driver
struct platform_device { const char * name; int id; struct device dev; u32 num_resources; struct resource * resource;};
struct platform_driver { int (*probe)(struct platform_device *); int (*remove)(struct platform_device *); void (*shutdown)(struct platform_device *); int (*suspend)(struct platform_device *, pm_message_t state); int (*suspend_late)(struct platform_device *, pm_message_t state); int (*resume_early)(struct platform_device *); int (*resume)(struct platform_device *); struct pm_ext_ops *pm; struct device_driver driver;}; struct bus_type platform_bus_type = { .name = "platform", .dev_attrs = platform_dev_attrs, .match = platform_match, .uevent = platform_uevent, .pm = PLATFORM_PM_OPS_PTR,};
其中platform的资源结构体如下
struct resource { resource_size_t start; resource_size_t end; const char *name; unsigned long flags; struct resource *parent, *sibling, *child;};
需要关注的是platform_match成员函数确定了platform_device和platform_driver之间如何匹配
引入platform的概念至少有如下两大好处:
一、使得设备被挂接到一个总线上
二、隔离BSP和驱动。在BSP中定义platform设备和设备使用资源、设备的具体配置信息,而在驱动中。只需要通过API去获取资源和数据,做到了板上相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
设备驱动核心层
在面向对象的程序设计中,可以为某一类相似的事物定义一个基类,而具体的事物可以继承这个基类的函数。如果对于继承的这个事物而言,其某成员函数的实现与基类一致,那它就可以直接继承基类的函数,相反可以重载之,这种面向对象的设计思想极大地提高了代码的可重用能力,是对现实世界事物间关系的一种良好呈现。
设备驱动中的电源管理:
主要提供挂起(suspend)和恢复(resume)两个函数
suspend()函数里面会停止设备,并会关闭给它的时钟,resume是相反的操作;
suspend_late工作于中断都被禁止的情况下,而且还有一个CPU是活跃的,suspend之后会进一步的进睡眠操作,resume_early 是suspend_late反操作。
mis设备驱动
LINUX里面无法归类的设备定义为MIS设备,LINUX的内核提供的miscdevice有很强的包容性
miscdevice共享一个主设备号MISC_MAJOR(10),但次设备号不同,所有的discdevice设备形成一个链表,对设备访问时内核根据次设备号查找对应的miscdevice设备,然后调用其file_operations结构体中注册的文件操作接口进行操作。
ANDROID设备驱动
Android对LINUX引入了补丁
1、binder IPC系统,binder机制是android提供的一种进程间通信的方法,使一个进程可以以类似远程过程调用的形式调用另一个进程所提供的功能。android的binder通信基于service/client模型,所有需要binder通信的进程都必须创建一个ibinder接口。
2、ashmem是android新增的一种内存分配/共享机制
内存空间与IO空间
内存地址可以直接由C语言指针操作
unsigned char * = (unsigned char) 0xf000ff000;
*p = 11;
typedef void (*lpfunction) ();/*定义一个无参数、无返回类型的函数指针类型*/
lpfunction lpreset = (lpfunction) 0xf000fff0;
lpreset();
在内核中访问IO内存之前,首先要使用ioremap()函数将设备所处的物理地址映射到虚拟地址
void __iomem *ioremap(phys_addr_t addr, unsigned long size)
LINUX提供了一组函数用于申请和释放I/O内存
struct resource *request_mem_region(unsigned long start , unsigned long len , char *name) ;
I/O口映射为内存进行访问的过程:
在设备打开或驱动模块被加载时,申请I/O端口区域并使用ioport_map() /ioremap()映射到内存,之后使用I/O内存函数访问端口,在设备关闭或者驱动被卸载时释放I/O口并释放映射
具体是 :首先是调用request_mem_region()申请资源,接着将寄存器地址通过ioport_map()映射到内核空间虚拟地址,之后就可以通过LINUX设备访问编程接口访问这些设备的寄存器了。
访问完成后 释放I/O口并释放映射
内存管理单元MMU
高性能处理器一般会提供一个内存管理单元(MMU),该单元辅助操作系统进行内存管理:
1、提供虚拟地址和物理地址的映射
2、内存访问权限保护和CACHE缓存控制等硬件支持
操作系统的MMU可以让用户感觉到好像程序可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机中的物理内存的实际容量,这使LINUX操作系统能单独为系统的每个用户进程分配独立的内存并保证用户空间不能访问内核空间的地址,为系统的虚拟内存管理模块提供配件基础
MMU中的几个概念
一、TLB快表
二、TTW转换表温游,TTW成功后,结果应写入TLB
LINUX内存管理
在LINUX系统中,进程的4GB内存空间被分为两个部分:
用户空间:地址一般分布为0~3GB
内核空间: 剩下来的3~4GB为内核空间
kmalloc()和__get_free_pages() 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系
vmalloc()在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续 .
vmalloc()一般只存在于软件中的较大顺序缓冲区分配内存,vmalloc()的开销远远大于__get_free_pages(),为了完成vmalloc(),新的页表需要被建立,只是分配少量的内存使用vmalloc显然是不妥的。
虚拟地址与物理地址关系
virt_to_phys() #define virt_to_phys (x) ((x) - PAGE_OFFSET + PHYS_OFFSET) PAGE_OFFSET通常是3GB,而PHYS_OFFSET则定义为系统DRAM内存地基地址
phys_to_virt()
DMA
DMA是一种无需CPU参与就可以让外设与系统内存之间进行双向数据传输的硬件机制,DMA方式的数据传输由DMA控制器(DMAC)控制,在传输期间,CPU可以并发地执行其他任务。
当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,然后同CPU执行相应的中断服务程序进行后处理。
DMA与CACHE的一致性问题最简单的解决方法是直接禁止DMA目标地址范围内内存的CACHE功能
延时的用法
ndelay()、mdelay忙等待机制在驱动中通常是为了配合硬件上短时延迟要求
nsleep则是一种睡眠等待的机制
中断与时钟
中断服务程序的执行并不存在于进程上下文,因此,要求中断服务程序的时间尽可能地短,因此LINUX在中断处理中引入了顶半部和底半部分离的机制。另外内核中对时钟的处理也采用中断方式,而内核软件定时器最终依赖于时钟中断
LINUX中断会打断内核中进程的正常调度和运行,系统对于更高吞吐率的追求势必要求中断服务程序尽可能的短小精悍
为了在中断执行时间尽可能地短和中断处理完成大量工作之间找到一个平衡点,LINUX将中断处理程序分解为两个半部:
顶半部(top halt) 和 底半部 (bottom half)
disable_irq_nosync() 会立即返回
disable_irq() 等待目前的中断处理完成,因些如果在n号中断的顶半部调用disable_irq(n),会引系统的死锁,这种情况下,通常只能调用disable_irq_nosync();
底半部机制主要有 tasklet、工作队列和软中断
#define DECLARE_TASKLET(name, func, data) 实现了定义名称为name的tasklet并将其与 func这个函数绑定,而传入这个函数的参数为data,
在需要调度tasklet的时候引用tasklet_schedule()函数就能使系统在适当的时候进行调度运行 tasklet_schedule(&name)
软中断和tasklet运行于软中断上下文,仍然属于原子上下文的一种,而工作队列则运行于进程上下文中,因此,在软中断 和tasklet处理函数中不能睡眠,而工作队列处理函数中允许睡眠。
在LINUX设备驱动中,可以利用LINUX内核中提供的一组函数和数据结构来完成定时触发工作或者完成某周期性的事务
struct timer_list { struct list_head entry;/*定时器列表*/ unsigned long expires;/*定时器到期时间*/ void (*function)(unsigned long);/*定时器处理函数*/ unsigned long data;/*作为参数被传入定时器处理函数*/ struct tvec_base *base;#ifdef CONFIG_TIMER_STATS void *start_site; char start_comm[16]; int start_pid;#endif};
LINUX设备驱动中的异步通知
异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上中断的概念,比较准确的称谓是“信号驱动的异步IO”
信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的
使用信号进行进程间通信(IPC)是LINUX中的一种传统机制,LINUX也支持这种机制
在用户程序中,为了捕获信号,可以使用signal()函数来设置对应信号的处理函数:
void (*signal (int signum, void (*handler))(int)) (int);
该函数原型较难理解,它可以分解为:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum , sighandler_t handler);
第一个参数指定信号的值,第二个参数指定针对面前信号值的处理函数,若为SIG_IGN,表示忽略该信号,若为SIG_DFL,表示采用系统默认方式处理信号,若为用户自定义的函数,则信号被捕获到后,该函数将被执行。
阻塞与非阻塞I/O
阻塞操作:是指执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再执行操作,被挂起的进程进入休眠状态,被从调度器的运行队列中移走,直到等待的条件被满足;
非阻塞操作:进程在不能进行设备操作时,并不挂起,或者放弃,或者不停地查询,直到可以进行操作为止。
因为阻塞的进程会进入休眠状态,因此必须确保有一个地方能够唤醒休眠的进程,唤醒进程的最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断
在LINUX驱动中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。
wait_event(wq, condition)
wait_event_interruptible(wq, condition)
wait_event_timeout(wq, condition, timeout)
wait_event_interruptible_timeout(wq, condition, timeout)
第一个参数wq作为等待队列头的等待队列被唤醒,而且 第二个参数condition必须满足,否则继续阻塞,
wait_event() 和 wait_event_interruptible() 的区别在于后者可以被信号打断,而前都不能。加上timeout后的宏意味着阻塞等待的超时时间,在超时后,不论condition是否满足均返回
阻塞流程:
一,定义并初始化一个等待队列,将进程状态改变为 TASK_UNINTERRUPTINLE (不能被信号打断) 或TASK_INTERRUPTIBLE (可以信号打断) ,并将等待队列添加到等待队列头
二,通过schedule_timeout()放弃CPU,调度其他进程执行。
三,进程被其他地方唤醒,将等待队列移出等待队列头。
在内核中使用 set_current_state() 函数或 __add_curret_state() 函数来实现目前进程状态的改变,直接采用current->state = TASK_UNINTERRUPTINLE 类似的赋值语句也是可行的
并发控制
LINUX设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态
并发指的是多个执行单元同时、并发被执行,而并发执行单元对共享资源的访问则很容易导致竞态
在LINUX内核中,竞态发生于如下几种情况:
1、多对称处理器(SMP)的多个CPU
2、单CPU内进程与抢占它的进程
3、中断(硬中断、软中断、TASKLET、底半部)与进程之间
解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥是指一个执行单元在访问共享资源的时候,其他执行单元被禁止访问
访问共享资源代码区域称为临界区,临界区需要被以某种互斥机制加以保护
LINUX设备驱动中采用的互斥机制有:中断屏蔽、原子操作、自旋锁和信号量
中断屏蔽
在单CPU范围内避免竞态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断,可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生,而且由于LINUX的内核进程调度等操作
都依赖于中断来实现,内核抢占进程之间的并发也得以避免了
中断屏蔽使用方法:
local_irq_disable() /*屏蔽中断*/
...
critical section /* 临界区*/
...
local_irq_enable() /* 开中断*/
local_irq_disable()只能禁止本CPU内的中断,因此不能解决SMP多CPU引发的竞态,它适宜与自旋锁联合使用。
local_irq_save(flags) 除了进行禁止中断的操作以外,还保存目前CPU的中断信息
local_irq_restore(flags)
local_hb_disable() 禁止中断的底半部
local_hb_enable()
由于LINUX的异步I/O、进程调度等很多操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的事情,有可能造成数据丢失乃至系统崩溃的后果。
这就要求在屏蔽了中断之后,当前的内核执行的路径应当尽快地执行完临界区的代码。
原子操作
原子操作是指在执行过程中不会被别的代码路径所中断的操作。
LINUX内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类:分别针对位和整形变量进行原子操作
atomic_set(v,i) 设置原子变量的值为i
ATOMIC_INIT(i) 初始化一个原子变量值为i,并返回该值
atomic_read(v) 返回原子变量的值
atomic_add(int i, atomic_t * v) 原子变量增加i
atomic_sub(int i, atomic_t * v)
atomic_inc(v) 原子变量值增加1
atomic_dec(v) 原子变量值减少1
atomic_inc_and_test(v) 对原子变量执行自增1后测试其是否为0,为0返回TRUE,否则返回FLASE
atomic_dec_and_test(v)
atomic_sub_and_test(i,v)
atomic_inc_return(v)
atomic_dec_return(v)
自旋锁
自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。
为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试开设置某个内存变量,由于它是原子操作,由于在该操作完成之前其他执行单元不可能访问这个内存变量,如果测试结果表明锁已经空闲,则程序获得这个自旋并继续执行,如果测试结果表明锁仍在被占用,程序将在一个小的循环内重复这个测试并设置操作,即进行所谓的自旋
spinlock_t lock; 定义自旋锁
spin_lock_init(s) 初始化自旋锁
spin_lock(l) 用于获得自旋锁,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到获得该自旋锁的保持释放
spin_trylock(l) 尝试获得自旋锁,如果能够立即获得并返回真,否则立即返回假,不再原地打转
spin_unlock(l) 释放自旋锁
自旋锁主要针对SMP或单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。
尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的进程抢占打扰,但是得到锁的代码路径在执行临界区时候,还可能受到中断到底半部的影响,所以中断进程中遇到自旋锁就永远出不来,所以中断一般不允许访问全局变量
为了防止这种影响,就需要到自旋锁的衍生:
spin_lock_irq = spin_lock + local_irq_disable
spin_unlock_irq = spin_unlock + local_irq_enable
spin_lock_irqsave = spin_lock + local_irq_save
spn_unlock_irqrestore = spin_unlock + local_irq_restore
spin_lock_bh = spin_lock + local_bh_disable
spin_unlock_bh = spin_unlock + local_bh_enable
一、自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环测试并设置该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待,因此,只有占用锁的时间极短的情况下,使用自旋锁者是合理的,当临界区很大,或者有共享设备的时候,需要较长的时间,使用自旋锁会降低系统的性能
二、自旋锁可能导致系统死锁,引发这个问题的最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二资获得这个自旋锁,则该CPU将死锁。
三、自旋锁锁定期间不能调用可能引起进程调度的函数,如果进程获得自旋锁之后再阻塞,可能导致内核崩溃。
信号量
信号量(semaphore)是用于保护临界区的一种方法,只有获得信号量的进程才能执行临界区代码,
sema_init(struct semaphore *sem, int val) 初始化信号量,并设置信号量sem的值为val。
down(struct semaphore *sem); 获得信号量,它会导致睡眠,因此不能在中断上下文中使用,进入睡眠状态的进程不能被信号打断
down_interruptible(struct semaphore *sem); 进入睡眠的进程能被信号打断,信号也会导致该函数返回
up(struct semaphore *sem);
信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的,如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,CPU将运行其他进程,鉴于进程上下亠切换的开销也很大,因此只有当进程占用资源时间较长时,用信号量才是较好的选择。
字符设备
struct cdev { struct kobject kobj;/*内嵌的kobject对象*/ struct module *owner;/*所属模块*/ const struct file_operations *ops;/*文件操作结构体*/ struct list_head list; dev_t dev;/*设备号*/ unsigned int count;};
由于内核空间与用户空间的内存不能直接互访,因此
借助函数copy_from_user(to, from, n)完成用户空间到内核空间的拷贝
函数copy_to_user(to, from, n) 完成内核空间到用户空间的拷贝
container_of(ptr, type, member)的作用是通过结构体成员的指针找到对应的结构体的指针,这个技巧在LINUX内核编程中十分常用,
container_of(ptr, type, member),第一个参数是结构体成员的指针,第二个参数是整个结构体的类型,第三个参数是传入的第一个参数即结构体成员
返回值为整个结构体的指针。
LINUX下编译C语言gcc -o pointer pointer.c执行编译结果./pointerVI使用方法vi test.c 进入VI在Command mode下按‘i’进入insert modeESC键转换回Command mode:wq (输入“wq”,因为进入之时已经指定文件名testfile,所以会写入testfile并离开vi) :q! (输入“q!”,强制离开并放弃编辑的文件)IPC:内存间通信1、共享内存的创建 asmlinkage long sys_shmget(key_t key, size_t size, int flag);2、共享内存的映射 asmlinkage long sys_shmat(int shmid, char __user *shmaddr, int shmflg);3、释放内存 asmlinkage long sys_shmdt(char __user *shmaddr);消息队列是FIFO的特性,一个或多个进程读写,写消息写在队列的尾部,读消息从头部读走,但是它能够现实随机访问boot 内核自解压目录arch architecture目录IPC Inter-Process Communication 进程间通信head_armv.ovmlinux