linux设备驱动读书笔记 3.docx
《linux设备驱动读书笔记 3.docx》由会员分享,可在线阅读,更多相关《linux设备驱动读书笔记 3.docx(18页珍藏版)》请在冰点文库上搜索。
linux设备驱动读书笔记3
linux设备驱动读书笔记(3)转
2009年08月30日星期日13:
26
原子变量:
atomic_t.所有原子量操作必须使用如下函数
atomic_tv=ATOMIC_INIT(0):
声明并初始化atomic变量
voidatomic_set(atomic_t*v,inti):
设置值
intatomic_read(atomic_t*v):
读值
voidatomic_add(inti,atomic_t*v):
值加i
voidatomic_sub(inti,atomic_t*v):
值减i
voidatomic_inc(atomic_t*v):
值+1
voidatomic_dec(atomic_t*v):
值-1
intatomic_inc_and_test(atomic_t*v):
值+1,并测试值是否为0,若为0返回真,否则返回假
intatomic_dec_and_test(atomic_t*v):
值-1,并测试值是否为0,若为0返回真,否则返回假
intatomic_sub_and_test(inti,atomic_t*v):
值-i,并测试值是否为0,若为0返回真,否则返回假
intatomic_add_negative(inti,atomic_t*v):
值+i,并测试值是否为负,若为负返回真,否则返回假
intatomic_add_return(inti,atomic_t*v):
值+i,并返回值
intatomic_sub_return(inti,atomic_t*v):
值-i,并返回值
intatomic_inc_return(atomic_t*v):
值+1,并返回值
intatomic_dec_return(atomic_t*v):
值-1,并返回值
位操作:
位操作依赖于体系结构.nr参数(描述要操作哪个位)常常定义为int,也可能是unsignedlong;要修改的地址常常是一个unsignedlong指针,但是几个体系使用void*代替.
voidset_bit(nr,void*addr):
在addr指向的数据项中将第nr位设置为1
voidclear_bit(nr,void*addr):
在addr指向的数据项中将第nr位设置为0
voidchange_bit(nr,void*addr):
对addr指向的数据项中的第nr位取反
test_bit(nr,void*addr):
返回addr指向的数据项中的第nr位
inttest_and_set_bit(nr,void*addr):
在addr指向的数据项中将第nr位设置为1,并返回设置前的值
inttest_and_clear_bit(nr,void*addr):
在addr指向的数据项中将第nr位设置为0,并返回设置前的值
inttest_and_change_bit(nr,void*addr):
对addr指向的数据项中的第nr位取反,并返回取反前的值
Mark:
大部分现代代码不使用位操作,而是使用自选锁
seqlock:
2.6内核提供的,
seqlock_tlock1=SEQLOCK_UNLOCKED;
seqlock_tlock2;
seqlock_init(&lock2);
读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作.在退出时,那个序列值与当前值比较;如果不匹配,读存取必须重试.结果是,读者代码象下面的形式:
unsignedintseq;
do{
seq=read_seqbegin(&the_lock);
/*Dowhatyouneedtodo*/
}whileread_seqretry(&the_lock,seq);
这个类型的锁常常用在保护某种简单计算,需要多个一致的值.如果这个计算最后的测试表明发生了一个并发的写,结果被简单地丢弃并且重新计算.
如果你的seqlock可能从一个中断处理里存取,你应当使用IRQ安全的版本来代替:
unsignedintread_seqbegin_irqsave(seqlock_t*lock,unsignedlongflags);
intread_seqretry_irqrestore(seqlock_t*lock,unsignedintseq,unsignedlongflags);
写者必须获取一个排他锁来进入由一个seqlock保护的临界区.为此,调用:
voidwrite_seqlock(seqlock_t*lock);
写锁由一个自旋锁实现,因此所有的通常的限制都适用.调用:
voidwrite_sequnlock(seqlock_t*lock);
来释放锁.因为自旋锁用来控制写存取,所有通常的变体都可用:
voidwrite_seqlock_irqsave(seqlock_t*lock,unsignedlongflags);
voidwrite_seqlock_irq(seqlock_t*lock);
voidwrite_seqlock_bh(seqlock_t*lock);
voidwrite_sequnlock_irqrestore(seqlock_t*lock,unsignedlongflags);
voidwrite_sequnlock_irq(seqlock_t*lock);
voidwrite_sequnlock_bh(seqlock_t*lock);
还有一个write_tryseqlock在它能够获得锁时返回非零.
读-拷贝-更新(Read-Copy-Update)
针对很多读但是较少写的情况.所有操作通过指针.
读:
直接操作
写:
将数据读出来,更新数据,将原指针指向新数据(这一步需要另外做原子操作的保护)
作为在真实世界中使用RCU的例子,考虑一下网络路由表.每个外出的报文需要请求检查路由表来决定应当使用哪个接口.这个检查是快速的,并且,一旦内核发现了目标接口,它不再需要路由表入口项.RCU允许路由查找在没有锁的情况下进行,具有相当多的性能好处.内核中的Startmode无线IP驱动也使用RCU来跟踪它的设备列表.
在读这一边,使用一个RCU-保护的数据结构的代码应当用rcu_read_lock和rcu_read_unlock调用将它的引用包含起来.
RCU代码往往是象这样:
structmy_stuff*stuff;
rcu_read_lock();
stuff=find_the_stuff(args...);
do_something_with(stuff);
rcu_read_unlock();
rcu_read_lock调用是很快的:
它禁止内核抢占但是没有等待任何东西.在读"锁"被持有时执行的代码必须是原子的.在对rcu_read_unlock调用后,没有使用对被保护的资源的引用.
需要改变被保护的结构的代码必须进行几个步骤:
分配一个新结构,如果需要就从旧的拷贝数据;接着替换读代码所看到的指针;释放旧版本数据(内存).
在其他处理器上运行的代码可能仍然有对旧数据的一个引用,因此不能立刻释放.相反,写代码必须等待直到它知道没有这样的引用存在了.因为所有持有对这个数据结构引用的代码必须(规则规定)是原子的,我们知道一旦系统中的每个处理器已经被调度了至少一次,所有的引用必须消失.这就是RCU所做的;它留下了一个等待直到所有处理器已经调度的回调;那个回调接下来被运行来进行清理工作.
改变一个RCU-保护的数据结构的代码必须通过分配一个structrcu_head来获得它的清理回调,尽管不需要以任何方式初始化这个结构.通常,那个结构被简单地嵌入在RCU所保护的大的资源里面.在改变资源完成后,应当调用:
voidcall_rcu(structrcu_head*head,void(*func)(void*arg),void*arg);
给定的func在安全的时候调用来释放资源
全部RCU接口比我们已见的要更加复杂;它包括,例如,辅助函数来使用被保护的链表.详细内容见相关的头文件
阻塞I/O
运行在原子上下文时不能睡眠.持有一个自旋锁,seqlock,RCU锁或中断已关闭时不能睡眠.但在持有一个旗标时睡眠是合法的
不能对醒后的系统状态做任何的假设,并且必须检查来确保你在等待的条件已经满足
确保有其他进程会做唤醒动作
明确的非阻塞I/O由filp->f_flags中的O_NONBLOCK/O_NDELAY标志来指示.只有read,write,和open文件操作受到非阻塞标志影响
下列情况下应该实现阻塞
如果一个进程调用read但是没有数据可用(尚未),这个进程必须阻塞.这个进程在有数据达到时被立刻唤醒,并且那个数据被返回给调用者,即便小于在给方法的count参数中请求的数量.
如果一个进程调用write并且在缓冲中没有空间,这个进程必须阻塞,并且它必须在一个与用作read的不同的等待队列中.当一些数据被写入硬件设备,并且在输出缓冲中的空间变空闲,这个进程被唤醒并且写调用成功,尽管数据可能只被部分写入如果在缓冲只没有空间给被请求的count字节.
若需要在open中实现对O_NONBLOCK的支持,可以返回-EAGAIN
DECLARE_WAIT_QUEUE_HEAD(name);
定义并初始化一个等待队列
init_waitqueue_head(wait_queue_head_t*name);
初始化一个等待队列
wait_event(queue,condition)
wait_event_interruptible(queue,condition)
wait_event_timeout(queue,condition,timeout)
wait_event_interruptible_timeout(queue,condition,timeout)
queue:
是要用的等待队列头.注意它是"通过值"传递的,而不是指针
condition:
需要检查的条件,只有这个条件为真时才会返回.注意条件可能被任意次地求值,因此它不应当有任何边界效应(sideeffects,按我的理解就是当外界条件未改变时,每次计算得到的结果应该相同)
timeout:
超时值.表示要等待的jiffies数,是一个相对时间值.如果这个进程被其他事件唤醒,它返回以jiffies表示的剩余超时值
上述4个函数带interruptible的是可被中断的
voidwake_up(wait_queue_head_t*queue);
voidwake_up_interruptible(wait_queue_head_t*queue);
wake_up唤醒所有的在给定队列上等待的进程.wake_up_interruptible限制只唤醒因wait_event_interruptible/wait_event_interruptible_timeout睡眠的进程
wake_up_nr(wait_queue_head_t*queue,intnr);
wake_up_interruptible_nr(wait_queue_head_t*queue,intnr);
类似wake_up,但它们能够唤醒nr个互斥等待者,而不只是一个.注意:
0被解释为请求所有的互斥等待者都被唤醒
wake_up_all(wait_queue_head_t*queue);
wake_up_interruptible_all(wait_queue_head_t*queue);
唤醒所有的进程,不管它们是否进行互斥等待(尽管可中断的类型仍然跳过在做不可中断等待的进程)
wake_up_interruptible_sync(wait_queue_head_t*queue);
正常地,一个被唤醒的进程可能抢占当前进程,并且在wake_up返回之前被调度到处理器.换句话说,调用wake_up可能不是原子的.如果调用wake_up的进程运行在原子上下文(它可能持有一个自旋锁,例如,或者是一个中断处理),这个重调度不会发生.但是,如果你需要明确要求不要被调度出处理器在那时,你可以使用wake_up_interruptible的"同步"变体.这个函数唤醒其他进程,但不会让新唤醒的进程抢占CPU
6.2.5.1.一个进程如何睡眠
放弃处理器是最后一步,但是要首先做一件事:
你必须先检查你在睡眠的条件.做这个检查失败会引入一个竞争条件;如果在你忙于上面的这个过程并且有其他的线程刚刚试图唤醒你,如果这个条件变为真会发生什么?
你可能错过唤醒并且睡眠超过你预想的时间.因此,在睡眠的代码下面,典型地你会见到下面的代码:
if(!
condition)
schedule();
通过在设置了进程状态后检查我们的条件,我们涵盖了所有的可能的事件进展.如果我们在等待的条件已经在设置进程状态之前到来,我们在这个检查中注意到并且不真正地睡眠.如果之后发生了唤醒,进程被置为可运行的不管是否我们已真正进入睡眠.
如果在if判断之后,schedule之前,有其他进程试图唤醒当前进程,那么当前进程就会被置为可运行的(但可能会到下次调度才会再次运行),所以这个过程是安全的
手动睡眠
DEFINE_WAIT(my_wait);
定义并初始化一个等待队列入口项
init_wait(wait_queue_t*);
初始化等待队列入口项
voidprepare_to_wait(wait_queue_head_t*queue,wait_queue_t*wait,intstate);
添加你的等待队列入口项到队列,并且设置进程状态.
queue:
等待队列头
wait:
等待队列入口项
state:
进程的新状态;TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE
调用prepare_to_wait之后,需再次检查确认需要等待,便可调用schedule释放CPU
在schedule返回之后需要调用下面的函数做一些清理动作
voidfinish_wait(wait_queue_head_t*queue,wait_queue_t*wait);
在此之后需要再次检查是否需要再次等待(条件已经满足还是被其他等待的进程占用?
)
互斥等待
当一个等待队列入口有WQ_FLAG_EXCLUSEVE标志置位,它被添加到等待队列的尾部.没有这个标志的入口项添加到开始.
wake_up调用在唤醒第一个有WQ_FLAG_EXCLUSIVE标志的进程后停止(即进行互斥等待的进程一次只以顺序的方式唤醒一个).但内核仍然每次唤醒所有的非互斥等待者.
用函数
voidprepare_to_wait_exclusive(wait_queue_head_t*queue,wait_queue_t*wait,intstate);
代替手动睡眠中的prepare_to_wait即可实现互斥等待
老版本的函数
voidsleep_on(wait_queue_head_t*queue);
voidinterruptible_sleep_on(wait_queue_head_t*queue);
这些函数无法避免竞争(在条件判断和sleep_on之间会存在竞争).见上面"6.2.5.1.一个进程如何睡眠"的分析
时间
HZ:
1秒产生的tick.是一个体系依赖的值.内核范围此值通常为100或1000,而对应用程序,此值始终是100
jiffies_64:
从系统开机到现在经过的tick,64bit宽
jiffies:
jiffies_64的低有效位,通常为unsignedlong.编程时需要考虑jiffies隔一段时间会回绕(重新变成0)的问题
u64get_jiffies_64(void);
得到64位的jiffies值
inttime_after(unsignedlonga,unsignedlongb);
若a在b之后,返回真
inttime_before(unsignedlonga,unsignedlongb);
若a在b之前,返回真
inttime_after_eq(unsignedlonga,unsignedlongb);
若a在b之后或a与b相等,返回真
inttime_before_eq(unsignedlonga,unsignedlongb);
若a在b之前或a与b相等,返回真
unsignedlongtimespec_to_jiffies(structtimespec*value);
voidjiffies_to_timespec(unsignedlongjiffies,structtimespec*value);
unsignedlongtimeval_to_jiffies(structtimeval*value);
voidjiffies_to_timeval(unsignedlongjiffies,structtimeval*value);
structtimeval和structtimespec与jiffies之间的转换
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
readtsc/readtsclow/readtsclonglong:
读取短延时(很容易回绕),高精度的时间值.不是所有平台都支持
cycles_tget_cycles(void);
类似rdtsc,但每个平台都提供.若CPU为提供相应功能则此函数一直返回0
注意:
它们在一个SMP系统中不能跨处理器同步.为保证得到一个一致的值,你应当为查询这个计数器的代码禁止抢占
短延时(很容易回绕),高精度
不是所有平台都支持
unsignedlongmktime(unsignedintyear,unsignedintmon,unsignedintday,unsignedinthour,unsignedintmin,unsignedintsec);
转变一个墙上时钟时间到一个jiffies值
voiddo_gettimeofday(structtimeval*tv);
获取当前时间
structtimespeccurrent_kernel_time(void);
获取当前时间.注意:
返回值是个结构体不是结构体指针
延时
长延时之忙等待(cpu密集型进程会有动态降低的优先级)
while(time_before(jiffies,j1))
cpu_relax();
让出CPU(无法确定什么时候重新得到CPU,即无法确定什么时候循环结束,也即无法保证延时精度)
while(time_before(jiffies,j1)){
schedule();
}
longwait_event_timeout(wait_queue_head_tq,condition,longtimeout);
longwait_event_interruptible_timeout(wait_queue_head_tq,condition,longtimeout);
timeout表示要等待的jiffies数,是相对时间值,不是一个绝对时间值.如果超时到,这些函数返回0;如果这个进程被其他事件唤醒,它返回以jiffies表示的剩余超时值.
set_current_state(TASK_INTERRUPTIBLE);
signedlongschedule_timeout(signedlongtimeout);
不需要等待特定事件的延时
注意:
上述几个函数在超时事件到与被调度之间有一定的延时
短延时之忙等待
voidndelay(unsignedlongnsecs);
voidudelay(unsignedlongusecs);
voidmdelay(unsignedlongmsecs);
这几个函数是体系特定的.每个体系都实现udelay,但是其他的函数可能或者不可能定义
获得的延时至少是要求的值,但可能更多
voidmsleep(unsignedintmillisecs);
unsignedlongmsleep_interruptible(unsignedintmillisecs);
voidssleep(unsignedintseconds)
使调用进程进入睡眠给定的毫秒数.
如果进程被提早唤醒(msleep_interruptible),返回值是剩余的毫秒数
内核定时器
定时器是基于中断的(通常来讲是通过软中断),所以会缺少进程上下文.
定时器
Timer运行在非进程上下文中.通常只能提供tick为单位的精度
structtimer_list{
/*...*/
unsignedlongexpires;
void(*function)(unsignedlong);
unsignedlongdata;
};
expires:
超时时间
function:
时间到时需要回调的函数
data:
function的参数
voidinit_timer(structtimer_list*timer);
初始化一个timer
structtimer_listTIMER_INITIALIZER(_function,_expires,_data);
定义并初始化一个timer
voidadd_timer(structtimer_list*timer);
将timer加入调度.每次这个timer被调度后,就会从调度链表中去掉,所以如果需要反复运行,需要重新add_timer或者用mod_timerintdel_timer(structtimer_list*timer);
将timer从调度链表中去除
intmod_timer(structtimer_list*timer,unsignedlongexpires);
更新一个定时器的超时时间(并将其重新加入到调度链表中去).mod_timer也可代替add_timer