<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=839&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / Gentoo 之 Preemption Model]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=839</link>
		<description><![CDATA[Gentoo 之 Preemption Model 最近发表的帖子。]]></description>
		<lastBuildDate>Sat, 23 Mar 2024 07:47:18 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[Gentoo 之 Preemption Model]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=958#p958</link>
			<description><![CDATA[<p>今天要分享的是抢占相关的基础知识。本文以内核抢占为引子，概述一下 Linux 抢占的图景。我尽量避开细节问题和源码分析。</p><p>什么是内核抢占？</p><p>别急，咱们慢慢来。</p><p>先理解抢占 （preemption） 这个概念：</p><p>involuntarily suspending a running process is called preemption</p><p>夺取一个进程的 cpu 使用权的行为就叫做抢占。</p><p>根据是否可以支持抢占，多任务操作系统 （multitasking operating system） 分为 2 类：</p><p>1、cooperative multitasking os</p><p>这种 os，进程会一直运行直到它自愿停下来。这种自愿停止运行自己的行为称为 yielding。协作式多任务系统，一听就知道这是一个乌托邦式的系统，只有当所有进程都很 nice 并乐意经常 yielding 时，系统才能正常工作。如果某个进程太傻或者太坏，系统很快就完蛋了。</p><p>2、preemptive multitasking os</p><p>这种 os，会有一个调度器 （scheduler，其实就是一段用于调度进程的程序），scheduler 决定进程何时停止运行以及新进程何时开始运行。当一个进程的 cpu 使用权被 scheduler 分配给另一个进程时，就称前一个进程被抢占了。</p><p>你可以把 sheduler 想象成非常智能的交警，交警按照一定的交通规则、当前的交通状况以及车辆的优先级 （救护车之类的），决定了哪些车可以行驶、哪些车要停下来等待。</p><p>很明显，现阶段，preemptive os 优于 cooperative os。所以 Linux 被设计成 preemptive。</p><p>抢占的核心操作包括 2 个步骤：</p><p>1、从用户态陷入到内核态 （trap kernel），3 个路径：</p><p>a. 系统调用，本质是 soft interrupt，通常就是一条硬件指令 （x86 的 int 0x80）。</p><p>b. 硬件中断，最典型的就是会周期性发生的 timer 中断，或者其他各种外设中断。</p><p>c. exception，例如 page fault、div 0。</p><p>2、陷入到内核态后，在合适的时机下，调用 sheduler 选出一个最重要的进程，如果被选中的不是当前正在运行的进程的话，就会执行 context switch 切换到新的进程。</p><p>根据抢占时机点的不同，抢占分为 2 种类型：</p><p>1、user preemption</p><p>这里的 user 并不是指在 user-space 里进行抢占，而是指在返回 user-space 前进行抢占，具体的：</p><p>When returning to user-space from a system call</p><p>When returning to user-space from an interrupt handler</p><p>即从 system call 和 interrupt handler 返回到 user-space 前进行抢占，这时仍然是在 kernel-space 里，抢占是需要非常高的权限的事情，user-space 没权利也不应该干这事。</p><p>2、kernel preemption</p><p>Linux 2.6 之前是不支持内核抢占的。这意味着当处于用户空间的进程请求内核服务时，在该进程阻塞（进入睡眠）等待某事（通常是 I/O）或系统调用完成之前，不能调度其他进程。支持内核抢占意味着当一个进程在内核里运行时，另一个进程可以抢占第一个进程并被允许运行，即使第一个进程尚未完成其在内核里的工作。</p><p>支持内核抢占 vs 不支持内核抢占</p><p>在上图中，进程 A 已经通过系统调用进入内核，也许是对设备或文件的 write（） 调用。内核代表进程 A 执行时，具有更高优先级的进程 B 被中断唤醒。内核抢占进程 A 并将 CPU 分配给进程 B，即使进程 A 既没有阻塞也没有完成其在内核里的工作。</p><p>内核抢占的时机：</p><p>When an interrupt handler exits， before returning to kernel-space</p><p>When kernel code becomes preemptible again</p><p>If a task in the kernel explicitly calls schedule（）</p><p>If a task in the kernel blocks （which results in a call to schedule（） ）</p><p>为什么要引入内核抢占？</p><p>根本原因：</p><p>trade-offs between latency and throughput</p><p>在系统延迟和吞吐量之间进行权衡。</p><p>并不是说内核抢占就是绝对的好，使用什么抢占机制最优是跟你的应用场景挂钩的。如果不是为了满足用户，内核其实是完全不想进行进程切换的，因为每一次 context switch，都会有 overhead，这些 overhead 就是对 cpu 的浪费，意味着吞吐量的下降。</p><p>但是，如果你想要系统的响应性好一点，就得尽量多的允许抢占的发生，这是 Linux 作为一个通用操作系统所必须支持的。当你的系统做到随时都可以发生抢占时，系统的响应性就会非常好。</p><p>为了让用户根据自己的需求进行配置，Linux 提供了 3 种 Preemption Model。</p><p>CONFIG_PREEMPT_NONE=y：不允许内核抢占，吞吐量最大的 Model，一般用于 Server 系统。</p><p>CONFIG_PREEMPT_VOLUNTARY=y：在一些耗时较长的内核代码中主动调用cond_resched（）让出CPU，对吞吐量有轻微影响，但是系统响应会稍微快一些。</p><p>CONFIG_PREEMPT=y：除了处于持有 spinlock 时的 critical section，其他时候都允许内核抢占，响应速度进一步提升，吞吐量进一步下降，一般用于 Desktop / Embedded 系统。</p><p>另外，还有一个没有合并进主线内核的 Model： CONFIG_PREEMPT_RT，这个模式几乎将所有的 spinlock 都换成了 preemptable mutex，只剩下一些极其核心的地方仍然用禁止抢占的 spinlock，所以基本可以认为是随时可被抢占。</p><p>抢占前的检查</p><p>这里的检查是同时针对所有的 preemption 的。如果你理解了前面的 4 种 preempiton model 的话，应该能感觉到其实是不用太严格区分 user / kernel preemption，所有抢占的作用和性质都一样：降低 lantency，完全可以将它们一视同仁。</p><p>抢占的发生要同时满足两个条件：</p><p>需要抢占;</p><p>能抢占;</p><p>1、是否需要抢占？</p><p>判断是否需要抢占的依据是：thread_info 的成员 flags 是否设置了 TIF_NEED_RESCHED 标志位。</p><p>相关的 API：</p><p>set_tsk_need_resched（） 用于设置该 flag。</p><p>tif_need_resched（） 被用来判断该 flag 是否置位。</p><p>resched_curr（struct rq *rq），标记当前 runqueue 需要抢占。</p><p>2、是否能抢占？</p><p>抢占发生的前提是要确保此次抢占是安全的 （preempt-safe）。什么才是 preempt-safe：不产生 race condition / deadlock。</p><p>值得注意的是，只有 kernel preemption 才有被禁止的可能，而 user preemption 总是被允许，因此这时马上就要返回 user space 了，肯定是处于一个可抢占的状态了。</p><p>在引入内核抢占机制的同时引入了为 thread_info 添加了新的成员：preempt_count ，用来保证抢占的安全性，获取锁时会增加 preempt_count，释放锁时则会减少。抢占前会检查 preempt_count 是否为 0，为 0 才允许抢占。</p><p>相关的 API：</p><p>preempt_enable（），使能内核抢占，可嵌套调用。</p><p>preempt_disable（），关闭内核抢占，可嵌套调用。</p><p>preempt_count（），返回 preempt_count。</p><p>什么场景会设置需要抢占 （TIF_NEED_RESCHED = 1）</p><p>通过 grep resched_curr 可以找出大多数标记抢占的场景。</p><p>下面列举的是几个我比较关心的场景。</p><p>1、周期性的时钟中断</p><p>时钟中断处理函数会调用 scheduler_tick（），它通过调度类（scheduling class） 的 task_tick 方法 检查进程的时间片是否耗尽，如果耗尽则标记需要抢占：</p><p>// kernel/sched/core.c</p><p>void scheduler_tick（void）</p><p>{</p><p>［。..］</p><p>curr-》sched_class-》task_tick（rq， curr， 0）;</p><p>［。..］</p><p>}</p><p>Linux 的调度策略被封装成调度类，例如 CFS、Real-Time。CFS 调度类的 task_tick（） 如下：</p><p>// kernel/sched/fair.c</p><p>task_tick_fair（）</p><p>-》 entity_tick（）</p><p>-》 resched_curr（rq_of（cfs_rq））;</p><p>2、唤醒进程的时候</p><p>当进程被唤醒的时候，如果优先级高于 CPU 上的当前进程，就会触发抢占。相应的内核代码中，try_to_wake_up（） 最终通过 check_preempt_curr（） 检查是否标记需要抢占：</p><p>// kernel/sched/core.c</p><p>void check_preempt_curr（struct rq *rq， struct task_struct *p， int flags）</p><p>{</p><p>const struct sched_class *class;</p><p>if （p-》sched_class == rq-》curr-》sched_class） {</p><p>rq-》curr-》sched_class-》check_preempt_curr（rq， p， flags）;</p><p>} else {</p><p>for_each_class（class） {</p><p>if （class == rq-》curr-》sched_class）</p><p>break;</p><p>if （class == p-》sched_class） {</p><p>resched_curr（rq）;</p><p>break;</p><p>}</p><p>}</p><p>}</p><p>［。..］</p><p>}</p><p>参数 “p” 指向被唤醒进程，“rq” 代表抢占的 CPU。如果 p 的调度类和 rq 当前的调度类相同，则调用 rq 当前的调度类的 check_preempt_curr（） （例如 cfs 的 check_preempt_wakeup（）） 来判断是否要标记需要抢占。</p><p>如果 p 的调度类 》 rq 当前的调度类，则用 resched_curr（） 标记需要抢占，反之，则不标记。</p><p>3、新进程创建的时候</p><p>如果新进程的优先级高于 CPU 上的当前进程，会需要触发抢占。相应的代码是 sched_fork（），它再通过调度类的 task_fork（） 标记需要抢占：</p><p>// kernel/sched/core.c</p><p>int sched_fork（unsigned long clone_flags， struct task_struct *p）</p><p>{</p><p>［。..］</p><p>if （p-》sched_class-》task_fork）</p><p>p-》sched_class-》task_fork（p）;</p><p>［。..］</p><p>}</p><p>// kernel/sched/fair.c</p><p>static void task_fork_fair（struct task_struct *p）</p><p>{</p><p>［。..］</p><p>if （sysctl_sched_child_runs_first &amp;&amp; curr &amp;&amp; entity_before（curr， se）） {</p><p>resched_curr（rq）;</p><p>}</p><p>［。..］</p><p>}</p><p>4、进程修改 nice 值的时候</p><p>如果修改进程 nice 值导致优先级高于 CPU 上的当前进程，也要标记需要抢占，代码见 set_user_nice（）。</p><p>// kernel/sched/core.c</p><p>void set_user_nice（struct task_struct *p， long nice）</p><p>{</p><p>［。..］</p><p>// If the task increased its priority or is running and lowered its priority， then reschedule its CPU</p><p>if （delta 《 0 || （delta 》 0 &amp;&amp; task_running（rq， p）））</p><p>resched_curr（rq）;</p><p>}</p><p>还有很多场景，这里就不一一列举了。</p><p>什么场景下要禁止内核抢占 （preempt_count 》 0）</p><p>有几种场景是明确需要关闭内核抢占的。</p><p>1、访问 Per-CPU data structures 的时候</p><p>看下面这个例子：</p><p>struct this_needs_locking tux［NR_CPUS］;</p><p>tux［smp_processor_id（）］ = some_value;</p><p>/* task is preempted here.。. */</p><p>something = tux［smp_processor_id（）］;</p><p>如果抢占发生在注释所在的那一行，当进程再次被调度时，smp_processor_id（） 值可能已经发生变化了，这种场景下需要通过禁止内核抢占来做到 preempt safe。</p><p>2、访问 CPU state 的时候</p><p>这个很好理解，你正在操作 CPU 相关的寄存器以进行 context switch 时，肯定是不能再允许抢占。</p><p>asmlinkage __visible void __sched schedule（void）</p><p>{</p><p>struct task_struct *tsk = current;</p><p>sched_submit_work（tsk）;</p><p>do {</p><p>// 调度前禁止内核抢占</p><p>preempt_disable（）;</p><p>__schedule（false）;</p><p>sched_preempt_enable_no_resched（）;</p><p>} while （need_resched（））;</p><p>sched_update_worker（tsk）;</p><p>}</p><p>3、持有 spinlock 的时候</p><p>支持内核抢占，这意味着进程有可能与被抢占的进程在相同的 critical section 中运行。为防止这种情况，当持有自旋锁时，要禁止内核抢占。</p><p>static inline void __raw_spin_lock（raw_spinlock_t *lock）</p><p>{</p><p>preempt_disable（）;</p><p>spin_acquire（&amp;lock-》dep_map， 0， 0， _RET_IP_）;</p><p>LOCK_CONTENDED（lock， do_raw_spin_trylock， do_raw_spin_lock）;</p><p>}</p><p>还有很多场景，这里就不一一列举了。</p><p>真正执行抢占的地方</p><p>这部分是 platform 相关的，下面以 ARM64 Linux-5.4 为例，快速看下执行抢占的具体代码。</p><p>执行 user preemption</p><p>系统调用和中断返回用户空间的时候：</p><p>它们都是在 ret_to_user（） 里判断是否执行用户抢占。</p><p>// arch/arm64/kernel/entry.S</p><p>ret_to_user（） // 返回到用户空间</p><p>work_pending（）</p><p>do_notify_resume（）</p><p>schedule（）</p><p>// arch/arm64/kernel/signal.c</p><p>asmlinkage void do_notify_resume（struct pt_regs *regs，</p><p>unsigned long thread_flags）</p><p>{</p><p>do {</p><p>［。..］</p><p>// 检查是否要需要调度</p><p>if （thread_flags &amp; _TIF_NEED_RESCHED） {</p><p>local_daif_restore（DAIF_PROCCTX_NOIRQ）;</p><p>schedule（）;</p><p>} else {</p><p>［。..］</p><p>} while （thread_flags &amp; _TIF_WORK_MASK）;</p><p>}</p><p>执行 kernel preemption</p><p>中断返回内核空间的时候：</p><p>// arch/arm64/kernel/entry.S</p><p>el1_irq</p><p>irq_handler</p><p>arm64_preempt_schedule_irq</p><p>preempt_schedule_irq</p><p>__schedule（true）</p><p>// kernel/sched/core.c</p><p>/* This is the entry point to schedule（） from kernel preemption */</p><p>asmlinkage __visible void __sched preempt_schedule_irq（void）</p><p>{</p><p>［。..］</p><p>do {</p><p>preempt_disable（）;</p><p>local_irq_enable（）;</p><p>__schedule（true）;</p><p>local_irq_disable（）;</p><p>sched_preempt_enable_no_resched（）;</p><p>} while （need_resched（））;</p><p>exception_exit（prev_state）;</p><p>}</p><p>内核恢复为可抢占的时候：</p><p>前面列举了集中关闭抢占的场景，当离开这些场景时，会恢复内核抢占。</p><p>例如 spinlock unlock 时：</p><p>static inline void __raw_spin_unlock（raw_spinlock_t *lock）</p><p>{</p><p>spin_release（&amp;lock-》dep_map， 1， _RET_IP_）;</p><p>do_raw_spin_unlock（lock）;</p><p>preempt_enable（）; // 使能抢占时，如果需要，就会执行抢占</p><p>}</p><p>// include/linux/preempt.h</p><p>#define preempt_enable（）</p><p>do {</p><p>barrier（）;</p><p>if （unlikely（preempt_count_dec_and_test（）））</p><p>__preempt_schedule（）;</p><p>} while （0）</p><p>内核显式地要求调度的时候：</p><p>内核里有大量的地方会显式地要求进行调度，最常见的是：cond_resched（） 和 sleep（）类函数，它们最终都会调用到 __schedule（）。</p><p>内核阻塞的时候：</p><p>例如 mutex，sem，waitqueue 获取不到资源，或者是等待 IO。这种情况下进程会将自己的状态从TASK_RUNNING 修改为 TASK_INTERRUPTIBLE，然后调用 schedule（） 主动让出 CPU 并等待唤醒。</p><p>// block/blk-core.c</p><p>static struct request *get_request（struct request_queue *q， int op，</p><p>int op_flags， struct bio *bio，</p><p>gfp_t gfp_mask）</p><p>{</p><p>［。..］</p><p>prepare_to_wait_exclusive（&amp;rl-》wait［is_sync］， &amp;wait，</p><p>TASK_UNINTERRUPTIBLE）;</p><p>io_schedule（）; // 会调用 schedule（）;</p><p>［。..］</p><p>}</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Sat, 23 Mar 2024 07:47:18 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=958#p958</guid>
		</item>
	</channel>
</rss>
