深入理解 Linux 内核(二)(上)

简介: 深入理解 Linux 内核(二)

六、定时测量

1、时钟和定时器电路

  • 实时时钟(RTC)
  • 时间戳计数器(TSC)
  • 可编程间隔定时器(PIT)
  • CPU 本地定时器
  • 高精度时间定时器(HPET)
  • ACPI 电源管理定时器

2、Linux 计时体系结构

(1)计时体系机构的数据结构

  • 定时器对象
  • jiffies 变量
  • xtime 变量

(2)软定时器和延迟函数

   Linux定时器分为动态定时器(dynamic timer)和间隔定时器(interval timer)。第一种类型由内核使用,而间隔定时器可以由进程在用户态创建。


   这里是有关 Linux 定时器的警告:因为对定时器函数的检查总是由可延迟函数进行,而可延迟函数被激活以后很长时间才能被执行,因此,内核不能确保定时器面数正好在定时到期时开始执行,而只能保证在适当的时间执行它们,或者假定延迟到几百毫秒之后执行它们。因此,对于必须严格遵守定时时间的那些实时应用而言,定时器并不适合。


   除了软定时器外,内核还使用了延迟函数,它执行一个紧凑的指令循环直到指定的时间间隔用完。我们将在后面的 “延迟函数” 一节对它们进行讨论。

(a)动态定时器

    动态定时器(dynamic timer)被动态地创建和撤消,对当前活动动态定时器的个数没有限制。

// include/linux/timer.h
struct timer_list {
  struct list_head entry;
  unsigned long expires;

  void (*function)(unsigned long);
  unsigned long data;

  struct tvec_base *base;
#ifdef CONFIG_TIMER_STATS
  void *start_site;
  char start_comm[16];
  int start_pid;
#endif
#ifdef CONFIG_LOCKDEP
  struct lockdep_map lockdep_map;
#endif
};

   function 字段包含定时器到期时执行函数的地址。data 字段指定传递给定时器函数的参数。正是由于 data 字段,就可以定义一个单独的通用函数来处理多个设备驱动程序的超时问题,在 data 字段可以存放设备 ID,或其他有意义的数据,定时器函数可以用这些数据区分不同的设备。


   expires 字段给出定时器到期时间,时间用节拍数表示,其值为系统启动以来所经过的节拍数。当 expires 的值小于或等于 jiffies 的值时,就说明计时器到期或终止。


   entry 字段用于将软定时器插入双向循环链表队列中,该链表根据定时器 expires 字段的值将它们分组存放。我们将在本章后面描述使用这些链表的算法。


   为了创建并激活一个动态定时器,内核必须:


如果需要,创建一个新的 timer_list 对象,比如说设为 t。这可以通过以下几种方式来进行:

在代码中定义一个静态全局变量。

在函数内定义一个局部变量:在这种情况下,这个对象存放在内核堆栈中。

在动态分配的描述符中包含这个对象。

调用 init_timer(&t) 函数初始化这个对象。实际上是把 t.base 指针字段置为 NULL 并把 t.lock 自旋锁设为 “打开”。

把定时器到期时激活函数的地址存入 function 字段。如果需要,把传递给函数的参数值存入 data 字段。

如果动态定时器还没有被插入到链表中,给 expires 字段赋一个合适的值并调用 add_timer(&t) 函数把 t 元素插入到合适的链表中

否则,如果动态定时器已经被插入到链表中,则调用 mod_timer() 函数来更新 expires 字段,这样也能将对象插入到合适的链表中(下面将讨论)。

动态定时器与竞争条件
// kernel/timer.c
int del_timer(struct timer_list *timer);
int del_timer_sync(struct timer_list *timer);

动态定时器的数据结构

  选择合适的数据结构实现动态定时器并不是件容易的事。把所有定时器放在一个单独的链表中会降低系统的性能,因为在每个时钟节拍去扫描一个定时器的长链表太费时。另一方面,维护一个排序的链表效率也不高,因为插入和删除操作也非常费时。


  解决的办法基于一种巧妙的数据结构,即把 expires 值划分成不同的大小,并允许动态定时器从大 expires 值的链表到小 expires 值的链表进行有效的过滤。此外,在多处理器系统中活动的动态定时器集合被分配到各个不同的 CPU 中。


  动态定时器的主要数据结构是一个叫做 tvec_bases 的每 CPU 变量(参见第五章的 "每 CPU 变量"一节):它包含 NR_CPUS 个元素,系统中每个 CPU 各有一个。每个元素是一个 tvec_base_t 类型的数据结构、它包含相应 CPU 中处理动态定时器需要的所有数据。

// kernel/timer.c
#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)

struct tvec {
  struct list_head vec[TVN_SIZE];
};

struct tvec_root {
  struct list_head vec[TVR_SIZE];
};
struct tvec_base {
  spinlock_t lock;
  struct timer_list *running_timer;
  unsigned long timer_jiffies;
  unsigned long next_timer;
  struct tvec_root tv1;
  struct tvec tv2;
  struct tvec tv3;
  struct tvec tv4;
  struct tvec tv5;
} ____cacheline_aligned;

  字段 tv1 的数据结构为 tvec_root_t 类型,它包含一个 vec 数组,这个数组由 256 个 list_head 元素组成(即由 256 个动态定时器链表组成)。这个结构包含了在紧接着到来的 255 个节拍内将要到期的所有动态定时器。


  字段 tv2、tv3 和 tv4 的数据结构都是 tvec 类型,该类型有一个数组 vec(包含 64 个 list_head 元素)。这些链表包含在紧接着到来的 214-1(8+6)、 220-1(8+6+6) 以及 226-1(8+6+6+6) 个节拍内将要到期的所有动态定时器。


  字段 tv5 与前面的字段几乎相同,但唯一区别就是 vec 数组的最后一项是一个大 expires 字段值的动态定时器链表。tv5 从不需要从其他的数组补充。图 6-1 用图例说明了 5 个链表组。


   timer_jiffies 字段的值表示需要检查的动态定时器的最早到期时间:如果这个值与 jiffies 的值一样,说明可延迟函数没有积压; 如果这个值小于 jiffies,说明前几个节拍相关的可延迟函数必须处理。该字段在系统启动时被设置成 jiffies 的值,且只能由 run_timer_softirq() 函数(将在下一节描述)增加它的值。注意当处理动态定时器的可延迟函数在很长一段时间内都没有被执行时(例如由于这些函数被禁止或者已经执行了大量中断处理程序),timer_jiffies 字段可能会落后 jiffies 许多。

动态定时器处理

    尽管软定时器具有巧妙的数据结构,但是对其处理是一种耗时的活动,所以不应该被时钟中断处理程序执行。在 Linux 2.6 中该活动由可延迟函数来执行,也就是由 TIMER_SOFTIRQ 软中断执行。

// kernel/timer.c
static void run_timer_softirq(struct softirq_action *h)
{
  struct tvec_base *base = __get_cpu_var(tvec_bases);

  hrtimer_run_pending();

  if (time_after_eq(jiffies, base->timer_jiffies))
    __run_timers(base);
}

static inline void __run_timers(struct tvec_base *base)
{
  struct timer_list *timer;

  spin_lock_irq(&base->lock);
  while (time_after_eq(jiffies, base->timer_jiffies)) {
    struct list_head work_list;
    struct list_head *head = &work_list;
    int index = base->timer_jiffies & TVR_MASK;

    /*
     * Cascade timers:
     */
    if (!index &&
      (!cascade(base, &base->tv2, INDEX(0))) &&
        (!cascade(base, &base->tv3, INDEX(1))) &&
          !cascade(base, &base->tv4, INDEX(2)))
      cascade(base, &base->tv5, INDEX(3));
    ++base->timer_jiffies;
    list_replace_init(base->tv1.vec + index, &work_list);
    while (!list_empty(head)) {
      void (*fn)(unsigned long);
      unsigned long data;

      timer = list_first_entry(head, struct timer_list,entry);
      fn = timer->function;
      data = timer->data;

      timer_stats_account_timer(timer);

      set_running_timer(base, timer);
      detach_timer(timer, 1);

      spin_unlock_irq(&base->lock);
      {
        int preempt_count = preempt_count();

#ifdef CONFIG_LOCKDEP
        /*
         * It is permissible to free the timer from
         * inside the function that is called from
         * it, this we need to take into account for
         * lockdep too. To avoid bogus "held lock
         * freed" warnings as well as problems when
         * looking into timer->lockdep_map, make a
         * copy and use that here.
         */
        struct lockdep_map lockdep_map =
          timer->lockdep_map;
#endif
        /*
         * Couple the lock chain with the lock chain at
         * del_timer_sync() by acquiring the lock_map
         * around the fn() call here and in
         * del_timer_sync().
         */
        lock_map_acquire(&lockdep_map);

        trace_timer_expire_entry(timer);
        fn(data);
        trace_timer_expire_exit(timer);

        lock_map_release(&lockdep_map);

        if (preempt_count != preempt_count()) {
          printk(KERN_ERR "huh, entered %p "
                 "with preempt_count %08x, exited"
                 " with %08x?\n",
                 fn, preempt_count,
                 preempt_count());
          BUG();
        }
      }
      spin_lock_irq(&base->lock);
    }
  }
  set_running_timer(base, NULL);
  spin_unlock_irq(&base->lock);
}

   run_timer_softirq() 函数是与 TIMER_SOFTIRQ 软中断请求相关的可延迟函数。它实质上执行如下操作:


把与本地 CPU 相关的 tvec_base_t 数据结构的地址存放到 base 本地变量中。


获得 base->lock 自旋锁并禁止本地中断。

开始执行一个 while 循环,当 base->timer_jiffies 大于 jiffies 的值时终止。在每一次循环过程中,执行下列子步骤:


计算 base->tv1 中链表的索引,该索引保存着下一次将要处理的定时器:

index = base->timer_jiffies & 255;

如果索引值为 0,说明 base->tv1 中的所有链表已经被检查过了,所以为空:于是该通数通过调用 cascade() 来过滤动态定时器:


考虑第一次调用 cascade() 函数的情况:它接收 base 的地址、base->tv2 的地址、base->tv2(包括在紧接着到来的 256 个节拍内将要到期的定时器)中链表的索引作为参数。该索引值是通过观察 base->timer_jiffies 的特殊位上的值来决定的。


cascade() 函数将 base->tv2 中链表上的所有动态定时器移到 base->tv1 的适当链表上。然后,如果所有 base->tv2 中的链表不为空,它返回一个正值。如 base->tv2 中的链表为空,cascade() 将再次被调用,把 base->tv3 中的某个链表上包含的定时器填充到 base->tv2 上,如此等等。

使 base->timer_jiffies 的值加 1。

对于 base->tv1.vec[index] 链表上的每一个定时器,执行它所对应的定时器函数。特别说明的是,链表上的每个 timer_list 元素 t 实质上执行以下步骤:

(1)将 t 从 base->tv1 的链表上删除。

(2) 在多处理器系统中,将 base->running_timer 设置为 &t (t 的地址)。

(3)设置 t.base 为 NULL。

(4) 释放 base->lock 自旋锁,并允许本地中断。

(5) 传递 t.data 作为参数,执行定时器函数 t.function。

(6) 获得 base->lock 自旋锁,并禁止本地中断。

(7) 如果链表中还有其他定时器,则继续处理。

链表上的所有定时器已经被处理。继续执行最外层 while 循环的下一次循环。

最外层的 while 循环结束,这就意味着所有到期的定时器已经被处理了。在多处理器系统中,设置 base->running_timer 为 NULL。


释放 base->lock 自旋锁并允许本地中断。


   由于 jiffies 和 timer_jiffies 的值经常是一样的,所以最外层的 while 循环常常只执行一次。一般情况下,最外层循环会连续执行 jiffies - base->timer_jiffies + 1 次。此外,如果在 run_timer_softirq() 正在执行时发生了时钟中断,那么也得考虑在这个节拍所出现的到期动态定时器,因为 jiffies 变量的值是由全局时钟中断处理程序异步增加的(参见前面的 “时钟中断处理序” 一节)。


   请注意,就在进入最外层循环前,run_timer_softirq() 要禁止中断并获取 base->lock 自旋锁,调用每个动态定时器函数前,激活中断并释放自旋锁,直到函数执行结束。这就保证了动态定时器的数据结构不被交错执行的内核控制路径所破坏。


   综上所述可知,这种相当复杂的算法确保了极好的性能。让我们来看看为什么,为了简单起见,假定 TIMER_SOFTIRQ 软中断正好在相应的时钟中断发生后执行。那么,在 256 次中出现的 255 次时钟中断(也就是在 99.6% 的情况中),run_timer_softirq() 仅仅运行到期定时器的函数。为了周期性地补充 base->tv1.vec,在 64 次补充当中,63 次足以把 base->tv2 指向的链表分成 base->tv1 指向的 256 个链表。依次地,base->tv2.vec 数组必须在 0.006% 的情况下得到补充,即每 16.4 秒一次。类似地,每 17 分 28 秒补充一次 base->tv3.vec,每 18 小时 38 分补充一次 base->tv4.vec,而 base->tv5.vec 不需被补充。

动态定时器应用之一:nanosleep()系统调用

   为了说明前面所有活动的结果如何在内核中实际使用,我们给出创建和使用进程延时的例子。


   让我们考虑 nanosleep() 系统调用的服务例程程,即 sys_nanosleep(),它接收一个指向 timespec 结构的指针作为参数,并将调用进程挂起直到特定的时间间隔用完。服务例程首先调用 copy_from_user() 将包含在 timespec 结构(用户态下)中的值复制到局部变量 t 中。假设 timespec 结构定义了一个非空的延迟,接着函数执行如下代码:

  current->state = TASK_INTERRUPTIBLE;
  remaining = schedule_timeout(timespec_to_jiffies(&t)+1);

timespec_to_jiffies() 函数将存放在 timespec 结构中的时间间隔转换成节拍数。为保险起见,sys_nanosleep()timespec_to_jiffies() 计算出的值加上一个节拍。

    内核使用动态定时器来实现进程的延时。它们出现在 schedule_timeout() 函数中,该

函数执行下列语句:

  struct timer_list timer;
  unsigned long expire = timeout + jiffies;
  init_timer(&timer);
  timer.expires = expire;
  timer.data = (unsigned long) current;
  timer.function = process_timeout;
  add_timer(&timer);
  schedule();   /* 进程挂起直到定时器到时 */
  del_singleshot_timer_sync(&timer);
  timeout = expire - jiffies;
  return (timeout < 0 ? 0 : timeout);

  当 schedule() 被调用时,就选择另一个进程执行; 当前一个进程恢复执行时,该函数就删除这个动态定时器。在最后一句中,函数返回的值有两种可能,0 表示延时到期,timeout 表示如果进程因某些其他原因被唤醒,到延时到期时还剩余的节拍数。

    当延时到期时,内核执行下列函数:

void process_timeout(unsigned long __data) {
  wake_up_process((task_t *)__data);
}

    process_timeout() 接收进程描述符指针作为它的参数,该指针存放在定时器对象的 data 字段。结果,挂起的进程被唤醒。

   一旦进程被唤醒,它就继续执行 sys_nanosleep() 系统调用。如果 schedule_timeout() 返回的值表明进程延时到期(值为 0),系统调用就结束。否则,系统调用将自动重新启动,正如第十一章的 “系统调用的重新执行” 一节中解释的那样。

(b)延迟函数

    当内核需要等待一个较短的时间间隔 —— 比方说,不超过几毫秒时,就无需使用软定时器。例如,通常设备驱动器会等待预先定义的数个微秒直到硬件完成某些操作。由于动态定时器通常有很大的设置开销和一个相当大的最小等待时间(1ms),所以设备驱动器使用它会很不方便。

    在这些情况下,内核使用 udelay()ndelay() 函数:前者接收一个微秒级的时间间隔作为它的参数,并在指定的延迟结束后返回,后者与前者类似,但是指定延迟的参数是纳秒级的。

可参考 ==> 4、延迟执行

3、与定时测量相关的系统调用

(1)time() 和 gettimeofday() 系统调用

time()

    返回从 1970 年 1 月 1 日午夜(UTC)开始所走过的秒数。

   

gettimeofday()

    返回从 1970 年 1 月 1 日午夜(UTC)开始所走过的秒数及在前 1 秒内走过的微秒数,这个值存放在数据结构 timeval 中

(2)adjtimex() 系统调用

    网络定时协议(NTP)

(3)setitimer() 和 alarm() 系统调用

    setitimer() 系统调用可以激活间隔定时器。

       #include <sys/time.h>
       
       int setitimer(int which, const struct itimerval *new_value,
                     struct itimerval *old_value);


(4)与POSIX定时器相关的系统调用



深入理解 Linux 内核(二)(下):https://developer.aliyun.com/article/1597425

目录
相关文章
|
7天前
|
存储 安全 Linux
探索Linux操作系统的心脏:内核
在这篇文章中,我们将深入探讨Linux操作系统的核心—内核。通过简单易懂的语言和比喻,我们会发现内核是如何像心脏一样为系统提供动力,处理数据,并保持一切顺畅运行。从文件系统的管理到进程调度,再到设备驱动,我们将一探究竟,看看内核是怎样支撑起整个操作系统的大厦。无论你是计算机新手还是资深用户,这篇文章都将带你领略Linux内核的魅力,让你对这台复杂机器的内部运作有一个清晰的认识。
21 3
|
16天前
|
缓存 安全 Unix
Linux 内核黑客不可靠指南【ChatGPT】
Linux 内核黑客不可靠指南【ChatGPT】
|
16天前
|
Linux 开发者
Linux内核贡献成熟度模型 【ChatGPT】
Linux内核贡献成熟度模型 【ChatGPT】
|
16天前
|
网络协议 Ubuntu Linux
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
用Qemu模拟vexpress-a9 (三)--- 实现用u-boot引导Linux内核
|
16天前
|
Linux
用clang编译Linux内核
用clang编译Linux内核
|
16天前
|
Linux API C语言
Linux 内核补丁提交的清单 【ChatGPT】
Linux 内核补丁提交的清单 【ChatGPT】
|
16天前
|
安全 Linux 开发者
Linux内核管理风格 【ChatGPT】
Linux内核管理风格 【ChatGPT】
|
17天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
17天前
|
存储 Linux 开发工具
如何进行Linux内核开发【ChatGPT】
如何进行Linux内核开发【ChatGPT】
|
17天前
|
Unix Linux API
Linux内核许可规则 【ChatGPT】
Linux内核许可规则 【ChatGPT】