嵌入式八股RTOS与Linux--中断篇
前言
中断可以说是操作系统最重要的部分,其实前面的讲解我们也看到了,操作系统不是每时每刻都在运行的程序,很多时候都触发了中断之后进行处理,操作系统是由中断驱动的;关于中断我前面的文章有提到过:
https://blog.csdn.net/qq_45731845/article/details/146291274
RTOS的中断管理
首先要把中断优先级设置为4:即没有响应优先级 都是抢占优先级
RTOS的中断管理除了涉及到了中断优先级配置的寄存器,这几个寄存器也同样重要
- 受RTOS的中断和不受RTOS的中断
我们可以配置哪些中断优先级受RTOS管理 哪些中断优先级不受RTOS管理// 优先级数小于5的不归FreeRTOS管理 #define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
- 为什么还要有不受RTOS管理的中断呢?
除了什么硬件异常啊之类的,我们试想这样一个场景:假设我有一个电机的传感器要求每100us采集一次数据–这项工作去抽象成一个任务去做是不可能的,延时的处理是放在sysTickHandler中的,不可能把这个时钟配置的这么快,而且用任务处理时效性也不是很好(做不到100us精确延时)
假设我们设置一个定时器中断不受RTOS的中断管理,就可以通过配置定时器和中断处理函数做到这件事了
- 为什么还要有不受RTOS管理的中断呢?
- 相关的函数
- 什么是临界区与临界资源?
临界资源:是指一次仅允许一个进程或线程访问的共享资源,如果多个进程/线程同时访问,可能会导致数据不一致或竞态条件
临界区: 访问临界资源的代码叫做临界区 - taskENTER_CRITICAL与对应的ISR(在临界区进入中断)
实际上taskENTER_CRITICAL就是通过写BASEPRI寄存器来暂时屏蔽对受RTOS管理的中断的响应
对应的ISR函数版本,为啥这么做我想是因为在中断里不想去修改全局变量uxCriticalNesting去进行嵌套吧,而是需要我们手动处理状态void vPortEnterCritical( void ) { portDISABLE_INTERRUPTS(); uxCriticalNesting++; // 这个修改全局变量是为了支持嵌套 if( uxCriticalNesting == 1 ) { configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 ); } } static portFORCE_INLINE void vPortRaiseBASEPRI( void ) { uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; // 这个宏跟我们设置的configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY宏相关的哟 __asm { /* Set BASEPRI to the max syscall priority to effect a critical * section. */ /* *INDENT-OFF* */ msr basepri, ulNewBASEPRI dsb isb /* *INDENT-ON* */ } }
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void ) { uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; __asm { /* Set BASEPRI to the max syscall priority to effect a critical * section. */ /* *INDENT-OFF* */ mrs ulReturn, basepri msr basepri, ulNewBASEPRI dsb isb /* *INDENT-ON* */ } return ulReturn; }
- dsb与isb指令
dsb和isb和我们前面讲的内存屏障有关,作用就是确保后面所有的函数都是用的bssepri的新值(毕竟我们的CPU和编译器有时候会优化指令执行顺序,从而引起不可知的错误)- dsb
确保所有内存访问指令(如 LDR/STR)在屏障前完成,再执行后续指令。
解决多级流水线或总线乱序执行导致的数据同步问题 - isb
清空处理器流水线,确保后续指令从内存重新预取,使用最新的寄存器或内存值。
解决指令预取导致的上下文不一致问题。
- dsb
- xxFromISR探究
我们随便找一个FromISR就会发现进入中断服务函数做的第一件事往往是调用portASSERT_IF_INTERRUPT_PRIORITY_INVALID();函数BaseType_t xQueueGiveFromISR( QueueHandle_t xQueue, BaseType_t * const pxHigherPriorityTaskWoken ) { BaseType_t xReturn; UBaseType_t uxSavedInterruptStatus; Queue_t * const pxQueue = xQueue; configASSERT( pxQueue ); configASSERT( pxQueue->uxItemSize == 0 ); configASSERT( !( ( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX ) && ( pxQueue->u.xSemaphore.xMutexHolder != NULL ) ) ); portASSERT_IF_INTERRUPT_PRIORITY_INVALID(); /**.....*/ }
这个函数干了什么呢?–就是查看调用该函数的中断的优先级是不是受FreeRTOS管理
```C
void vPortValidateInterruptPriority( void )
{
uint32_t ulCurrentInterrupt;
uint8_t ucCurrentPriority;
// 得到当前中断的中断号
ulCurrentInterrupt = vPortGetIPSR();
// portFIRST_USER_INTERRUPT_NUMBER是16 因为前15个都是系统异常
if( ulCurrentInterrupt >= portFIRST_USER_INTERRUPT_NUMBER )
{
// 通过寄存器得到中断优先级
ucCurrentPriority = pcInterruptPriorityRegisters[ ulCurrentInterrupt ];
configASSERT( ucCurrentPriority >= ucMaxSysCallPriority );
// ucMaxSysCallPriority的值在xPortStartScheduler时被配置了,配置为
//ucMaxSysCallPriority = configMAX_SYSCALL_INTERRUPT_PRIORITY & ucMaxPriorityValue;
}
}
```
- 为什么FreeRTOS要区分带中断的API和不带中断的API
- 在中断里不能延时 阻塞,毕竟你总不能在中断里vTaskDelay()多久
- vPortValidateInterruptPriority函数
验证中断优先级:确保中断的优先级符合 FreeRTOS 的要求,特别是 configMAX_SYSCALL_INTERRUPT_PRIORITY 的要求,避免高优先级中断调用不安全的 FreeRTOS API 函数。
Linux的中断管理
- 用户态与内核态
- 用户态和内核态分别是什么?为什么要区分?
- 用户态: 应用程序拥有有限的系统资源访问权限,只能在操作系统划定的特定空间内运行
- 内核态: 拥有最高权限,可直接操作硬件、管理内存、调度进程等
- 为什么要区分: 安全性 / 稳定性(一个应用崩溃不影响其他) / 提高性能
- 用户态切换内核态的方式?
- 系统调用: fork() / read() / write()
- 异常:比如缺页异常
- 外设中断
- 用户态如何访问内核态资源?
- 系统调用
- 库函数 : malloc() / printf()
- shell : cat / …
- 切换过程会保存那些信息
- 保存用户态信息:将用户态的寄存器信息保存到内核栈中。
- 切换到内核栈:设置堆栈指针寄存器为内核栈的地址。
- 执行内核态程序:加载中断处理程序的地址到寄存器,开始执行内核态程序。
- 恢复用户态
- 系统调用
-
系统调用是什么?
在系统调用时会触发一个特殊的中断,CPU此时就会从用户态切换内核态 不同的系统调用有不同的系统调用号 从而执行不同的处理
系统调用并不直接返回错误码,而是将错误码放入一个名为errno的全局变量中。通常用一个负的返回值来表明错误。返回一个0值通常表明成功。
-
系统调用的具体步骤
- 设置参数(用户态)
- 触发系统调用 int 0x80 此时用户态会切换到内核态
- 内核检查传入参数的合法性
- 根据系统调用号找到处理程序并运行
- 返回参数
-
减少系统调用的次数
- 合并系统调用: read/write—>readv / writev
- 避免拷贝: read/write–>mmap
- 缓存数据
- Linux的上半/下半部中断
-
为什么要区分上/下半部
中断作为异步执行,其时间应该越短越好 所以只在中断里做必须执行的部分 而把剩下的部分"推迟"处理就好,从而平衡实时性与系统响应能力,减少中断屏蔽时间 -
下半部的实现 tasklet / softirq / workqueue
下半部在执行的时候是开中断的 可以被打断的
概括性总结
因为软中断和tasklet还是在中断上下文 所以不能睡眠 但是workqueue就是个线程 是可以阻塞和睡眠的
-
软中断
软中断是一个数组 在编译期间就被确定了的
软中断在设计上要求支持重入(多个CPU同时调用都没问题)
通过do_softirq()来执行软中断
- do_softirq()的时机
中断退出的时候,会唤醒并调用软中断
raise_softirq是会处理软中断
ksoftirqd线程:避免大量的软中断导致用户态啥也不做
- do_softirq()的时机
-
tasklet函数
tasklet是通过软中断实现的,不过它有了新的定义:那就是不可能多个CPU同时运行一个tasklet函数怎么做到的 通过一个原子变量进行计数
tasklet本身就是在中断执行完的时候通过tasklet_schedule()添加到本CPU链表的尾部
tasklet_action来执行中断
- tasklet_action的调用时机—同软中断
-
workqueue
对于tasklet和软中断,都不能睡眠,这在有时候可太不好用了
工作队列的话是吧工作推后,交给一个内核线程去执行—内核线程就可以正常被CPU调度了 这样的话就可以去执行一些阻塞之类的操作了
一般每隔CPU都有一个线程专门处理工作队列–循环遍历链表
但因为是依次遍历 所以如果我们任务前面有非常耗时的任务就得等待了–此时我们可以创建一个新的线程专门处理我们的中断下半部任务
-
-
中断的处理流程
cpu接受中断->保存中断上下文跳转到中断处理例程->执行中断上半部(并在最后调用软中断)->执行中断下半部->恢复中断上下文。irqreturn_t my_isr(int irq, void *dev_id) { /.../ tasklet_schedule(&my_tasklet); // 触发下半部 return IRQ_HANDLED; }
-
中断处理函数的注册
中断的申请request_irq的正确位置:应该是在第一次打开 、硬件被告知终端之前if (request_irq(irq, my_isr, IRQF_SHARED, "my_device", dev)) { printk("注册中断失败\n"); return -EIO; }
-
中断处理函数编写的注意事项
- 尽量短小
- 不要休眠与阻塞
- 中断的返回值: 裸机没有返回值 / Linux内核中必须正确返回irqreturn_t
- 内核线程与用户线程
用户线程是在用户空间的 每个线程是独立的拥有自己的用户空间
而内核线程是在内核空间 共享内存的
我们前面讲过每个线程都是有自己的task_struct的 不管你是哪个属于哪个进程