本文全程参考正点院子文档,只供学习使用
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)