内核中时间的相关概念
节拍率:系统定时器以某种频率触发时钟中断,这个频率就称为节拍率(tick rate)。节拍率是通过静态预处理定义的,被定义为HZ。对于x86体系结构来说,它的HZ为100。
节拍:两次时钟中断的间隔就称为节拍(tick)。可以看到,节拍等于节拍率分之一。
jiffies:全局变量jiffies用来记录系统自启动以来产生的节拍总数。通过jiffies/HZ就可获取系统自启动以来的秒数。
内核定时器
内核定时器,也称为动态定时器,是管理内核时间的基础,它是一种用来推迟执行程序的工具。前面中断的文章中我们说到内核将在稍候的时间执行下部分工作,具体是何时来执行就需要内核定时器。我们在使用关于定时器的相关函数时,先来看看内核是如何描述定时器的:
10struct tvec_base; 11 12struct timer_list { 13 /* 14 * All fields that change during normal runtime grouped to the 15 * same cacheline 16 */ 17 struct list_head entry;//定时器链表入口 18 unsigned long expires;//以jiffies为单位的定时值 19 struct tvec_base *base;// 20 21 void (*function)(unsigned long);//定时器处理函数 22 unsigned long data;//传给定时器处理函数的参数,无符号长整型 23 24 int slack; 25 26#ifdef CONFIG_TIMER_STATS 27 void *start_site; 28 char start_comm[16]; 29 int start_pid; 30#endif 31#ifdef CONFIG_LOCKDEP 32 struct lockdep_map lockdep_map; 33#endif 34};
内核定时器使用起来也是相当简单的,基本的使用思路是:定义一个timer_list;初始化定时器,即对timer_list结构中的相关字段进行赋值;定义你自己的定时器处理函数;激活定时器;修改定时器,如果有必要的话;删除定时器。上述过程通过以下的API就可简单的完成。
秒字符驱动与内核定时器API
下面要分析的这个程序实现了一个秒字符设备驱动,程序整体的框架与之前我们分析的字符设备驱动相同。内核模块加载函数和卸载函数的功能和之前的字符设备驱动完全相同,因此接下来主要分析这个设备的操作函数。我们使用下面的结构来描述我们当前所讨论的秒字符设备。
struct second_dev { struct cdev cdev; atomic_t counter; struct timer_list s_timer; };
cdev即为内核中描述字符设备的结构体;counter用来记录秒数,它在测试程序中用来显示当前程序运行的时间。同时也是此设备驱动进行读写的数据;s_timer是为此设备所定义的定时器。每次定时一个HZ,也就是1秒,当定时到后,通过定时器处理函数对其进行修改,每次增加一个HZ。
接下来,我们一边分析驱动程序中的设备文件操作函数,一边学习内核定时器的API。
1.打开函数
秒字符设备在文件打开函数中实现对定时器的初始化以及赋值。
static int second_open(struct inode*inode,struct file*filp) { //initializing the timer init_timer(&second_devp->s_timer); second_devp->s_timer.function=&second_timer_handler; second_devp->s_timer.expires=jiffies+HZ; add_timer(&second_devp->s_timer);//add a timer atomic_set(&second_devp->counter,0);//clear the counter return 0; }
可以看到,内核定时器函数通过:init_timer(&t);来进行定时器的初始化,其中t为timer_list类型的指针。expires是用来设置定时器的定时值,单位是节拍数。我们可以看到初始定时值为一秒,即在当前时钟节拍数上加上一个节拍率。function则是上面提到的定时器处理函数。
另外,在open函数中,还通过add_timer(&t);来进行定时器的激活,也就是把当前这个定时器加入到内核定时器链表当中。由于我们在该设备对应的街头体中将counter设置为atomic_t(关于atomic.h头文件,本博客稍候会专门来分析,请持续关注~ ),因此我们使用atomic_set函数对其进行初始化,其值设置为0。
2.读函数
static ssize_t second_read(struct file *filp,char __user *buf,size_t count,loff_t *ppos) { int counter; counter=atomic_read(&second_devp->counter); if(put_user(counter,(int*)buf)) { return -EFAULT; } else { return sizeof(unsigned int); } }
这个读函数的意思是从内核中通过atomic_read函数读取counter,再通过put_user函数将其值返回至用户空间。
3.定时器处理函数
static void second_timer_handler(unsigned long arg) { mod_timer(&second_devp->s_timer,jiffies+HZ); atomic_inc(&second_devp->counter); printk("Current jiffies is %ld\n",jiffies); }
执行此处理函数时,说明先前定时器的时间已经到期。我们通过mod_timer(&t,jiffies+newdelay);来改变定时器的超时时间。这里我们每次增加一秒的时间。接着通过atomic_inc函数对counter加一,并且在内核中显示当前的节拍数。
4.文件释放函数
static int second_release(struct inode*inode,struct file*filp) { del_timer(&second_devp->s_timer); return 0; }
在文件释放函数中,通过del_timer(&t);就可以删除这个定时器。
测试
1.make编译并将此模块插入内核:sudo insmod secondcdev.ko
2.查看/proc/devices文件中是否有主设备号为250的设备
3.创建该设备文件节点,使用命令:sudo mknod /dev/secondcdev c 250 0
4.修改该设备文件权限:sudo chmod 777 /dev/secondcdev
5.编译测试文件:gcc -o test test.c
6.运行测试程序:./test;
关于测试程序代码如下:
#include < stdio.h > #include < sys/stat.h > #include < sys/types.h > #include < fcntl.h > #include < stdlib.h > int main() { int fd; int counter=0; int old_counter=0; //open the /dev/secondcdev device file if((fd=open("/dev/secondcdev",O_RDONLY))<0) { printf("Device open failed\n"); return -1; } while(1) { read(fd,&counter,sizeof(unsigned int));//read the current second number if(counter!=old_counter) { printf("seconds after open /dev/second:%d\n",counter); old_counter=counter; } } return 0;
在测试程序中,首先创建一个设备文件;然后循环读取counter的值,如果前后两次读取的counter值不一样,那么就输出;由于我们在定时器处理函数中是每隔一秒修改一次定时器,因此测试程序将每个一秒输出一次counter值。可以看到,我们所编写的秒字符设备驱动使得数据(即counter)在内核空间和用户空间之间进行交互。