Table of Contents
7.1.1. Using the jiffies Counter
7.1.2. Processor-Specific Registers
7.3.1.2 Yielding the processor
7.4.2. The Implementation of Kernel Timers
一个真正的device driver除了要实现device control,以及基本的读写之外,还涉及到很多其他的东西,比如timing,内存管理,硬件访问等等。kernel提供了很多接口,来帮助driver实现上面这些功能。
比如,kernel提供了一些跟时间管理相关的功能:
1, 测量时间和比较时间
2, 如何获取当前时间
3, 延迟某些操作,直到某个时刻才执行
4, 异步函数的调度
7.1. Measuring Time Lapses
整个kernel的运行非常依赖时间,因此kernel会通过定时器中断来测量时间。定时器中断是硬件在固定的间隔产生的,这个间隔的时间是kernel根据当前HZ的值,在bootup的阶段写到定时器硬件里去的。HZ的值是架构相关的,每个架构不一样,如果有真正的硬件,这个HZ的值在50-1200次/秒,如果只有纯软件的,这个值就是24次/秒。大部分平台上都是100-1000次/秒这样的频率,X86架构上是默认1000。这个值device driver一般不需要直接使用。
当然,HZ的值可以修改,需要直接修改code,然后重新编译kernel,大部分情况下直接使用默认值就可以了。
kernel中有一个jiffies_64,这个值在系统刚起来是被初始化为0,每产生一个计时中断,这个值就加1,所以这个值实际上存储的是自系统开机以来kenrel经过的时间中断个数。除了jiffies_64,还有一个变量是jiffies,一般是jiffies_64(如果unsigned long是8byte)或者它的低bit(如果unsigned long是4byte),device driver如果需要获取中断个数的值,就访问jiffies,而不是jiffies_64,访问这个值要比jiffies_64更加快速,而且访问jiffies_64在所有的 架构上都不是原子操作。
除了jiffies这个counter之外,有些平台还有精度更高的计时器,这里暂时先不讨论。
7.1.1. Using the jiffies Counter
首先要知道jiffies和jiffies_64都是只读的,软件不允许修改。
根据jiffies,计算将来的某个时间:
#include <linux/jiffies.h>
unsigned long j, stamp_1, stamp_half, stamp_n;
j = jiffies; /* read the current value */
stamp_1 = j + HZ; /* 1 second in the future */
stamp_half = j + HZ/2; /* half a second */
stamp_n = j + n * HZ / 1000; /* n milliseconds */
jiffies里的值,是中断的个数,系统每秒会产生HZ个中断,所以当jiffies增加了HZ,说明过去了一秒钟。时间的比较:
#include <linux/jiffies.h>
// return true if a is after b
int time_after(unsigned long a, unsigned long b);
// return true if a is before b
int time_before(unsigned long a, unsigned long b);
// return true if a is after or equal b
int time_after_eq(unsigned long a, unsigned long b);
// return true if a is before or equal b
int time_before_eq(unsigned long a, unsigned long b);
kernel时间的比较,就是把两个时间转换为long,然后减,再比较差值。可以把时间转换为毫秒:
msec = diff * 1000 / HZ;
有两个结构体可以表示时间,timeval和timespec。timeval是旧的方式,里面记录的是秒和毫秒;timespec是新的方式,里面记录了秒和微秒。kernel提供了一些函数,方便的把jiffies转换为这两个结构体:
#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);
如果在32bit机器上,访问jiffies_64不是原子操作,因为这个值需要两个unsigned int来存储,那就需要读两次才能完整的读到jiffies的值,有可能读第一次的时候另外一个值被改了,这就导致读出来的两个值不是一个有效的jiffies_64值。针对这种情况,kernel提供了函数来访问:
#include <linux/jiffies.h>
u64 get_jiffies_64(void);
真正的clock频率对user space是不可见的,当user space引用时,HZ的值都会是100,并且每个报给user的counter都会被相应的做转换,比如clock或者time函数,以及相关的其他函数。
7.1.2. Processor-Specific Registers
有些情况下,需要使用很小的interval,或者更高精度的时间,实现更高精度的方式是依赖于架构的。有些架构的CPU中有更高精度的计时器,是一个per CPU的寄存器,里面记的是clock cycle的counter,每一个cycle clock,这个值就加1. X86上这个寄存器是TSC(timestamp counter),这是一个64bit寄存器,在kernel和user mode两个space里都可以读取。在X86架构上,可以这么读:
#include <asm/msr.h>
// read tsc to two 32bit value
rdtsc(low32,high32);
// read low half value to 32bit value
rdtscl(low32);
// read long long value to 64bit value
rdtscll(var64);
不同的架构可能提供不同的函数来实现,所以kernel对此做了封装,提供了统一的接口:
#include <linux/timex.h>
cycles_t get_cycles(void);
kernel中所有的平台都有这个函数,对于不支持TSC的平台,get_cycles返回0. cycles_t类型就是unsigned类型,总能保证可以存下整个TSC。
除了通过get_cycles获取TSC,也可以使用架构相关的代码来实现,比如这里有一个在MIPS架构上,通过汇编代码获取TSC的例子:
#define rdtscl(dest) \
_ _asm_ _ _ _volatile_ _("mfc0 %0,$9; nop" : "=r" (dest))
注意,因为TSC不会在多个CPU之间进行实时的同步,所以在query这个TSC时,当前的code不能被抢占。(不太理解为什么不能被抢占)
7.2. Knowing the Current Time
kernel内部使用的时间都是系统启动以后的时间,根据jiffies以及HZ的值可以计算出系统启动了多久。在大多数情况下,driver只需要知道这个时间就够了,通过jiffies可以计算时间间隔等等。如果需要更高的精度的当前时间,kenrel也是支持的。
kernel和driver很少会使用wall-clock time,也就是绝对的时间,比如哪一年哪一月哪一天,这个其实没有必要知道。这种时间一般是给user mode application使用的,所以kernel也提供了函数,使得wall-clock time可以转换为jiffies。
#include <linux/time.h>
unsigned long mktime (unsigned int year, unsigned int mon,
unsigned int day, unsigned int hour,
unsigned int min, unsigned int sec);
如果你的driver中需要使用wall-clock time,说明你的driver实现了机制,而不是策略,应该避免。
有时候driver需要知道timestamp,可以调用do_gettimeofday获取:
#include <linux/time.h>
void do_gettimeofday(struct timeval *tv);
通过do_gettimeofday,可以获取精度为微秒的时间戳。也可以通过别的函数获取当前时间:
#include <linux/time.h>
struct timespec current_kernel_time(void);
7.3. Delaying Execution
有时候driver需要把某些操作延迟,以等待hardware完成某些事情,kernel提供了很多方法来实现这种延迟操作。
根据delay时间的长短,这里分为几种。如果大于一个clock tick,就认为是long delay,比如一两秒的这种;也有很短的delay,比如只有自几个clock cycle,就用忙等待就可以了。在这二者中间,根据delay时间的长度,就有分别的实现方法了。比如long delay,这里的long delay,按照书里的说法,是毫秒级,在CPU和kernel看来,毫秒级已经是很长的时间了,所以用long delay描述。
7.3.1. Long Delays
long delay的几种实现方式。
7.3.1.1 Busy waiting
也就是通过loop check jiffies来判断是否delay了需要的时间。不推荐使用,因为在浪费CPU。
while (time_before(jiffies, j1))
cpu_relax( );
在时间没有到达j1之前,driver可以通过cpu_relax主动告知kernel我现在不需要使用CPU,可以给别人用。在大部分架构上,cpu_relax什么也不做,而在超线程处理器上,可以把CPU给另一个超线程使用。但是无论如何,通过这种方式实现long delay是非常不推荐的。而且jiffies是通过中断来更新的,如果在执行这段code的时候恰好中断是关闭的,那么这个code可能永远没机会读到更新的jiffies值。
7.3.1.2 Yielding the processor
和busy wait不同,这种方式在没有delay足够的时间时,直接放弃CPU。
#include <linux/sched.h>
while (time_before(jiffies, j1)){
schedule( );
}
这种方式有其明显的缺点,就是一旦放弃了CPU,你就不知道什么时候才会再被调度,间隔的时间有可能会比你期望的大。而且当当前CPU没有别的process可以调度时,这个进程就是唯一可以被运行的进程,所以schedule的还是它自己,这样仍然浪费了CPU,在空等。
7.3.1.3 Timeouts
上面的两种方法都是自己通过检测jiffies的值来判断否是等到了足够的delay,这个事情可以交给kernel来做,效率更高。根据driver是否等待了某个event,调用kernel接口的方式有两种。
a)如果你等待了某个event,可以使用:
#include <linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q,
condition, long timeout);
通过增加一个timeout,可以让process在timeout之后被唤醒。注意,这里的timeout值都是jiffies值,不是绝对时间。如果是因为超时返回,返回值是0,如果是中断或者等待的事件发生而返回,返回值为剩余的jiffies。
b)如果你没有等待任何的event,纯粹想等待一段时间后继续执行,那么可以调用:
#include <linux/sched.h>
//timeout是delay的jiffies个数
signed long schedule_timeout(signed long timeout);
在使用schedule_timeout之前,要先设置process的状态,所以一般这么使用:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (timeout);
同样的,返回值是剩余的jiffies个数,如果为0,说明是等待了timeout个jiffies返回;如果返回的非零值,说明是因为被signal唤醒而返回。如果调用schedle_timeout之前没有设置进程状态,默认就是runnable,就和调用schedule()是一样的效果。
7.3.2. Short Delays
上面的long delay是基于jiffies的delay,时间会比较长,比如几十毫秒或者秒级。在device driver中,如果要等待hardware,这个delay可能会非常短,比如纳秒级,微秒级,或者最多到毫秒级。针对这种短时间的delay,kernel提供了如下函数:
#include <linux/delay.h>
void ndelay(unsigned long nsecs); //纳秒级delay
void udelay(unsigned long usecs); //微秒级delay
void mdelay(unsigned long msecs); //毫秒级delay
这些delay的底层实现一般依赖于架构,每个架构都实现了udelay,但是ndelay/mdelay有些架构上并没有实现,如果架构本身没有实现,那么kernel就会基于udelay来实现。需要注意的是,等待的时间只会比请求的长,不会比请求的短。而且,这几种delay其实都是busy waitting,也就说他们在delay的时候一直占用CPU,别的task会无法执行,如果等待的时间长,尽量避免使用上面的接口。udelay其实是在系统启动式计算CPU的速度,利用计算出的速度通过software loop的方式来实现delay,这个速度值就是loops_per_jiffy。
如果需要delay的时间比较长,并且不想busy waitting,那么可以使用:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)
这几种函数都是sleep的方式来等待,所以期间会失去CPU,当然就会导致实际等待的时间有可能比需要的长。
总而言之,如果你不介意delay的比你想要的长,那么最好使用schedule_timeout,msleep或者ssleep,这样可以避免CPU的浪费。
7.4. Kernel Timers
上面说的delay,都是以block的方式来实现,即当前的process不能继续执行,要么是CPU busywait,要么是sleep。如果device driver有个task,想在将来的某个时刻处理,但是不希望block当前的process,那么可以使用timer。通过使用timer,你可以在指定的时间,使用指定的参数,运行指定的函数,比如某些hardware不能产生中断,就可以通过kernel timer的方式按照固定的间隔产生中断,而且schedule_timeout这个kernel接口就是通过timer来实现的。kernle的timer就是一个数据结构,相关的定义都在<linux/timer.h> and kernel/timer.c里。
当timer被调度时,会和当前注册它的process使用不同的context,而且他们是异步执行,当process在注册timer的时候,timer一般都没开始被调用,而是将来的某个时刻才运行,那个时候process甚至可能已经不存在了。
因为timer这套机制是由kernel来维护,由kernel来调用,其实现原理是基于软中断,和硬件中断有些类似,所以你注册的函数实际上是运行在atomic context里面,而不是process的context。所以,timer调用的函数就有很多的限制条件:
1, 不允许访问user space。因为没有process text,没有办法访问user space。
2, 因为没有process context,也就不能使用current指针,这个指针是只有process context才有,代表了当前的进程。
3, 不允许sleep,也不允许被调度出去。不能使用可能会引起休眠的函数,或者类似与wait_event之类的函数,甚至是kmalloc也要谨慎使用,以及semaphore,这些函数都有可能导致休眠。
kernel自身提供了一些函数,判断是否运行在interrupt context:in_interrupt(),不需要参数,返回非0值说明运行在interrupt context里,可能是hardware interrupt context,也可能是software interrupt context。还有一个函数,in_atomic(),返回非0值说明是在atomic,并且不允许调度,其中就包括software/hardware interrupt,以及持有spinlock锁的情况下。在持有spinlock的情况下,in_atomic返回非零值,这个时候可能kernel运行在process conetext,也就是current指针是有效的,但是仍然不允许访问user space,因为此时访问可能会发生调度,导致atomic code被打断。
另外,自己注册的这个函数callback,在timer到了被调度以后,可以再次放入list,可以实现重复的调度。另外,timer function和注册它的code,会在同一个CPU上执行,主要是为了cache和性能的考虑。timer function因为运行在另一个线程,所以一定要考虑和其他线程的竞争条件,并做好资源的保护。
7.4.1. The Timer API
kernel提供了初始化,注册以及删除timer的接口函数:
#include <linux/timer.h>
struct timer_list {
/* ... */
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
};
void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
timer_list里还有更多的成员变量,但是这里列出来的三个会经常被device driver使用到。expires代表了jiffies值,也就是多久之后timer被调度;当timer被调度的时候,会执行function;调用function使用的参数是data。data这个是一个unsigned long,其实很多时候存储的是一个指针,指向了device driver自己定义的一个结构体。unsigned long的大小和指针的大小相同,因此适合用来存储指针。
在使用timer之前,一定要先初始化timer,可以通过init_timer来初始化,也可以通过TIMER_INIITIALIZER来初始化,初始化完成以后,driver可以修改timer的成员,尤其是expires/function/data这三个成员;在修改好以后,就可以调用add_timer把timer加到kernel的list里面去。当需要disable timer的时候,直接调用del_timers即可。
使用timer的例子:
//driver实现的timer function
void jit_timer_fn(unsigned long arg)
{
struct jit_data *data = (struct jit_data *)arg;
unsigned long j = jiffies;
data->buf += sprintf(data->buf, "%9li %3li %i %6i %i %s\n",
j, j - data->prevjiffies, in_interrupt( ) ? 1 : 0,
current->pid, smp_processor_id( ), current->comm);
if (--data->loops) {
data->timer.expires += tdelay;
data->prevjiffies = j;
add_timer(&data->timer);
} else {
wake_up_interruptible(&data->wait);
}
}
//注册timer
unsigned long j = jiffies;
/* fill the data for our timer function */
data->prevjiffies = j;
data->buf = buf2;
data->loops = JIT_ASYNC_LOOPS;
/* register the timer */
data->timer.data = (unsigned long)data;
data->timer.function = jit_timer_fn;
data->timer.expires = j + tdelay; /* parameter */
add_timer(&data->timer);
/* wait for the buffer to fill */
wait_event_interruptible(data->wait, !data->loops);
除了上面介绍的timer相关的接口以外,kernel还有一些别的function:
//修改timer的expire time。
int mod_timer(struct timer_list *timer, unsigned long expires);
//同步删除timer
int del_timer_sync(struct timer_list *timer);
int timer_pending(const struct timer_list * timer);
del_timer_sync用来删除timer,和del_timer的功能类似,区别在于,当del_timer_sync返回时,任何CPU上都不会在执行这个timer了,也就是说这个函数是同步函数,如果执行时发现有CPU正在执行这个timer,那么分为两种情况:1,调用del_timer_sync的是non-atomic context,那么调用者发生sleep,直到那个CPU执行timer完成;2,调用del_timer_sync的是atomic context,那么busy wait,直到timer执行完成。尤其要注意在持有锁的情况下调用del_timer_sync,如果del先拿到了锁,timer执行的时候就会wait,那么del在等待timer结束是等不到的,这就发生了死锁。此外,因为timer允许把自己再注册进去,所以如果要del timer,那么要阻止timer注册自己。
timer_pending用来query这个timer是否正在被调度,是的话返回true,否则返回false。
7.4.2. The Implementation of Kernel Timers
关于kernel内部对timer的实现,有几个原则:
1, timer的management要足够的轻量级,因为kernel中需要做timer的太多了,如果太复杂,performance势必会差。
2, 要能支持足够过的timer item,整个kernel都要用,timer自然有很多。
3, 大多数timer都是在比较短的时间内需要被执行,很少有长时间delay的
4, timer要能和注册它的函数跑在同样的CPU上,自然也是为了CPU cache和performance考虑。
基于以上几个原则,kernel最终选择了一个per-CPU的数据结构来实现timer。数据结构timer_list中包含这个per-CPU的数据结构,也就是里面的成员base。如果base为NULL,表示timer不可运行,如果非NULL,则代表了是哪个数据在哪个CPU上运行。
在device driver调用了add_timer/mod_timer以后,kernel都会通过internal_add_timer这个函数把timer结构体放到一个双向链表中,这个链表位于一个“cascading table”,而这个table也是一个per-CPU的结构体。
根据expire的时间长短,这些timer会被放到不同的list里面去,比如在0-255 jiffies内会触发的timer,通过计算它的expires的LSB,放到对应的256个list里面去;在256-16384个jiffies后会被触发的timer,会根据expires的第9-14bit 的值,放到对应的64个list里面去;类似的,更久以后会被触发的会被分别放到第15-20bit,21-26bit,27-31bit对应的list里面去,如果更久的(一般在64bit机器上),会用0xffffffff做一个hash来存放。到期了的timer会在下一个timer tick被调用。当run_timer被调用时,它会执行所有pending的timer function。
当__run_timers在执行的时候,它会把所有到期的timer都执行。
If jiffies is currently a multiple of 256, the function also rehashes one of the next- level lists of timers into the 256 short-term lists, possibly cascading one or more of the other levels as well, according to the bit representation of jiffies.
关于timer list的处理过程,看doc没太懂,改天写个单独的文章研究下timer的处理过程。这种机制,虽然看上去比较复杂,但是performance确不错,无论timer有多少,都不会有影响。唯一的缺点在于会占用存储空间,因为list比较多。
7.5. Tasklets
kernel中另外一个比较延迟操作的重要实现,是tasklet。它主要用在中断处理例程中,和之前的timer有一些相似之处,也有不同点。相似之处:
1, 都是运行在interrupt context上,也就是“software interrupt”,是kernel提供的一种异步运行task的机制。
2, 和注册它进程运行在同一个CPU上
3, 都使用一个unsigned long作为参数
不同之处:tasklet不能像timer一样,让他delay某一个具体的时间之后再运行。它的执行时间由kernel自己决定,不受driver的控制。
tasklet在使用之前必须初始化,初始化tasklet的代码如下:
#include <linux/interrupt.h>
struct tasklet_struct {
/* ... */
void (*func)(unsigned long);
unsigned long data;
};
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);
DECLARE_TASKLET(name, func, data);
DECLARE_TASKLET_DISABLED(name, func, data);
tasklet的性质有如下几个:
1, 可以被disable和enable,并且enable的次数要于被disable的一致才会继续工作
2, tasklet也能自己注册自己,也就是可以在被调用以后再把自己加到list里。
3, tasklet可以设置优先级,normal或者high,high的话会优先运行。
4, tasklet有可能会被立即执行,也可能以后某个时刻运行,但不会晚于下一次的timer tick。(不懂)
5, 多个tasklet可能会并行运行,但是同一个tasklet一定按顺序被调度,不会同时被调度,而且会和schedule它的运行在同一个CPU上。
在kernel中,每一个CPU都有一个ksoftirqd线程,用来执行这个CPU上的software irq操作,在CPU idle的时候,tasklet会被立即执行,如果busy,也会等到下一个timer tick执行,执行的函数是tasklet_action。
在tasklet初始化完成以后,后续tasklet的相关操作:
void tasklet_disable(struct tasklet_struct *t);
void tasklet_disable_nosync(struct tasklet_struct *t);
void tasklet_enable(struct tasklet_struct *t);
void tasklet_schedule(struct tasklet_struct *t);
void tasklet_hi_schedule(struct tasklet_struct *t);
void tasklet_kill(struct tasklet_struct *t);
tasklet_disable:tasklet被disable。虽然tasklet仍然可以通过tasklet_schedule调度,但是除非被enable,否则不会被真正执行。另外,如果disable的时候tasklet正在执行,那么disable函数会busy wait,直到tasklet执行完毕并设置disable,所以,只要调用了disable返回,你可以认为当前任何CPU上都不会再有tasklet执行了。
tasklet_disable_nosync:和tasklet_disable的不同点在于,如果disable的时候发现tasklet正在执行,不会busy wait,设置tasklet disable之后立即返回,可能此时tasklet仍然在别的CPU上执行。
tasklet_enable:设置tasklet为enable,如果之前已经调用了tasklet_schedule,那么tasklet很快会被执行。注意,enable的次数要和disable的次数match,因为kernel保存了一个disable counter。
tasklet_schedule:schedule tasklet执行,如果此时tasklet被schedule过并且还没执行,那么tasklet只会被执行一次;如果tasklet已经在执行,那么在它执行完成后会再次被schedule。
tasklet_hi_schedule:shedule tasklet,按照高优先级执行。当软中断handler开始执行时,它会优先处理high priority的tasklet,其他的软中断task,包括normal的tasklet,都会被之后才处理。通常情况下,除非必要,比如audio buffer,否则一般不使用这个。
tasklet_kill:kill tasklet,调用完成后,tasklet以后不能再被调度,通常在device被close或者module被remove的时候调用。如果kill的时候发现tasklet还在运行,那么等待tasklet运行结束再kill掉。注意,如果tasklet中会schedule自己,那么必须阻止tasklet自己schedule自己后,再调用kill,就像del_timer_sync。
normal和high priority这两个tasklet list都是per CPU的list,和timer类似。
7.6. Workqueues
workqueue和tasklet有些类似,都是在将来的某个不确定的时刻执行某些函数。他们的不同点在于:
1, tasklet运行在kernel的software interrupt线程里,操作都是atomic操作。但是workqueue运行在kernel一个特殊的process里,因此更加灵活,既然不在atomic context里,workqueue里就允许sleep。
2, workqueue可以设定在将来的哪个时间来运行,也就是可以设置interval,而tasklet不行。
相同点在于 :workqueue和tasklet一样,和submit它的运行在同一个CPU上,主要是为了performance考虑,充分利用CPU的cache。
总之,二者的最主要区别就是:tasklet被调度的时间更快,并且是atomic;workqueue时间更慢,并且不是atomic。因此,二者有不同的使用场景。
workqueue用到的结构体workqueue_struct,使用之前必须create一个出来。使用方式如下:
#include <linux/workqueue.h>
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
每一个workqueue都有一个专门的kernel thread,用来从这个workqueue里执行work struct。
如果直接使用create_workqueue,那么在创建workqueue的同时,会给每个CPU创建一个thread,这些thread都会从这个workqueue中获取task来执行,相当于一个workqueue,每个CPU都会从queue里获取task执行。然而在某些时候,给每个CPU都创建thread可能没必要,所以有create_singlethread_workqueue,它只创建一个thread,也就是同一时刻只会有一个CPU从queue里取task执行。
workqueue创建好了以后,还需要创建work_struct,并submit到workqueue里面去。初始化work_struct:
//编译时初始化,name就是work struct name,function是work struct被调度时执行的函数,data就是function的参数。
DECLARE_WORK(name, void (*function)(void *), void *data);
//运行时初始化,会初始化work struct里的指针。
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
//运行时初始化,不会初始化work struct里的指针。
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data);
submit work_struct到queue里面去有两个函数:
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
//把work_struct放到queue里,并在至少delay个jiffies的时间后才会执行。
int queue_delayed_work(struct workqueue_struct *queue,struct work_struct *work, unsigned long delay);
返回0,表示成功的加到了queue,非0表示work_struct已经在queue里面,不会再次添加。在将来的某个时刻,这个work_struct就会被kernel调用,因为是在kernel的thread,不是ISR,所以可以sleep,只是需要注意如果sleep,会对这个queue里的其他task造成的影响。另外,这个kernel thread,没有user space对应,所以不可以访问user space。
如果需要从queue里面取消一个pending的work_struct:
int cancel_delayed_work(struct work_struct *work);
如果返回非零值,说明work struct还没有被执行,cancel成功;如果返回0,表明这个work_struct可能当前正在别的CPU上执行,也可能在cancel_delayed_work返回后某个时刻被执行,这种情况下,如果想要保证以后work_struct不会被任何CPU执行,可以在调用了cancel之后,再调用:
void flush_workqueue(struct workqueue_struct *queue);
在flush_workqueue返回之后,之前submit的work_struct不会再执行了。
如果work_queue不再需要了,那么可以:
void destroy_workqueue(struct workqueue_struct *queue);
7.6.1. The Shared Queue
如果driver不需要自己创建一个单独的work_queue,可以使用kernel share的一个work_queue,只是work_struct多久能被调用,这个问题是不确定的,有可能需要等很久的时间。
如果使用share的work_queue,就直接调用:
int schedule_work(struct work_struct *work);
在work_struct的function里,可以再次把自己放到work_queue里去做schedule。
同样的,如果你需要cancel,那么调用cancel_delayed_work,在之后调用
void flush_scheduled_work(void);
需要注意的是,因为是kernel share的global work queue,flush多久能够完成也是不确定的。