内核抢占,让世界变得更美好 | Linux 内核

大家好,我是老吴。

今天要分享的是抢占相关的基础知识。

本文以内核抢占为引子,概述一下 Linux 抢占的图景。

我尽量避开细节问题和源码分析。

什么是内核抢占?

别急,咱们慢慢来。

先理解抢占 (preemption) 这个概念

involuntarily suspending a running process is called preemption

夺取一个进程的 cpu 使用权的行为就叫做抢占。

根据是否可以支持抢占,多任务操作系统 (multitasking operating system) 分为 2 类

1、cooperative multitasking os

这种 os,进程会一直运行直到它自愿停下来。这种自愿停止运行自己的行为称为 yielding。协作式多任务系统,一听就知道这是一个乌托邦式的系统,只有当所有进程都很 nice 并乐意经常 yielding 时,系统才能正常工作。如果某个进程太傻或者太坏,系统很快就完蛋了。

2、preemptive multitasking os

这种 os,会有一个调度器 (scheduler,其实就是一段用于调度进程的程序),scheduler 决定进程何时停止运行以及新进程何时开始运行。当一个进程的 cpu 使用权被 scheduler 分配给另一个进程时,就称前一个进程被抢占了。

你可以把 sheduler 想象成非常智能的交警,交警按照一定的交通规则、当前的交通状况以及车辆的优先级 (救护车之类的),决定了哪些车可以行驶、哪些车要停下来等待。

很明显,现阶段,preemptive os 优于 cooperative os。所以 Linux 被设计成 preemptive。

抢占的核心操作包括 2 个步骤

1、从用户态陷入到内核态 (trap kernel),3 个路径:

a. 系统调用,本质是 soft interrupt,通常就是一条硬件指令 (x86 的 int 0x80)。

b. 硬件中断,最典型的就是会周期性发生的 timer 中断,或者其他各种外设中断.

c. exception,例如 page fault、div 0。

点击查看大图

2、陷入到内核态后,在合适的时机下,调用 sheduler 选出一个最重要的进程,如果被选中的不是当前正在运行的进程的话,就会执行 context switch 切换到新的进程。

根据抢占时机点的不同,抢占分为 2 种类型

1、user preemption

这里的 user 并不是指在 user-space 里进行抢占,而是指在返回 user-space 前进行抢占,具体的:

When returning to user-space from a system call

When returning to user-space from an interrupt handler

即从 system call 和 interrupt handler 返回到 user-space 前进行抢占,这时仍然是在 kernel-space 里,抢占是需要非常高的权限的事情,user-space 没权利也不应该干这事。

2、kernel preemption

Linux 2.6 之前是不支持内核抢占的。这意味着当处于用户空间的进程请求内核服务时,在该进程阻塞(进入睡眠)等待某事(通常是 I/O)或系统调用完成之前,不能调度其他进程。支持内核抢占意味着当一个进程在内核里运行时,另一个进程可以抢占第一个进程并被允许运行,即使第一个进程尚未完成其在内核里的工作。

支持内核抢占 vs 不支持内核抢占

举个例子:

点击查看大图

在上图中,进程 A 已经通过系统调用进入内核,也许是对设备或文件的 write() 调用。内核代表进程 A 执行时,具有更高优先级的进程 B 被中断唤醒。内核抢占进程 A 并将 CPU 分配给进程 B,即使进程 A 既没有阻塞也没有完成其在内核里的工作。

内核抢占的时机:

When an interrupt handler exits, before returning to kernel-space

When kernel code becomes preemptible again

If a task in the kernel explicitly calls schedule()

If a task in the kernel blocks (which results in a call to schedule() )


为什么要引入内核抢占?

根本原因

trade-offs between latency and throughput

在系统延迟和吞吐量之间进行权衡。

并不是说内核抢占就是绝对的好,使用什么抢占机制最优是跟你的应用场景挂钩的。如果不是为了满足用户,内核其实是完全不想进行进程切换的,因为每一次 context switch,都会有 overhead,这些 overhead 就是对 cpu 的浪费,意味着吞吐量的下降。

但是,如果你想要系统的响应性好一点,就得尽量多的允许抢占的发生,这是 Linux 作为一个通用操作系统所必须支持的。当你的系统做到随时都可以发生抢占时,系统的响应性就会非常好。

为了让用户根据自己的需求进行配置,Linux 提供了 3 种 Preemption Model。

点击查看大图

CONFIG_PREEMPT_NONE=y:不允许内核抢占,吞吐量最大的 Model,一般用于 Server 系统。

点击查看大图

CONFIG_PREEMPT_VOLUNTARY=y:在一些耗时较长的内核代码中主动调用cond_resched()让出CPU,对吞吐量有轻微影响,但是系统响应会稍微快一些。

点击查看大图

CONFIG_PREEMPT=y:除了处于持有 spinlock 时的 critical p,其他时候都允许内核抢占,响应速度进一步提升,吞吐量进一步下降,一般用于 Desktop / Embedded 系统。

点击查看大图

另外,还有一个没有合并进主线内核的 Model: CONFIG_PREEMPT_RT,这个模式几乎将所有的 spinlock 都换成了 preemptable mutex,只剩下一些极其核心的地方仍然用禁止抢占的 spinlock,所以基本可以认为是随时可被抢占。

点击查看大图


抢占前的检查

这里的检查是同时针对所有的 preemption 的。如果你理解了前面的 4 种 preempiton model 的话,应该能感觉到其实是不用太严格区分 user / kernel preemption,所有抢占的作用和性质都一样:降低 lantency,完全可以将它们一视同仁。

抢占的发生要同时满足两个条件

  • 需要抢占;

  • 能抢占;


1、是否需要抢占?

判断是否需要抢占的依据是:thread_info 的成员 flags 是否设置了 TIF_NEED_RESCHED 标志位。

相关的 API:

  • set_tsk_need_resched() 用于设置该 flag。

  • tif_need_resched() 被用来判断该 flag 是否置位。

  • resched_curr(struct rq *rq),标记当前 runqueue 需要抢占。


2、是否能抢占?

抢占发生的前提是要确保此次抢占是安全的 (preempt-safe)。什么才是 preempt-safe:不产生 race condition / deadlock。

值得注意的是,只有 kernel preemption 才有被禁止的可能,而 user preemption 总是被允许,因此这时马上就要返回 user space 了,肯定是处于一个可抢占的状态了。

在引入内核抢占机制的同时引入了为 thread_info 添加了新的成员:preempt_count ,用来保证抢占的安全性,获取锁时会增加 preempt_count,释放锁时则会减少。抢占前会检查 preempt_count 是否为 0,为 0 才允许抢占。

相关的 API:

  • preempt_enable(),使能内核抢占,可嵌套调用。

  • preempt_disable(),关闭内核抢占,可嵌套调用。

  • preempt_count(),返回 preempt_count。


什么场景会设置需要抢占 (TIF_NEED_RESCHED = 1)

通过 grep resched_curr 可以找出大多数标记抢占的场景。

下面列举的是几个我比较关心的场景。


1、周期性的时钟中断

时钟中断处理函数会调用 scheduler_tick(),它通过调度类(scheduling class) 的 task_tick 方法 检查进程的时间片是否耗尽,如果耗尽则标记需要抢占:

// kernel/sched/core.c
void scheduler_tick(void)
{[...]curr->sched_class->task_tick(rq, curr, 0);[...]
}

Linux 的调度策略被封装成调度类,例如 CFS、Real-Time。CFS 调度类的 task_tick() 如下:

// kernel/sched/fair.c
task_tick_fair()-> entity_tick()-> resched_curr(rq_of(cfs_rq));


2、唤醒进程的时候

当进程被唤醒的时候,如果优先级高于 CPU 上的当前进程,就会触发抢占。相应的内核代码中,try_to_wake_up() 最终通过 check_preempt_curr() 检查是否标记需要抢占:

// kernel/sched/core.c
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{const struct sched_class *class;if (p->sched_class == rq->curr->sched_class) {rq->curr->sched_class->check_preempt_curr(rq, p, flags);} else {for_each_class(class) {if (class == rq->curr->sched_class)break;if (class == p->sched_class) {resched_curr(rq);break;}}}[...]
}

参数 "p" 指向被唤醒进程,"rq" 代表抢占的 CPU。如果 p 的调度类和 rq 当前的调度类相同,则调用 rq 当前的调度类的 check_preempt_curr() (例如 cfs 的 check_preempt_wakeup()) 来判断是否要标记需要抢占。

如果 p 的调度类 > rq 当前的调度类,则用 resched_curr() 标记需要抢占,反之,则不标记。


3、新进程创建的时候

如果新进程的优先级高于 CPU 上的当前进程,会需要触发抢占。相应的代码是 sched_fork(),它再通过调度类的 task_fork() 标记需要抢占:

// kernel/sched/core.c
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{[...]if (p->sched_class->task_fork)p->sched_class->task_fork(p);[...]
}// kernel/sched/fair.c
static void task_fork_fair(struct task_struct *p)
{[...]if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {resched_curr(rq);}[...]
}


4、进程修改 nice 值的时候

如果修改进程 nice 值导致优先级高于 CPU 上的当前进程,也要标记需要抢占,代码见 set_user_nice()。

// kernel/sched/core.c
void set_user_nice(struct task_struct *p, long nice)
{[...]// If the task increased its priority or is running and lowered its priority, then reschedule its CPUif (delta < 0 || (delta > 0 && task_running(rq, p)))resched_curr(rq);
}

还有很多场景,这里就不一一列举了。


什么场景下要禁止内核抢占 (preempt_count > 0)

有几种场景是明确需要关闭内核抢占的。

1、访问 Per-CPU data structures 的时候

看下面这个例子:

struct this_needs_locking tux[NR_CPUS];
tux[smp_processor_id()] = some_value;
/* task is preempted here... */
something = tux[smp_processor_id()];

如果抢占发生在注释所在的那一行,当进程再次被调度时,smp_processor_id() 值可能已经发生变化了,这种场景下需要通过禁止内核抢占来做到 preempt safe。


2、访问 CPU state 的时候

这个很好理解,你正在操作 CPU 相关的寄存器以进行 context switch 时,肯定是不能再允许抢占。

asmlinkage __visible void __sched schedule(void)
{struct task_struct *tsk = current;sched_submit_work(tsk);do {// 调度前禁止内核抢占preempt_disable();__schedule(false);sched_preempt_enable_no_resched();} while (need_resched());sched_update_worker(tsk);
}


3、持有 spinlock 的时候

支持内核抢占,这意味着进程有可能与被抢占的进程在相同的 critical p 中运行。为防止这种情况,当持有自旋锁时,要禁止内核抢占。

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{preempt_disable();spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

还有很多场景,这里就不一一列举了。


真正执行抢占的地方

这部分是 platform 相关的,下面以 ARM64 Linux-5.4 为例,快速看下执行抢占的具体代码。

执行 user preemption

系统调用和中断返回用户空间的时候:

它们都是在 ret_to_user() 里判断是否执行用户抢占。

// arch/arm64/kernel/entry.S
ret_to_user() // 返回到用户空间work_pending()do_notify_resume()schedule() // arch/arm64/kernel/signal.c
asmlinkage void do_notify_resume(struct pt_regs *regs,unsigned long thread_flags)
{do {[...]// 检查是否要需要调度if (thread_flags & _TIF_NEED_RESCHED) {local_daif_restore(DAIF_PROCCTX_NOIRQ);schedule();} else {[...]} while (thread_flags & _TIF_WORK_MASK);
}


执行 kernel preemption

中断返回内核空间的时候:

// arch/arm64/kernel/entry.S
el1_irqirq_handlerarm64_preempt_schedule_irqpreempt_schedule_irq__schedule(true) // kernel/sched/core.c
/* This is the entry point to schedule() from kernel preemption */
asmlinkage __visible void __sched preempt_schedule_irq(void)
{[...]do {preempt_disable();local_irq_enable();__schedule(true);local_irq_disable();sched_preempt_enable_no_resched();} while (need_resched());exception_exit(prev_state);
}

内核恢复为可抢占的时候:

前面列举了集中关闭抢占的场景,当离开这些场景时,会恢复内核抢占。

例如 spinlock unlock 时:

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{spin_release(&lock->dep_map, 1, _RET_IP_);do_raw_spin_unlock(lock);preempt_enable();  // 使能抢占时,如果需要,就会执行抢占
}// include/linux/preempt.h
#define preempt_enable() \
do { \barrier(); \if (unlikely(preempt_count_dec_and_test())) \__preempt_schedule(); \
} while (0)

内核显式地要求调度的时候:

内核里有大量的地方会显式地要求进行调度,最常见的是:cond_resched() 和 sleep()类函数,它们最终都会调用到 __schedule()。

内核阻塞的时候:

例如 mutex,sem,waitqueue 获取不到资源,或者是等待 IO。这种情况下进程会将自己的状态从 TASK_RUNNING 修改为 TASK_INTERRUPTIBLE,然后调用 schedule() 主动让出 CPU 并等待唤醒。

// block/blk-core.c
static struct request *get_request(struct request_queue *q, int op,int op_flags, struct bio *bio,gfp_t gfp_mask)
{[...]prepare_to_wait_exclusive(&rl->wait[is_sync], &wait,TASK_UNINTERRUPTIBLE);io_schedule();  // 会调用 schedule();[...]
}


相关参考

《Linux Kernel Development, Third Edition》

《Understanding the Linux Kernel, Third Edition》

《Linux Device Drivers, Third Edition》

《深入理解 Linux 设备驱动程序内核机制》

《Embedded Linux Primer》

https://www.kernel.org/doc/Documentation/preempt-locking.txt


推荐阅读:

专辑|Linux文章汇总

专辑|程序人生

专辑|C语言

我的知识小密圈

关注公众号,后台回复「1024」获取学习资料网盘链接。

欢迎点赞,关注,转发,在看,您的每一次鼓励,我都将铭记于心~

嵌入式Linux

微信扫描二维码,关注我的公众号

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/465374.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ESXI3.5 单网卡实现iSCSi连接

其实这个也没啥好说的&#xff0c;只是很多文章都是建议大家在做ESXI方案时&#xff0c;要配置专用的设备卡/网络卡来接 SAN设备&#xff0c;由于环境限制&#xff0c;我这里是用Openfiler2.3来当SAN用&#xff01; 步骤: 1.直接启用 iSCSI Software Adapter 连接 2.启动 i…

体制内工作了十几年,跳出来学嵌入式合适吗?

这说的不是我&#xff0c;是我原公司的一个朋友&#xff0c;他原来的同事。我这个朋友原来毕业是在体制内工作&#xff0c;但学习的是电子专业&#xff0c;后因为体制内不适应&#xff0c;或者说是体制内太轻松了&#xff0c;就出来折腾嵌入式。这一折腾就是快十来年&#xff0…

适合嵌入式的C++开源项目-Workflow

在来腾讯之前&#xff0c;我是没有真正用C开发过一个完整的项目的&#xff0c;然后&#xff0c;赶鸭子上架&#xff0c;鹅厂的人特别喜欢用C&#xff0c;而且用的特别好。我这次推荐一个开源的C项目&#xff0c;希望喜欢C&#xff0c;或者想往C方向发展的同学可以看看。面向过程…

几段失败的代码

1、下面一段代码将注释和代码混在了一起&#xff0c;不认真看还真不知道。高亮显示后&#xff1a;2、看到这种多层嵌套恶心到头大。3、据说某俄国特工经过九死一生偷到了NASA的太空火箭发射程序的源代码的最后一页&#xff0c;代码是&#xff1a;)))))))))))))))))))))))))))))…

java 发送邮件添加附件_java邮件自动发送时添加网络附件

查了很多资料都是使用本地资源作为附件发送。但是数据和程序分开时&#xff0c;还需要先下载数据&#xff0c;保存到本地再发送邮件。查了很多资料后&#xff0c;发现apach提供了很好的一个commons-mail.jar包&#xff0c;做了很好的封装&#xff0c;简单易用。下载地址是http:…

Excel 取消Internet及网络路径自动替换为超链接

Excel 取消Internet及网络路径自动替换为超链接&#xff1a; 1.文件 》选项 》校对 点击自动更正选项 2. 自动更正 配置修改 点击“键入时自动套用格式”tab页&#xff0c;取消勾选 internet及网络路径替换为超链接 转载于:https://www.cnblogs.com/byronliu029/p/9408468.htm…

那些喝吐的酒~

酒逢知己千杯少我喝不了酒&#xff0c;但是高兴的时候也能喝两杯。入职腾讯&#xff0c;参加了两次部门聚会&#xff0c;都离不开喝酒&#xff0c;吃饭喝酒这事&#xff0c;大家都很开心&#xff0c;那种眼前朦胧&#xff0c;胆子很肥的感觉特别好。领导们也很给面子&#xff0…

实时内核(Core)和实时操作系统(RTOS)有何不同?

大家都知道什么是实时操作系统&#xff0c;但是&#xff0c;大家熟知的实时操作系统&#xff0c;站在更广的角度来看&#xff0c;其实它就是一个实时内核。一、实时内核和实时操作系统一个实时内核是管理微处理器&#xff08;MPU&#xff09;、微控制器&#xff08;MCU&#xf…

linux java远程调试_idea远程linux代码调试

如果线上代码出现问题&#xff0c;如何用本地idea排查问题查找问题&#xff1f;第一步&#xff1a;保证本地代码与线上代码一致&#xff1b;第二步&#xff1a;在线上tomcat文件中/tomcat/bin/catalina.sh文件中添加如下配置找到下面一行信息#----- Execute The Requested Comm…

[文档]. Xilinx - 编写有效的Testbenches

This application note is written for logic designers who are new to HDL verification flows, and who do not have extensive testbench-writing experience. 下载

mysql inner join where_详解mysql 使用left join添加where条件的问题分析

当前需求&#xff1a;有group和factor两张表&#xff0c;一个group对应多个factor&#xff0c;现在想查询有效的group和对应的有效的factor&#xff0c;两个表都有isDel逻辑删除标志。最开始的错误写法一&#xff1a;SELECT g.*,f.* FROM groups g LEFT JOIN factor f ON f.gro…

误打误撞的模板字符串

前情回顾&#xff1a;之所以用到模板字符串呢&#xff0c;是在这样一个场景中用到一个类似微信发朋友圈时选择图片时的一个场景&#xff0c;如图所示 当时脑抽是怎么想的呢&#xff0c;其实也很简单&#xff0c;当时想的就是新建div嘛&#xff0c;然后append到父容器就ok啦&…

java类型转换方法_Java中基本数据类型转换的方法

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼一、自动类型转换(隐式类型转换)整型、实型(常量)、字符型数据可以混合运算。运算中&#xff0c;不同类型的数据先转化为同一类型&#xff0c;然后进行运算。转换从低级到高级。自动转换有以下规律&#xff1a;小的类型自动转化为大…

你知道GNU C 对C语言的扩展吗?

为了方便使用&#xff0c;GNU C在标准C语言的基础上进行了部分方便开发的扩展。这里讲解一些开发中可能会用到的&#xff0c;或者使用频率比较高的内容。零长度数组和变量长度数组GNU C 允许使用零长度数组&#xff0c;比如&#xff1a;char data[0];GNU C 允许使用一个变量定义…

java 反射 内存_Java内存到反射入门

Java内存到反射入门(一)初识Java内存平常我们最常提到的的JAVA分区是这三个分区其中方法区是一个特殊的堆&#xff0c;功能如图上所示。初识反射&#xff1a;框架设计的灵魂反射的功能&#xff1a;将类的各个组成部分封装为对象&#xff0c;并在程序运行的过程中可以调用它们。…

stopping hbasecat: /tmp/hbase-elastic-master.pid: 没有那个文件或目录

stop-hbase.sh 挂了。。。 原因其实也简单&#xff0c;默认情况下hbase的pid文件保存在/tmp目录下&#xff0c;/tmp目录下的文件很容易丢失&#xff0c;不知道为什么他不会自动创建一个非临时目录&#xff0c;大概是因为权限不好控制&#xff0c;需要手动来&#xff1f; 修改为…

服务容错模式

0.背景 随着服务框架和服务治理体系的逐步成熟&#xff0c;服务化已成为系统设计的趋势。随着业务复杂度的增加&#xff0c;依赖的服务也逐步增加&#xff0c;出现了不少由于服务调用出现异常问题而导致的重大事故&#xff0c;如&#xff1a; 1&#xff09;系统依赖的某个服务发…

从大学到结婚,我和小云的这13年

看了龙哥的文章&#xff0c;感触很深&#xff0c;我有好几次想写一篇我和小云的文章&#xff0c;记录下自己和小云这些年一起经历的很多事情&#xff0c;但是每每都停下来去做了觉得更重要的其他事情&#xff0c;这次真的决定要写了。我和小云是大学同学&#xff0c;我们大一认…

xshell如何运行java_利用Xshell往应用服务器上部署项目

1.首先查询中间件的进程Idps -ef|grep ./startapusic2.然后杀掉进程kill -9 16833.再次查询进程&#xff0c;查看是否关闭成功&#xff0c;如果没有/bin目录下的东西了说明关闭成功4.利用ls查看当前路径下的文件5.启动中间件./startapusic6.如果显示地址已在使用&#xff0c;则…

详解Linux内核在arm上的启动过程

Linux内核加载过程通常&#xff0c;Linux内核都是经过gzip加载过之后的映像文件。bootloader复制压缩内核到内存空间。内核自解压。运行内核。编译完成的Linux内核存放在哪里&#xff1f;./vmlinux elf格式未压缩内核。arch/arm/boot/compressed/vmlinux 压缩以后的elf格…