码迷,mamicode.com
首页 > 系统相关 > 详细

Linux驱动时间、延迟以及延缓操作

时间:2020-08-31 13:10:37      阅读:67      评论:0      收藏:0      [点我收藏+]

标签:err   fun   刷新   参数   latency   ros   操作   param   处理   

本文主要讲述Linux驱动里面的时间操作,延迟(休眠),以及定时器。

1.度量时间差

1.1 HZ

内核通过定时器中断来跟踪时间流。时钟中断由系统定时器一周期性的间隔产生,这个间隔有内核根据HZ的值决定。HZ是一个与体系结构有关的常数,定义在<linux/param.h>或者该文件包含的某个子平台的相关文件中。HZ是用来定义每一秒中有多少次时钟中断。例如HZ为1000,代码每秒产生1000次时钟中断。我们不要轻易修改这个值,要相信内核工作者在此方面已经对不同硬件平台得出了最优秀的HZ经验值。

1.2jiffies

全局变量jiffies用于记录系统启动以来产生的滴答总数,一个滴答代表多长时间,与HZ大小有关,例如HZ = 200,则一个滴答对应5ms(1s = 1000ms,1000ms / 200 = 5ms)时间。比较两个节点的宏如下,包含头文件<linux/jiffies.h>

int time_after(unsigned long a, unsigned long b); b > a 时返回真
int time_before(unsigned long a, unsigned long b); b < a 时返回真
int time_after_eq(unsigned long a, unsigned long b); b >= a 时返回真
int time_before_eq(unsigned long a, unsigned long b); b <= a 时返回真
计算两个jiffies实例之间的差:diff = (long)t2 - (long)t1;
将差值转换为毫秒值:msec = diff * 1000 / HZ;

2.获取当前时间

内核提供了系统调用gettimeofday的底层函数do_gettimeofday,以及current_kernel_time。

#include <linux/time.h>

struct timeval {
    time_t tv_sec; /* seconds */
    suseconds_t tv_usec; /* microseconds */
};
void do_gettimeofday(struct timeval *tv);

struct timespec {
    time_t tv_sec; // seconds 
    long tv_nsec; // and nanoseconds 
};
struct timespec current_kernel_time(void);

3.延迟执行

3.1长延迟

通过jiffies来进行长延时,不是很精确的情况下可以使用。尤其是在多进程(内核进程过多)的时候不太精确,有时候会延迟很大。
(1)忙等待(尽量别用,占用cpu时间)
忙等待,精度不高,但是最简单,最容易实现,使用一个监视 jiffy 计数器的循环。

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

(2)让出处理器
不需要CPU时释放CPU 。 这可通过调用schedule函数实现(在 <linux/sched.h> 中声明),但是如果系统中只有这一个进程,也是占用CPU的,但是这种情况概率几乎为0,所以不用关心。

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

(3)超时
实现延迟的最好方法应该是让内核为我们完成相应的工作。
a.若驱动使用一个等待队列来等待某些其他事件,并想确保它在一个特定时间段内运行,可使用:

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

这些函数在给定队列上睡眠, 但是它们在超时(以 jiffies 表示)到后返回。如果超时,函数返回 0; 如果这个进程被其他事件唤醒,则返回以 jiffies 表示的剩余的延迟实现;返回值从不会是负值
b.为了实现进程在超时到期时被唤醒而又不等待特定事件(避免声明和使用一个多余的等待队列头),内核提供了 schedule_timeout 函数:

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

timeout 是要延时的 jiffies 数。除非这个函数在给定的 timeout 流失前返回,否则返回值是 0 。schedule_timeout 要求调用者首先设置当前的进程状态。为获得一个不可中断的延迟, 可使用 TASK_UNINTERRUPTIBLE 代替。如果你忘记改变当前进程的状态, 调用 schedule_time 如同调用 shcedule,建立一个不用的定时器。一个典型调用如下:

set_current_state(TASK_INTERRUPTIBLE); //必须设置进程状态
schedule_timeout (delay);
3.2短延时

当一个设备驱动需要处理硬件的延迟(latency潜伏期), 涉及到的延时通常最多几个毫秒,在这个情况下, 不应依靠时钟嘀哒,而是内核函数 ndelay, udelay和 mdelay ,他们分别延后执行指定的纳秒数, 微秒数或者毫秒数,定义在 <asm/delay.h>,原型如下:

#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs); //每个硬件体系架构都实现了这个函数,其他两个未必有实现
void mdelay(unsigned long msecs);

这 3 个延时函数是忙等待; 其他任务在时间流失时不能运行。每个体系都实现 udelay, 但是其他的函数可能未定义; 如果它们没有定义, <linux/delay.h> 提供一个缺省的基于 udelay 的版本。在所有的情况中, 获得的延时至少是要求的值, 但可能更多。udelay 的实现使用一个软件循环, 它基于在启动时计算的处理器速度和使用整数变量 loos_per_jiffy确定循环次数。
为避免在循环计算中整数溢出, 传递给udelay 和 ndelay的值有一个上限,如果你的模块无法加载和显示一个未解决的符号:__bad_udelay, 这意味着你调用 udleay时使用太大的参数。
作为一个通用的规则:若试图延时几千纳秒, 应使用 udelay 而不是 ndelay; 类似地, 毫秒规模的延时应当使用 mdelay 完成而不是一个更细粒度的函数。

有另一个方法获得毫秒(和更长)延时而不用涉及到忙等待的方法是使用以下函数(在<linux/delay.h> 中声明):

void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds)

若能够容忍比请求的更长的延时,应使用 schedule_timeout, msleep 或 ssleep。

4.内核定时器

内核定时器用来调度一个函数在将来一个特定的时间(基于时钟嘀哒)执行,从而可完成各类任务。内核定时器是一个数据结构, 它告诉内核在一个用户定义的时间点使用用户定义的参数执行一个用户定义的函数,函数位于 <linux/timer.h> 和 kernel/timer.c 。被调度运行的函数是异步运行的。实际上, 内核定时器通常被作为一个"软件中断"的结果而实现。当在进程上下文之外(即在中断上下文)中运行程序时, 必须遵守下列规则:
(1)不允许访问用户空间;
(2)current 指针在原子态没有意义;
(3)不能进行睡眠或者调度. 例如:调用 kmalloc(..., GFP_KERNEL) 是非法的,信号量也不能使用因为它们可能睡眠。

通过调用函数 in_interrupt()能够告知是否它在中断上下文中运行,无需参数并如果处理器当前在中断上下文运行就返回非零。
通过调用函数 in_atomic()能够告知调度是否被禁止,若调度被禁止返回非零; 调度被禁止包含硬件和软件中断上下文以及任何持有自旋锁的时候。
在后一种情况, current 可能是有效的,但是访问用户空间是被禁止的,因为它能导致调度发生. 当使用 in_interrupt()时,都应考虑是否真正该使用的是 in_atomic 。他们都在 <asm/hardirq.h> 中声明。

内核定时器的另一个重要特性是任务可以注册它本身在后面时间重新运行,因为每个 timer_list 结构都会在运行前从激活的定时器链表中去连接,因此能够立即链入其他的链表。一个重新注册它自己的定时器一直运行在同一个 CPU。任何被定时器函数访问的数据结构应当通过原子类型或自旋锁被保护,避免并发访问。

4.1内核定时器API

内核提供给驱动许多函数来声明、注册以及删除内核定时器:

#include <linux/timer.h>
struct timer_list {
    struct list_head entry;
    unsigned long expires;/*期望定时器运行的绝对 jiffies 值,不是一个 jiffies_64 值,因为定时器不被期望在将来很久到时*/
    void (*function)(unsigned long); /*期望调用的函数*/
    unsigned long data;/*传递给函数的参数,若需要在参数中传递多个数据项,可以将它们捆绑成单个数据结构并且将它的指针强制转换为 unsiged long 的指针传入。这种做法在所有支持的体系上都是安全的并且在内存管理中相当普遍*/
    struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
};

这个结构必须在使用前初始化,以保证所有的成员被正确建立(包括那些对调用者不透明的初始化):

void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);

在初始化后和调用 add_timer 前,可以改变 3 个公共成员:expires、function和data

void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);/*在到时前禁止一个已注册的定时器*/
int del_timer_sync(struct timer_list *timer); /*如同 del_timer ,但还保证当它返回时, 定时器函数不在任何 CPU 上运行,以避免在 SMP 系统上竞态, 并且在 单处理器内核中和 del_timer 相同。这个函数应当在大部分情况下优先考虑。 如果它被从非原子上下文调用, 这个函数可能睡眠,但是在其他情况下会忙等待。当持有锁时要小心调用 del_timer_sync ,如果这个定时器函数试图获得同一个锁, 系统会死锁。如果定时器函数重新注册自己, 调用者必须首先确保这个重新注册不会发生; 这通常通过设置一个" 关闭 "标志来实现, 这个标志被定时器函数检查*/
int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一个定时器的超时时间, 常用于超时定时器。也可在正常使用 add_timer时在不活动的定时器上调用mod_timer*/
int timer_pending(const struct timer_list * timer); /*通过调用timer_list结构中一个不可见的成员,返回定时器是否在被调度运行*/
4.2内核定时器的实现简介

一个内核定时器还远未完善,因为它受到 jitter 、硬件中断,还有其他定时器和其他异步任务的影响。虽然一个简单数字 I/O关联的定时器对简单任务是足够的,但不合适在工业环境中的生产系统,对于这样的任务,你将最可能需要实时内核扩展(RT-Linux)。因为Linux内核不是一个实时操作系统内核。

5.Tasklets

tasklet类似内核定时器,在中断时间运行且运行同一个 CPU 上, 并接收一个 unsigned long 参数。不同的是:无法要求在一个指定的时间执行函数,只能简单地要求它在以后的一个由内核选择的时间执行。它对于中断处理特别有用:硬件中断必须尽快处理, 但大部分的数据管理可以延后到以后安全的时间执行。 实际上, 一个 tasket, 就象一个内核定时器, 在一个"软中断"的上下文中执行(以原子模式)。软件中断是在使能硬件中断时执行异步任务的一个内核机制。

tasklet 以一个数据结构形式存在,使用前必须被初始化。初始化能够通过调用一个特定函数或者通过使用某些宏定义声明结构:

#include <linux/interrupt.h>
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data);

#define DECLARE_TASKLET(name, func, data) struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

void tasklet_disable(struct tasklet_struct *t);/*函数暂时禁止给定的 tasklet被 tasklet_schedule 调度,直到这个 tasklet 被再次被enable;若这个 tasklet 当前在运行, 这个函数忙等待直到这个tasklet退出*/
void tasklet_disable_nosync(struct tasklet_struct *t);/*和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU */
void tasklet_enable(struct tasklet_struct *t);/*使能一个之前被disable的 tasklet;若这个 tasklet 已经被调度, 它会很快运行。 tasklet_enable 和tasklet_disable必须匹配调用, 因为内核跟踪每个 tasklet 的"禁止次数"*/
void tasklet_schedule(struct tasklet_struct *t);/*调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应有的注意. 这个做法也允许一个 tasklet 重新调度它自己*/
void tasklet_hi_schedule(struct tasklet_struct *t);/*和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet 在其他软中断之前,只有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期*/
void tasklet_kill(struct tasklet_struct *t);/*确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet 正在运行, 这个函数等待直到它执行完毕。若 tasklet 重新调度它自己,则必须阻止在调用 tasklet_kill 前它重新调度它自己,如同使用 del_timer_sync*/

tasklet 的特点:
(1)一个 tasklet 能够被禁止并且之后被重新使能; 它不会执行,直到它被使能与被禁止相同的的次数;
(2)如同定时器, 一个 tasklet 可以注册它自己;
(3)一个 tasklet 能被调度来执行以正常的优先级或者高优先级;
(4) 如果系统不在重负载下,taslet 可能立刻运行, 但是从不会晚于下一个时钟嘀哒;
(5)一个 tasklet 可能和其他 tasklet 并发, 但是它自己是严格地串行的 ,且tasklet 从不同时运行在不同处理器上,通常在调度它的同一个 CPU 上运行。

5.工作队列

工作队列类似 taskets,允许内核代码请求在将来某个时间调用一个函数,不同在于:
(1)tasklet 在软件中断上下文中运行,所以 tasklet 代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行,有更多的灵活性,且能够休眠。
(2)tasklet 只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。
(3)内核代码可以请求工作队列函数被延后一个给定的时间间隔。
(4)tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。

工作队列有 struct workqueue_struct 类型,在 <linux/workqueue.h> 中定义。一个工作队列必须明确的在使用前创建,宏为:

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

每个工作队列有一个或多个专用的进程("内核线程"), 这些进程运行提交给这个队列的函数。 若使用 create_workqueue, 就得到一个工作队列它在系统的每个处理器上有一个专用的线程。在很多情况下,过多线程对系统性能有影响,如果单个线程就足够则使用 create_singlethread_workqueue 来创建工作队列。

有 2 个函数来提交工作给一个工作队列:

int queue_work(struct workqueue_struct *queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue, struct delayed_work *work, unsigned long delay);

每个都添加work到给定的workqueue。如果使用 queue_delay_work, 则实际的工作至少要经过指定的 jiffies 才会被执行。 这些函数若返回 1 则工作被成功加入到队列; 若为0,则意味着这个 work 已经在队列中等待,不能再次加入

在将来的某个时间, 这个工作函数将被传入给定的 data 值来调用。这个函数将在工作线程的上下文运行, 因此它可以睡眠 (你应当知道这个睡眠可能影响提交给同一个工作队列的其他任务) 工作函数不能访问用户空间,因为它在一个内核线程中运行, 完全没有对应的用户空间来访问。
取消一个挂起的工作队列入口项可以调用:

int cancel_delayed_work(struct delayed_work *work);
void cancel_work_sync(struct work_struct *work)

如果这个入口在它开始执行前被取消,则返回非零。内核保证给定入口的执行不会在调用 cancel_delay_work 后被初始化. 如果 cancel_delay_work 返回 0, 但是, 这个入口可能已经运行在一个不同的处理器, 并且可能仍然在调用 cancel_delayed_work 后在运行. 要绝对确保工作函数没有在 cancel_delayed_work 返回 0 后在任何地方运行, 你必须跟随这个调用来调用:

void flush_workqueue(struct workqueue_struct *queue);

而cancel_work_sync会取消相应的work,但是如果这个work已经在运行那么cancel_work_sync会阻塞,直到work完成并取消相应的work。
当用完一个工作队列,可以去掉它,使用:

void destroy_workqueue(struct workqueue_struct *queue);

6.共享队列

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

使用顺序:
(1) 建立 work_struct 或 delayed_work

static struct work_struct jiq_work;
static struct delayed_work jiq_work_delay;

INIT_WORK(&jiq_work, jiq_print_wq);
INIT_DELAYED_WORK(&jiq_work_delay, jiq_print_wq);

(2)提交工作

int schedule_work(&jiq_work);/*对于work_struct结构*/
int schedule_delayed_work(&jiq_work_delay, delay);/*对于delayed_work结构*/

返回值的定义和 queue_work 一样

若需取消一个已提交给工作队列入口项, 可以使用 cancel_delayed_work和cancel_work_sync, 但刷新共享队列需要一个特殊的函数:

void flush_scheduled_work(void);

因为不知道谁可能使用这个队列,因此不可能知道 flush_schduled_work 返回需要多长时间。

参考:
https://www.cnblogs.com/wangchenxicool/articles/2235079.html
https://blog.csdn.net/qq_33242956/article/details/98057515

Linux驱动时间、延迟以及延缓操作

标签:err   fun   刷新   参数   latency   ros   操作   param   处理   

原文地址:https://blog.51cto.com/14207158/2523477

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!