linux内核学习13:时钟、定时器、延时/定时机制

1、时钟和定时电路

Linux内核必须完成两种主要的定时测量 ,我们可以对此加以区分:

  • 保存当前的时间和日期 ,以便能通过time()、ftime()和gettimeofday()系统调用把它们返回给用户程序,也可以由内核本身把当前时间作为文件和网络包的时间戳。
  • 维持定时器 ,这种机制能够告诉内核或用户程序某一时间间隔已经过去

定时测量是由基于固定频率振荡器和计数器的几个硬件电路完成的。

[1]实时时钟(RTC)

所有的PC都包含一个叫实时时钟 (Renl Time Clock RTC)的时钟,它是独立于CPU和所有其他芯片的

即使当PC被切断电源,RTC还继续工作,因为它靠一个小电池或蓄电池供电。COMS RAM和RTC被集成在一个芯片上。

Linux只用RTC来获取时间和日期, 不过通过对/dev/rtc设备文件进行操作,也允许进程对RTC编程。内核通过0x70和0x71I/O端口访问RTC。系统管理员通过执行Unix系统时钟程序可以设置时钟。

[2]时间戳计数器(TSC)

所有的80x86微处理器都包含一条CLK输入引线,它接收外部振荡器的时钟信号。

与可编程间隔定时器传递来的时间测量相比,Linux利用这个寄存器可以获得更精确的时间测量

[3]可编程间隔定时器(PIT)

可编程间隔定时器(Programmable Interval Timer PIT).PIT的作用类似于微波炉的闹钟,即让用户意识到烹调的时间间隔已经过了。所不同的是,这个设备不是通过振铃,而是发出一个特殊的中断,叫做时钟中断(timerinterupt)来通知内核又一个时间间隔过去了。与闹钟的另一个区别是,PIT永远以内核确定的固定频率不停地发出中断

Linux给PC的第一个PIT进行编程,是它以(大约)1000Hz的频率向IRQ0发出时钟中断,即每1ms产生一次时钟中断。这个时间间隔叫做一个节拍(tick),它的长度以纳秒为单位存放在tick_nsec变量中。

短的节拍产生较高分辨率的定时器,当这种定时器执行同步I/O多路复用(poll()和select()系统调用)时,有助于多媒体的平滑播放和较快的响应时间

时钟中断的频率取决于硬件体系结构。

[4]CPU本地定时器

CPU本地定时器是一种能够产生单步中断或周期性中断的设备,它类似于可编程间隔定时器

[5]高精度事件定时器(HPET)

高精度事件定时器是由Intel和Microsoft联合开发的一种新型定时器芯片。尽管这种定时器在终端用户机器上还并不普遍,但Linux2.6已经能够支持它们

[6]ACPI电源管理定时器

ACPI电源管理定时器(或称ACPI PMT)是另一种时钟设备,包含在几乎所有基于ACPI的主板上。它的时钟信号拥有大约为3.58MHz的固定频率。该设备实际上是一个简单的计数器, 它在每个时钟节拍到来时增加一次。为了读取计数器的当前值,内核需要访问某个I/O端口,该I/O端口的地址由BIOS在初始化阶段确定

如果操作系统或者BIOS可以通过动态降低CPU的工作频率或者工作电压来节省电池的电能,那么ACPI电源管理定时器就比TSC更优越。当发生ACPI PMT的频率不会改变。而另一方面,TSC计数器的高频率非常便于测量特别小的时间间隔。

ACPI 控制CPU的频率,从而控制系统的功耗。

2、xtime和jiffies

linux的时钟中断需要两个全局变量,分别是xtime与jiffies。

  1. xtime
    一个timeval结构类型变量,是从cmos电路中取得的时间,一般是从某一历史时刻开始到现在的时间,也就是为了取得我们操作系统上显示的日期。这个就是“实时时钟”,它的精确度是微秒。获取方式是通过sys/time.h头文件里面的gettimeofday函数获取。
  2. Jiffies
    内核一般通过jiffies值来获取当前时间。尽管该数值表示的是自上次系统启动到当前的时间间隔,但因为驱动程序的生命期只限于系统的运行期 (uptime),所以也是可行的。驱动程序利用jiffies的当前值来计算不同事件间的时间间隔。 硬件给内核提供一个系统定时器用以计算和管理时间,内核通过编程预设系统定时器的频率,即节拍率(tick rate),每一个周期称作一个tick(节拍)。Linux内核从2.5版内核开始把频率从100调高到1000(当然带来了很多优点,
    也有一些缺点)。jiffies是内核中的一个全局变量,用来记录自系统启动一来产生的节拍数。譬如,如果计算系统运行了多长时间,可以用 jiffies/tick rate 来计算。

    jiffies 机制是一个完全独立且兼容性极好的计时系统。任何Linux系统都有 jiffies 。因此,在大多数情况下它可以被设备驱动等程序作为时间参考工具,即使它的精度并没有那么高。jiffies 的精度只能到达 ms 级,如果您的设备驱动需要更高精度的计时,那可能得另寻它法了,通常是直接读取相应寄存器来实现。不过这样一来,代码的兼容性就会差很多了。但在绝大多数情况下,jiffies 的精度都足够了。

HZ、tick

Linux核心每隔固定周期会发出timer interrupt (IRQ 0),HZ是用来定义每一秒有几次timer interrupts,也叫节拍。举例来

说,HZ为1000,代表每秒有1000次timer interrupts,比较常见的设置是HZ=100。

Tick是HZ的倒数,意即timer interrupt每发生一次中断的时间。如HZ为250时,tick为4毫秒(millisecond)。

jiffies 忙等待

在Linux内核中,最简单粗暴的延时方式就是“忙等待”。

它说白了就是设定一个终止时间点,然后一直去检测系统时钟当前又没有到达这个终止时间点来判定是否执行下一步操作。这种方式虽然会浪费掉很多CPU的运算力,但不可否认的是它的编写也相当简单。一种比较常见的方式就是检测jiffies来延时。如下代码所示:

unsigned long end_time = jiffies + HZ * 5; //表示在当前时间点5秒后为终止时间点。
while(time_before(jiffies, end_time)); //jiffies未到end_time的话会一直在这个循环中打转。time_before()函数位于jiffies.h中。

do_anything_you_wanna_do(); //时间到!

前面我们知道jiffies是内核中的一个全局变量,用来记录自系统启动一来产生的节拍数,对于以上代码需要解释的下的就是第一行的那个 HZ 。HZ是一个被定义在 中的宏。它的值会根据相应平台的不同而不同。总之这个值就表示当前系统在1秒钟时间里可以跳跃的jiffies数量。总之记住 jiffies + HZ 表示1秒钟以后的 jiffes 变量的值就行了。

以此类推
ms级别:

jiffies + (HZ/1000)*n

3、Linux下实现定时器Timer的几种方法总结

定时器Timer应用场景非常广泛,在Linux下,有以下几种方法:

1,使用delay和sleep()

在内核程序开发中。短期延时通常直接使用内核提供的几个延时函数即可。如:

#include 
void ndelay(unsigned long nsecs); //纳秒延时
void udelay(unsgined long usecs); //微秒延时
void mdelay(unsigned long msecs); //毫秒延时

这里得注意一下,一般“纳秒延时”是做不到的。只能说是起个参考作用罢了。其实严格来讲,这三个延时都只能起到个参考作用而已。并且还有个很重要的:这三个延时函数是“忙等待型”。即在等待延时到期的过程中,CPU会在那死等而不会让出调度权。

与之相对应的还有几个延时时间更长的,且不属于“忙等待型”的延时函数:

#include 
void msleep(unsigned int ms);
void ssleep(unsigned int seconds);
unsigned long msleep_interruptible(unsigned int ms);

前面两个函数的延时是不可中断的。如果希望在延时等待过程中进程能被相应中断唤醒,则可以使用第三个函数。更具体的用法,还得各位同学自行实践掌握了。

delay()是循环等待,该进程还在运行,占用处理器。
sleep()不同,它会被挂起,把处理器让给其他的进程。

2,使用信号量SIGALRM + alarm()

alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。

这种方式的精度能达到1秒,其中利用了系统的信号量机制,首先注册信号量SIGALRM处理函数,调用alarm(),设置定时长度,代码如下:

#include 
#include 
 
void timer(int sig)
{
    if(SIGALRM == sig)
    {
        printf("timer\n");
        alarm(1);    //we contimue set the timer
    }
 
    return ;
}
 
int main()
{
    signal(SIGALRM, timer); //relate the signal and function
 
    alarm(1);    //trigger the timer
 
    getchar();
 
    return 0;
}

上面程序的分析:定义了一个时钟alarm(1),它的作用是让信号SIGALRM在经过1秒后传送给目前main()所在进程

alarm方式虽然很好,但是无法实现低于1秒的精度。

alarm()函数的主要功能是设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。

3,使用RTC机制

RTC时间:是指系统中包含的RTC芯片内部所维护的时间。RTC芯片都有电池+系统电源的双重供电机制,在系统正常工作时由系统供电,在系统掉电后由电池进行供电。因此系统电源掉电后RTC时间仍然能够正常运行。

RTC机制利用系统硬件提供的Real Time Clock机制,通过读取RTC硬件/dev/rtc,通过ioctl()设置RTC频率,代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
int main(int argc, char* argv[])
{
    unsigned long i = 0;
    unsigned long data = 0;
    int retval = 0;
    int fd = open ("/dev/rtc", O_RDONLY);
 
    if(fd < 0)
    {
        perror("open");
        exit(errno);
    }
 
    /*Set the freq as 4Hz*/
    if(ioctl(fd, RTC_IRQP_SET, 1) < 0)
    {
        perror("ioctl(RTC_IRQP_SET)");
        close(fd);
        exit(errno);
    }
    /* Enable periodic interrupts */
    if(ioctl(fd, RTC_PIE_ON, 0) < 0)
    {
        perror("ioctl(RTC_PIE_ON)");
        close(fd);
        exit(errno);
    }
 
    for(i = 0; i < 100; i++)
    {
        if(read(fd, &data, sizeof(unsigned long)) < 0)
        {
            perror("read");
            close(fd);
            exit(errno);
 
        }
        printf("timer\n");
    }
    /* Disable periodic interrupts */
    ioctl(fd, RTC_PIE_OFF, 0);
    close(fd);
 
    return 0;
}
4,使用select()

使用select函数,我们能实现微妙级别精度的定时器。同时,select函数也是我们在编写非阻塞程序时经常用到的一个函数。

首先看看select函数原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds,
                  fd_set *exceptfds, struct timeval *timeout);

说明:
1)select函数使用了一个结构体timeval作为其参数。
2)select函数会更新timeval的值,timeval保持的值为剩余时间。

timeval的结构如下:

struct timeval{

long tv_sec;/*secons*

long tv_usec;/*microseconds*/

}

我们可以看出其精确到microseconds也即微妙。

Linux环境下可以有多种方法实现定时。

//秒级定时器
void seconds_sleep(unsigned seconds){
	struct timeval tv;
	tv.tv_sec=seconds;
	tv.tv_usec=0;
	int err;
	do{
	   err=select(0,NULL,NULL,NULL,&tv);
	}while(err<0 && errno==EINTR);
}
//毫秒级别定时器
void milliseconds_sleep(unsigned long mSec){
	struct timeval tv;
	tv.tv_sec=mSec/1000;
	tv.tv_usec=(mSec%1000)*1000;
	int err;
	do{
	   err=select(0,NULL,NULL,NULL,&tv);
	}while(err<0 && errno==EINTR);
}
//微妙级别定时器
void microseconds_sleep(unsigned long uSec){
	struct timeval tv;
	tv.tv_sec=uSec/1000000;
	tv.tv_usec=uSec%1000000;
	int err;
	do{
	    err=select(0,NULL,NULL,NULL,&tv);
	}while(err<0 && errno==EINTR);
}
#include 
#include 
#include 
int main()
{
    int i;
    for(i=0;i<5;++i){
	printf("%d\n",i);
	//seconds_sleep(1);
	//milliseconds_sleep(1500);
	microseconds_sleep(1900000);
    }
}

总结:如果对系统要求比较低,可以考虑使用简单的sleep(),毕竟一行代码就能解决;如果系统对精度要求比较高,则可以考虑RTC机制和select()机制。

4.linux驱动之定时器的使用

Linux的内核中定义了一个定时器的结构:

#include

struct timer_list

 {

    struct list_head list;

  unsigned long expires; //定时器到期时间

  unsigned long data; //作为参数被传入定时器处理函数

  void (*function)(unsigned long);

};

利用这个结构我们可以在驱动中很方便的使用定时器。

一: timer的API函数:

初始化定时器:

void init_timer(struct timer_list * timer);

增加定时器:

void add_timer(struct timer_list * timer);

删除定时器:

int del_timer(struct timer_list * timer);

修改定时器的expire:

int mod_timer(struct timer_list *timer, unsigned long expires);

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

(1)创建timer、编写超时定时器处理函数function;
(2)为timer的expires、data、function赋值;
(3)调用add_timer将timer加入列表----添加一个定时器;
(4)在定时器到期时,function被执行;
(5)在程序中涉及timer控制的地方适当地调用del_timer、mod_timer删除timer或修改timer的expires。

三:下面看一个例子:

#include 
#include 
#include 
#include //jiffies在此头文件中定义
#include 
#include 
struct timer_list mytimer;//定义一个定时器
void  mytimer_ok(unsigned long arg)
{
           printk("Mytimer is ok\n");
           printk("receive data from timer: %d\n",arg);
   }
 
static int __init hello_init (void)
{
    printk("hello,world\n");
    init_timer(&mytimer);     //初始化定时器
    mytimer.expires = jiffies+100;//设定超时时间,100代表1秒
    mytimer.data = 250;    //传递给定时器超时函数的值
    mytimer.function = mytimer_ok;//设置定时器超时函数
    add_timer(&mytimer); //添加定时器,定时器开始生效
    return 0;
}
   
static void __exit hello_exit (void)
 
{
    del_timer(&mytimer);//卸载模块时,删除定时器
    printk("Hello module exit\n");
}
 
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("CXF");
MODULE_LICENSE("Dual BSD/GPL");

四:交叉编译后,放到开发板上:

#insmod timer.ko

可以发现过一秒后定时器过期函数被执行了,打印出了信息,250也被正确传递了。

#rmmod timer.ko

我们也可以用lsmod | grep timer 来查看是否加载了timer驱动。

可以用dmesg | tail -20 查看驱动打印的信息

dmesg -c 清楚信息

交叉编译后,放到开发板上

#insmod timer.o

发现每隔一秒,mytimer_ok函数就执行一次,这是因为每次定时器到期后,都又重新给它设置了一个新的超时时间,并且新的超时函数指向自己,形成一个递归,所以就会一直执行下去。

参考:https://www.jb51.net/article/96052.htm
https://www.cnblogs.com/chorm590/p/12879510.html
https://www.iteye.com/blog/wangyuxxx-1725792

你可能感兴趣的:(#,linux内核,linux,运维,服务器)