深入理解 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

目录
相关文章
|
4月前
|
缓存 Linux 开发者
Linux内核中的并发控制机制
本文深入探讨了Linux操作系统中用于管理多线程和进程的并发控制的关键技术,包括原子操作、锁机制、自旋锁、互斥量以及信号量。通过详细分析这些技术的原理和应用,旨在为读者提供一个关于如何有效利用Linux内核提供的并发控制工具以优化系统性能和稳定性的综合视角。
|
4月前
|
缓存 负载均衡 算法
深入探索Linux内核的调度机制
本文旨在揭示Linux操作系统核心的心脏——进程调度机制。我们将从Linux内核的架构出发,深入剖析其调度策略、算法以及它们如何共同作用于系统性能优化和资源管理。不同于常规摘要提供文章概览的方式,本摘要将直接带领读者进入Linux调度机制的世界,通过对其工作原理的解析,展现这一复杂系统的精妙设计与实现。
171 8
|
4月前
|
算法 Linux 调度
深入理解Linux内核调度器:从基础到优化####
本文旨在通过剖析Linux操作系统的心脏——内核调度器,为读者揭开其高效管理CPU资源的神秘面纱。不同于传统的摘要概述,本文将直接以一段精简代码片段作为引子,展示一个简化版的任务调度逻辑,随后逐步深入,详细探讨Linux内核调度器的工作原理、关键数据结构、调度算法演变以及性能调优策略,旨在为开发者与系统管理员提供一份实用的技术指南。 ####
126 4
|
2月前
|
安全 Linux 测试技术
Intel Linux 内核测试套件-LKVS介绍 | 龙蜥大讲堂104期
《Intel Linux内核测试套件-LKVS介绍》(龙蜥大讲堂104期)主要介绍了LKVS的定义、使用方法、测试范围、典型案例及其优势。LKVS是轻量级、低耦合且高代码覆盖率的测试工具,涵盖20多个硬件和内核属性,已开源并集成到多个社区CICD系统中。课程详细讲解了如何使用LKVS进行CPU、电源管理和安全特性(如TDX、CET)的测试,并展示了其在实际应用中的价值。
|
2月前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
114 15
|
4月前
|
负载均衡 算法 Linux
深入探索Linux内核调度器:公平与效率的平衡####
本文通过剖析Linux内核调度器的工作机制,揭示了其在多任务处理环境中如何实现时间片轮转、优先级调整及完全公平调度算法(CFS),以达到既公平又高效地分配CPU资源的目标。通过对比FIFO和RR等传统调度策略,本文展示了Linux调度器如何在复杂的计算场景下优化性能,为系统设计师和开发者提供了宝贵的设计思路。 ####
89 26
|
4月前
|
缓存 并行计算 Linux
深入解析Linux操作系统的内核优化策略
本文旨在探讨Linux操作系统内核的优化策略,包括内核参数调整、内存管理、CPU调度以及文件系统性能提升等方面。通过对这些关键领域的分析,我们可以理解如何有效地提高Linux系统的性能和稳定性,从而为用户提供更加流畅和高效的计算体验。
110 17
|
3月前
|
算法 Linux
深入探索Linux内核的内存管理机制
本文旨在为读者提供对Linux操作系统内核中内存管理机制的深入理解。通过探讨Linux内核如何高效地分配、回收和优化内存资源,我们揭示了这一复杂系统背后的原理及其对系统性能的影响。不同于常规的摘要,本文将直接进入主题,不包含背景信息或研究目的等标准部分,而是专注于技术细节和实际操作。
|
3月前
|
存储 缓存 网络协议
Linux操作系统的内核优化与性能调优####
本文深入探讨了Linux操作系统内核的优化策略与性能调优方法,旨在为系统管理员和高级用户提供一套实用的指南。通过分析内核参数调整、文件系统选择、内存管理及网络配置等关键方面,本文揭示了如何有效提升Linux系统的稳定性和运行效率。不同于常规摘要仅概述内容的做法,本摘要直接指出文章的核心价值——提供具体可行的优化措施,助力读者实现系统性能的飞跃。 ####
|
3月前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####