Linux内核定时器和工作队列的总结和实例

1、综述

基于硬件工程师的提出的一个测试需求:每隔5秒钟拉高PA的使能脚,间隔5秒钟再拉低PA使能脚(这里的PA 指的是power amplifier->即功率放大器的意思)
换句话说,就是播放音乐时,每隔5秒钟有声音,5秒钟没声音。

学习本文你会掌握以下知识点:
1.linux中定时器的概念和使用
2.linux工作队列的概念和使用
3.如何使能PA脚
4.定时器和工作队列在linux中的实际运用
5.需求实现

2、基本知识

一、linux中定时器的概念和使用

Linux内核中,如果想要周期性的做一件事情,或者在某个特定的时间点去做一件事,比如每过5秒让闪光灯亮一下等,应该怎么办呢?
Linux给我们提供了timer_list (内核定时器)来实现相应的功能。
timer_list结构体:(路径: kernel-3.18/include/linux/timer.h)

timer.h

包含的主要成员:
a. data:传递到超时处理函数的参数,主要在多个定时器同时使用时,区别是哪个timer超时。
b. expires:定时器超时的时间,以linux的jiffies来衡量。
c. void (*function)(unsigned long):定时器超时处理函数。

1.相关API函数

a. init_timer(struct timer_list*):定时器初始化函数;

b. add_timer(struct timer_list*):往系统添加定时器;

c. mod_timer(struct timer_list *, unsigned long jiffier_timerout):
修改定时器的超时时间为jiffies_timerout;

(Linux系统中的jiffies类似于Windows里面的TickCount,
它是定义在内核里面的一个全局变量,
只是它的单位并不是秒或是毫秒。
通常是250个jiffies为一秒,在内核里面可以直接使用宏定义:HZ
)

d. timer_pending(struct timer_list *):定时器状态查询,
如果在系统的定时器列表中则返回1,否则返回0;

e. del_timer(struct timer_list*):删除定时器。

2.相关API函数源码解析

a)init_timer函数

a)init_timer函数(路径: kernel-3.18/include/linux/timer.h)
#define init_timer(timer) \
__init_timer((timer), 0)
#define __init_timer(_timer, _flags)\
init_timer_key((_timer), (_flags), NULL, NULL)

可以看出 实际上是调用init_timer_key()函数去初始化
(路径: kernel-3.18/kernel/time/timer.c)
void init_timer_key(struct timer_list *timer, unsigned int flags,  
            const char *name, struct lock_class_key *key)
{
    debug_init(timer);
    do_init_timer(timer, flags, name, key);
}
  • init_timer_key -初始化一个计时器
  • @timer:要初始化的计时器。
  • @flags:定时器的旗帜
  • @name:计时器名称
  • @key:用于跟踪计时器 同步锁的依赖关系

*注意: 必须先调用init_timer_key()进行初始化,然后才能调用其他跟定时器相关的方法,例如add_timer,mod_timer等

b)add_timer函数

b)add_timer函数(路径: kernel-3.18/kernel/time/timer.c)
void add_timer(struct timer_list *timer)
{
    BUG_ON(timer_pending(timer));//打印相关log
    mod_timer(timer, timer->expires);//调用mod_timer设置时间
}
  • add_timer -启动一个计时器,或者说激活一个定时器。
  • @timer:要添加的定时器

分析:add_timer用于往系统中添加一个定时器,参数timer为要添加的定时器(timer_list)
,当系统时间经过timer->expires这么多时间,就会去调用timer->function回调方法去完成相应的任务。

注意:必须先初始化timer->expires,timer->function,timer->data这三个成员变量,才能调用add_timer()这个方法

c)mod_timer函数

c)mod_timer函数(路径: kernel-3.18/kernel/time/timer.c)
int mod_timer(struct timer_list *timer, unsigned long expires)
{
    expires = apply_slack(timer, expires);
    if (timer_pending(timer) && timer->expires == expires)
        return 1;
    return __mod_timer(timer, expires, false, TIMER_NOT_PINNED);
}

  • mod_timer -修改一个timer(定时器)的超时时间
  • @timer:要修改的计时器。
  • @expires:新的超时,单位jiffies

分析:mod_timer()是更新活动计时器过期字段(即expires参数)的一种更有效的方法(如果计时器没有被激活,mod_timer会先激活计时器,然后在重新设定超时时间)
实际上 调用mod_timer(timer, expires)相当于
del_timer(timer); timer->expires = expires; add_timer(timer);
注意:如果有多个未序列化的并发用户使用相同的计时器,则mod_timer()是修改超时的唯一安全方法,因为add_timer()不能修改已经运行的计时器。
该函数返回是否已经修改了一个待定定时器
如果调用mod_timer去修改一个定时器,
如果当前定时器处于非激活状态,则该函数返回0,
如果当前定时器处于激活状态,则该函数返回1
Ps:调用了add_timer(),就表示该定时器处于激活状态

d)add_timer函数

d)add_timer函数 (路径: kernel-3.18/include/linux/timer.h)
static inline int timer_pending(const struct timer_list * timer)   
{
    return timer->entry.next != NULL;
}
  • timer_pending——是否有一个计时器正在等待?
  • @timer:给定的定时器

timer_pending会告诉给定的计时器是否正在等待
返回值:如果计时器挂起,则为1;如果不是,则为0
如果timer->entry.next为NULL,表示计时器没有挂起,返回0
如果timer->entry.next不等于NULL,表示计时器挂起,返回1

e)del_timer函数

e)del_timer函数 (路径: kernel-3.18/kernel/time/timer.c)
int del_timer(struct timer_list *timer)
{
    struct tvec_base *base;
    unsigned long flags;
    int ret = 0;
    debug_assert_init(timer);
    timer_stats_timer_clear_start_info(timer);
    if (timer_pending(timer)) {
        base = lock_timer_base(timer, &flags);
        ret = detach_if_pending(timer, base, true);
        spin_unlock_irqrestore(&base->lock, flags);
    }
    return ret;
}
  • del_timer -删除(停用)计时器。
    *@timer:被停用的计时器

分析:del_timer()禁用计时器——这对激活的和非激活的计时器都有效。

函数返回是否已经禁用了一个待定定时器。
(即。一个非激活计时器的del_timer()返回0,激活计时器返回1)

3.使用定时器的一般流程为:

  • 1)定义timer_list,、初始化timer_list、编写function;
  • 2)为timer的expires、data、function赋值;
  • 3)调用add_timer将timer往系统添加定时器;(或者直接调用mod_timer方法)
  • 4)在定时器到期时,function被运行;
  • 5)在程序中涉及timer控制的地方适当地调用del_timer、mod_timer删除timer或改动timer的expires。
实例:
static struct timer_list  timer;//定义计时器
/*回调函数*/
static void miki_test_callback(unsigned long a)
{
    //这里添加相应的逻辑,比如每隔5秒让闪关灯亮一次等
}

//初始化相关参数
static void miki_init(void)
{
    init_timer(&timer);//先初始化timer
    test_timer.expires = jiffies + (20 * HZ);//设置超时 20*HZ 表示20秒
    test_timer.function = &miki_test_callback;//设置回调函数
    test_timer.data = ((unsigned long)0);//设置data参数,一般传入0即可
    add_timer(&test_timer);//把定时器添加到系统中,激活定时器
}
/*主函数*/
void mian()
{
    miki_init();
    //如果需要修改定时器的时间,则调用mod_timer
mod_timer(&test_timer, jiffies + (10 * HZ));
}

二、linux中工作队列(workqueue)的概念和使用

1.什么是workqueue(工作队列)
Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue是内核中实现简单而有效的机制,他显然简化了内核daemon的创建,方便了用户的编程.
工作队列(workqueue)是另外一种将工作推后执行的形式.工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。最重要的就是工作队列允许被重新调度甚至是睡眠

2.相关数据结构
Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内

我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct

路径: kernel-3.18/include/linux/workqueue.h

workqueue.h

这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct:

路径: kernel-3.18/include/linux/workqueue.h


workqueue_struct

3.相关API(接口)函数:

路径:kernel-3.18/kernel/workqueue.c
路径:kernel-3.18/include/linux/workqueue.h

1) create_workqueue(name)
用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程。
输入参数:@name:workqueue的名称

2) create_singlethread_workqueue(name)
用于创建workqueue,只创建一个内核线程。输入参数:
输入参数:@name:workqueue名称
 
3)destroy_workqueue(struct workqueue_struct *wq)
释放workqueue队列。输入参数:
输入参数:@ workqueue_struct:需要释放的workqueue队列指针

4) schedule_work(struct work_struct *work);
调度执行一个具体的任务
输入参数:
@ work_struct:具体任务对象指针

5) schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间,
输入参数:
@work_struct:具体任务对象指针
@delay:延迟时间

6)queue_work(struct workqueue_struct *wq, struct work_struct *work)
调度执行一个指定workqueue中的任务。
输入参数:
@ workqueue_struct:指定的workqueue指针
@work_struct:具体任务对象指针

7)queue_delayed_work(struct workqueue_struct *wq,
                           struct delayed_work *dwork, unsigned long delay)
延迟调度执行一个指定workqueue中的任务,
功能与queue_work类似,输入参数多了一个delay。 

4使用工作队列的一般流程为:

  • 1)定声明工作处理函数function、指向工作队列的指针和工作结构体变量work_struct;
  • 2)调用create_singlethread_workqueue或者create_workqueue创建自己的工作队列;
  • 3)将工作添加入自己创建的工作队列等待执行->queue_work
  • 4)删除自己的工作队列;
实例:
static struct workqueue_struct *miki_test_wq;//声明工作队列
static struct work_struct miki_test_work;;//声明工作
/*工作处理函数*/
static void miki_test_work_callback ()
{
    //这里添加相应的逻辑,比如每隔5秒让闪关灯亮一次等
}

/*主函数*/
void mian()
{
    //创建自己的工作队列
miki_test_wq = create_singlethread_workqueue("miki_test");
//初始化工作,实际上是让工作work绑定工作处理函数miki_test_work_callback
INIT_WORK(&miki_test_work, miki_test_work_callback);
    //调度执行一个指定(miki_test_wq)中的任务miki_test_work
queue_work(miki_test_wq, &miki_test_work); 
}

三.如何使能PA脚

在mt_soc_codec_mt63xx.c中Ext_Speaker_Amp_Change函数中进行外部PA的gpio控制就可以。

路径:
kernel-3.18/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c

因此,
使能PA : Ext_Speaker_Amp_Change(true)
关闭PA : Ext_Speaker_Amp_Change(false)
提示:关于mt_soc_codec_63xx.c文件的路径
【7.0】
kernel-3.18/sound/soc/mediatek/mt_soc_audio_v3/mt_soc_codec_63xx.c

7.0 out目录

【8.0】
kernel-3.18/sound/soc/mediatek/mt6735/mt_soc_codec_63xx.c
可以通过编译生成的out目录来查看系统编译了哪些文件

8.0 out 目录

四.定时器和工作队列在linux中的实际运用

kernel-3.18/drivers/misc/mediatek/accdet/mt6580/accdet.c
在耳机驱动中,可以看到定时器和工作队列的使用



分析:设置了一个定时器micbias_timer,设置时间为6秒,最后调用mod_timer去激活定时器,6秒后会自动调用disable_micbias函数
创建了一个名称为accdet工作队列,
为accdet_work设置回调方法accdet_work_callback
我们知道工作是要调用queue_work()这个方法把工作提交到工作队列中,才会回调
accdet_work_callback方法去完成相应的任务,那么在哪里调用了该方法呢?

这就要去看disable_micbias函数了

小结:定时器micbias_timer每隔6秒钟就会去调用queue_work方法,告诉系统,你要去调用accdet_work_callback方法去完成相应的任务

accdet_work_callback函数->主要用于检测并且设置耳机的状态

五、 需求实现

依葫芦画瓢,我们可以模仿在耳机驱动中,Linux的使用定时器和工作队列的方式去完成这个需求。

步骤一:声明变量
static struct timer_list  test_timer;//定义定时器
static struct workqueue_struct *miki_test_wq;//定义工作
static struct work_struct miki_test_work;//定义工作队列
步骤二:编写回调方法
//工作队列的回调方法
static void miki_test_work_callback(struct work_struct *work)
{
   Ext_Speaker_Amp_Change(true);//打开PA
   msleep(5 * 1000);//休眠5秒
   Ext_Speaker_Amp_Change(false);//关闭PA
   mod_timer(&test_timer, jiffies + (5* HZ));//重新激活定时器
}
//定时器的回调方法
static void miki_test_callback(unsigned long a)
{
    queue_work(miki_test_wq, &miki_test_work);
}
步骤三:初始化定时器和工作队列
static void miki_init (void)
{
    miki_test_wq = create_singlethread_workqueue("miki_test");
    INIT_WORK(&miki_test_work, miki_test_work_callback);

    init_timer(&test_timer);
    test_timer.expires = jiffies + (5 * HZ);//时间设置为5秒
    test_timer.function = &miki_test_callback;//设置定时器回调方法
    test_timer.data = ((unsigned long)0);
    mod_timer(&test_timer, test_timer.expires);//激活定时器
}
步骤四:在模块入口函数中调用miki_init()方法
static int __init mtk_mt6331_codec_init(void)->模块入口函数
{
    //省略部分源码
    miki_init();
    return platform_driver_register(&mtk_codec_6331_driver);
}

到此,本文就结束了,希望有所收获,lol开启,嘻嘻(#^.^#)。

Stay hungry,Stay foolish!
荆轲刺秦王

推荐阅读更多精彩内容