tasklet && workqueue && kernel timer

来源:互联网 发布:nginx 虚拟目录配置 编辑:程序博客网 时间:2024/05/05 19:34

一、kernel timer

1、适用环境
内核中许多部分的工作都高度依赖于时间信息。Linux内核利用硬件提供的不同的定时器以支持忙等待或睡眠等待等时间相关的服务。忙等待时,CPU会不断运转。但是睡眠等待时,进程将放弃CPU。因此,只有在后者不可行的情况下,才考虑使用前者。当然内核也提供了某些便利,如果我们需要在将来的某个时间点调度执行某个动作,同时在该时间点到达之前不会阻塞当前进程,则可以使用内核定时器。

2. HZ和jiffies
内核通过定时器中断来跟踪时间流。系统定时器能以可编程的频率中断处理器。此频率即为每秒的定时器节拍数,也可以说是系统时钟中断发生的频率,对应着内核变量HZ。选择合适的HZ值需要权衡。HZ值大,定时器间隔时间就小,因此进程调度的准确性会更高。但是,HZ值越大也会导致开销和电源消耗更多,因为更多的处理器周期将被耗费在定时器中断上下文中。
HZ的值取决于体系架构,而且目前内核支持无节拍的内核(CONFIG_NO_HZ),它会根据系统的负载动态触发定时器中断。
每次当时钟中断发生时,内核内部计数器的值就增加一。这个计数器的值在系统引导时被初始化为0,因此,它的值就是自上次操作系统引导以来的时钟滴答数。这个计数器是一个64位的变量(即使是在32位架构上也是64位),称为"jiffies_64",但是驱动程序开发者通常访问的是jiffies变量,它是unsigned long型的变量,要么和jiffies_64相同,要么仅仅是jiffies_64的低32位。通常首选使用jiffies,因为对它的访问很快,从而对64位jiffies_64值的访问并不需要在所有架构上都是原子的。
 因此jiffies变量记录了系统启动以来,系统定时器已经触发的次数。内核每秒钟将jiffies变量增加HZ次。因此,对于HZ值为100的系统,1个jiffy等于10ms,而对于HZ为1000的系统,1个jiffy仅为1ms。

3.比较缓存值与当前值所用到的宏

#include <linux/jiffies.h>

int time_after(unsigned long a,unsigned long b) 如果a(jiffies的某个快照)所代表的时间比b靠后,则返回真
int time_before(unsigned long a,unsigned long b) 如果a 比 b靠前,则返回真
int time_after_eq(unsigned long a,unsigned long b) 如果 a 比 b靠后或者相等,返回真
int time_before_eq(unsigned long a,unsigned long b) 如果 a 比 b靠前或者相等,返回真

4. 内核空间和用户空间时间表述方法的转换

(1) 有关时间的结构体
struct timeval
{
int tv_sec;
int tv_usec;
};
其中tv_sec是由凌晨开始算起的秒数,tv_usec则是微秒(10E-6 second)。

struct timezone
{
int tv_minuteswest;
int tv_dsttime;
};
tv_minuteswest是格林威治时间往西方的时差,tv_dsttime则是时间的修正方式。

struct timespec
{
long int tv_sec;
long int tv_nsec;
};
其中tv_sec是由凌晨开始算起的秒数,tv_nsec是nano second(10E-9 second)。

struct tm
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
tm_sec表「秒」数,在[0,61]之间,多出来的两秒是用来处理跳秒问题用的。
tm_min表「分」数,在[0,59]之间。
tm_hour表「时」数,在[0,23]之间。
tm_mday表「本月第几日」,在[1,31]之间。
tm_mon表「本年第几月」,在[0,11]之间。
tm_year要加1900表示那一年。
tm_wday表「本第几日」,在[0,6]之间。
tm_yday表「本年第几日」,在[0,365]之间,闰年有366日。
tm_isdst表是否为「日光节约时间」。

struct itimerval
{
struct timeval it_interval;
struct timeval it_value;
};
it_interval成员表示间隔计数器的初始值,而it_value成员表示间隔计数器的当前值。

(2) 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)

(3)对 jiffies_64 进行读取的特殊辅助函数
#include <linux\jiffies>
u64 get_jiffies_64(void)
5.短延时
当设备驱动程序需要处理硬件的延迟时,这种延迟通常最多涉及到几十个毫秒,这种情况下,依赖于时钟滴答是不正确的。以下几个函数可很好阿文拿出短延迟任务:
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
这三个延迟函数均是忙等待函数,因为在延迟过程中无法运行其他函数。

实现毫秒级(或者更长)延迟还有另外一种方法,这种方法不涉及忙等待。
#include <linux/delay.h>
void msleep(ungigned int millisecs)
unsgined long msleep_interruptible(unsigned int millisecs)
void ssleep(unsigned int seconds)
前两个函数将调用进程休眠以给定的millisecs.
对msleep的调用是不可中断的,我们可以确信进程至少休眠给定的毫秒数。
对msleep_interruptible的调用是可以中断的,如果驱动程序正在某个等待队列上
等待,而又希望有唤醒能够打断这个等待的话,使用此函数。
对ssleep的调用将使进程进入不可中断的休眠,但休眠时间以秒计。

6.长延时
在内核中,以jiffies为单位进行的延迟通常被认为是长延时。
(1)忙等待
实现忙等待的函数本身不利用CPU进行有用的工作,同时还不让其他程序使用CPU.这并不
实现长延时的好方法。
(2)睡眠等待
睡眠等待比忙等待的方式更好一些,本进程会在等待时将处理器让给其他进程。
用于睡眠等待的两个函数是:
#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);
函数使用场合是:在一个特定的条件满足或者超时发生后,希望代码继续运行。
它们的实现都是基于schedule_timeout();
#include <linux/sched.h>
signed long schedule_timeout(signed long timeout)

(3)内核定时器
内核定时器可以用在未来的某个特定时间点(基于时钟滴答)调度执行某个函数,从而
可用于完成许多任务。
@ 定时器API
#include <linux/timer.h>
truct timer_list {
struct list_head entry; timer_list结构体链表的头部
unsigned long expires;
用于存放延时结束时间
void (*function)(unsigned long); 延时结束时执行的回调函数,注意这里传递一个无符号长整型数字
unsigned long data;
常用于存储数据的指针
......
}
DEFINE_TIMER() 静态创建定时器
init_timer() 动态定义一个定时器
struct timer_list TIMER_INITIALIZER(_function,_expires,_data) 初始化timer_list数据结构
void add_timer(struct timer_list *timer); 注册定时器结构,以在当前CPU上运行
int mod_timer(struct timer_list *timer,unsigned long expires) 修改一个已经调度的定时器结构的到期时间,它也可以代替add_timer 函数使用
int timer_pending (struct timer_list *timer) 返回布尔值,用来判断给定的定时器结构是否已经被注册运行
void del_timer(struct timer_list *timer)
void del_timer_sync(struct timer_list *timer)从活动定时器清单中删除一个定时器。后一个函数确保定时器不会在其他CPU上运行。
@定时器的应用实例
可以通过init_timer()动态定义一个定时器,也可以通过DEFINE_TIMER()静态创建定时器。然后,将处理函数的地址和参数绑定给一个timer_list,并使用add_timer()注册它即可:
#include <linux/timer.h>
struct timer_list my_timer;

init_timer(&my_timer); /* Also see setup_timer() */
my_timer.expire = jiffies + n*HZ; /* n is the timeout in number of seconds */
my_timer.function = timer_func; /* Function to execute after n seconds */
my_timer.data = func_parameter; /* Parameter to be passed to timer_func */
add_timer(&my_timer); /* Start the timer */

上述代码只会让定时器运行一次。如果想让timer_func()函数周期性地执行,需要在timer_func()加上相关代码,指定其在下次超时后调度自身:
static void timer_func(unsigned long func_parameter)
{
/* Do work to be done periodically */
/* ... */

init_timer(&my_timer);
my_timer.expire = jiffies + n*HZ;
my_timer.data = func_parameter;
my_timer.function = timer_func;
add_timer(&my_timer);
}

   可以使用mod_timer()修改my_timer的到期时间,使用del_timer()取消定时器,或使用timer_pending()以查看my_timer当前是否处于等待状态。
查看kernel/timer.c源代码,会发现schedule_timeout()内部就使用了这些 API。
二、tasklet
1.tasklet介绍
tasklet是一个延迟方法,可以实现将已登记的函数进行推后运行。一个给定的tasklet只运行在一个 CPU 中(就是用于调用该tasklet的那个 CPU), 同一tasklet永远不会同时运行在多个 CPU 中。 但是不同的tasklet可以
同时运行在不同的 CPU 中。和内核定时器的相同点是:(1)它们始终在中断期间运行;(2)始终会在调度它们的同一CPU上运行;(3)都接受一个unsigned long参数;(4)也会在“软件中断”上下文以原子模式来执行。
和内核定时器不同的是:不能要求在某个给定时间执行给定的函数。调度一个tasklet,表明我们只是希望内核选择某个其后的时间来执行给定的函数。这个对中断例程来说尤其有用。
tasklet以数据结构的形式存在,并在使用前必须初始化。 tasklet_struct 结构体包含了用于管理和维护tasklet的必要数据 (状态,通过 atomic_t 来实现允许/禁止控制,函数指针,数据,以及链表引用)

2.tasklet的API

#include <linux/interrupt.h>

该宏调用只是利用所提供的信息对结构体 tasklet_struct 进行初始化(tasklet名,函数,以及tasklet专有数据)。 默认情况下,微线程处于允许状态,这意味着它可以被调度。
DECLARE_TASKLET( name, func, data );

将tasklet默认声明为禁止状态。这时需要调用函数tasklet_enable来实现tasklet可被调度。
DECLARE_TASKLET_DISABLED( name, func, data);

此函数初始化一个通过分配或者其他途径获得的tasklet结构。
void tasklet_init( struct tasklet_struct *, void (*func)(unsigned long),unsigned long data );

禁用指定的tasklet,但不会等待任何正在运行的tasklet退出。返回后,tasklet是禁用的直到重新启用之前,不会再次调度;返回时,有可能tasklet仍在运行。
void tasklet_disable_nosync( struct tasklet_struct * );

函数禁用指定的tasklet,但仍可以被调度,但会推迟执行,直到该tasklet被重新启用。如果tasklet正在运行,该函数会进入忙等待直到tasklet退出为止。
void tasklet_disable( struct tasklet_struct * );

存在两个 enable 函数: 一个用于正常优先级调度(tasklet_enable),另一个用于允许高优先级调度(tasklet_hi_enable)。
正常优先级调度通过 TASKLET_SOFTIRQ-level 软中断来执行, 高优先级调度则通过 HI_SOFTIRQ-level 软中断执行。
void tasklet_enable( struct tasklet_struct * );
void tasklet_hi_enable( struct tasklet_struct * );

由于存在正常优先级和高优先级的 enable 函数, 因此要有正常优先级和高优先级的调度函数,每个函数利用特殊的软中断矢量来为tasklet排队(tasklet_vec 用于正常优先级, 而 tasklet_hi_vec 用于高优先级)。
来自高优先级矢量的tasklet先得到服务,随后是来自正常优先级矢量的tasklet。 注意,每个 CPU 维持其自己的正常优先级和高优先级软中断矢量。
void tasklet_schedule( struct tasklet_struct * );
void tasklet_hi_schedule( struct tasklet_struct * );

tasklet 生成之后,可以通过函数tasklet_kill来停止。函数tasklet_kill保证tasklet不会再运行。而且,如果按照进度该tasklet应该运行,将会等到它运行完后,再kill该线程。
tasklet_immediate 只在指定的CPU处于dead状态时被采用。
void tasklet_kill( struct tasklet_struct * );
void tasklet_kill_immediate( struct tasklet_struct *, unsigned int cpu );

上述API的实现可以通过./kernel/softirq.c 与 ./include/linux/interrupt.h来了解。

3.应用实例

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>

char my_tasklet_data[]="my_tasklet_function was called";

void my_tasklet_function( unsigned long data )
{
printk( "%s\n", (char *)data );
return;
}

DECLARE_TASKLET( my_tasklet, my_tasklet_function, (unsigned long) &my_tasklet_data );

int init_module( void )
{
tasklet_schedule( &my_tasklet );

return 0;
}

void cleanup_module( void )
{

tasklet_kill( &my_tasklet );

return;
}

三、workqueue
1.workqueue介绍

工作队列是实现延迟的新机制,从 2.5 版本 Linux 内核开始提供该功能。 不同于tasklet一步到位的延迟方法,工作队列采用通用的延迟机制, 工作队列的处理程序函数能够休眠(这在tasklet模式下无法实现)。
工作队列可以有比微线程更高的时延,并为任务延迟提供功能更丰富的 API。 处于核心的是工作队列(结构体 workqueue_struct), 任务被安排到该结构体当中。 任务由结构体 work_struct 来说明, 用来鉴别哪些任务
被延迟以及使用哪个延迟函数。events/X 内核线程(每 CPU 一个)从工作队列中抽取任务并激活一个处理程序(由处理程序函数在结构体 work_struct 中指定)。
表明类似于tasklet,它们都允许内核代码请求某个函数在将来的时间被调用。但两者之间存在一些非常重要的区别,包括:
(1)tasklet在软件中断上下文中运行,因此所有的tasklet代码都必须是原子的。相反,工作队列在一个特殊内核进程的上下文中运行,因此它们具有更好的灵活性。尤其是,工作队列函数可以休眠。
(2)tasklet始终运行在被初始提交的同一个处理器上,但这只是工作队列的默认方式。
(3)内核代码可以请求工作队列函数的执行延迟给定的时间间隔。
两者的关键区别在于:tasklet会在很短的时间段内很快执行,并且以原子模式执行,而工作队列函数可具有更长的延迟且不必原子化。两种机制有各自适应的情形。

2.相关的API
#include <linux/workqueue.h>

工作队列通过宏调用生成 create_workqueue,返回一个 workqueue_struct 值。 可以通过调用函数 destroy_workqueue 销毁一个工作队列。
其中,每个工作队列有一个或者多个专用的进程(“内核线程”),这些进程运行提交到该队列的函数。如果我们使用create_workqueue,则内核会在系统中的每个处理器上为该工作队列创建专用的线程。
在许多情况下,众多的线程可能对性能具有某种程度的杀伤力;因此,如果单个线程够用,那么应该使用create_singlethread_workqueue创建工作队列。
struct workqueue_struct *create_workqueue( const char *name );
struct workqueue_struct *create_singlethread_workqueue(const char * name );
void destroy_workqueue( struct workqueue_struct * );

通过工作队列与之通信的任务可以由结构体 work_struct 来定义。 通常,该结构体是用来进行任务定义的结构体的第一个元素。要向工作队列提交一个任务,需要填充一个work_struct结构。
如果在编译时创建和初始化一个work_struct结构,就使用DECLARE_WORK来完成;如果在运行时构造work_struct结构,就使用INIT_WORK和PREPARE_WORK这个两个宏。
INIT_WORK完成更加彻底的结构初始化工作;首次构造该结构时,应该使用这个宏;PREPARE_WORK完成几乎相同的工作,但它不会初始化用来将work_struct结构链接到工作队列的指针。如果结构已经被提交到工作队列,而只是需要
修改该结构,则应该使用PREPARE_WORK,而不是INIT_WORK.如果开发人员需要在任务被排入工作队列之前发生延迟,可以使用宏 INIT_DELAYED_WORK 和 INIT_DELAYED_WORK_DEFERRABLE。

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);
INIT_DELAYED_WORK( work, func );
INIT_DELAYED_WORK_DEFERRABLE( work, func );

任务结构体的初始化完成后,接下来要将任务安排进工作队列。 可采用多种方法来完成这一操作。 首先,利用 queue_work 简单地将任务安排进工作队列(这将任务绑定到当前的 CPU)。 或者,
可以通过 queue_work_on 来指定处理程序在哪个 CPU 上运行。 两个附加的函数为延迟任务提供相同的功能(其结构体装入结构体 work_struct 之中,并有一个计时器用于任务延迟 )
int queue_work( struct workqueue_struct *wq, struct work_struct *work );
int queue_work_on( int cpu, struct workqueue_struct *wq, struct work_struct *work );
int queue_delayed_work( struct workqueue_struct *wq,struct delayed_work *dwork, unsigned long delay );
int queue_delayed_work_on( int cpu, struct workqueue_struct *wq,struct delayed_work *dwork, unsigned long delay );

在使用全局的内核共享工作队列的时候,不需要定义工作队列结构体,利用以下函数来为工作队列定位。由于是共享队列,我们不应该长期占用该队列,即不能长时间休眠,而且我们的任务可能需要更长时间才能获得处理器时间。
int schedule_work( struct work_struct *work );
int schedule_work_on( int cpu, struct work_struct *work );
int scheduled_delayed_work( struct delayed_work *dwork, unsigned long delay );
int scheduled_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay );

还有一些帮助函数用于清理或取消工作队列中的任务。想清理特定的任务项目并阻塞任务,直到操作完成为止, 可以调用 flush_work 来实现。 指定工作队列中的所有任务的清理能够通过调用 flush_workqueue 来完成。
这两种情形下,调用者阻塞直到操作完成为止。 为了清理内核全局工作队列,可调用 flush_scheduled_work。
int flush_work( struct work_struct *work );
int flush_workqueue( struct workqueue_struct *wq );
void flush_scheduled_work( void );

还没有在处理程序当中执行的任务可以被取消。 调用 cancel_work_sync 将会终止队列中的任务或者阻塞任务直到回调结束(如果处理程序已经在处理该任务)。 如果任务被延迟,可以调用 cancel_delayed_work_sync。
int cancel_work_sync( struct work_struct *work );
int cancel_delayed_work_sync( struct delayed_work *dwork );

可以通过调用 work_pending 或者 delayed_work_pending 来确定任务项目是否在进行中。
work_pending( work );
delayed_work_pending( work );

结束对工作队列的使用后,可调用下面的函数释放相关资源:
void destroy_workqueue(struct workqueue_struct *queue)

这就是工作队列 API 的核心。API 在 ./include/linux/workqueue.h 中定义,在 ./kernel/workqueue.c 中能够找到工作队列 API 的实现方法。

3.应用实例

工作队列的使用又分两种情况,一种是利用系统共享的全局工作队列来添加自己的工作,这种情况处理函数不能消耗太多时间,这样会影响共享队列中其他任务的处理;另外一种是创建自己的工作队列并添加工作。

一、利用系统共享的工作队列添加工作:
第一步:声明或编写一个工作处理函数
void my_func();

第二步:创建一个工作结构体变量,并将处理函数和参数的入口地址赋给这个工作结构体变量
DECLARE_WORK(my_work,my_func,&data); 编译时创建名为my_work的结构体变量并把函数入口地址和参数地址赋给它;
如果不想要在编译时就用DECLARE_WORK()创建并初始化工作结构体变量,也可以在程序运行时再用INIT_WORK()创建
struct work_struct my_work;
创建一个名为my_work的结构体变量,创建后才能使用INIT_WORK()
INIT_WORK(&my_work,my_func,&data); 初始化已经创建的my_work,其实就是往这个结构体变量中添加处理函数的入口地址和data的地址,通常在驱动的open函数中完成

第三步:将工作结构体变量添加入系统的共享工作队列
schedule_work(&my_work);
添加入队列的工作完成后会自动从队列中删除

schedule_delayed_work(&my_work,tick); 延时tick个滴答后再提交工作

二、创建自己的工作队列来添加工作
第一步:声明工作处理函数和一个指向工作队列的指针
void my_func();
struct workqueue_struct *p_queue;

第二步:创建自己的工作队列和工作结构体变量(通常在open函数中完成)
p_queue=create_workqueue("my_queue");
创建一个名为my_queue的工作队列并把工作队列的入口地址赋给声明的指针

struct work_struct my_work;
INIT_WORK(&my_work,my_func,&data); 创建一个工作结构体变量并初始化,和第一种情况的方法一样

第三步:将工作添加入自己创建的工作队列等待执行
queue_work(p_queue,&my_work);
作用与schedule_work()类似,不同的是将工作添加入p_queue指针指向的工作队列而不是系统共享的工作队列

第四步:删除自己的工作队列
destroy_workqueue(p_queue); 一般是在close函数中删除
0 0