LINUX内核定时器和中断

 本文全程参考正点院子文档,只供学习使用

1、内核定时器

硬件定时器提供时钟源,时钟源的频率可以设置。 设置好以后就周期性的产生定时中断,系统使用定时中断来计时。中断周期性产生的频率就是系统频率,也叫做节拍率(tick rate)(有的资料也叫系统频率)。系统节拍率是可以设置的,单位是 Hz,我们在编译 Linux 内核的时候可以通过图形化界面设置系统节拍率,设置好以后打开 Linux 内核源码根目录下的.config 文件,Linux 内核会使用 CONFIG_HZ 来设置自己的系统时钟。

//include/asm-generic/param.h 文件代码段
#undef HZ
#define HZ                CONFIG_HZ
#define USER_HZ            100
#define CLOCKS_PER_SEC    (USER_HZ)

Linux 内核使用全局变量 jiffies 来记录系统从启动以来的系统节拍数,系统启动的时候会将 jiffies 初始化为 0。前面说了 HZ 表示每秒的节拍数,jiffies 表示系统运行的 jiffies 节拍数,所以 jiffies/HZ 就是系统运行时间,单位为秒。不管是 32 位还是 64 位的 jiffies,都有溢出的风险,溢出以后会重新从 0 开始计数,相当于绕回。

Linux 内核提供了如下所示的几个 API 函数来处理绕回。

 为了方便开发,Linux 内核提供了几个 jiffies 和 ms、us、ns 之间的转换函数

Linux 内核定时器使用很简单,只需要提供超时时间(相当于定时值)和定时处理函数即可,当超时时间到了以后设置的定时处理函数就会执行,和我们使用硬件定时器的套路一样,只是使用内核定时器不需要做一大堆的寄存器初始化工作。在使用内核定时器的时候要注意一点,内核定时器并不是周期性运行的,超时以后就会自动关闭,因此如果想要实现周期性定时,那么就需要在定时处理函数中重新开启定时器。Linux 内核使用 timer_list 结构体表示内核定时器。

struct timer_list {
    struct list_head entry;
    unsigned long expires;
    struct tvec_base *base; /* 定时器超时时间,单位是节拍数 */
    void (*function)(unsigned long); /* 定时处理函数 */
    unsigned long data; /* 要传递给 function 函数的参数 */
    int slack;
};

 使用框架

struct timer_list timer; /* 定义定时器 */

/* 定时器回调函数 */
void function(unsigned long arg)
{

/* 定时器处理代码
* 如果需要定时器周期性运行的话就使用 mod_timer
* 函数重新设置超时值并且启动定时器。
*/
    mod_timer(&dev->timertest, jiffies + msecs_to_jiffies(2000));
}

/* 初始化函数 */
void init(void)
{

    init_timer(&timer);            /* 初始化定时器*/
    timer.function = function;    /* 设置定时处理函数*/
    timer.expires=jffies + msecs_to_jiffies(2000);/* 超时时间 2 秒 */
    timer.data = (unsigned long)&dev;    /* 将设备结构体作为参数 */
    add_timer(&timer);/* 启动定时器*/
}

/* 退出函数 */
void exit(void)
{
    del_timer(&timer); /* 删除定时器 */
    /* 或者使用 */
    del_timer_sync(&timer);
}

有时候我们需要在内核中实现短延时,尤其是在 Linux 驱动中。Linux 内核提供了毫秒、微
秒和纳秒延时函数。

2、LINUX中断

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在 Linux 内核中使用一个 int 变量表示中断号。

在 Linux 内核中要想使用某个中断是需要申请的,request_irq 函数用于申请中断, request_irq
函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq 函
数。request_irq 函数会激活(使能)中断,所以不需要我们手动去使能中断。

使用中断的时候需要通过 request_irq 函数申请,使用完成以后就要通过 free_irq 函数释放掉相应的中断。如果中断不是共享的,那么 free_irq 会删除中断处理函数并且禁止中断。

上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可
以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部
去执行,这样中断处理函数就会快进快出。

2.1软中断

 Linux 内核使用结构体 softirq_action 表示软中断,softirq_action结构体定义在文件 include/linux/interrupt.h 中,内容如下:

struct softirq_action
{
    void (*action)(struct softirq_action *);
};

在 kernel/softirq.c 文件中一共定义了 10 个软中断,如下所示:

static struct softirq_action softirq_vec[NR_SOFTIRQS];

 NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下:

enum
{
    HI_SOFTIRQ=0, /* 高优先级软中断 */
    TIMER_SOFTIRQ, /* 定时器软中断 */
    NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ, /* 网络数据接收软中断 */
    TASKLET_SOFTIRQ, /* tasklet 软中断 */
    SCHED_SOFTIRQ, /* 调度软中断 */
    HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
    RCU_SOFTIRQ, /* RCU 软中断 */
    BLOCK_IOPOLL_SOFTIRQ,
    NR_SOFTIRQS
};

 可以看出一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数。

void open_softirq(int nr, void (*action)(struct softirq_action *))

注册好软中断以后需要通过 raise_softirq 函数触发

void raise_softirq(unsigned int nr)

软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断,softirq_init 函数定义在 kernel/softirq.c 文件里面。

2.2tasklet

tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议大家使
用 tasklet。Linux 内核使用 tasklet_struct 结构体来表示 tasklet:

struct tasklet_struct
{
    struct tasklet_struct *next; /*下一个 tasklet*/
    unsigned long state; /* tasklet 状态*/
    atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
    void (*func)(unsigned long); /* tasklet 执行的函数*/
    /* 函数 func 的参数*/
    unsigned long data;
};

func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet

void tasklet_init(struct tasklet_struct *t,
                    void (*func)(unsigned long),
                    unsigned long data);

 在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运
行,tasklet_schedule 函数原型如下:

void tasklet_schedule(struct tasklet_struct *t)

 2.3工作队列

工作队列是另外一种下半部执行方,,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。

Linux 内核使用 work_struct 结构体表示一个工作,内容如下(省略掉条件编译):

struct work_struct {
    atomic_long_t data;
    struct list_head entry;
    work_func_t func;
    /* 工作队列处理函数 */
};

这些工作组织成工作队列,作队列使用 workqueue_struct 结构体表示

Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux 内核使用worker 结构体表示工作者线程。

每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,INIT_WORK 宏定义如下:

#define INIT_WORK(_work, _func)

和 tasklet 一样,工作也是需要调度才能运行的,工作的调度函数为 schedule_work,函数原
型如下所示

bool schedule_work(struct work_struct *work)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值