Time Delays and deferred work

kernel提供了一些跟时间管理相关的函数,device driver中往往需要这些函数来实现driver的部分功能。

1, 测量时间和比较时间

2, 如何获取当前时间

3, 延迟某些操作,直到某个时刻

4, 异步函数的调度

整个kernel的运行非常依赖时间,因为kernel里面很多的工作需要做同步,如果计时器不准确,那kernel运行起来就不可控了。kernel的时间需要靠硬件的计时器产生中断来维护,中断产生的间隔根据kernel的变量HZ来控制,这个值是架构相关的,X86上一般是1000. jiffies_64是一个64bit值,每当timer中断产生,这个值就加1,在系统boot起来的时候,这个值是0,所以这个值实际上存储的是自系统开机以来kenrel经过的时间中断个数。除了jiffies_64,还有一个变量是jiffies,一般是jiffies_64的低bit,访问这个值要比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 */

以及时间的比较:

#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);

有两个结构体可以表示时间,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来存储,并且需要访问两次才能拿到值,所以kernel提供了函数来访问,这个函数是加了锁的:

#include <linux/jiffies.h>
u64 get_jiffies_64(void);

有些架构的CPU中有更高精度的计时器,是一个per CPU的寄存器。这个依赖于具体架构的实现,也就是CPU自己的实现。某些CPU里有一个单独的寄存器,用来计数,每一个cycle clock,这个值就加1. X86上这个寄存器是TSC(timestamp counter),这个寄存器在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);

对于不支持的平台,get_cycles就返回0.

获取当前时间:

kernel内部一般都是用时间间隔,很少会使用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);

有时候需要知道timestamp,可以调用do_gettimeofday:

#include <linux/time.h>
void do_gettimeofday(struct timeval *tv);

也可以通过别的函数获取当前时间:

#include <linux/time.h>
struct timespec current_kernel_time(void);

Delaying Execution

取决于delay时间的长短,这里分为几种。如果大于一个clock tick,就认为是long delay。(不太理解,一个clock tick应该很短,怎么会是long delay?)long delay的几种实现方式:

1, busy waitting

也就是通过loop check jiffies来判断是否delay了需要的时间。不推荐使用,因为在浪费CPU。

while (time_before(jiffies, j1))
    cpu_relax( );

2, Yielding the processor

和busy wait不同,这种方式在没有delay足够的时间时,主动放弃CPU。

while (time_before(jiffies, j1)){ 
    schedule( );
}

这种方式有其明显的缺点,就是一旦放弃了CPU,你就不知道什么时候才会再被调度,间隔的时间有可能会比你期望的大。

3, Timeout

1和2都是自己通过检测jiffies的值来判断否是等到了足够的delay,这个事情可以交给kernel来做。根据是否等待了某个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个jiffies后重新获得CPU继续执行。注意,这里的timeout值都是jiffies值,不是绝对时间。如果是因为超时返回,返回值是0;如果是中断或者等待的事件发生而返回,返回值为剩余的jiffies。

b)如果你没有等待任何的event,纯粹想等待一段时间后继续执行,那么可以调用:

#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);

在使用schedule_timeout之前,一般要先设置process的状态,所以一般这么使用:

set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);

Short Delays

这种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的底层实现一般依赖于架构,并且等待的时间只会比请求的长,不会比请求的短。而且,这几种delay其实都是busy waitting,也就说他们在delay的时候一直占用CPU,如果等待的时间长,尽量避免使用上面的接口。

如果需要delay的时间比较长,那么可以使用:

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的浪费。

Kernel Timers:

上面说的delay,都是block的方式来delay,也就是当前的process要等在这里,要么是busywait,要么是sleep。如果driver有个task,想在将来的某个时刻处理,但是不希望block当前的process,那么可以使用timers。通过使用timer,你可以在指定的时间,使用指定的参数,运行指定的函数。使用timer,会和当前的process使用不同的context,因为timer这套机制是由kernel来维护,由kernel来调用,其实现原理是基于软中断,所以你注册的函数实际上是运行在atomic context里面,而不是process的context。所以,timer调用的函数就有很多的限制条件:

1, 不允许访问user space。因为没有process text,没有办法访问user space context。

2, 因为没有process context,也就不能使用current指针,这个指针是只有process context才有。

3, 不允许sleep,也不允许调度(?)。因此不能使用可能会引起休眠的函数,或者类似与wait_event之类的函数,甚至是kmalloc也要谨慎使用。

kernel自身提供了一些函数,判断是否运行在interrupt context:in_interrupt(),不需要参数,返回非0值说明运行在interrupt context里,可能是hardware interrupt context,也可能是software interrupt context。还有一个函数,in_atomic(),返回非0值说明是在atomic,不允许调度,其中就包括software/hardware interrupt,以及持有spinlock锁的情况下。

另外,自己注册的这个函数callback,在timer到了被调度以后,可以再次放入list,可以实现重复的调度。timer function和注册它的function,会在同一个CPU上执行,主要是为了cache和性能的考虑。timer function因为运行在另一个线程,所以一定要考虑竞争条件,并做好资源的保护。

关于timer的API:

#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);
int mod_timer(struct timer_list *timer, unsigned long expires);
int del_timer_sync(struct timer_list *timer);
int timer_pending(const struct timer_list * timer);

关于kernel内部对timer的实现,有几个原则:

1, timer的management要足够的轻量级

2, 要能支持足够过的timer item

3, 大多数timer都是在比较短的时间内被调度,很少有长时间delay的

4, timer要能和注册它的函数跑在同样的CPU上

基于以上几个原则,kernel最终选择了一个per-CPU的数据结构来实现,timer_list中包含这个数据结构,如果为NULL,表示timer不可运行,如果非NULL,则代表了在哪个CPU上运行。在调用add_timer/mod_timer以后,kernel通过internal_add_timer把timer结构体放到了一个双向链表中。

根据expire的时间不同,这些timer会被放到不同的list里面去,比如在0-255 jiffies内会触发的timer就放到对应的256个list里面去;在256-16384个jiffies触发的timer,会根据9-14bit 的值,放到对应的64个listi里面去;类似的,后面触发的会被分别放到15-20,21-26,27-31对应的list里面去。过期的timer会在下一个timer click被调用。当run_timer被调用时,它会执行所有pending的timer function。

When __run_timers is fired, it executes all pending timers for the current timer tick. 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的处理过程。

Tasklets

主要用在中断处理例程中,它和timer有一些相似之处,也有一些不同点。

相似之处:

1, 都是运行在interrupt context

2, 和注册它的运行在同一个CPU上,为了cache考虑

3, 都接受一个unsigned long参数

不同之处:tasklet不能像timer一样,让他delay某一个具体的时间之后再运行。它的执行时间由kernel自己决定,不受driver的控制。使用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的次数多才会开始work

2, tasklet也能自己注册自己,也就是可以在被调用以后再把自己加到list里。

3, tasklet可以设置优先级,normal或者high,high的话会优先运行。

4, tasklet有可能会被立即执行,也可能以后某个时刻运行,但不会晚于下一次的timer tick。

5, 多个tasklet可能会并行运行,但是同一个tasklet一定按顺序被调度,不会同时运行,而且会和schedule它的运行在同一个CPU上。

在kernel中,每一个CPU都有一个ksoftirqd线程,用来执行这个CPU上的software irq操作。

kernel中tasklet的相关操作:

//tasklet被disable,虽然tasklet仍然可以通过tasklet_schedule调度,但是除非被enable,否则不会被真正执行。另外,如果disable的时候tasklet正在执行,那么disable函数会busy wait,直到tasklet执行完毕并设置disable,所以,只要调用了disable返回,tasklet就不会再被执行了。
void tasklet_disable(struct tasklet_struct *t);
//和tasklet_disable的不同点在于,如果disable的时候发现tasklet正在执行,不会busy wait,设置tasklet disable之后立即返回,可能此时tasklet在别的CPU上执行。
void tasklet_disable_nosync(struct tasklet_struct *t);
//设置tasklet为enable,如果之前已经调用了tasklet_schedule,那么tasklet很快会被执行。注意,enable的次数要和disable的次数match,因为kernel保存了一个disable counter。
void tasklet_enable(struct tasklet_struct *t);
//schedule tasklet执行,如果此时tasklet被schedule过并且还没执行,那么tasklet只会被执行一次;如果tasklet已经在执行,那么在它执行完成后会再次被schedule。
void tasklet_schedule(struct tasklet_struct *t);
//shedule tasklet,按照高优先级执行。
void tasklet_hi_schedule(struct tasklet_struct *t);
//kill tasklet,调用完成后,tasklet不能再被调度。通常在device被close或者module被remove的时候调用。如果kill的时候发现tasklet还在运行,那么等待tasklet运行结束再kill掉。注意,如果tasklet中会schedule自己,那么必须阻止tasklet自己schedule自己后,再调用kill。就像del_timer_sync。
void tasklet_kill(struct tasklet_struct *t);

normal和high priority这两个tasklet list都是per CPU的list,和timer类似。

Workqueue

workqueue和tasklet有些类似,都是在将来的某个不确定的时刻执行某些函数,异同点在于:

1, tasklet运行在kernel的software interrupt线程里,操作都是atomic操作。但是workqueue运行在kernel一个特殊的process里,因此更加灵活,而且,workqueue里允许sleep。

2, workqueue和tasklet一样,和submit它的运行在同一个CPU上。主要是为了performance考虑,充分利用CPU的cache。

3, workqueue可以设定在将来的哪个时间来运行,也就是可以设置interval。

总之,二者的最主要区别就是:tasklet被调度的时间更快,并且是atomic;workqueue时间更慢,并且不是atomic。workqueue用到的结构体workqueue_struct,使用方式如下:

struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);

如果使用create_workqueue,就会创建一个新的workqueue,同时给每个CPU创建一个thread,这些thread都会从这个workqueue中获取task来执行。然而在某些时候,给每个CPU都创建thread可能没必要,所以可以通过create_singlethread_workqueue只创建一个thread。workqueue创建好了以后,就可以创建work_struct,并submit到queue里面去。

初始化work_struct:

//编译时初始化
DECLARE_WORK(name, void (*function)(void *), void *data);
//运行时初始化
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data);
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里面删除work_struct:

 int cancel_delayed_work(struct work_struct *work);

在调用了cancel之后,kernel保证不会在此之后调度这个work_struct。如果cancel返回非0,表明work_struct从queue里移除;如果返回0,表明这个work_struct可能正在别的CPU上执行,如果想要保证以后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);

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

发布了32 篇原创文章 · 获赞 6 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/scutth/article/details/105329270