LA and RTOS Base Knowledge

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zoosenpin/article/details/79752311

1 Interrupt
1.1 临界区开关CPU中断API
arm:local_irq_disable() / local_irq_enable() 在ARM V6以上使用汇编指令CPSID / CPSIE(Current Program Status Interrupt Disable / Enable)用于快速的开关中断,早期版本使用汇编指令mrs和msr去更新cpsr。

x86:local_irq_disable调用汇编指令CLI(clear interrupt-enable flag,IF);local_irq_enable调用汇编指令STI(set interrupt-enable flag,IF)。

arm的cpsid和x86的CLI同时禁止了核间中断(IPI),CPU-x还会用IPI通知CPU-y进行resched,但是CPU-y可能已经禁用了中断而不会响应。

在中断中不能产生调度,在中断返回时才可能发生调度。事实上,早期的内核很大程度上是依赖local_irq_disable来做资源保护,这个看看2.4的内核源码就很清楚了,里面有大量的对local_irq_disable函数的直接调用。

1.2 Linux Kernel中断处理
request_threaded_irq() - 实时中断处理,比较好。

1.3 Cortex-M BASEPRI-IPR-SHPRx
1.3.1 BASEPRI
FreeRTOS中进入临界区时没有关闭所有中断,而是使用优先级屏蔽寄存器BASEPRI(= configMAX_SYSCALL_INTERRUPT_PRIORITY)关闭了部分中断;这个寄存器最多有9位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成0,则不关闭任何中断,0也是缺省值。

FreeRTOS任务代码中临界段的进入和退出主要是通过操作寄存器BASEPRI实现的。进入临界区BASEPRI关闭了所有大于等于宏定义configMAX_SYSCALL_INTERRUPT_PRIORITY所定义的中断优先级,这样临界段代码就不会被中断干扰到,而且实现任务切换功能的 PendSV 中断和SysTick滴答定时器中断是最低优先级中断,所以此任务在执行临界段代码期间是不会被其它高优先级任务打断的。退出临界段时重新操作BASEPRI寄存器,即打开被关闭的中断(这里我们不考虑不受FreeRTOS 管理的更高优先级中断)。

Cortex-M内核的“中断优先级寄存器”是以最高位(MSB)对齐的。STM32使用了优先级寄存器中的4位,则这4个位位于中断优先级寄存器的bit 4、bit5、bit6、bit7位。剩余的bit0 ~ bit3可以设置成任何值。所以FreeRTOS中的中断优先级计算是有移位操作的。

CMSIS以及不同的微控制器供应商提供了可以设置某个中断优先级的库函数。一些库函数的参数使用最低位对齐,另一些库函数的参数可能使用最高位对齐,所以,使用时应该查阅库函数的应用手册进行正确设置。

可以在FreeRTOSConfig.h中设置宏configMAX_SYSCALL_INTERRUPT_PRIORITY和configKERNEL_INTERRUPT_PRIORITY的值。(关于这两个宏可以参考参数设置一章,网址:http://openmcu.net/post/kernel-config.html)。这两个宏需要根据Cortex-M内核自身的情况进行设置,要以最高有效位对齐。比如某MCU使用中断优先级寄存器中的4位,设置configKERNEL_INTERRUPT_PRIORITY的值为5,则代码为:
#define configKERNEL_INTERRUPT_PRIORITY (5<<(8-4))

对于每一个官方FreeRTOS演示例程,这也是在FreeRTOSConfig.h中要设置宏configKERNEL_INTERRUPT_PRIORITY为最低优先级时,为什么要将它设置为255(1111 1111B)的原因。使用这种方式指定这个值的原因是:FreeRTOS内核是直接在Cortex-M内核硬件上运行的(没有使用第三方接口库函数),要比大多数库函数先运行。现在也有开发第一个Cortex-M库函数计划。

1.3.2 IPR
typedefstruct
{
    vu32 ISER[2];
    u32 RESERVED0[30];
    vu32 ICER[2];
    u32 RSERVED1[30];
    vu32 ISPR[2];
    u32 RSERVED2[30];
    vu32 ICPR[2];
    u32 RSERVED3[30];
    vu32 IABR[2];
    u32 RSERVED4[30];
    vu32 IPR[15];
} NVIC_TypeDef;

NVIC IPR[15]:Interrupt Priority Registers,中断优先级控制寄存器组。STM32的中断分组与这个寄存器密切相关。因为STM32的中断多达60多个,所以STM32采用中断分组的办法来确定中断的优先级。IPR寄存器由15个32bit的寄存器组成,每个可屏蔽中断占8bit,这样总共可以表示15x4=60个可屏蔽中断。IPR[0]的[31:24],[23:16],[15:8],[7:0]分别对应中断3到0,总共对应60个外部中断。而每个可屏蔽中断占用的8bit并没有全部使用,只用了高4位。这4位又分为抢占优先级和子优先级。这两个优先级要根据SCB->AIRCR(System Control Block,Application Interrupt and Reset Register)中断分组的设置来决定。

简单介绍STM32的中断分组:STM32将中断分为0~4共5个组,该组是由SCB->AIRCR寄存器的bit10:8来定义的。如Table 1-1所示。
Table 1-1 AIRCR中断分组设置表

AIRCR[10:8]

bit[7:4]分配情况

分配结果

0

111

0:4

0位抢占优先级,4位响应优先级

1

110

1:3

1位抢占优先级,3位响应优先级

2

101 

2:2

2位抢占优先级,2位响应优先级

3

100 

3:1  

3位抢占优先级,1位响应优先级

4

011

4:0

4位抢占优先级,0位响应优先级

通过这个表,可以清楚的看到组0~4对应的配置关系,例如组设置为0x03,此时所有的60个中断,每个中断的中断优先级寄存器的高四位中最高3位是抢占优先级,低1位是响应优先级。每个中断都可以设置抢占优先级为0~7,响应优先级为1或0。抢占优先级的级别高于响应优先级,数值越小所代表的优先级越高。

具体优先级的确定和嵌套规则:
(1)只能高抢先优先级的中断可以打断低抢占优先级的中断服务,构成中断嵌套;
(2)当2个(N个)相同抢占优先级的中断出现,他们之间不能构成中断嵌套,但STM32首先响应子优先级高的中断;
(3)当2个(N个)个抢占优先级和子优先级相同的中断出现,STM32首先响应中断通道所对应的中断向量地址低的中断,就是谁先发生谁先被执行。

1.3.3 SHPRx - System Handler Priority Registers
Cortex M4系统中断的中断号从-15~-1(优先级由SHPRx控制),外设中断的中断号从0~63(优先级由IPR控制)。

SHPR1至SHPR3,3个32位寄存器,每8位设置一个中断优先级,共12个可配置的系统中断,8为里面用了高位configPRIO_BITS,其它位写无效,读为0。还有3个中断的优先级系统默认为-3、-2、-1。
Figure 1-1 Cortex-M 中断分布


Figure 1-2 SHPRx寄存器的位分配


1.3.4 示例代码
/* interrupt priority register */
static int command_dump_ipr(cli_node_t *cmd, int argc, char **argv)
{
    const volatile uint8_t * const pcInterruptPriorityRegisters =
        (const volatile uint8_t * const)0xE000E3F0;
    uint8_t i;

    console_puts_lite(""CR_LF);
    console_puts_lite("configPRIO_BITS                                            = %d"CR_LF,
            configPRIO_BITS);
    console_puts_lite("configKERNEL_INTERRUPT_PRIORITY           = 0x%02x"CR_LF,
            configKERNEL_INTERRUPT_PRIORITY);
    console_puts_lite("configMAX_SYSCALL_INTERRUPT_PRIORITY = 0x%02x"CR_LF,
            configMAX_SYSCALL_INTERRUPT_PRIORITY);
    console_puts_lite(""CR_LF);

    /* (4-7, 8-11, 12-15) */
    console_puts_lite("System Handler Priority Registers"CR_LF);
    for (i = 0; i < 12; i++){
        console_puts_lite("PRI_%d - 0x%02x"CR_LF, (4 + i), SCB->SHP[i]);
    }
    console_puts_lite(""CR_LF);

    console_puts_lite("User Interrupt Priority Registers"CR_LF);
    for (i = 16; i < (16 + IRQ_NUMBER_MAX); i++) {
        console_puts_lite("IRQ%d - 0x%02x"CR_LF,
                (i - 16), pcInterruptPriorityRegisters[i]);
    }
    return 0;
}
DECLARE_CONSOLE_COMMAND(dump_ipr, command_dump_ipr, NULL);

1.3.5 中断延迟
性能相关的另外一个指标是中断延迟。这通常用从中断请求到中断服务程序第一条指令执行的时钟周期数来衡量。Table 1-2列出了Cortex-M处理器在零等待内存系统条件下的中断延迟比较。
Table 1-2 零等待内存系统条件下的中断延迟比较

 

2 Memory
2.1 栈空间查看(单位KB)
Linux查看修改线程默认栈空间大小(ulimit -s),Linux默认是8MB。

2.1.1 kmalloc
用于kmalloc可分配的内存大小范围在32~131027(128k)字节,并且由于它用slab分配器来分配内存的,所以得到的内存大小可能比你申请的要大一些(它向上取2的N次幂整数)。而且如果开启了CONFIG_LARGE_ALLOCS选项,这个值可以更大,可以达到了32M。

2.1.2 malloc
malloc申请的空间大于128KB的话,使用mmap系统调用在stack 和 heap中间的区域(共享内存映射区域)进行虚拟内存分配,其大小应该就是栈底与heap顶之间的空间最大值。

2.2 Buddy and Slab
slab字面意思是被分割的厚板/石板,类似与buddy算法的含义(页可以按照2的次幂组织成一个元素,也可以分离,就像小伙伴一样,buddy算法因“时聚时散”这个表现而得名);slab的含义就是把大块的空间,分解为小块的空间。slab具体的特性是它包含不同的对象,就像书架一样,同一类型的对象就放在什么同一类型的slab(书架)里,因为一个slab可能装不下所有的对象,所以有些对象类型会有多个slab。slab并不是一开始就有的算法,它的出现是为了优化buddy算法,buddy算法以页(4096字节)为单位,进行管理,即使很小的数据修改,也会动用4096字节,花销大,而且数据这一块,那一块,不好管理。

所以,buddy系统就像一个大的书架,slab就像书架的格子一样。书架用于管理一个专业领域的书,如C语言/C++/JAVA/C#,而格子用于管理一个小类,C语言放在一个slab,C++放入一个slab....。而组成slab的“对象”就是具体的书本,如《C专家编程》,《C语言陷阱》等就是C语言这个slab 的对象。

Buddy APIs:alloc_pages()和__get_free_pages()等。

因为内存初始化的时间比分配和释放时间长好多,slab增加一个类似对象池的特性,slab会缓存已经被“释放”的对象,以便下次重用,不必再初始化。对象用该高速缓存结构kmem_cache描述。通过kmem_cache_create创建。虽然名为高速缓存,但是它不是硬件上的高速缓存,它只是主存上的区域,但是和硬件上的高速缓存高度相关。

2.3 ZONE_HIGHMEM
当内核空间大小是3GB – 4GB时,并不是所有的物理内存都是映射的,而是部分映射,需要时再动态将需要访问的物理内存映射到(3.896GB - 4GB)地址空间。使用CONFIG_HIGHMEM来配置。

linux 用户空间与内核空间——高端内存详解
https://www.cnblogs.com/zlcxbb/p/5841417.html

Figure 2-1 高端内存

vmalloc和kmalloc的区别:
- vmalloc分配的一般为高端内存,只有当内存不够的时候才分配低端内存;kmalloc从低端内存分配
- vmalloc分配的物理地址一般不连续,而kmalloc分配的物理地址连续,两者分配的虚拟地址都是连续的
- vmalloc分配的一般为大块内存,而kmalloc一般分配的为小块内存(不超过128k)

3 Schedule
#define DECLARE_BITMAP(name,bits) \
unsigned long name[BITS_TO_LONGS(bits)]

实时调度的结构体:
struct rt_prio_array {
    DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
    struct list_head queue[MAX_RT_PRIO];
};

struct rt_rq {  
    struct rt_prio_array  active;  
    [...]
    unsigned long rt_nr_running;         // 可运行实时任务个数  
    unsigned long rt_nr_migratory;       // 该运行队列上可以迁移到其他运行队列的实时任务个数
    unsigned long rt_nr_uninterruptible;  
    int highest_prio;  
    int overloaded;  
};  

3.1 调度策略
linux的实时进程有两种调度策略,SCHED_FIFO和SCHED_RR(类似于打麻将,轮流坐庄),SCHED_FIFO是简单的队列调度,并且没有timeslice的限制,谁先来谁运行,除非是阻塞或者主动yield,否者将一直占用CPU,即使关中断也不能阻止优先级高的进程被调度运行。

实时进程从不激活到激活状态时,根据进程的优先级,找到对应的优先级列表头,然后插入到该优先级列表尾部。多个实时进程在SMP之间采用push和pull调度机制。

对于普通进程,是通过nice系统调用来调整优先级的。从内核角度讲[100,139]是普通进程的优先级的范围,100最高,139最低,默认是120。普通进程的优先级的作用和实时进程不同,普通进程优先级表示的是占用CPU的时间。深入linux内核架构中提到,普通优先级越高(100最高,139最低),享受的CPU time越多,相邻的两个优先级,高一级的进程比低一级的进程多占用10%的CPU,比如内核优先级数值为120的进程要比数值是121的进程多占用10%的CPU。

我们知道,linux的进程调度时机有:
1、进程状态转换的时刻:进程终止、进程睡眠
2、当前的进程的时间片用完(current->counter = 0)
3、设备驱动程序主动调用schedule
4、进程从中断、异常及系统调用返回用户态

struct sched_param {
/* ... */
int sched_priority;
/* ... */
};

int sched_setscheduler (pid_t pid,
int policy,
const struct sched_param *sp);

sched_setscheduler函数的第二个参数调度方法 :

#define SCHED_OTHER 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#ifdef __USE_GNU
# define SCHED_BATCH 3
#endif

SCHED_OTHER表示普通进程,对于普通进程,第三个参数sp->sched_priority只能是0
SCHED_FIFO 和SCHED_RR表示实时进程的调度策略,第三个参数的取值范围为[1,99]。
如果sched_setscheduler 优先级设置的值和调度策略不符合的话,会返回失败的。

3.2 进程抢占
在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。
spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。

举个例子:进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!

但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。 因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。

所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。

3.3 优先级反转
3.3.1 什么是优先级反转
优先级反转是指一个低优先级的任务持有一个被高优先级任务所需要的共享资源。高优先任务由于因资源缺乏而处于受阻状态,一直等到低优先级任务释放资源为止。而低优先级获得的CPU时间少,如果此时有优先级处于两者之间的任务,并且不需要那个共享资源,则该中优先级的任务反而超过这两个任务而获得CPU时间。如果高优先级等待资源时不是阻塞等待,而是忙循环,则可能永远无法获得资源,因为此时低优先级进程无法与高优先级进程争夺CPU时间,从而无法执行,进而无法释放资源,造成的后果就是高优先级任务无法获得资源而继续推进。

3.3.2 解决方案
(1)设置优先级上限,给临界区一个高优先级,进入临界区的进程都将获得这个高优先级,如果其他试图进入临界区的进程的优先级都低于这个高优先级,那么优先级反转就不会发生。

eCos优先级上限(ceiling)例子:
@ packages/kernel/v3_0/src/sync/mutex.cxx
cyg_bool
Cyg_Mutex::lock(void)
{
    [...]
}

void
Cyg_Mutex::unlock(void)
{
    [...]
}

(2)优先级继承,当一个高优先级进程等待一个低优先级进程持有的资源时,低优先级进程将暂时获得高优先级进程的优先级别,在释放共享资源后,低优先级进程回到原来的优先级别。嵌入式系统FreeRTOS和VxWorks就是采用这种策略。

这里还有一个八卦,1997年的美国的火星探测器(使用的就是vxworks)就遇到一个优先级反转问题引起的故障。简单说下,火星探测器有一个信息总线,有一个高优先级的总线任务负责总线数据的存取,访问总线都需要通过一个互斥锁(共享资源出现了);还有一个低优先级的,运行不是很频繁的气象搜集任务,它需要对总线写数据,也就同样需要访问互斥锁;最后还有一个中优先级的通信任务,它的运行时间比较长。平常这个系统运行毫无问题,但是有一天,在气象任务获得互斥锁往总线写数据的时候,一个中断发生导致通信任务被调度就绪,通信任务抢占了低优先级的气象任务,而无巧不成书的是,此时高优先级的总线任务正在等待气象任务写完数据归还互斥锁,但是由于通信任务抢占了CPU并且运行时间比较长,导致气象任务得不到CPU时间也无法释放互斥锁,本来是高优先级的总线任务也无法执行,总线任务无法及时执行的后果被探路者认为是一个严重错误,最后就是整个系统被重启。Vxworks允许优先级继承,然而遗憾的工程师们将这个选项关闭了。

eCos优先级继承例子:
@ packages/kernel/v3_0/src/sync/mutex.cxx
cyg_bool
Cyg_Mutex::lock(void)
{
    [...]
}

void
Cyg_Mutex::unlock(void)
{
    [...]
}

FreeRTOS优先级继承例子:
@ tasks.c
vTaskPriorityInherit() - 需要mutex的高优先级Task,发现mutex被低优先级的Task持有。调整持有mutex的Task优先级等于当前等待mutex的Task优先级,这样持有mutex的低优先级Task获得了运行机会。
xTaskPriorityDisinherit() - 返回原来的优先级

(3)第三种方法就是使用中断禁止,通过禁止中断来保护临界区,采用此种策略的系统只有两种优先级:可抢占优先级和中断禁止优先级。前者为一般进程运行时的优先级,后者为运行于临界区的优先级。火星探路者正是由于在临界区中运行的气象任务被中断发生的通信任务所抢占才导致故障,如果有临界区的禁止中断保护,此一问题也不会发生。

4 Assembler Instruction
精简指令集和复杂指令集的主要区别是精简指令集的指令一般是定长指令,通常都是4个字节。也有2个字节的thumb和mips16等。

精简指令集简单的多,一条指令一般就opcode、oprand、(condtion)几部份,opcode常用的也就是load/store、branch/jump、add/shift等等。

复杂指令集就炫酷多了,x86最长一条指令15byte,其实现在cisc也就剩下x86了。 因为要保持前向兼容,所以x86 40多年的精华糟粕全在里面。结合abi看的话,x64已经吸取了很多risc处理器的经验,更多的寄存器,更合理的abi。比如push/pop指令这种risc上根本看不到的东西也都不见了。

Figure 4-1 x86指令编码

BCLR(BIC):Bit Clear
BSET:Bit Set
CPSID / CPSIE:Current Program Status Interrupt Disable / Enable
DSB:Data Sync Barrier
ISB:Instruction Sync Barrier

生成C语言的汇编代码:
gcc -S main.c
vi main.s

5 Linux进程创建知识
FD_CLOEXEC用法及原因_转
https://www.cnblogs.com/embedded-linux/p/6753617.html

6 URL
Linux内核抢占机制(preempt)
http://blog.sina.com.cn/s/blog_640531380101dlg9.html

Linux内核之实时进程调度和组调度
http://blog.chinaunix.net/uid-27052262-id-3239263.html

Linux内核之禁止中断和禁止内核抢占
https://blog.csdn.net/woshijidutu/article/details/68952702

linux 用户空间与内核空间——高端内存详解
https://www.cnblogs.com/zlcxbb/p/5841417.html

ARM Cortex-M、中断和FreeRTOS (Part 3)
https://coyee.com/article/11061-arm-cortex-m-interrupts-and-freertos-part-3

基于STM32应用的FreeRTOS中断设置
http://www.stmcu.org/article/id-328007

RTOS 在Cortex-M中应用时的中断优先级设置
https://www.douban.com/note/548730851/

7 Abbreviations
ARM CM4 BASEPRI:优先级屏蔽寄存器最多有9位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成0,则不关闭任何中断,0也是缺省值。FreeRTOS进入临界区时BASEPRI的值等于configMAX_SYSCALL_INTERRUPT_PRIORITY
BFS:Brain Fuck Scheduler,又称脑残调度器
CM4 R13:for MSP & PSP,the tasks are using the PSP(Process Stack Pointer), while the interrupts are using the MSP(Main Stack Pointer)
CMSIS:Cortex-Microcontroller Software Interface Standard
NVIC:Nested Vectors Interrupts Controller
NVIC IPR:Interrupt Priority Registers
PendSV:Pendable 服务是一个中断请求,如果没有其他中断需要响应时,系统将强制执行上下文切换
SVCall:SuperVisor Call由SVC指令触发,FreeRTOS用它来启动任务调度
SCHED_RR:Round-Robin,轮流调度算法(实时调度策略)
SHPRx:CM4 System Handler Priority Registers
STM32 AIRC:Application Interrupt and Reset Register
TIF_NEED_RESCHED:Thread Info Flag

Bionic brk():break(堆顶端称作program break)
intercepting:拦截系统调用
ptrace:process trace
pt_regs:ptrace registers(对应命令PTRACE_GETREGS)
strace:system call trace
 

猜你喜欢

转载自blog.csdn.net/zoosenpin/article/details/79752311
LA