一文讲透linux内核抢占

本文详细介绍了Linux内核的三种抢占模型:非抢占式(适合服务器)、抢占式(适合桌面系统)和自愿内核抢占(介于两者之间)。通过实验展示了不同模型下进程的执行情况,并分析了代码实现,解释了抢占点的增加如何使得内核在特定情况下变为可抢占。

三种抢占模型概述


在linux内核选项中存在存在三种抢占模型:

       │ │       ( ) No Forced Preemption (Server)                   │ │  
       │ │       (X) Voluntary Kernel Preemption (Desktop)           │ │  
       │ │       ( ) Preemptible Kernel (Low-Latency Desktop) 
  • No Forced Preemption (Server)
    非抢占式,适合server系统,这是因为非抢占式内核会减少进程上下文切换的次数,从而能将节省下来的这部分开销用在其他有用的任务上。另外这里要注意的是非抢占是指内核态任务非抢占,用户态任务是可以抢占的,试想如果用户态的任务都无法抢占,linux怎么还能称之为多任务操作系统。
  • Preemptible Kernel (Low-Latency Desktop)
    抢占式,是指内核态任务是可以抢占的,适合桌面系统,这是因为桌面系统比较注重响应速度,所以该抢占时就要抢占。
  • Voluntary Kernel Preemption (Desktop)
    自愿内核抢占,也就是说内核可以自愿被抢占,也可用于桌面系统,介于可抢占和不可抢占之间。

亲身感受抢占与非抢占内核


通过如下三个实验先来感受一下这三种抢占模型的效果:

  • 实验代码代码内核部分
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/delay.h>

struct cdev cdev;
dev_t devno;
struct class *class;

int my_open(struct inode *inode, struct file *file)
{
	printk("open\n");
	mdelay(5000);
	return 0;
}

static const struct file_operations my_ops = {
	.owner =  THIS_MODULE,
	.open  =  my_open,
};
static int __init my_test_init(void)
{
	unsigned int major;
	
	printk("my test init\n");
	
	alloc_chrdev_region(&devno, 0, 1, "mytest");
	major = MAJOR(devno);
	cdev_init(&cdev, &my_ops);
	cdev_add(&cdev, devno, 1);
	
	class = class_create(THIS_MODULE, "mytest");	
	device_create(class, NULL, MKDEV(major, 0), NULL, "mytest");
	return 0;
}

static void __exit my_test_exit(void)
{
	cdev_del(&cdev);
	unregister_chrdev_region(devno, 1);
}


module_init(my_test_init);
module_exit(my_test_exit);

MODULE_AUTHOR("HanterLiu");
MODULE_DESCRIPTION("test");
MODULE_LICENSE("GPL v2");

代码很简单,创建了一个字符设备,(exit接口没有删干净,偷懒了)。打开该字符设备的时候,会忙等5秒钟。根据抢占和非抢占的定义,如果是抢占式内核,用户程序A打开该设备时,其他用户程序依然可以执行;如果是非抢占式内核,用户程序A打开该设备时,其他用户程序无法执行。

  • 非抢占式内核体验
    cat /dev/mytest &
    虽然是在后台执行,但是执行该命令后,命令行已经不动了,5秒后,命令行才恢复。这是因为cat进程陷入内核态后,忙等5秒,这5秒内是无法抢占的,这显然无法被桌面系统接受。
  • 抢占式内核体验
    cat /dev/mytest &
    执行该命令后,命令行依然可以相应其他命令。
  • 自愿内核抢占
    cat /dev/mytest &
    效果跟非抢占式内核一样,why?
    其实自愿内核抢占的意思是内核自愿被抢占,所以内核要自己声明可以被抢占。将my_open函数做如下修改:
int my_open(struct inode *inode, struct file *file)
{
	printk("open\n");
	mdelay(1000);
	_cond_resched();
	mdelay(4000);
	return 0;
}

再次执行cat /dev/mytest &命令,一直敲回车键,会发现1秒后命令行响应了一下,然后又不动了,4秒后,命令行恢复,也就是说1秒后,允许内核被抢占了一次。

简单撸一下代码

上面的实验能给人直观的感受,但是要想过瘾,还是要撸代码。进程调度/抢占这一块的代码很多,乍一看很难下手。幸运的是,linux内核主调度器给了明确的注释。

/*
 * __schedule() is the main scheduler function.
 *
 * The main means of driving the scheduler and thus entering this function are:
 *
 *   1. Explicit blocking: mutex, semaphore, waitqueue, etc.
 *
 *   2. TIF_NEED_RESCHED flag is checked on interrupt and userspace return
 *      paths. For example, see arch/x86/entry_64.S.
 *
 *      To drive preemption between tasks, the scheduler sets the flag in timer
 *      interrupt handler scheduler_tick().
 *
 *   3. Wakeups don't really cause entry into schedule(). They add a
 *      task to the run-queue and that's it.
 *
 *      Now, if the new task added to the run-queue preempts the current
 *      task, then the wakeup sets TIF_NEED_RESCHED and schedule() gets
 *      called on the nearest possible occasion:
 *
 *       - If the kernel is preemptible (CONFIG_PREEMPT=y):
 *
 *         - in syscall or exception context, at the next outmost
 *           preempt_enable(). (this might be as soon as the wake_up()'s
 *           spin_unlock()!)
 *
 *         - in IRQ context, return from interrupt-handler to
 *           preemptible context
 *
 *       - If the kernel is not preemptible (CONFIG_PREEMPT is not set)
 *         then at the next:
 *
 *          - cond_resched() call
 *          - explicit schedule() call
 *          - return from syscall or exception to user-space
 *          - return from interrupt-handler to user-space
 *
 * WARNING: must be called with preemption disabled!
 */

注释说的非常清楚,timer tick中断函数里面判断是否需要重新调度,如果需要的话设置TIF_NEED_RESCHED,之后会在某些特定的点执行调度,也就是意味着可以抢占。

未设置CONFIG_PREEMPT的情况

有四种情况会触发重新调度

  • 执行cond_resched
  • 执行schedule
  • 从系统调用或者异常返回用户空间
    以ARM64为例,对应的代码是:
ret_fast_syscall:
	disable_irq				// disable interrupts
	str	x0, [sp, #S_X0]			// returned x0
	ldr	x1, [tsk, #TI_FLAGS]		// re-check for syscall tracing
	and	x2, x1, #_TIF_SYSCALL_WORK
	cbnz	x2, ret_fast_syscall_trace
	and	x2, x1, #_TIF_WORK_MASK
	cbnz	x2, work_pending
  • 从中断返回用户空间
    以ARM64为例,对应的代码是
ret_to_user:
	disable_irq				// disable interrupts
	ldr	x1, [tsk, #TI_FLAGS]
	and	x2, x1, #_TIF_WORK_MASK
	cbnz	x2, work_pending

非常明显了,如果从用户态入中断或者异常,则在退出中断或者异常的时候可以重新调度,也就是说用户态的任务可以被抢占,如果当前工作在内核态,则无法重新调度,也就是所谓的内核不可抢占。

设置了CONFIG_PREEMPT的情况

针对内核态新增了一些抢占点:

  • 调用preempt_enable使能抢占的时候
  • 从中断返回的时候,这里其实主要增加了从内核态进入中断时,退出中断的时候可以重新调度,这也就意味着内核态的任务可以被抢占。以ARM64为例,代码如下:
el1_irq:
	kernel_entry 1
	enable_dbg
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_off
#endif

	irq_handler

#ifdef CONFIG_PREEMPT
	ldr	w24, [tsk, #TI_PREEMPT]		// get preempt count
	cbnz	w24, 1f				// preempt count != 0
	ldr	x0, [tsk, #TI_FLAGS]		// get flags
	tbz	x0, #TIF_NEED_RESCHED, 1f	// needs rescheduling?
	bl	el1_preempt
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_on
#endif
	kernel_exit 1
ENDPROC(el1_irq)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值