时间、延迟及延缓操作

13 篇文章 0 订阅
8 篇文章 0 订阅

1.度量时间差

内核通过定时器中断来跟踪时间流,时钟中断有系统定时硬件以周期性的间隔产生,这个间隔有内核根据HZ的值设定,HZ是一个与体系结构有关的常量,定义在<asm-generic/param.h>中。常用值为100或1000.每发生一次时钟中断,内核内部计数器的值增1,该值在系统引导时被初始化为0。该计数器是一个64位的变量(即使是32位架构也是这样),称为jiffies_64。但是驱动开发人员通常访问jiffies变量,它是unsigned long变量,通常是jiffies_64或者jiffies_64的低32位值。除了jiffy机制,某些cpu平台还有一个软件可读取的高分辨路计数器,以适用于特殊需求。
1.1 使用jiffies计数器
<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 */

//当条件为真,返回真;
int time_after(unsigned long a, unsigned long b);//a>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
//计算时间差
diff = (long)t2 - (long)t1;
msec = diff * 1000 / HZ; 
//timespec、timeval与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);
//在32位平台上原子的获取jiffies_64的值
#include <linux/jiffies.h> 
u64 get_jiffies_64(void);
1.2 处理器特定的计数器
1.x86平台常用的TSC计数器寄存器:
它是一个 64-位 寄存器计数 CPU 的时钟周期; 它可从内核和用户空间读取.
<asm/msr.h>
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
第一个宏自动读取 64位 值到 232位 变量;下一个读取寄存器的低半部到一个32位变量;最后一个读64位值到一个long long变量. 所有这些宏存储数值到它们的参数中.
2.体系无关函数get_cycles:
#include <linux/timex.h>
cycles_t get_cycles(void); 
在无时钟计数器的平台,返回03.通过内嵌汇编实现:
#define rdtscl(dest) \
__asm__ __volatile__("mfc0 %0,$9; nop" : "=r" (dest))

2.获取当前时间

内核通常通过查看 jifies 的值来获取当前时间,该数值表示最近一次系统启动到当前的时间间隔,他和设备驱动程序无关,因为他的生命周期只限于系统的运行周期。
驱动程序一般不需要知道墙钟时间(日常时间,年月日的表达),只有cron、syslogd类似程序才需要知道。
常用的时间相关函数:

#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); //将墙钟时间转化为jiffies值
void do_gettimeofday(struct timeval *tv);//返回当前时间,但很难原子获取timeval的两个变量
struct timespec current_kernel_time(void);//原子的获取当前时间

3.延迟执行

3.1 长延迟
1.忙等待
如果想执行若干个时钟嘀嗒的延迟,或者对延迟的精度要求不高,可通过一下方法实现:
<linux/jiffies.h>
unsigned long j, j1;
j = jiffies;
j1=j+HZ;
while (time_before(jiffies, j1))
    cpu_relax();
注意: cpu_relax函数在许多系统上不会做任何事情,而在对称多线程系统上,它可能让出处理器。但是通常情况下我们都应避免这种方式,因为这会严重降低系统性能。在非抢占系统上,在延迟过程中会锁住处理器;如果在进入忙等待前禁止了中断,jiffies值变得不到更新,系统就会死掉!
2.让出处理器
上面忙等待的方法会增加系统的负担,但延迟的实现有多种方法。延迟的另一种实现方式如下:
<linux/jiffies.h>
unsigned long j, j1;
j = jiffies;
j1=j+HZ;
while (time_before(jiffies, j1))
    schedule();
注意:这种方式释放了cpu而不做任何事情,但他仍在运行队列中。若系统中只有当前进程运行,这当前进程会立即执行,因此,系统一直处于运行中,空闲任务不会运行,cpu并没有空闲下来。从cpu的角度看,这种方式和忙等待类似。
3.超时
上面两种方式都会给cpu带来负担,目前最好的方式是让内核完成我们的任务。当前有两种构造基于jiffies超时的途径,这主要取决于驱动程序是否在等待其他事件。
第一种途径,驱动程序使用等待队列等待其他一些事件:
#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);
参数:
q:队列头
condition:条件
timeout:返回要等待的事件的jiffies值
返回:
若是被其他时间唤醒,返回剩余的jiffies值;若是超时到期,返回0。
例:
#include <linux/wait.h>
wait_queue_head_t wait; 
init_waitqueue_head (&wait); 
wait_event_interruptible_timeout(wait, 0, delay); 
第二种途径,驱动程序无需等待其他事件:
#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);
参数:
timeout:延迟时间的jiffies值;
返回:
正常返回0;
注意:使用schedule_timeout前需要设置当前进程的状态
例:
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);
3.2 短延迟
如果驱动程序需要处理硬件的延迟,这种延迟通常最多涉及到几十个毫秒。这是通常用ndelay、udelay,以及 mdelay来完成延迟任务。
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
这三个方法都是忙等待函数,当然不涉及忙等待的方法也有相应的实现:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);

4.内核定时器

内核定时器使用场景:在将来的某个时间点调度执行某个函数,同时在该时间点到达之前不会阻塞当前进程。
内核定时器常常作为软件中断的结果而运行的,被调度运行的函数几乎不会再注册这些函数的进程正在执行是运行。在这种原子性的上下文中运行时,定时器函数必须以原子的方式运行(请参照自旋锁和原子上下文相关内容)。定时器函数通常需要遵循以下规则:
1.不允许访问用户空间,因为没有进程上下文,无法将任何特定进程和用户空间关联。
2.current指针在原子模式下无任何意义,因为相关的代码和被中断的进程没有任何关联。
3.不能执行休眠和调度,原子代码不能调用 schedule 或者某种 wait_event, 也不能调用任何其他可能睡眠的函数.。例如, 调用 kmalloc(…, GFP_KERNEL) 是违犯规则的. 旗标也必须不能使用因为它们可能睡眠。
in_interrupt()和in_atomic():
包含在< asm/hardirq.h>中;都是用于内核代码判断自己是否处于中断上下文的函数,但是后者可以用在调度不被允许的情况下,包括硬件中断和软件中断上下文以及拥有自旋锁的任何时间点。
注意:任何定时器函数访问的数据结构都应该针对并发访问进行保护。

4.1 定时器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);
//和del_timer类似,但是del_timer_sync确保返回时没有任何CPU在运行定时器函数
int del_timer_sync(struct timer_list *timer);
//返回定时器是否正在被调度运行
int timer_pending(const struct timer_list * timer);
4.2 内核定时器的实现
内核定时器的实现要求:
1.定时器的管理必须做到轻量级;
2.其设计必须在活动定时器大量增加时具有良好的伸缩性;
3.大部分定时器会在最多几秒或者几分钟内到期,很少存在长期延迟的定时器;
4.定时器应该在注册他的当前cpu上运行。

目前的解决方案是利用per-CPU数据结构。
无论何时内核代码注册一个定时器( 通过 add_timer 或者 mod_timer), 最终由 internal_add_timer ( 在kernel/timer.c)执行, 它依次添加新定时器到一个双向定时器链表在一个关联到当前 CPU 的"级联表" 中。
级联表的工作工作方式: 如果定时器在下一个 0255 jiffies 内到期, 则它会被添加到专供短时定时器 256 列表中的一个(这取决于expires 成员的低8位)。如果它在将来更久时间到期(但是在 16,384 jiffies 之前), 它会被添加到基于 expires 成员的 9 - 14 位的 64 个列表中一个. 对于更长的定时器, 同样的技巧用在 15 - 20 位, 21 - 26 位, 和 27 - 31 位. 带有一个指向将来还长时间的 expires 成员的定时器(一些只可能发生在 64-位 平台上的事情)被使用一个延时值 0xffffffff 进行哈希处理, 而在过去时间内到期的定时器将在下一个时钟嘀哒被调度(如果在高负荷情况,有可能注册一个已经到期的定时器,尤其在运行抢占式内核时)。
当触发 __run_timers, 它为当前定时器嘀哒执行所有挂起的定时器。如果当前jiffies是256的倍数, 这个函数还会将下一级定时器链表重新散列到256个短期列表中, 同时还可能根据上面jiffies的位划分对其他级别的定时器做级联处理。

5.tasklet

另一个有关于定时问题的内核设施是 tasklet 机制. 它大部分用在中断管理。

5.1 tasklet 与内核定时器的异同

1.它们一直在中断时间运行;
2.它们一直运行在调度它们的同一个 CPU 上;
3.它们接收一个 unsigned long 参数;
4.它们都在“软件中断”上下文原子性执行;
5.tasklet无法请求在一个指定的时间执行函数,调度tasklet,表示我们只希望内核在其后的某个时间执行指定的函数(因此适用于中断管理)。

5.2 tasklet 的特色

1.tasklet 能够被禁止之后能被重新使能; 它不会执行直到它被使能与被禁止相同的次数;
2.tasklet 可以注册它自己;
3.tasklet 能以正常的优先级或者高优先级被调度执行,后一组一直是首先执行;
4.如果系统负荷不重,taslet 能立刻运行, 但是不会晚于下一个时钟嘀哒;
5.tasklet 可能和其他 tasklet 并发, 但是对它自己是严格地串行的,因为它始终会在地调度自己的同一cpu上运行,从不运行在不同的处理器上。

5.3 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
void tasklet_disable(struct tasklet_struct *t);
//禁用指定的tasklet,但不会等待任何正在运行的tasklet退出
void tasklet_disable_nosync(struct tasklet_struct *t);
//启用之前被禁用的tasklet
void tasklet_enable(struct tasklet_struct *t);
//调度指定的tasklet
void tasklet_schedule(struct tasklet_struct *t);
//禁用指定的tasklet以高优先级执行
void tasklet_hi_schedule(struct tasklet_struct *t);
//禁止指定的tasklet的再次调度,通用用于设备关闭和模块移除
void tasklet_kill(struct tasklet_struct *t);

6.工作队列

工作队列类似于tasklet,他们都允许内核代码请求某个函数在将来的时间被调用,当时两者之间还是有一些非常重要的区别:
1.tasklet 在软件中断上下文中运行的结果是所有的 tasklet 代码必须是原子的;相反, 工作队列函数在一个特殊内核进程上下文运行,所以它们有更多的灵活性。注意, 工作队列函数能够睡眠。
2.tasklet 常常在它们最初被提交的处理器上运行。但这只是工作队列默认的工作方式。
3.内核代码可以请求工作队列函数被延后指定的时间间隔后执行。
4.tasklet会在很短的会时间内很快的执行, 并且在原子态, 而工作队列函数可能有更长的延迟而且不需要是原子的。

6.1 工作队列的使用
<linux/workqueue.h>
//创建工作队列
struct workqueue_struct *create_workqueue(const char *name);
struct workqueue_struct *create_singlethread_workqueue(const char *name);
//编译时向工作队列递交任务
DECLARE_WORK(name, void (*function)(void *), void *data);
//运行时构造work_struct 结构
//INIT_WORK完成所有初始化工作
INIT_WORK(struct work_struct *work, void (*function)(void *), void *data); 
//同INIT_WORK,但不会初始化用来将work_struct结构连接到工作队列的指针
PREPARE_WORK(struct work_struct *work, void (*function)(void *), void *data); 
//将工作递交到工作队列
int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, unsigned long delay);
//取消某个挂起的工作队列入口项
int cancel_delayed_work(struct work_struct *work); 
//禁止运行被调用之前所有被递交的工作
void flush_workqueue(struct workqueue_struct *queue); 
//释放工作队列
void destroy_workqueue(struct workqueue_struct *queue); 
6.2 共享队列

设备驱动, 在许多情况下, 不需要有它自己的工作队列。如果你只偶尔需要提交任务给队列, 简单地使用内核提供的共享的默认工作队列可能更有效。如果你需要使用这个队列,你必须明白你将和别的在共享它,这意味着你不应当长时间独占队列(无长睡眠), 并且可能要更长时间我们的任务才能获得处理器。
1.共享队列使用:

//递交工作任务
int schedule_work(struct work_struct *work); 
//以延迟模式递交工作任务
int schedule_delayed_work(struct work_struct *work, unsigned long delay); 
//刷新共享队列
void flush_scheduled_work(void); 
例:
static struct work_struct jiq_work;
/* this line is in jiq_init() */
INIT_WORK(&jiq_work, jiq_print_wq, &jiq_data);
prepare_to_wait(&jiq_wait, &wait, TASK_INTERRUPTIBLE);
schedule_work(&jiq_work);
schedule();
finish_wait(&jiq_wait, &wait);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值