zoukankan      html  css  js  c++  java
  • Linux内核11-定时器和时间管理

    Linux内核第11章

    时间管理在内核中占有非常重要的地位。内核中有大量的函数都是基于时间驱动的。其中有些函数是周期执行的,像对调度程序的运行队列进行平衡调度或对屏幕进行刷新这样的函数,都需要定期执行,比如每秒执行100次;而另一些函数,比如需要退后执行的磁盘I/O操作等,则需要等待一个相对时间后才运行;内核还必须管理系统的运行时间以及当前日期和时间。

    周期性产生的事件---比如每10ms一次---都是由系统定时器驱动的。系统定时器是一种可编程硬件芯片,它能以固定频率产生中断。该中断就是所谓的定时器中断,它所对应的中断处理程序负责更新系统时间,也负责执行需要周期性运行的任务。

    系统定时器以某种频率自行触发(经常被称为击中hitting或射中popping)时钟中断,该频率可以通过编程预定,称作节拍率(tick rate)。当时钟中断发生时,内核就通过一种特殊的中断处理程序对其进行处理。

    因为预编的节拍率对内核来说是可知的,所以内核知道连续两次时钟中断的间隔时间。这个间隔时间就称为节拍(tick),它等于节拍率分之一秒。

    -时钟中断对于管理操作系统尤为重要,大量内核函数的生命周期都离不开流逝的时间的控制;

    利用时间中断周期执行的工作:

    -更新系统运行时间;

    -更新实际时间;

    -在smp系统上,均衡调度程序中各处理器上的运行队列。如果运行队列负载不均衡的话,尽量使它们均衡;

    -检查当前进程是否用尽了自己的时间片。如果用尽,就重新进行调度;

    -运行超时的动态定时器;

    -更新资源消耗和处理器时间的统计值;

    节拍率:HZ

    系统定时器频率(节拍率)是通过静态预处理定义的,也就是HZ(赫兹),在系统启动时按照HZ值对硬件进行设置。

    内核在<asm/param.h>文件中定义了这个值。

    编写内核代码时,不要认为HZ值是一个固定不变的值。可调的。

    内核中的全部时间概念都来源于周期运行的系统时钟。

    提高节拍率意味着时钟中断产生得更加频繁,所以中断处理程序也会更频繁地执行。带来的好处有:

    -更高的时钟中断解析度(resolution)可提高时间驱动事件的解析度;

    -提高了时间驱动事件的准确度(accuracy)。

    假定内核在某个随机时刻触发定时器,而它可能在任何时间超时,但由于只有在时钟中断到来时才可能执行它,所以平均误差大约为半个时钟中断周期。

    高HZ的优势:

    -内核定时器能够以更高的频度和更高的准确度运行;

    -依赖定时值执行的系统调用,比如poll()和select(),能够以更高的精度运行;

    -对诸如资源消耗和系统运行时间等的测量会有更精细的解析度;

    -提高进程抢占的准确度;(时钟中断处理程序负责减少当前进程的时间片计数)。

    高HZ的劣势:

    节拍率越高,意味着时钟中断频率越高,也就意味着系统负担越重。因为处理器必须花时间来执行时钟中断处理程序,所以节拍率越高,中断处理程序占用的处理器的时间越多。这样不但减少了处理器处理其他工作的时间,而且还会更频繁地打乱处理器高速缓存并增加耗电。

    jiffies:

    全局变量jiffies用来记录自系统启动以来产生得节拍的总数。启动时,内核将该变量初始化为0,此后,每次时钟中断处理程序就会增加该变量的值。

    jiffies定义于文件<linux/jiffies.h>中:

    extern unsigned long volatile jiffies;

    (jiffies/HZ)用来计算秒数;

    jiffies为无符号长整型,用其它任何类型存放它都不正确。

    因为jiffies是无符号长整型,在32位体系结构上是32位,在64位体系结构上是64位。32位的jiffies变量,在时钟频率为100HZ情况下,497天后溢出。而如果使用64位的jiffies,将很难看到其溢出。

    32位系统与63位系统都有jiffies和jiffies_64,在32位体系结构中,jiffies取jiffies_64的低32位(高64位不访问但时间管理仍会按64位存储);64体系结构中,jiffies与jiffies_64相同。

    通过get_jiffies_64()函数可以读取整个64位数值。

    由于变量溢出,jiffies存在回绕问题,超过最大值后会变为0。内核提供四个宏来正确处理节拍计数回绕问题:

    #define time_after(unknown, known)

    #define time_before(unknown, known)

    #define time_after_eq(unknown, known)

    #define time_before_eq(unknown, known)

    用户空间和HZ:

    内核以节拍数/秒的形式给用户空间导出节拍率的值,应用程序依赖于这个特定的节拍率值。所以如果在内核中更改了HZ的定义值,就打破了用户空间的常量关系---用户空间并不知道新的节拍率值。要避免这个错误,内核必须更改所有导出的jiffies值。因而内核定义了USER_HZ来代表用户空间看到的节拍率值。通过HZ/USER_HZ来计算,HZ是USER_HZ的整数倍,且HZ大于等于USER_HZ。内核通过jiffies_64_to_clock_t()将64位的jiffies值的单位从HZ转换为USER_HZ。

    硬时钟和定时器:

    体系结构提供了两种设备进行计时---一种是系统定时器,另一种是实时时钟。

    实时时钟:RTC

    实时时钟是用来持久存放系统时间的设备,即使系统关闭后,它也可以靠主板上的微型电池提供的电力保持系统的计时。当系统启动时,内核通过读取RTC来初始化墙上时间,该时间存放在xtime变量中。

    系统定时器:

    提供一种周期性触发中断机制。电子晶振进行分频、衰减测量器。

    x86体系结构中主要使用可编程中断时钟(PIT)。

    时钟中断处理程序:

    绝大多数处理程序最低限度要执行以下工作:

    -获得xtime_lock锁,以便对访问jiffies_64和墙上时间xtime进行保护;

    -需要时应答或重新设置系统时钟;

    -周期性地使用墙上时间更新实时时钟;

    -调用体系结构无关的时钟例程:tick_periodic();

    中断服务程序主要通过调用与体系结构无关的例程,tick_periodic()执行更多的工作:

    -给jiffies_64变量增加1(前面以获得xtime_lock锁);

    -更新资源消耗的统计值,比如当前进程所消耗的系统时间和用户时间;

    -执行已到期的动态定时器;

    -执行sheduler_tick()函数;(进程调度的)

    -更新墙上时间,该时间存放在xtime变量中;

    -计算平均负载值;

    内核对进程进行时间计数时,时根据中断发生时处理器所处的模式进行分类统计的,它把上一个节拍全部算给了进程。但是事实上进程在上一个节拍期间可能多次进入和退出内核模式,而且在上一个节拍期间,该进程也不一定是唯一一个运行进程。

    scheduler_tick()函数负责减少当前运行进程的时间片计数值并且在需要时设置need_resched标志。

    tick_periodic()函数执行完毕后返回与体系结构相关的中断处理程序,继续执行后面的工作,释放·xtime_lock锁,然后退出。

    以上全部工作要在1/HZ秒内发生一次。

    实际时间:

    当前实际时间(墙上时间)定义在文件kernel/time/timekeeping.c中:

    struct timespec xtime;

    timespec数据结构定义在文件<linux/time.h>中:

    struct timespec{

      _kernel_time_t tv_sec;  //秒

      long tv_nsec;  //ns

    };

    除了更新xtime时间以外,内核不会像用户空间程序那样频繁使用xtime。但也有需要注意的特殊情况,那就是在文件系统的实现代码中存放访问时间戳(创建、存取、修改等)时需要使用xtime。

    定时器:

    有时也称为动态定时器或内核定时器,是管理内核流逝的时间的基础。我们需要一种工具,能够使工作在指定时间点上执行----不长不短,正好在希望的时间点上。内核定时器正是解决这个问题的理想工具。

    定时器的是永恒简单。只需要一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。指定的函数在定时器到期时自动执行。注意定时器并不周期运行,它在超时后就自行撤销了,所以称为动态定时器。

    定时器由结构体timer_list表示,定义在文件<linux/timer.h>中:

    struct timer_list{

      struct list_head entry;  //定时器链表的入口

      unsigned long expires;  //以jiffies为单位的定时值

      void (*function)(unsigned long);  //定时器处理函数

      unsigned long data;  //传给处理函数的长整型参数

      struct tvec_t_base_s *base;  //定时器内部值,用户不要使用

    };

    内核中定时器接口文件声明在<linux/timer.h>中,实现在kernel/timer.c中:

    创建定时器时首先定义:struct timer_list my_timer;

    接着初始化定时器数据结构中的内部值,必须在使用其它定时器管理函数操作定时器之前完成:init_timer(&my_timer);

    填充结构中需要的值:my_timer.expires=jiffies+delay; my_timer.data=0; my_timer.function=my_function;

    data参数使你可以用同一个处理函数注册多个定时器,只需通过该参数就能区别对待它们。如果不需要这个参数,就可以简单的传递0(或其它任何值)给处理函数。

    最后,要激活定时器add_timer(&my_timer);

    一般来说定时器都在超时后马上就会执行,但是也有可能推迟到下一次时钟节拍才能执行,所以不能用定时器来实现任何硬实时任务。

    有时可能需要更改已经激活的定时器超时时间,所以内核通过函数mod_timer()来实现该功能,该函数可以改变指定的定时器超时时间:

    mod_timer(&my_timer, jiffies+new_delay);

    如果调用时定时器未被激活,mod_timer()函数返回0,否则返回1。一旦mod_timer()返回后,定时器都将被激活而且设置了新的定时器值。

    如果需要在定时器超时之前停止定时器,可以使用del_timer()函数:

    del_timer(&my_timer);

    当del_timer()函数返回后,可以保证的只是:定时器不会再被激活(也就是将来不会执行),但是在多处理器机器上定时器中断可能在其它处理器上运行了,所以删除定时器时需要等待可能在其它处理器上运行的定时器处理程序都退出。这时就需要del_timer_sync()函数删除工作:

    del_timer_sync(&my_timer);

    del_timer_sync()函数不能在中断上下文中使用。

    内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。具体来说,时钟中断处理程序会执行update_process_times()函数,该函数随即调用run_local_timers()函数;ruan_local_timers()将软中断TIMER_SOFTIRQ加入到softirq;run_timer_softirq()函数处理软中断TIMER_SOFTIRQ,从而在当前处理器上运行所有的超时定时器。

    虽然定时器都以链表形式存放在一起。但为了提高搜索效率,内核将定时器按他们的超时时间划分为五组。当定时器超时时间接近时,定时器将随组一起下移

    延迟执行:

    内核代码除了使用定时器或下半部机制以外,还需要其它方法来推迟执行任务。

    忙等待:

    最简单的延迟方法是忙等待(忙循环)。该方法仅仅在想要延迟的时间是节拍的整数倍,或者精确率要求不高时才可以使用。

    忙循环在循环中不断旋转直到希望的时钟节拍数耗尽。

    短延迟:

    有时内核不但需要很短暂的延迟(比时钟节拍还短),而且还要求延迟的时间很精确。这种情况多发生在和硬件同步时,也就是说需要短暂等待某个动作的完成(等待时间往往小于1ms)。

    内核提供了三个可处理ms、ns和us级别的延迟函数,他们定义在<linux/delay.h>和<asm/delay.h>中:

    void udelay(unsigned long usecs);

    void ndelay(unsigned long nsecs);

    void mdelay(unsigned long msecs);

    schedule_timeout():

    该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。

    用法如下:

    set_current_state(TASK_INTERRUPTIBLE);  //将任务设置为可中断睡眠状态

    schedule_timeout(s*HZ);  //s秒后唤醒

    唯一的参数是延迟的相对时间,单位为jiffies。

    当定时器超时后,process_timeout()函数会被调用,该函数将任务设置为TASK_RUNNING状态,然后将其放入运行队列。

  • 相关阅读:
    table变宽格式
    IE11兼容性设定
    Spring AOP注解失效的坑及JDK动态代理
    关于何时执行shiro AuthorizingRealm 里的 doGetAuthenticationInfo与doGetAuthorizationInfo
    后端接收json数据交互
    关于JavaDate数据返回到前端变数字的问题(并引申到前后端时间的传输)
    git 列出两个分支 或者两个提交版本之间不同的文件名字
    map put相同的key
    MyBatis 中如何调用 Java 的 enum (枚举) 字段
    @ResponseBody 和 @RequestBody 的作用
  • 原文地址:https://www.cnblogs.com/cjj-ggboy/p/12284307.html
Copyright © 2011-2022 走看看