中断上下文中调度会怎样?

作者:linuxer 发布于:2017-3-20 19:08 分类:进程管理

一、前言

每一个Linux驱动工程师都知道这样一个准则:在中断上下文中不能睡眠。但是为什么interrupt context中不能调用导致睡眠的kernel API呢?如果驱动这么做会导致什么样的后果呢?这就是本文探讨的主题。为了理解这个主题,我们设计了一些非常简单的驱动程序和用户空间的程序,实际做实验观察实验效果,最后给出了结果和分析。

BTW,本文的实验在X86 64bit + 标准4.4内核上完成。

 

二、测试程序

1、cst驱动模块

我们首先准备一个能够在中断上下文中睡眠的驱动程序,在这里我称之Context schedule test module(后文简称cst模块)。这个驱动程序类似潜伏在内核中的“捣蛋鬼”,每隔1秒随机命中一个进程,然后引发调度。首先准备一个Makefile,代码如下:

KERNELSRC ?= /home/xxxx/work/linux-4.4.6

default:
        $(MAKE) -C $(KERNELSRC) M=$$PWD

clean:
        $(MAKE) -C $(KERNELSRC) M=$$PWD clean

按理说代码中的xxxx应该是我的名字,如果你愿意在你的环境中测试,可以修改成你的名字,当然,最重要的是需要某个版本的内核代码。在内核升级文档中,我已经编译了/home/xxxx/work/linux-4.4.6目录下的内核,并把我的计算机升级到4.4.6的内核上,如果你愿意可以按照那份文档进行升级,否则可能会有一些版本问题需要处理。除了Makefile之外,还需要一个Kbuild文件:

obj-m := cst.o

当然,最重要的是cst模块的源代码:

#include
#include
#include
#include

#define DRIVER_DESC "context schedule test driver"

static struct timer_list cst_timer;

static void cst_timer_handler (unsigned long data)
{
        struct task_struct *p = current;

        pr_info("=====in timer handler=====\n");
        pr_info("cst shoot %16s [%x] task:\n", p->comm, preempt_count());
        mod_timer(&cst_timer, jiffies + HZ);
        schedule();
}

static int __init cst_init(void)
{
        init_timer(&cst_timer);
        cst_timer.function = cst_timer_handler;
        cst_timer.expires = jiffies + HZ;
        add_timer(&cst_timer);

        pr_info(DRIVER_DESC " : init on cpu:%d\n", smp_processor_id());
        return 0;
}
module_init(cst_init);

static void __exit cst_exit(void)
{
        del_timer_sync(&cst_timer);
        pr_info(DRIVER_DESC " : exit\n");
}
module_exit(cst_exit);

MODULE_DESCRIPTION(DRIVER_DESC);
MODULE_AUTHOR("linuxer ");
MODULE_LICENSE("GPL");

代码非常的简单,无需多说,直接make就可以编译得到cst.ko的内核模块了。

2、用户空间测试程序

为了更方便的测试,我们需要准备一个“受害者”,代码如下:

#include
#include

int main(int argc, char **argv)
{
    int i = 0;

    while (1) {
        sqrt (rand ());

        if ((i % 0xffffff) == 0)
            printf ("=\n");

        if ((i % 0xfffffff) == 0)
            printf ("haha......still alive\n");

        i++;
    }

    return 0;
}

这段代码也很简单:不断的产生一个随机数,并运算其平方根,在使得的时候,向用户输出一些字符,表明自己的状态。当程序执行起来的时候,大部分时间在用户态(运算),偶尔进入内核态(printf)。这个进程并不知道在内核态有一个cst的模块,每隔一秒就发射一只休眠之箭,可能命中用户态,也可能命中内核态,看运气吧,但是无论怎样,该进程被射中之后都会进入睡眠。

 

三、执行测试并观察结果

1、先把用户空间的测试程序跑起来

要想测试导弹(呵呵~~我们的cst模块就是一个捣蛋) 的性能,必须要有靶机或者靶舰。当然也可以不用“靶机”程序,只不过捣蛋鬼cst总是命中swapper进程,有点无趣,因此这里需要把我们用户空间的那个测试程序跑起来,让CPU先活跃起来。

需要注意的是,在多核cpu上,我们需要多跑几个“靶机”进程才能让系统不会always进入idle状态。例如我的T450是4核cpu,因此我需要运行4个靶机程序才能让系统中的4个cpu core都燥起来。可以通过下面的命令确认:

ps –eo comm,psr | grep cst

BTW,靶机程序是cst_test。通过上面的命令,可以看到系统中运行了四个cst_test进程,分别在4个cpu上。

2、插入内核模块

靶机已经就绪,是时候发射捣蛋了,命令如下:

sudo insmod ./cst.ko

一旦插入了cst内核模块,捣蛋鬼就开始运作了,每隔1秒钟发射一次,总有一个倒霉蛋被命中,被调度。当然,在我们的测试中,一般总是cst_test这个进程被命中。

3、观察结果

一切准备就绪,是时候搬个小板凳坐下来看好戏了。当然,我们需要一个观察的工具,输入如下命令:

sudo tail –f /var/log/messages

在上面的cst模块中,输出并没有直接到控制台,因此我们需要通过内核日志来看看cst的运行情况。

 

四、结果和分析

1、结果

很奇怪,一切都是正常的,系统没有死,cst模块也运行正常,cst_test进程也始终保持alive状态,不断的运行在无聊的平方根、打印着无聊的字符串。唯一异常的是日志,每隔1秒钟就会dump stack一次。

2、分析

当cst模块命中cst_test进程,无论是userspace还是kernel space,都会在内核栈上保存中断发生那一点的上下文,唯一不同是如果发生在userspace,那么发生中断那一刻,内核栈是空的,而如果在kernel space,内核栈上已经有了cst_test通过系统调用进入内核的现场以及该系统调用各个函数的stack frame,当中断发生的时候,在当前内核栈的栈顶上继续压入了中断现场,之后就是中断处理的各个函数的stack frame,最后是cst_timer_handler的stack frame,由于调用了schedule函数,cst_test进程的现场被继续压入内核栈,调度器决定下一个调度的进程。

cst_test进程虽然被调度了,但是仍然在runqueue中,调度器一定会在适当的时机调度cst_test进程重新进入执行态,这时候恢复其执行就OK了,cpu执行cst_timer_handler函数schedule之后的代码,继续未完的中断上下文的执行,然后从内核栈恢复中断现场,一切又按照原路返回了。

当然,这里的测试看起来一切OK,但这并不是说可以自由的在中断上下文中调用导致睡眠的内核API,因为我们这里给出了一个简单的例子,实际上也有可能导致系统死锁。例如在内核态持有锁的时候被中断,然后发生调度。有兴趣的同学可以自己修改上面的代码实验这种情况。

3、why

最后还是回到这个具体的技术问题:为什么interrupt context中不能调用导致睡眠的kernel API?

我的看法是这样的:调度器是每一个OS的必备组件,在编码阶段之前,我们往往要制定下我们的设计概念。对于Linux 调度器,它的目标就是调度一个线程,一个线程就是调度实体(暂不考虑group sched)。中断上下文是不是调度实体呢?当然不是,它没有专属的task struct,内核无从调度。这是调度器设计者的决定,这样的决定让调度器设计起来简洁而美丽。

基于上面的设计概念,中断上下文(hard irq和softirq context)并不参与调度(暂不考虑中断线程化),它们是异步事件的处理机制,目标就是尽快完成处理,返回现场。因此,所有中断上下文的优先级都是高于进程上下文的,也就是说,对于用户进程(无论内核态还是用户态)或者内核线程,除非disable了CPU的本地中断,否则一旦中断发生,它们是没有任何能力阻挡中断上下文抢占当前进程上下文的执行的。

因此,Linux kernel的设计者制定了规则:

1、中断上下文不是调度实体

2、中断上下文的优先级高于进程上下文

而在中断上下文中调度毫无疑问会打破规则,因此不能在硬中断、软中断环境中调用阻塞函数。不过,在linux调度器的具体实现的时候,检测到了在中断上下文中调度schedule函数也并没有强制linux进入panic,可能是linux的开发者认为一个好的内核调度器无论如何也尽自己最大的努力让系统运行下去吧。但是,在厂商自己提供的内核中,往往修改调度器行为,在中断上下文中检测到调度就直接panic了,对于内核开发者而言,这样更好,可以尽早的发现问题。

 

原创文章,转发请注明出处。蜗窝科技

标签: 中断 调度

评论:

Chaotian
2017-06-01 19:36
实际上,你的这个实验,之所以还能回到irq handler是因为你在schedule()之前的mod_timer()的调用。
你把schedule()放在mod_timer()之前试试?
Chaotian
2017-06-01 19:38
@Chaotian:And,实际上,irq handler里面调用schedule()对当前cpu上的进程,可以说是毫无影响的。
因为这并没有改变进程的状态,进程仍然是在run queue中,从irq hander返回之后,依然会立刻调度到这个进程(如果need_resched没被设定的话)
Se7en
2017-08-08 14:39
@Chaotian:恩,我觉得这段有道理^_^,这段test code看起来没办法证明softirq里面schedule出去,再shedule回来后,irq context还是不是原来的那个context,也可能是新mod出来的timer导致看起来是"schedule 回来了",但其实此softirq context已经是非彼softirq context了,在schedule()后面print一个计数变量,也许可以证明这点

简而言之,目前kernel到底会不会保留irq context,如果会,才可能有schedule回来的可能 (只是kernel愿不愿意实现这部分为可schedule的),如果不会,应该是没可能才对 (比如,irq context执行当下的register值,如果没有保留下来,应该是无法schedule回来吧,现场都已经没了) ^_^ ^_^
球儿
2021-05-25 15:40
@Se7en:也可能是新mod出来的timer导致看起来是“schedule” 回来了,这个怎么理解啊?
test
2017-04-01 16:40
为什么interrupt context中不能调用导致睡眠的kernel API?
理由:
Linux kernel的设计者制定了规则:
1、中断上下文不是调度实体
2、中断上下文的优先级高于进程上下文

理是这么个理,但是感觉还没有回到的根本原因上.
那么问题更深入一步,为什么要这么设计?为什么要制定这样的规则?
wowo
2017-04-01 17:03
@test:可以尽量简洁的思考这个问题(暂时抛开Linux、Kernel等概念):
1)首先定义“睡眠”这个概念
是指进程暂时让出CPU,然后等待某个事件发生后,再把CPU要回来(注意和电源管理里面的“CPU睡眠“不同)。这里有几个重点:
    进程;
    让出CPU;
    要回CPU。
2)由1可知,“睡眠”这个概念本身,是依附于“任务管理”的。“任务管理”是一个复杂的概念,先不去深究,这里仅仅回答一个问题:如果你是“任务管理”的设计者,你会不会把“中断处理函数”当作一个普通任务?很显然,如果回答“会”,将会有很多麻烦(不一定不可行);如果回答“不会”,就简单了,这就是当前主流OS的现状。
3)基于“不会”这个假设,再来思考最初的问题
    首先,中断处理函数不是一个任务,那么何来“睡眠”呢?这是一个逻辑问题,没有商量的余地。
    其次,如果抛开逻辑,真有人把“睡眠”强加给中断处理函数了,那么:让出CPU好办,直接调度其它进程即可;但是,怎么把CPU再交还给中断处理函数呢?伪造一个中断?还是压根不还给它了?
    所以,如果真的要逆势而为,想想头就大啊!呵呵~~
test
2017-04-11 13:50
@wowo:@wowo 感觉你的回答反而将问题进一步的复杂化了.其实在这篇文章中已经提到了,即使在中断处理函数中(在准确的描述或是在某个外设的中断处理函数中)调用睡眠,实际上是能够再次被调用回来的,毕竟中断"依附于"被"打断"的进程的"环境",或者说它虽然没有自己的task_struct,但是它是借用了别的进程的,当调度器再次调度这个task_struct时,又回到了中断处理函数中.
其实最根本的原因,@linuxer在回复中提到了,设计源于需求,的确如此,举个例子来说,中断发生了,说明有非常紧急的事情需要处理了,如果这时候处理了一半而停下来做其他的事情去了,就可能导致各种各样不可预知的错误发生,比如网卡的接受数据的中断发生,如果还没有开始处理这个中断就被调度走了,那么这很可能导致网卡接受的数据溢出.
不过目前需求在变,设计也在变,比如@linuxer提到的中断线程化,为了提高kernel的实时性,在较新的kernel中,默认情况下很多外设的中断采用了线程化的中断处理函数.但是无论怎么着,不是所有的中断处理函数都能线程化,或者说中断处理函数有一部分是不能线程化的(不能睡眠,强制睡眠就会导致各种各样的问题出现,比如网络传输失败,音视频播放卡顿,触屏后反应不及时,敲键盘后没有反应等等)
wowo
2017-04-11 14:15
@test:我觉得“中断发生了,说明有非常紧急的事情需要处理了,如果这时候处理了一半而停下来做其他的事情去了,就可能导致各种各样不可预知的错误发生”这个例子是有先入为主的成分在里面的,从嵌入式的本质上看:中断只有一个特性,就是“突发”。
至于中断是否紧急,只有中断处理函数才知道。如果真有紧急的事情要处理,为什么要“睡眠”?假如kernel允许在中断中“睡眠”,某一个处理函数因为“睡眠”而丢了数据,这就是代码逻辑的错误,怪不得别人。
回到我的观点上,其实我关心的仅仅是“睡眠”、“调度”等词汇的基本概念,“中断处理”这个东西(也就是所谓的interrupt context),从逻辑上是没办法和“进程”等同的,就算OS的设计者尽量压缩它的代码量(仅仅触发一些处理中断事件的线程),也改变不了这个现状。
基于这个现状,“睡眠”等概念只是“进程”属性,没法用到“中断处理”也即interrupt context上(这样想就简单了)。
最后,实际场景中,interrupt context是依附于某个进程,可以“再回来”,但这在逻辑上不通:中断处理是由硬件事件触发的(这是基本定义),那么被调度器再调度回来算什么呢?
linuxer
2017-04-01 19:37
@test:大爱你这个问题,这个才是真正的“蜗窝”的精神啊,哈哈。

你的问题是:为什么要这么设计?为什么要制定这样的规则? 其实一切的设计都源自需求。对于Linux内核而言,她在设计的时候需要在优异的进程调度延迟和更高的外设thoughput之间进行选择。

我们先抛开进程上下文和中断上下文的定义,先看看通用的中断处理。无论如何,当中断发生的时候,总有若干的事情要处理,这时候,cpu必须要先停掉当前的事情,转而去处理那个异步到来的中断事件。由于产生了中断而导致cpu不得不被打断,转向执行该中断对应的内核控制路径代码我们称之interrupt routine(我也想不出来什么好词)。

当然,具体interrupt routine放到那里执行完全是和OS设计相关。对于RTOS而言,实时性要求极高,基本上interrupt routine必须要放到线程上下文中,以便确保在确定的时间内调度到某个线程上去(例如飞控线程),否则产生灰常严重的后果。

Linux是一个通用的OS,主要应用在普通的桌面、server和普通嵌入设备,HARD REALTIME不是其需求,因此其实是没有必要将interrupt routine放到进程上下文,将interrupt routine放到进程上下文执行会影响外设的throughput。

当然,为了照顾real timer的需求,linux引入了线程化的概念,有兴趣也可以去了解一下。
test
2017-04-11 13:51
@linuxer:赞
adance
2017-03-22 23:41
您好,方便留个QQ吗...想跟您探讨一下Linux知识
linuxer
2017-03-23 09:11
@adance:http://www.wowotech.net/contact_us.html
这里有一个QQ群,可以和蜗牛们一起讨论。

如果想和我讨论技术,这个网站,还有讨论区都可以的,我本人不是非常喜欢即时通信工具,太快、太即时的东西我都不是很喜欢,见谅。
randy
2017-03-22 11:32
good,以下猜测不知道是不是可以补充进去:有些阻塞操作会改变current线程的状态,即当前线程被打断,在中断处理中获取X mutex资源未果时修改了current的状态导致它不是可run的状态
linuxer
2017-03-22 17:32
@randy:对,这也是一个很有意思的状态。一般进程都是阻塞在系统调用中,都是进程自己主动的行为,而中断中由于试图获取某些锁资源而进入睡眠对current进程实在是太冤枉了,不知道怎么搞得,一个和自己毫无关系的中断来了,自己就莫名其妙的挂入等待队列了,当然,这也不算什么,但是谁来唤醒这个进程呢?
randy
2017-03-22 19:16
@linuxer:是的,这个进程可能再也没机会得到调度了。所以当时看到文章中“结果和分析”的地方说不会有影响,吓了我一跳。再往后看看,才知道你想表达的。其实我觉得可以把这部分再拓展着说一点,一是从设计的角度看应不应该这么做,这点你写的很清楚了,二是能不能做,做的话会导致哪些后果.BTW,看你的文章学到很多,加油、继续。
linuxer
2017-03-23 09:07
@randy:其实在写这份文档的时候想法也是挺多的,本来想给cst增加字符设备的接口,增加获取锁未果而进入睡眠的场景,想模拟各种场景,但是时间有限就作罢了,如果能实现财务自由,专心搞搞这些研究也是人生乐事啊。
randy
2017-03-23 14:58
@linuxer:真正醉心于技术的人特别是linux kernel这种博大精深的,常常会有这个想法。很佩服你还能挤出时间写这些技术博客,像我,工作时间越久越感觉能花在技术上的时间越少
冬瓜哥
2018-02-28 19:35
@linuxer:没太理解“怎么唤醒这个线程呢?”这个疑问,比如线程A被中断了,ISR内部调用了block read I/O进入休眠队列,那么block I/O自然会将事件注册到下游,I/O完成后,不就会唤醒A了么? 如果是走正常的休眠流程,一定会有人唤醒它啊。除非不将自己入队,直接强制睡眠,此时不被唤醒,不过也是自己选择的啊,所以,没明白这个疑问,或者说,不应该有这个疑问。请赐教。多谢!
linuxer
2018-03-01 15:40
@冬瓜哥:你说的那个场景应该不存在唤醒问题,因为调用导致阻塞的时候就已经处理完了“后事”安排(怎么唤醒),我说的是另外的场景,即sleep on和wake up分别在不同的内核控制路径中实现,那么一段sleep on,要确保有适合的场景触发wake up的动作。
dida
2017-03-21 10:30
"有可能导致系统死锁。例如在内核态持有锁的时候被中断,然后发生调度". 不太理解
如果只有一个timer在触发软中断干坏事,理论上每次mod_timer 会尽量放到local cpu的tvec_bases上,那边结果是总是在一个cpu上发生中断期间的切换。 那即使被害者带着lock被软中断,然后被切出。其他核上的类似线程加同样的锁被阻,但被害者始终都是会切回来,并释放锁的呀
linuxer
2017-03-21 11:47
@dida:我这里可能没有说的太清楚,我原文的表述是这样的:
-----------
当然,这里的测试看起来一切OK,但这并不是说可以自由的在中断上下文中调用导致睡眠的内核API,因为我们这里给出了一个简单的例子,实际上也有可能导致系统死锁。例如在内核态持有锁的时候被中断,然后发生调度。有兴趣的同学可以自己修改上面的代码实验这种情况。
-----------
我的意思是本文仅仅给出一个简单的例子,即在interrupt context中直接调用schedule函数的场景,本身这个场景非常简单,不会造成deadlock(因为进程仍然是runnable的,调度器早晚会调度回来的),如果复杂一些的场景,例如不是直接调用schedule函数而是由于获取某个mutex(或者semphore)失败而引起调度的情况,这时候,系统很容死锁。

我们举一个实际的例子:
1、timer命中了X mutex的临界区,在timer的回调函数中如果试图获取X锁资源,那么系统就deadlock了。
2、1、timer命中了X mutex的临界区,在timer的回调函数中如果试图获取Y mutex锁资源,未果,从而触发调度。如果另外一个进程持有Y锁,而等待X锁资源释放,这时候系统也进入deadlock了。
zhangtao
2020-11-21 19:19
@linuxer:这里没有很明白,是我理解有误么,临界区怎么会被中断呢?
狂奔的蜗牛
2017-03-21 10:16
越是基础越是重要,赞一个

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog