RTEMS 进程切换分析(基于i386体系)

摘要:
在多任务操作系统中,进程切换是不可避免的,因此可以在单个CPU上并发执行进程。进程调度涉及很多事情,如调度时间、调度策略等。这里我们只讨论RTEMS任务调度中进程切换的细节。通过分析,我们可以理解操作系统如何将CPU的使用权从一个任务切换到另一个任务。所谓上下文切换,就是保存Task1进程的执行上下文,并在Task2进程被切换之前恢复其执行上下文。

在支持多任务操作系统中,进程切换是不可避免的,以使进程能在单个CPU上并发执行。进程的调度涉及到的东西较多,例如调度的时机、调度的策略等等,在这里我们只讨论RTEMS任务调度中进程切换的细节,通过分析以明白操作系统如何做到使一个CPU的使用权如何从一个任务上切换到另一个任务。

下面假设两个任务TASK1和TASK2,当前正在执行的任务executing = TASK1,需要切换到的任务 heir = TASK2,下面为进程调度进行上下文切换的代码(最精简的一个函数,除去多核的配置、其他一些扩展函数、可配置的浮点上下文保存恢复等代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void_Thread_Dispatch( void)
{
Thread_Control *executing;
Thread_Control *heir;
ISR_Level level;
/*
* Now determine if we need to perform a dispatch on the current CPU.
*/

executing = _Thread_Executing;
_ISR_Disable( level );
while( _Thread_Dispatch_necessary == true) {
heir = _Thread_Heir;
_Thread_Dispatch_necessary =
false;
_Thread_Executing = heir;
/*
* When the heir and executing are the same, then we are being
* requested to do the post switch dispatching. This is normally
* done to dispatch signals.
*/
if( heir == executing )
gotopost_switch;
/*
* Since heir and executing are not the same, we need to do a real
* context switch.
*/

_ISR_Enable( level );
_Context_Switch( &executing->Registers, &heir->Registers );
executing = _Thread_Executing;
_ISR_Disable( level );
}
post_switch:
_ISR_Enable( level );
}

此函数执行之前,有两个全局变量_Thread_Executing 和 _Heir_Executing 分别指向 Task1 和 Task2 的进程控制块TCB,下面对此函数进行分析:

首先:切换之后全局变量 _Thread_Executing 应该指向 Task2(第15行);

其次,如果切换之前和切换之后是同一个任务,就无需进行上下文切换(第22行);

若不同,则必须进行上下文切换,使CPU的控制权转到Task2上。所谓的上下文切换,就是保存Task1进程执行的上下文(主要是一些重要的寄存器),并且恢复Task2进程被切换出去之前执行的上下文。

进程控制块TCB中有个字段Context_Control Registers 是用来保存/恢复上下文的,该结构体在i386体系下定义为:

typedefstruct
{
uint32_t eflags; /* extended flags register */
void *esp; /* extended stack pointer register */
void *ebp; /* extended base pointer register */
uint32_t ebx; /* extended bx register */
uint32_t esi; /* extended source index register */
uint32_t edi; /* extended destination index flags register */
} Context_Control;

也即意味着两个进程进行切换时,需要保存的寄存器有EFLAGS、ESP、EBP、EBX、ESI、EDI等,第32行_Context_Switch 函数完成:切换之前将这些寄存器的值保存在需要切换的进程(此处为Task1)TCB的Registers中,切换时这些寄存器的值从即将切换的进程(此处为Task2)TCB的Registers中恢复。这是一个与体系结构相关的操作,需要使用汇编去完成,我们看看_Context_Switch(&executing->Registers, &heir->Registers ) 的汇编实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
* Format of i386 Register structure
*/
.set REG_EFLAGS, 0
.set REG_ESP, REG_EFLAGS +
4
.set REG_EBP, REG_ESP +
4
.set REG_EBX, REG_EBP +
4
.set REG_ESI, REG_EBX +
4
.set REG_EDI, REG_ESI +
4
.set SIZE_REGS, REG_EDI +
4
BEGIN_CODE
/*
* void _CPU_Context_switch( run_context, heir_context )
*
*
Thisroutine performs a normal non-FP context.
*/
.p2align
1PUBLIC(_CPU_Context_switch)
.set RUNCONTEXT_ARG,
4/* save context argument */
.set HEIRCONTEXT_ARG,
8/* restore context argument */
SYM (_CPU_Context_switch):
movl RUNCONTEXT_ARG(
esp),eax/* eax= running threads context */
pushf/* pusheflags */
popl REG_EFLAGS(
eax) /* save eflags */
movl
esp,REG_ESP(eax) /* save stack pointer */
movl
ebp,REG_EBP(eax) /* save base pointer */
movl
ebx,REG_EBX(eax) /* save ebx*/
movl
esi,REG_ESI(eax) /* save source register */
movl
edi,REG_EDI(eax) /* save destination register */
movl HEIRCONTEXT_ARG(
esp),eax/* eax= heir threads context */
restore:
pushl REG_EFLAGS(
eax) /* pusheflags */
popf/* restore eflags */
movl REG_ESP(
eax),esp/* restore stack pointer */
movl REG_EBP(
eax),ebp/* restore base pointer */
movl REG_EBX(
eax),ebx/* restore ebx*/
movl REG_ESI(
eax),esi/* restore source register */
movl REG_EDI(
eax),edi/* restore destination register */
ret

在进入此汇编代码之前,Task1 栈为:

image

解释一下,C语言通过堆栈传参惯例,参数从右到左依次压栈,然后将下一条程序指针压栈,因此在进入_CPU_Context_Switch函数之后Task1栈如上图所示。函数体完成的功能:

第一步:将Task1上下文保存在Task1->Registers结构体中(第28~35行)。

28:将Task1->Register的指针(esp+4)保存在eax寄存器中;

29:将eflags寄存器压栈(pushl);

30:再出栈,存放在(&Task1->Register)->eflags中;

31~35:分别将esp、ebp、ebx、esi、edi寄存器的值保存在Task1->Register结构的相关字段中。

到此为止,完成了保存Task1进程的上下文,Task1切换出去之前栈还是如上图所示。

同理可以想象Task2被其他进程切换走之后一定也具有类似的栈结构,Task2->Registers中保存的是切换出去之前的它的上下文。

第二步:从Task2->Register结构中恢复Task2的上下文(第37~46行)。

37:将Task2->Registers的指针(esp+8)保存在eax寄存器中;

40:将Task2->Registers.eflags(即Task2切换出去之前保存的eflags寄存器)压栈;

41:出栈popf,此时eflags寄存器恢复为Task2上下文的eflags;

42:恢复esp,此步最为关键,因为此步之后esp寄存器将由Task1栈顶转移到Task2栈顶,此后操作将在Task2的栈上进行;

43~46:依次恢复esp、ebp、ebx、esi、edi寄存器。

至此,完成了恢复Task2进程的上下文。

第三步:从_CPU_Context_Switch 函数返回。

47:ret操作,弹出栈顶到eip中,注意esp此时已经指向的是Task2的栈,但是上面我们说过Task2栈与Task1栈切换之前的结构是类似的,因此Task2栈顶保存的仍然是_Context_Switch 下一条语句的代码。

从汇编函数中返回之后,又回到_Thread_Dispatch 函数体中,只不过与之前不同的是,此时处理器运行在Task2的上下文环境中。


还有一个问题需要我们去探索:如果Task2是第一次被调度执行,即Task2之前没有被切换出去,不曾执行到_Thread_Dispatch 中的 _Context_Switch 切换出去,也就是Task2栈并不是我们上面讨论的那样,同样我们也不希望Task2第一次被调度执行的第一条代码不是_Context_Switch 的下一条语句,因为Task2栈上并不存在_Thread_Dispatch函数的栈帧,如果这样,肯定会出现不可预期的错误。

所以,我们在创建任务时,就需要先初始化好Task2的栈,使得其第一次调度时,执行的是我们需要它需要执行的代码。

在rtems_task_start –> _Thread_Start –> _Thread_Load_environment –> _Context_initialize 函数会在任务加入就绪队列的时候进行上下文初始化。函数调用如下:

1
2
3
4
5
6
7
8
_Context_Initialize(
&the_thread->Registers, //任务上下文结构指针
the_thread->Start.Initial_stack.area, //指向新建的一个任务栈区域的基址
the_thread->Start.Initial_stack.size, //任务栈的大小
the_thread->Start.isr_level, //中断级别
_Thread_Handler, //任务执行时第一次要执行的代码
is_fp
);

我们看看这个函数实现的汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CPU_Context_Initialize( _the_context, _stack_base, _size, \
_isr, _entry_point, _is_fp ) \
do { \
uint32_t _stack; \
\
if( (_isr) ) (_the_context)->eflags = CPU_EFLAGS_INTERRUPTS_OFF; \
else(_the_context)->eflags = CPU_EFLAGS_INTERRUPTS_ON; \
\
_stack = ((uint32_t)(_stack_base)) + (_size)
; \
_stack &= ~ (CPU_STACK_ALIGNMENT - 1); \
_stack -= 2*sizeof(proc_ptr*); \
*((proc_ptr *)(_stack)) = (_entry_point); \
(_the_context)->ebp= (void *) 0; \
(_the_context)->esp= (void *) _stack; \
} while(0)

第6&7行通过参数isr来决定应初始化elfags寄存器的值;

第9&10行根据栈基址以及栈的大小计算出初始时栈顶的位置(需要对齐);

第11行将栈顶的位置往下移两个指针大小;

第12行将程序入口指针_entry_point写入栈顶;

第13&14行分别初始化ebp、esp寄存器。

初始任务栈的示意图如下:

image

至此,任务初始上下文初始化完成,主要初始化了eflags、esp、ebp等寄存器,保存在TCB的registers结构体中。

当一个从未执行的任务第一次被调度执行时,回到上下文切换函数_Context_Switch中,保存和恢复和上面一样,只不过在第三步ret操作时,会返回栈顶位置的值当作程序指针,和上面区别的是:这种情形eip不是跳到_Context_Switch的下一句,而是我们初始化保存在栈顶的值_entry_point。返回之后,就会执行所_entry_point指向的函数_Thread_Handle,在这个函数会执行到我们创建的任务体中。

结束语:上面讨论的是基于i386体系下RTEMS任务切换上下文的过程,虽然是基于特定的体系结构特定的操作系统,但对于任一个多任务的操作系统任务切换都大相径庭,无非就是保存上下文、恢复上下文,而若移植操作系统,这是需要针对特定平台进行改写的一段代码。

免责声明:文章转载自《RTEMS 进程切换分析(基于i386体系)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇windows2016server激活【MongoDB】应用场景下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

CentOS查看CPU、内存、网络流量和磁盘 I/O【详细】

安装 yum install -y sysstat sar -d 1 1 rrqm/s: 每秒进行 merge 的读操作数目。即 delta(rmerge)/swrqm/s: 每秒进行 merge 的写操作数目。即 delta(wmerge)/sr/s: 每秒完成的读 I/O 设备次数。即 delta(rio)/sw/s: 每秒完成的写 I/O 设备次数。...

Hystrix 如何解决 ThreadLocal 信息丢失

本文分享 ThreadLocal 遇到 Hystrix 时上下文信息传递的方案。 一、背景 笔者在业务开发中涉及到使用 ThreadLocal 来存放上下文链路中一些关键信息,其中一些业务实现对外部接口依赖,对这些依赖接口使用了Hystrix作熔断保护,但在使用Hystrix作熔断保护的方法中发现了获取 ThreadLocal 信息与预期不一致问题,本文旨...

sysbench的安装及使用

sysbench是一个模块化的、跨平台、多线程基准,主要用于评估测试各种不同系统参数下的数据库负载情况。它主要包括以下几种方式的测试:测试工具 文档顺序: 一、安装 二、测试 1、cpu性能2、磁盘io性能3、调度程序性能4、内存分配及传输速度5、POSIX线程性能6、数据库性能(OLTP基准测试)目前sysbench主要支持 MySQL,pgsql,or...

ios 编译openssl支持arm64(转)

最近在编译支付宝 快捷支付(无线) ios 端的时候发现demo不支持arm64。在网上找了下,看到客服说是openssl的库文件不支持arm64,于是自己编译了支持arm64的库文件,发现还是不行,提示原来淘宝的库文件也不支持。问他们客服,缺迟迟不给出解决方案,到后面居然连话都不回了。。 以上都是题外话,现在来看看如何编译支持arm64的openssl...

free命令常用参数详解及常用内存工具介绍

       free命令常用参数详解及常用内存工具介绍                        作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任。    一.内存空间使用状态 1>."-b"参数(以字节为单位显示内存使用情况) [root@node101.yinzhengjie.org.cn ~]# free -b...

关于Android的Build类——获取Android手机设备各种信息

经常遇到要获取Android手机设备的相关信息,来进行业务的开发,比如经常会遇到要获取CPU的类型来进行so库的动态的下载。而这些都是在Android的Build类里面。相关信息如下: privateString loadSystemInfo() { StringBuilder sb = newStringBuilder();...