在工作中,如果你正在做开发的工作,正在在写代码,这个时候测试同事在测试过程中测出了问题,需要你来定位解决,那么你就应该先暂停写代码的工作,转而来定位解决测试的问题;如果你正在定位测试的问题,这个时候线上系统出现了问题,你就需要先将测试的问题暂停,转而去定位线上的问题。这就是抢占,线上问题优先级比测试问题优先级高,所以线上问题可以抢占测试问题;测试问题比开发工作优先级高,所以测试问题可以抢占开发工作。
在非抢占式内核中,内核线程是不能被抢占的,只有线程主动调用 schedule(),或者显式睡眠以及发生阻塞时发生调度,否则内核其它线程是不能抢占这个线程的。
在抢占式内核中,即使一个线程在运行,没有主动调度 schedule() 或者睡眠以及阻塞,当一个更高优先级的线程被唤醒之后,也可以抢占当前这个线程。
1 线程调度次数
linux 中,每个进程在 /proc 文件夹下都有一个进程对应的文件夹,文件夹以进程 id(pid) 命名,如下图所示。每个进程的文件夹下包括这个进程的很多信息,其中 status 文件中保存着这个进程的基础信息,比如 pid, ppid,进程使用了多少内存,进程的调度次数。
如下截图,是进程 18414 的 status 文件的显示,使用 switch 进行了过滤。其中 voluntary_ctxt_switchs 是自愿调度,nonvoluntary_ctxt_switches 是非自愿调度。
自愿调度 | ① 调用 sleep() 的时候 ② 读写文件或者网络收发包时阻塞 ③ 使用互斥体加锁时,如果不能立即得到锁,那么线程会睡眠,属于自愿调度 |
非自愿调度 | 非自愿调度,意思是当线程还在运行,没有主动触发调度。比如,对于普通调度策略来说,时间片用完时,可以被抢占,这样就会统计一次非自愿调度。 |
自愿调度次数和非自愿调度次数,在进程控制块 struct task_struct 中用两个成员属性来表示,分别是 nvcsw 和 nivcsw。
struct task_struct {.../* Context switch counts: */unsigned long nvcsw;unsigned long nivcsw;...
};
在调度函数 __schedule() 中对自愿调度统计和非自愿调度统计进行递增。如果不是抢占调度,并且进程的状态不是 TASK_RUNNING 的话,就是自愿调度;否则,为非自愿调度。
怎么上一个线程不是 TASK_RUNNING 呢,其实在切换的时候,线程还是处于运行状态的,只不过在调用 schedule() 之前,线程会将自己设置为其它状态。比如在使用 mutex_lock() 加锁的时候,会先将自己设置为 TASK_UNINTERRUPTIBLE 状态,然后再调用 schedule() 进行等待。
static void __sched notrace __schedule(bool preempt)
{struct task_struct *prev, *next;unsigned long *switch_count;switch_count = &prev->nivcsw;if (!preempt && prev_state) {...switch_count = &prev->nvcsw;}if (likely(prev != next)) {...++*switch_count;} else {rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);rq_unlock_irq(rq, &rf);}
}
1.1 用户线程,自愿调度
如下代码,主线程中是一个死循环,每次循环 sleep 1s,每次 sleep 的时候会增加自愿调度计数。这是线程主动睡眠的,而不是时间片用完了被动调度走的,所以是自愿调度。
#include <iostream>
#include <string>
#include <thread>
#include <unistd.h>int main() {while (1) {sleep(1);}return 0;
}
程序运行之后,查看调度次数统计,可以看到自愿调度次数一直在增长,线程没有发生过非自愿调度。
1.2 用户线程,非自愿调度
如下代码,是一个单纯的死循环,在循环中什么都没做。程序运行之后,因为会一直占用 cpu,所以当线程的时间片用完时,线程就会被调度,这种情况下的调度被统计为非自愿调度。
#include <iostream>
#include <string>
#include <thread>
#include <unistd.h>int main() {while (1) {}return 0;
}
程序运行之后,查看调度统计,可以看到非自愿调度计数一直在增长,自愿调度计数是 0。
1.3 非抢占内核,内核线程不会被抢占
如果内核是非抢占内核,那么内核线程在运行的时候就不会被抢占,即使线程一直占用着 cpu,物理时间片和虚拟时间片一直在增长,也不会被抢占。
如下是一个内核模块,在内核模块中使用 kthread_run() 创建了一个内核线程,线程中是一个死循环。在线程中打印了线程的 id。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/sched.h>static struct task_struct *my_thread;// 内核线程函数
static int my_thread_func(void *data)
{// 内核线程的逻辑处理代码printk(KERN_INFO "My kernel thread is running, pid = %d\n", current->pid);while (1);return 0;
}// 模块初始化函数
static int __init my_module_init(void)
{// 创建内核线程my_thread = kthread_run(my_thread_func, NULL, "my_thread");if (IS_ERR(my_thread)) {printk(KERN_ERR "Failed to create kernel thread!\n");return PTR_ERR(my_thread);}printk(KERN_INFO "Module loaded!\n");return 0;
}// 模块清理函数
static void __exit my_module_exit(void)
{// 停止内核线程kthread_stop(my_thread);printk(KERN_INFO "Module unloaded!\n");
}MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Sample kernel module with a kernel thread");module_init(my_module_init);
module_exit(my_module_exit);
编译脚本:
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
线程一致在死循环,没有看到非自愿调度次数增长。
1.4 用户抢占和内核抢占
用户抢占,是指用户态的线程被抢占;内核抢占,是指内核态的线程被抢占。
linux 系统,默认情况下是支持用户抢占的。而是否支持内核抢占,需要看具体的内核配置,在一些嵌入式系统或者桌面系统,对实时性要求高,会打开内核抢占;而在服务器系统中,一般不会打开内核抢占。打开内核抢占的系统,使用 uname -a 可以看到 PREEMPT 标志,没有 PREEMPT 标志,说明没有打开内核抢占。如下是我笔记本上安装的 ubuntu 系统,没有打开内核抢占。
2 CONFIG_PREEMPTION 宏定义了什么内容
当打开内核抢占时,也就是定义了 CONFIG_PREEMPTION 这个宏。那么打开这个宏的时候,具体定义了那些内容呢 ?本人使用的源码版本是 5.10.186。
2.1 中断返回时
中断返回的时候,如果需要抢占调度,那么会调用函数 preempt_schedule_irq()。这段代码一般是使用汇编指令来实现的。如下是 arm 中的实现,下边这段代码,只有定义了 CONFIG_PREEMPTION 时,才会生效。
arch/arm/kernel/entry-armv.S
#ifdef CONFIG_PREEMPTION
svc_preempt:mov r8, lr
1: bl preempt_schedule_irq @ irq en/disable is done insideldr r0, [tsk, #TI_FLAGS] @ get new tasks TI_FLAGStst r0, #_TIF_NEED_RESCHEDreteq r8 @ go againb 1b
#endif
2.2 抢占计数器操作函数
preempt 是抢占的意思。linux 内核中有两个宏 preempt_enable() 和 preempt_disable() 分别时使能抢占和禁止抢占。当定义 CONFIG_PREEMOPTION 宏的时候,在 preempt_enable() 中会进行判断,如果当前条件满足,并且有更高优先级的线程需要抢占的话,那么就会进行抢占调度。如果没有定义 COFIG_PREEMPTION 宏,那么 preempt_enable() 中就不会做抢占调度的工作。
#ifdef CONFIG_PREEMPTION
#define preempt_enable() \
do { \barrier(); \if (unlikely(preempt_count_dec_and_test())) \__preempt_schedule(); \
} while (0)#define preempt_enable_notrace() \
do { \barrier(); \if (unlikely(__preempt_count_dec_and_test())) \__preempt_schedule_notrace(); \
} while (0)#define preempt_check_resched() \
do { \if (should_resched(0)) \__preempt_schedule(); \
} while (0)#else /* !CONFIG_PREEMPTION */
#define preempt_enable() \
do { \barrier(); \preempt_count_dec(); \
} while (0)#define preempt_enable_notrace() \
do { \barrier(); \__preempt_count_dec(); \
} while (0)#define preempt_check_resched() do { } while (0)
#endif /* CONFIG_PREEMPTION */
2.3 _cond_resched
从 _cond_resched 的定义来看,当没有定义 CONFIG_PREEMPTION 的时候,_cond_resched() 才会生效;当定义 CONFIG_PREEMPTION 的时候,直接返回 0。
_cond_resched() 主要是在非抢占内核中起作用,在一些消耗 cpu 的场景主动调用 _cond_resched() 来防止线程占用 cpu 太多。
#ifndef CONFIG_PREEMPTION
extern int _cond_resched(void);
#else
static inline int _cond_resched(void) { return 0; }
#endif#ifndef CONFIG_PREEMPTION
int __sched _cond_resched(void)
{if (should_resched(0)) {preempt_schedule_common();return 1;}rcu_all_qs();return 0;
}
EXPORT_SYMBOL(_cond_resched);
#endif