linux内核的nohz与hres

简介:

设计linux内核的那帮家伙想的可真周到啊,前面说过,linux内核的性格就是激情,只要硬件设计的足够灵活,那么设计者就会尽可能的发挥,不放过任 何可自由发挥的点和死角,而且他们从来不管后果,有时还毅然抛弃硬件的建议,最新内核的nohz可谓是一项创举。时钟中断是计算机系统必须的,就像人必须 有心跳一样,人的心跳是周期的,计算机系统的“心跳”也是周期的,因此,时钟中断每隔固定的时间就会发生。 
真的是这样吗?linux内核的设计者认为如果cpu在空闲态,那么就没有必要心跳了,毕竟计算机不是一个自组织系统,能源全靠外界电源供给,而人是一个 自组织实体,因此人必须要有周期的心跳来自己产生能量,计算机的外界电源只要不断,加上时钟可编程,那么非周期心跳甚至心跳停止就是可能的,linux内 核实现了这一点。在2.6.21内核之前,时钟中断是周期的,在那之后引入了新的时钟封装结构clock_event_device和 clocksource(参见我前面的两篇文章),于是可以更加灵活的实现自己设计的个性时钟,这个个性时钟就是nohz方式和hres方式。当然系统初 启的时候时钟中断还是周期的,当timer_interrupt被调用的时候,就会触发timer软中断,然后在接下来的软中断处理中找机会切到nohz 或者hres,具体代码如下:

void run_local_timers(void)

{

         hrtimer_run_queues();   //优先处理高精度时钟队列

         raise_softirq(TIMER_SOFTIRQ);  //触发软中断,处理函数见下:

         softlockup_tick();

}

static void run_timer_softirq(struct softirq_action *h)//软中断处理函数

{

         struct tvec_base *base = __get_cpu_var(tvec_bases);

         hrtimer_run_pending();           //这里有机会切换到nohz或者hres

         if (time_after_eq(jiffies, base->timer_jiffies))

                 __run_timers(base);

}

void hrtimer_run_pending(void)

{

         struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);

         if (hrtimer_hres_active())    //如果已经是了,就没有必要切换了,直接返回

                 return;

         if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))  //这个if判断就是具体切换到hres或者nohz的代码

                 hrtimer_switch_to_hres();

         run_hrtimer_pending(cpu_base);

}

int tick_check_oneshot_change(int allow_nohz)

{

         struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);

         if (!test_and_clear_bit(0, &ts->check_clocks))  //由此开始的种种判断说明切换所需要到种种条件

                 return 0;

         if (ts->nohz_mode != NOHZ_MODE_INACTIVE)

                 return 0;

         if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())

                 return 0;

         if (!allow_nohz)     //如果hres是允许的,那么返回1,这样就会切换到hres高精度模式了

                 return 1;

         tick_nohz_switch_to_nohz();  //如果没有机会切换到高精度模式,前面种种验证均通过,这里最起码切换到了nohz模式

         return 0;

}

hres 模式和nohz模式的具体切换由hrtimer_switch_to_hres和tick_nohz_switch_to_nohz负责。不能光一味的跟 踪代码,hres和nohz有何关联呢又分别是什么意义呢?hres实际上也不是周期中断的,而是很精确的确定中断,用最近到时的hrtimer的触发时 间来对时钟编程从而在那个时间到来的时候触发中断,而nohz仅仅说明可以用非周期的时间对时钟编程,对精度没有要求。在hres中,一切事物都由一个 hrtimer负责,比如原来的节拍调度,统计当前进程的时间等操作直接在timer_interrupt进行,而hres模式下,上述操作专门有一个 hrtimer,当clock_event_device的event_handler执行时(所有操作都被封装进了 clock_event_device的event_handler,而此event_handler在切换到hres或者nohz的时候被赋值),该函 数遍历所有的hrtimer,所有的hrtimer组织成红黑树,将到期的hrtimer链入一个链表,然后在软中断中执行这个链表的hrtimer的回 调函数,对于别的hrtimer则马上执行:所有hrtimer分为两类,一类不能在软中断中执行,属于比较紧急的,另一个可以在软中断中执行,属于不那 么紧急的。对于纯粹的nohz非hres模式,event_handler中还是传统的处理方式,只不过下次中断的时间可以任意编程。这种方式中,时间测量可以达到钠秒的精度。 
每当cpu执行cpu_idle的时候,内核就会找机会停掉系统的心跳,然后在适当时机触发心跳,而不是周期的心跳,这个时机是什么呢?如果一切都由 hrtimer负责了,那么这个时机就是找出的最近到期的timer的到期时刻,虽然停掉了周期的时钟中断,但是别的硬件中断是没有停掉的,而硬件中断可能触发一些事件,比如调度,比如发布一个新的timer,因此,每次硬件中断后都要检查最新的hrtimer的到期情况和重新调度请求,如果有那么马上停 掉关心跳模式切出idle进程。下面的代码体现了这一点,在每次进入硬件中断处理的时候都要调用irq_enter:

void irq_enter(void)

{

#ifdef CONFIG_NO_HZ

         int cpu = smp_processor_id();

         if (idle_cpu(cpu) && !in_interrupt())

                 tick_nohz_stop_idle(cpu);

#endif

         __irq_enter();

#ifdef CONFIG_NO_HZ

         if (idle_cpu(cpu))

                 tick_nohz_update_jiffies();  //更新计时,nohz模式由此来作为触发下一中断的时机参考。怎么理解呢?看看这个调用条件,只有在cpu处于idle状态时才更新时间,因为cpu处于idle时可能已经将周期时钟停掉了,为了不遗失时间信息,必须在中断中补上。

#endif

}

nohz 模式下的中断“几乎”是周期的,nohz的字面意义就是非周期,但是它还是基本周期的,因为它没有任何下一个时钟中断的时间点依据;但是hres却是完全 随机时钟中断的,因为它的event_handler中就是操作红黑树上的hrtimer们,因此,它完全可以将下一个到期的hrtimer的到期时刻作为下一个触发时钟中断的时刻,要知道在hres模式里面,所有的时间相关的操作比如计时,节拍调度等都是由hrtimer负责的,如果要选择下一次触发时 钟中断的时机就不能在某一个hrtimer的处理函数里面仲裁了,而必须在全局的处理所有的hrtimer的event_handler函数里面仲裁,这 就是一切。我们看一下cpu_idle:

void cpu_idle(void)

{

         int cpu = smp_processor_id();

         current_thread_info()->status |= TS_POLLING;

         /* endless idle loop with no priority at all */

         while (1) {

                 tick_nohz_stop_sched_tick(1); 

                 while (!need_resched()) {

                         check_pgt_cache();

                         rmb();

                         if (rcu_pending(cpu))

                                 rcu_check_callbacks(cpu, 0);

                         if (cpu_is_offline(cpu))

                                 play_dead();

                         local_irq_disable();

                         __get_cpu_var(irq_stat).idle_timestamp = jiffies;

                         /* Don't trace irqs off for idle */

                         stop_critical_timings();

                         pm_idle();

                         start_critical_timings();

                 }

                 tick_nohz_restart_sched_tick();

                 preempt_enable_no_resched();

                 schedule();

                 preempt_disable();

         }

}

其 中tick_nohz_stop_sched_tick里面调用了next_jiffies = get_next_timer_interrupt(last_jiffies);这一句,此句的意思就是找出下一个最近的timer或者hrtimer 用来将其到期时间作为下一个时钟中断的时间。在tick_nohz_stop_sched_tick中当然要检查重新调度标志,如果置位那么马上返回不再 nohz了,其实在每个硬件中断后的irq_exit里都要调用tick_nohz_stop_sched_tick函数用来在可能的情况下重新对时钟编 程。 
看来linux的设计者考虑的就是周到,这又是一个疯狂的使用并且灵活的发挥硬件作用的例子,linux本身不区分中断优先级在某种意义上纵容了nohz 和hres的出现和发展,如果有一天linux内核变得规则了,有原则了,像windows一样了或者说向unix靠齐了,那么linux的时代也就过去 了,它的性格也就磨平了。 
附加:调度相关的hrtimer 
内核有两个地方调用了调度类的task_tick函数,就是在时钟中断(不考虑nohz和hres)和每运行队列的hrtimer的hrtick处理函数中:

void scheduler_tick(void)

{

         int cpu = smp_processor_id();

         struct rq *rq = cpu_rq(cpu);

         struct task_struct *curr = rq->curr;

         sched_clock_tick();

         spin_lock(&rq->lock);

         update_rq_clock(rq);

         update_cpu_load(rq);

         curr->sched_class->task_tick(rq, curr, 0);  //注意参数

         spin_unlock(&rq->lock);

#ifdef CONFIG_SMP

         rq->idle_at_tick = idle_cpu(cpu);

         trigger_load_balance(rq, cpu);

#endif

}

static enum hrtimer_restart hrtick(struct hrtimer *timer)

{

         struct rq *rq = container_of(timer, struct rq, hrtick_timer);

         WARN_ON_ONCE(cpu_of(rq) != smp_processor_id());

         spin_lock(&rq->lock);

         update_rq_clock(rq);

         rq->curr->sched_class->task_tick(rq, rq->curr, 1);  //注意参数

         spin_unlock(&rq->lock);

         return HRTIMER_NORESTART;

}

以fair调度类为例,其task_tick为task_tick_fair,其中按调度组向上调用了entity_tick:

static void entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)

{

         update_curr(cfs_rq);

#ifdef CONFIG_SCHED_HRTICK

         if (queued) {

                 resched_task(rq_of(cfs_rq)->curr);  // 在hrtimer相关的task_tick的参数为1正是这里的情况,强行调度然后返回,这么猛干嘛啊?要理解这里的方式就要理解每队列 hrtimer 的作用,此hrtimer专门负责记录一个调度时机,该时机必须要调度,为何一定要调度呢?因为在计算这个时机并设置hrtimer的时候要先计算当前进 程还能运行多久,在过了这个时间后hrtimer到期,强制调度,也就是说只要到了hrtick,那就意味着一次调度马上发生

                 return;

         }

         if (!sched_feat(DOUBLE_TICK) &&    //如果上述的hrtimer正在计时,那么就用hrtimer的方式,不再向下进行了。

                         hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))

                 return;

#endif

         if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))   //否则到此处进行常规的更新,检查,调度。

                 check_preempt_tick(cfs_rq, curr);

}

为 何附上这么一段呢?因为每队列的hrtimer要调用task_tick,而如果event_handler中还是要走到task_tick,两个地方做 一件事岂不多余,实际上只有一个地方进行了真正的task_tick,从上面的代码就可以看出来,如果是常规的task_tick进入,那么检查到if (queued) {或者if (!sched_feat(DOUBLE_TICK) &&...的时候如果有每队列hrtimer活动的话,就直接返回了,不会处理下去了,因此可以看出并没有重复。看看怎么设置每队列的 hrtimer吧:

static void hrtick_start_fair(struct rq *rq, struct task_struct *p)

{

         struct sched_entity *se = &p->se;

         struct cfs_rq *cfs_rq = cfs_rq_of(se);

         WARN_ON(task_rq(p) != rq);

         if (hrtick_enabled(rq) && cfs_rq->nr_running > 1) {

                 u64 slice = sched_slice(cfs_rq, se);    //由weight计算出这个进程应该运行多久

                 u64 ran = se->sum_exec_runtime - se->prev_sum_exec_runtime;  //计算这个进程实际运行了多久

                 s64 delta = slice - ran;     //计算二者之差

                 if (delta < 0) {

                         if (rq->curr == p)    //若运行超时那么马上调度

                                 resched_task(p);

                         return;

                 }

                 if (rq->curr != p)

delta = max_t(s64, 10000LL, delta);

                 hrtick_start(rq, delta);    //否则设置定时期hrtimer

         }

}

 本文转自 dog250 51CTO博客,原文链接:http://blog.51cto.com/dog250/1274041



相关文章
|
13天前
|
Linux C语言
Linux内核队列queue.h
Linux内核队列queue.h
|
1月前
|
存储 Shell Linux
【Shell 命令集合 系统设置 】Linux 生成并更新内核模块的依赖 depmod命令 使用指南
【Shell 命令集合 系统设置 】Linux 生成并更新内核模块的依赖 depmod命令 使用指南
31 0
|
1月前
|
Shell Linux C语言
【Shell 命令集合 系统设置 】⭐Linux 卸载已加载的内核模块rmmod命令 使用指南
【Shell 命令集合 系统设置 】⭐Linux 卸载已加载的内核模块rmmod命令 使用指南
29 1
|
2月前
|
Ubuntu Linux 虚拟化
Linux下的IMX6ULL——构建bootloader、内核、文件系统(四)
Linux下的IMX6ULL——构建bootloader、内核、文件系统(四)
75 0
Linux下的IMX6ULL——构建bootloader、内核、文件系统(四)
|
6天前
|
算法 Linux 调度
深入理解Linux内核的进程调度机制
【4月更文挑战第17天】在多任务操作系统中,进程调度是核心功能之一,它决定了处理机资源的分配。本文旨在剖析Linux操作系统内核的进程调度机制,详细讨论其调度策略、调度算法及实现原理,并探讨了其对系统性能的影响。通过分析CFS(完全公平调度器)和实时调度策略,揭示了Linux如何在保证响应速度与公平性之间取得平衡。文章还将评估最新的调度技术趋势,如容器化和云计算环境下的调度优化。
|
11天前
|
算法 Linux 调度
深度解析:Linux内核的进程调度机制
【4月更文挑战第12天】 在多任务操作系统如Linux中,进程调度机制是系统的核心组成部分之一,它决定了处理器资源如何分配给多个竞争的进程。本文深入探讨了Linux内核中的进程调度策略和相关算法,包括其设计哲学、实现原理及对系统性能的影响。通过分析进程调度器的工作原理,我们能够理解操作系统如何平衡效率、公平性和响应性,进而优化系统表现和用户体验。
19 3
|
18天前
|
负载均衡 算法 Linux
深度解析:Linux内核调度器的演变与优化策略
【4月更文挑战第5天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分——内核调度器。文章将首先回顾Linux内核调度器的发展历程,从早期的简单轮转调度(Round Robin)到现代的完全公平调度器(Completely Fair Scheduler, CFS)。接着,分析当前CFS面临的挑战以及社区提出的各种优化方案,最后提出未来可能的发展趋势和研究方向。通过本文,读者将对Linux调度器的原理、实现及其优化有一个全面的认识。
|
18天前
|
Ubuntu Linux
Linux查看内核版本
在Linux系统中查看内核版本有多种方法:1) 使用`uname -r`命令直接显示版本号;2) 通过`cat /proc/version`查看内核详细信息;3) 利用`dmesg | grep Linux`显示内核版本行;4) 如果支持,使用`lsb_release -a`查看发行版及内核版本。
36 6
|
21天前
|
Linux 内存技术
Linux内核读取spi-nor flash sn
Linux内核读取spi-nor flash sn
16 1
|
27天前
|
存储 网络协议 Linux
【Linux 解惑 】谈谈你对linux内核的理解
【Linux 解惑 】谈谈你对linux内核的理解
23 0