RTOS部分
RTOS共同部分
操作系统与前后程序区别
前后台程序:各个模块顺序在一个死循环循环执行!除了中断,各个模块之间是不会相互打断,上个模块执行完了才到下个模块!
实时操作系统:各个模块任务是个独立死循环,时间片轮到了,不管模块任务是否执行完,根据任务优先级会有可能会打断。
操作系统任务优先级?有哪些信号量?操作系统低功耗?
你用了操作系统的项目任务分了哪几块?优先级是怎样的?
BLE、wifi串口是单独任务处理吗?任务参数是通过啥传递的?
描述实时系统的基本特性
答 :在特定时间内完成特定的任务,实时性与可靠性。
什么是不可剥夺型内核?
答:各个任务彼此合作共享一个CPU 内核要求每个任务自我放弃CPU的所有权。不可剥夺型调度法也称作合作型多任务,各个任务彼此合作共享一个CPU。
什么是可剥夺型内核?
答:根据优先级可占用当前CPU的使用权。
什么情况下用可剥夺型内核?
答:当系统响应时间很重要时,要使用【可剥夺型】内核。最高优先级的任务一旦就绪,总能得到CPU的控制权。
什么是【可重入型】函数?
答:可以被多个任务调用的函数,而不必担心数据的破坏。
可剥夺型内核是否可以直接使用【可重入型】函数?
答:使用可剥夺型内核时,应用程序不应直接使用不可重入型函数。
调用不可重入型函数时,要满足互斥条件,这一点可以用【互斥型信号量】来实现。
一个应用程序为什么一定要使用空闲任务?
答:为了让CPU在没有用户任务可执的时有事可做。
volatile 概念作用
volatile(英译:易变的)是一个特征修饰符关键字,防止编译器对修饰的变量相关代码进行优化,每次使用都重新读取变量的值,而不是使用寄存器里的备份。
volatile字面意思不太好理解,其实它是提醒编译器这个变量是易变的,不要去优化它!
XBYTE[2]=0x55;
XBYTE[2]=0x56;
XBYTE[2]=0x57;
XBYTE[2]=0x58;
对外部硬件而言,上述四条语句分别表示不同的操作,会产生四种不同的动作,但是编译器却会对上述四条语句进行优化,认为只有XBYTE[2]=0x58(即忽略前三条语句,只产生一条机器代码)。如果键入volatile,则编译器会逐一地进行编译并产生相应的机器代码(产生四条代码)。
变量声明时使用 volatile 修饰的几种情况
下面几个情况在声明的时候需要用volatile关键字对其修饰:
1)、并行设备的硬件寄存器(如:状态寄存器)
2)、一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3)、多线程应用中被几个任务共享的变量
一个参数既可以是 const 还可以是 volatile 吗?解释为什么。
答: 是的。
例如是只读的状态寄存器。
volatile 是不让编译器去优化它。const 是不让程序去修改它。
一个指针可以是 volatile 吗?解释为什么。
答: 是的。
例如当一个中断服务子程序修改一个指向一个buffer的指针时。
下面的函数被用来计算某个整数的平方,它能实现预期设计目标吗?如果不能,试回答存在什么问题:
int square(volatile int *ptr)
{
return ((*ptr) * (*ptr));
}
答: 不能。该代码本意是返回 ptr 指向的值的平方,但*ptr指向一个volatile型参数,编译器将产生类似下面的代码:
int square(volatile int* &ptr) //这里参数应该申明为引用,不然函数体里只会使用副本,外部没法更改
{
int a = *ptr;
int b = *ptr;
return a*b;
}
由于*ptr的值可能在两次取值语句之间发生改变,因此a和b可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:
int square(volatile int*ptr)
{
int a = *ptr;
return a*a;
}
volatile 用在如下的几个地方:
1)、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2)、多任务环境下各任务间共享的标志应该加volatile;
3)、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能有不同意义;
以上几种情况还要同时考虑数据的完整性(比如标志读了一半被打断了重写),volatile 是不具备原子性的,所以要考虑临界资源的访问冲突问题。
1)中可以通过关中断来实现,或其他方法实现;
2)中可以禁止任务调度,或其他方法实现;
3)中则只能依靠硬件的良好设计了。
临界区、临界资源
临界区:是指一个访问共用资源的程序片段。共用资源称为临界资源。
临界资源:具有无法同时被多个线程(比如多任务,比如中断服务程序)访问的特性,一次只能供一个线程使用。例如:共用的设备或存储器。打印机;中断服务程序与其他程序、中断与任务、任务与任务共享的变量等。
原子操作、原子性、原子
原子:指化学反应不可再分的基本微粒。这是物理化学中的概念,原子基本特性就是不可分割性。
原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。
原子操作:是指一系列不能被打断的操作。可以是一个步骤的操作,也可以是多个步骤的操作。原子操作依赖底层CPU实现。原子操作与临界区是密切相关的,可以说原子操作就是临界区引发出来的需求。
临界资源的保护
临界资源保护的核心就是在临界资源竞争中,让其不被破坏。为达到不被破坏目的,那就让临界资源一次只能供一个线程使用。能实现这样效果的立马会想到开关中断,开停调度器等。
像在FreeRTO临界段的实现就是用开关中断实现的。
功能 | API函数 | 说明 |
---|---|---|
进入临界区 | taskENTER_CRITICAL() | |
taskENTER_CRITICAL_FROM_ISR() | 用于中断 | |
退出临界区 | taskEXIT_CRITICAL() | |
taskEXIT_CRITICAL_FROM_ISR() | 用于中断 |
临界资源的保护--原子操作
如果访问=临界资源本身就是一个原子操作(比如一条指令就可以访问完成),这样也就不需要做开关中断的处理了。像早期的CPU的汇编一条指令是原子操作,但后来指令变啦,比如X86的块操作、SIMD之类的指令都不再是原子操作了。所以即使在单核微处理器中也不能把每一条汇编指令都认为是原子操作了,唯一能确定是原子操作的,恐怕只有读写寄存器了。
在linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。位和整型变量原子操作都依赖底层CPU的原子操作实现,因此所有这些函数都与CPU的架构密切相关。
临界资源的保护--锁
在linux中,实现文件上锁的函数有lock和fcntl,其中lock用于对文件施加建议性锁,而fcntl不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl还能对文件的莫一记录进行上锁,也即是记录锁。记录锁分为读取锁(共享锁)和写入锁(排斥锁)。
自旋锁,是锁的一种,自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
自旋锁的基本形式如下:
spin_lock(&mr_lock);
//临界区
spin_unlock(&mr_lock);
因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。
死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了
单片机裸机临界资源保护的实现
临界资源的保护在liunx、RTOS系统中都有封装好的API函数,在单片机裸机就没有API,需要用户自己处理。不过原理方法是一样的。
临界段开关中断的实现
(1)第一种方法:直接利用开启或者关闭中断的语句来控制。
过程:关中断-->>临界资源-->>开中断
#define INT_DIS() disable_interrupt() //关闭中断
#define INT_EN() enable_interrupt() //打开中断
优点:简单,执行速度快(只有一条指令),在临界保护操作频繁的场合优势突出。
缺点:不能临界区嵌套,临界区嵌套存在隐患。如果在A函数的临界代码区调用了另一个函数B,B函数内也有临界代码区,从B函数返回时,中断被打开了。这将造成A函数后续代码失去临界保护。
示例:禁止中断方法保护临界资源
大循环(后台)
INT_DIS() //禁止定时中断
访问临界资源;
INT_EN() //开启允许定时中断
定时器中断(前台)
操作全局变量A;
(2)第二种方法:嵌入式通用的做法
关中断前将总中断允许控制位状态所在的寄存器压入堆栈保存起来,然后再关中断保护临界区代码,之后根据堆栈内保存的控制字决定是否开启中断。在临界代码执行完毕之后,将中断允许状态将恢复到进入临界区之前的状态。
(3)第三种方法:
关中断前将总中断允许控制位状态保存到一个变量里,然后再关中断保护临界代码,之后根据保存的控制字决定是否恢复中断。同样可以实现退出临界区时恢复进入前的中断允许状态。
过程:中断状态存入变量-->>关中断-->>全局变量A-->>根据中断状态使能中断
缺点:每一段临界代码都要额外耗费两个字节的存储空间。
void EnterCritical(unsigned int *pSRVal)
{
*pSRVal = _get_SR_register(); //保存中断状态
INT_DIS(); //禁止中断,进入临界区。这里要考虑:进入临界区之前是什么状态,如果本来就是禁止中断的呢?
}
void ExitCritical(unsigned int * pSRVal)
{
if(*pSRVal & GIE) //判断进入临界区前的状态,如果是使能中断的状态,则开启中断。
{
INT_EN();
}
}
void Function_A(void)
{
unsigned int GIE_Val;
....
EnterCritical(&GIE_Val); //进入临界代码区,保存当前中断状态在GIE_Val变量中;
......
...... //临界区代码
ExitCritical(&GIE_Val); //退出临界区,并根据GIE_Val变量决定是否开中断;
.....
}
(4)第四种方法:用软件模拟堆栈行为。
将进入临界代码的次数和退出临界代码的次数进行统计,如果各临界代码之间有调用关系,则只是对最外层的临界代码区进行中断开关操作。
过程:类同第三种方法,不过中断只操作最外层
unsigned char criticalNesting = 0;
unsigned int interruptStatusValue = 0;
void EnterCritical(void)
{
if(criticalNesting == 0) //只对最外层进行操作
{
interruptStatusValue = _get_SR_register(); //保存中断状态
INT_DIS(); //禁止中断,进入临界区。这里要考虑:进入临界区之前是什么状态,如果本来就是禁止中断的呢?
}
criticalNesting++; //全局变量,是临界段的嵌套计数
}
void ExitCritical(void)
{
criticalNesting--;
if(criticalNesting == 0) //只对最外层操作.
{
if(interruptStatusValue & GIE) //判断进入临界区前的状态,如果是使能中断的状态,则开启中断。
{
INT_EN();
}
}
}
操作临界资源副本
有的时候,如果访问临界资源的过程比较长,可以对临界资源做一个副本拷贝,用拷贝的副本作为模块处理的数据。
以上文‘第一种方法’为例说明实现方法:
关中断-->>访问全局变量A-->>副本拷贝a-->>开中断->>操作副本拷贝a
锁
如果说临界资源比较复杂,若用拷贝副本方式也是很消耗资源的问题,这种情况可以做一个锁来解决!
下面以51内核裸机前后台程序架构为例:定时器与大循环共享临界资源。
示例:加锁的方法保护临界资源
大循环(后台)
ET0 = 0; //禁止定时中断
Lock = 1;
ET0 = 1; //开启允许定时中断
访问临界资源;
ET0 = 0; //禁止定时中断
Lock = 0;
ET0 = 1; //开启允许定时中断
定时器中断(前台)
if(lock == 0)
{
操作临界资源;
}
else
{
;
}
若Lock = 1;
与Lock = 0;
对应汇编指令是原子操作可以不用开关中断保护此锁
示例:加锁的方法保护临界资源
大循环(后台)
Lock = 1; //若此条语句对应汇编指令是原子操作可以不用开关中断保护此锁
访问临界资源;
Lock = 0; //若此条语句对应汇编指令是原子操作可以不用开关中断保护此锁
定时器中断(前台)
if(lock ==0)
{
操作临界资源;
}
else
{
;
}
51单片机实现互斥信号实现临界资源保护
举个例子:假如A线程用变量a作为临时存储区时,如果运行到一半中断发生了,而中断里也会用到该变量,等中断返回时,变量a中的内容已经被破坏了,进程A并不知道这一点,于是得到错误的运行结果.
避免的办法看似简单:拿个变量来当标志,为0时表示没有用共享资源,为1时表示正在使用.使用时先检查该标志,为1时等待,为0时则置1,并使用资源,用完后将标志置回0,于是资源冲突问题就解决了.
是吗?问题转移了----当你检查完变量,发现它为0,正要将它置1时,此时进程被打断,于是别的进程仍会毫不客气的使用该资源,要命的是在多任务系统中,当CPU控制权回来时,刚才使用这个资源的进程还没用完这个资源呢,于是出错了。
可以看出来,必须有一种这样的指令:先检查某变量,当它为0时则置1并跳转,而这一系列过程不允许被打断,拥有这种能力的指令就叫作"原子操作指令".
由于51没有这种指令,这也就是为什么楼上很多人说要关中断的原因了.事实上,关中断这种方式也用于我们桌面机系统! LINUX就用了很多关中断。
虽然51没有上面说的这种指令,却有另一个指令:DJNZ---先减再检查,并根据比较结果跳转,而这足够完成临界区操作了.系统初始化是必须事先将s置初值为1。
mutex_loop:
DJNZ s, mutex_wait ;先将s减1,再判断s是否为0,不为0跳到mutex_wait处执行,否则顺序执行。
....共享资源操作代码(也就是临界区)
INC s
RET
mutex_wait:
INC s
JMP mutex_loop
以上代码在任何情况下(跑飞不算,呵呵)不会发生两个进程同时进人临界区的情况.
你一定会想到,假如将上述的代码写成一个⼦程序,那就可以作为一个标准函数供C调用了,恭喜你,这就是标准的信号量。
中断与其他函数共享变量、临界资源的保护_匠在江湖的博客-CSDN博客
uCOS操作系统
从存储结构上看,任务是如何组成的?
答:从存储结构上看,任务由任务控制块、任务堆栈、任务代码三个部分组成。系统通过任务控制块感知和控制任务;任务堆栈主要用于保护断点和恢复断点;任务代码是一个超循环结构,描述了任务的执行过程。在创建一个任务时,函数OSTaskCreate()或OSTaskCreateExt()负责给任务分配任务控制块和任务堆栈,并对他们进行初始化,然后将任务控制块、任务堆栈、任务代码三者关联起来形成一个完整的任务。
系统是按照任务就绪表和任务的优先级来调度任务的。执行任务调度工作的是调度器,它负责查找具有最高优先级别的就绪任务并运行它,同时将该任务的TCB指针存放至OSTCBCur(即获取该任务的TCB指针)。通常系统在调用API函数和运行中断服务函数程序之后都要调用OSShed()来进行一次任务调度。
系统是按照任务就绪表和任务的优先级来调度任务的。
答:执行任务调度动作的是调度器,它负责查找具有最高优先级别的就绪任务并运行它,
同时将该任务的TCB指针存放在 OSTCBCur(即获取该任务的TCB指针)。
通常系统在调用API函数和运行中断服务函数程序之后都要调度OSShed()来进行一次任务调度。
什么是空任务控制块链表?什么是任务控制块链表?
答:在任务控制块的管理上,ucosii采用两条链表进行管理:
一条空任务块链表(其中所有任务控制块还未分配给任务)和
一条任务控制块链表(其中所有任务控制块已分配给任务)。
具体做法:系统在进行初始化时,先在RAM中建立一个OS_TCB结构类型的数组OSTCBTbl[],
然后将各个元素链接成一个链表,从而形成一个空任务块链表。
当系统创建一个任务时,就会将空任务控制块链表表头指针指向的任务控制块分配给该任务。
在给任务控制块中各成员赋值后,系统将按任务控制块链表的表头指针将其加⼊到任务控制块链表中。
注:空任务控制块链表为单向链表,任务控制块链表为双向链表
ucos的启动
先通过调用OSInit()函数对全局变量以及数据结构进行初始化,以建立ucosii的运行环境,
再通过调用OSStart()函数开始进进行多任务管理的,但在调用OSStart()函数之前必须创建至少一个用户任务。
就绪的最高优先级的任务
答:ucosii中存在任务就绪组OSRdyGrp和任务就绪表OSRdyTbl[ ],通过就绪表某位置1将某个任务进入就绪态;
同时,ucosii中还定义了一个OSUnMapTbl[ ],结合OSRdyGrp和OSRdyTbl[ ]可以很快确定哪个优先级的任务处于就绪状态。
==任务切换的核心工作就是任务堆栈指针的切换==。
最高优先级的任务进行任务切换—进入中断,切换任务堆栈的实现
答:通过任务就绪表机制可以快速查询到当前最高的优先级任务,通过OS_TASK_SW()这个宏可以实现任务上下文的切换,该宏即OSCtxSw()函数通过触发PendSV异常从而完成切换的工作。OSCtxSw()要依次完成如下7项工作:
1)将被终止的任务的断点指针保存到任务堆栈中;
2)将CPU通用寄存器的内容保存至任务堆栈中;
3)将被终止任务的任务堆栈指针当前值保存至该任务控制块的OSTCBStkPtr中;
4)获取待运行任务的任务控制块;
5)使CPU通过任务控制块获取待运行任务的任务堆栈指针;
6)将待运行任务堆栈中的通用寄存器的内容恢复到CPU通用寄存器中;
7)使CPU获得待运行任务的断点指针;
注:由于ucosii中总是把当前正在运行任务的任务控制块的指针存放在一个指针变量OSTCBCur中,并且对于待运行任务的任务控制块指针也很好获得(OSTCBHighRdy),所以2-6步骤很容易完成,问题是1和7的处理,并没有将断点指针(在PC寄存器中)压入堆栈的指令,从而想到使用引发中断的方式,利用系统在跳转到ISR时会自动将断点指针压入堆栈的功能将断点指针存入堆栈,返回时同理。
为什么采用PendSV异常这种方式来引发上下文切换?
答:对于Cortex-M3,这种方式是最推荐的,因为Cortex-M3会自动保存任何异常发生时一半的处理器上下文,并且在异常处理完后会自动恢复那一部分的值
OSStart()中调用OSStartHighRdy()函数完成的功能是?为什么调用它?
答:OSStartHighRdy()会触发一个PendSV异常,引发系统中第一个任务的运行。它设置系统的运行标志位OSRunning为True,将就绪表中最高优先级任务的堆栈指针Load到SP中,并强制中断返回。这样做的目的是让就绪的最高优先级任务就如同从中断返回到运行状态一样,使得整个系统得以运转。
任务调度由谁完成
因为uc/os-ii总是运行进入就绪状态的最高优先级的任务。所以,确定哪个任务优先级最高,
下面该哪个任务运行,这个工作就是由调度器【(scheduler)来完成的。】
任务级、中断级的调度
【任务级】的调度是由函数 OSSched() 完成; OSSched() 内部调用的是【OS_TASK_SW()】完成的调度。
【中断级】的调度是由函数 OSIntExt()完成; OSIntExt() 内部调用的是【 OSCtxSw() 】 完成的调度。
任务切换其实很简单,由如下2步完成:
(1)将被挂起任务的处理器寄存器推入自己的==任务堆栈==。
(2)然后将进入就绪状态的最高优先级的任务的寄存器值从堆栈中恢复到【寄存器】中。
任务的5种状态。
【睡眠态 (task dormat) 】: 任务驻留于程序空间(rom或ram)中,暂时没交给ucos-ii处理。
【就绪态(task ready)】: 进程已处于准备好运行的状态,即进程已分配到除CPU外的所有必要资源后,只要再获得CPU,便可立即执行。任务一旦建立,这个任务就进入了就绪态。
【运行态(task running)】:进程已经获得CPU,程序正在执行状态。调用OSStart()可以启动多任务。OSStart()函数只能调用一次,一旦调用,系统将运行进入就绪态并且优先级最高的任务。
【阻塞态(task waiting)】:正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态。正在运行的任务,通过延迟函数或pend(挂起)相关函数后,将进入等待状态。
【中断态(ISR running)】: 正在运行的任务是可以被中断的,除非该任务将中断关闭或者ucos-ii将中断关闭。
进程的常见状态?以及各种状态之间的转换条件?
答:就绪态、运行态、阻塞态。
FreeRTOS操作系统
FreeRTOS 移植到哪些平台,讲讲移植过程,占用哪些硬件资源?
STM32H743 FreeRTOS开发手册_V1.0 - 道客巴巴
FreeRTOS笔记—第一章 FreeRTOS概述_匠在江湖的博客-CSDN博客_freertos 介绍
FreeRTOS 都需要配置哪些,中断是怎么配置的 ,需要注意什么?
FreeRTOS 中的 IPC 通信都用过哪些?
FreeRTOS 任务栈 你是怎么设定的,参考依据是什么?
FreeRTOS 的调度方式是什么?
Linux部分
单选题
FTP服务和SMTP服务的端口默认分别是(C)
A 20与25
B 21与25
C 20,21与25
D 20与21
操作系统采用缓冲技术,通过减少对CPU的(A)次数,提高资源的利用率。
A 中断
B 访问
C 控制
D 依赖
多选题
关于红黑树和AVL树,以下哪种说法正确? ABC
A 两者都属于自平衡二叉树
B 两者查找,插入,删除的时间复杂度相同
C 包含n个内部节点的红黑树的高度是O(log(n))
D JDK的TreeMap是一个AVL的实现
Servlet的生命周期可以分为初始化阶段,运行阶段和销毁阶段三个阶段,以下过程属于初始化阶段是(ACD)。
A 加载Servlet类及.class对应的数据
B 创建serletRequest和servletResponse对象
C 创建ServletConfig对象
D 创建Servlet对象
Linux执行ls,会引起哪些系统调用(BC)
A nmap
B read
C execve
D fork