Time Delays and Deferred Work [LDD3 07]

16 篇文章 0 订阅
14 篇文章 1 订阅

 

Table of Contents

7.1. Measuring Time Lapses

7.1.1. Using the jiffies Counter

7.1.2. Processor-Specific Registers

7.2. Knowing the Current Time

7.3. Delaying Execution

7.3.1. Long Delays

7.3.1.1 Busy waiting

7.3.1.2 Yielding the processor

7.3.1.3 Timeouts

7.3.2. Short Delays

7.4. Kernel Timers

7.4.1. The Timer API

7.4.2. The Implementation of Kernel Timers

7.5. Tasklets

7.6. Workqueues

7.6.1. The Shared Queue


一个真正的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多久能够完成也是不确定的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值