美文网首页转载部分Audio Architecture
Linux内核定时器和工作队列的总结和实例

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

作者: c枫_撸码的日子 | 来源:发表于2018-07-19 17:15 被阅读0次

    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!
    荆轲刺秦王

    相关文章

      网友评论

        本文标题:Linux内核定时器和工作队列的总结和实例

        本文链接:https://www.haomeiwen.com/subject/scmopftx.html