嵌入式面经问题总结
Posted Jocelin47
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了嵌入式面经问题总结相关的知识,希望对你有一定的参考价值。
arm工作模式
在特权模式下程序可以访问所有的系统资源。非特权模式和特权模式之间的区别在于有些操作只能在特权模式下才被允许,例如直接改变模式和中断使能等。而且为了保证数据安全,一般MMU会对地址空间进行划分,只有特权模式才能访问所有的地址空间。而用户模式如果需要访问硬件,必须切换到特权模式下,才允许访问硬件。
用户模式user是用户程序的工作模式,它运行在操作系统的用户态,它没有权限去操作其它硬件资源,只能执行处理自己的数据,也不能切换到其它模式下,要想访问硬件资源或切换到其它模式只能通过软中断或产生异常。
管理模式Supervisor是CPU上电后默认模式,因此在该模式下主要用来做系统的初始化,软中断处理也在该模式下。当用户模式下的用户程序请求使用硬件资源时,通过软件中断进入该模式。相比与IRQ和FIQ通过硬件触发,Supervisor优先级最低,而且是通过软件触发。
ARM系统 包括两类中断:一类是IRQ中断,另一类是FIQ中断。IRQ是普通中断,FIQ是快速中断,在进行大批量的复制、数据传输等工作时,常使用FIQ中断。FIQ的优先级高于IRQ。
IRQ和FIQ中断都属于ARM异常模式
软中断是什么?(ARM7中是软中断SWI,C-M3是SVC它们是一样的)
软中断在裸机上似乎不是那么重要,但是在操作系统上是必须的。对Linux来说,软中断是用户空间进入内核空间的唯一接口,我们常用的printf,fprintf()等函数的源码中就包含了系统调用(system call),而系统调用中就触发了软中断,然后将应用程序中的一些信息打印到屏幕,或写到硬盘。调用软中断相对应用程序的运行来说是比较费时间的,所以在编写Linux应用程序时要尽可能少的减少系统调用。
PendSV和上面软中断的SVC的区别是什么?
应用程序执行SVC 时都是希望所需的请求立即得到响应。另一方面,PendSV 则不同,它是可以像普通的中断一样被悬起的(不像SVC 那样会上
访)。OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。
悬起PendSV 的方法是:手工往NVIC 的PendSV 悬起寄存器中写1。悬起后,如果优先级不高,则将缓期等待执行。
PendSV的典型使用场合是在上下文切换中;
一个系统中有两个就绪的任务,上下文切换被触发的场景可以是
1、执行一个系统调用
2、系统滴答定时器(SYSTICK)中断
OS部分
中断部分
中断触发到返回的具体行为(CM3权威指南里有)
概念引导:R14链接寄存器一般保存中断运行前程序运行PC的地址,在中断恢复后将R14中保存的地址恢复到R15 PC寄存器中。
具体行为:
1、保存当前的PC值到R14;保存PC值后将当前程序运行状态保存到SPSR(程序状态备份寄存器)中
2、切换到相应的中断类型,IRQ或者FIQ模式
3、根据异常向量表跳转到相应的中断服务子程序的地址,PC进入中断服务子程序中开始处理中断
4、中断完成后,需要恢复现场,将SPSR保存的程序运行状态恢复到CPSR中,R14中保存的被中断程序的地址恢复到PC中
权威指南中的分析:
一、中断的响应:
三步操作:
1、入栈: 把8个寄存器的值压入栈
2、取向量:从向量表中找出对应的服务程序入口地址
3、堆栈指针MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC
入栈
响应异常的第一个行动,就是自动保存现场的必要部分。
依次把xPSR, PC, LR, R12以及R3‐R0由硬件自动压入适当的堆栈中:如果当响应中断时,当前的代码正在使用PSP,则压入PSP,即使用线程堆栈;否则压入MSP,使用主堆栈。一旦进入了服务例程,就将一直使用主堆栈。
取向量
更新寄存器
二、异常返回
比如第一个指令:通过BX LR告诉CPU我现在需要异常返回了。因为现在LR种保存的是EXC_RETURN,EXC_RETURN具体位段如下:可以知道是继续中断嵌套执行中断还是返回用户程序。
中断嵌套如何实现(以cortex-M架构下的FreeRTOS系统为例)
以下内容参考野火FreeRTOS内核实现与应用开发实战。
异常是导致处理器脱离正常运行转向执行特殊代码的任何事件
通常分为同步异常和异步异常。
- 由内部事件(像处理器指令运行产生的事件)引起的异常称为同步异常,例如造成被零除的算术运算引发一个异常。
- 异步异常主要是指由于外部异常源产生的异常,是一个由外部硬件装置产生的事件引起的异步异常。
同步异常触发后,系统必须立刻进行处理而不能够依然执行原有的程序指令步骤;而异步异常则可以延缓处理甚至是忽略,例如按键中断异常,虽然中断异常触发了,但是系统可以忽略它继续运行
这句话提到同步中断会立即执行,我的理解是比如在cotex-M中的优先级如下图所示:系统的优先级会高于外部中断,所以会优先执行同步异常,而外部中断会延缓执行。这一点与Linux下的机制正好相反,详细的介绍看下面的内容。
ARM Cortex-M NVIC 支持中断嵌套功能:当一个中断触发并且系统进行响应时,处理器硬件会将当前运行的部分上下文寄存器自动压入中断栈中,这部分的寄存器包括PSR,R0,R1,R2,R3 以及R12 寄存器。当系统正在服务一个中断时,如果有一个更高优先级的中断触发,那么处理器同样的会打断当前运行的中断服务例程,然后把老的中断服务例程上下文的PSR,R0,R1,R2,R3 和R12 寄存器自动保存到中断栈中。这些部分上下文寄存器保存到中断栈的行为完全是硬件行为,这一点是与其他ARM 处理器最大的区别(以往都需要依赖于软件保存上下文)。
另外,在ARM Cortex-M 系列处理器上,所有中断都采用中断向量表的方式进行处理,即当一个中断触发时,处理器将直接判定是哪个中断源,然后直接跳转到相应的固定位置进行处理。而在ARM7、ARM9 中,一般是先跳转进入IRQ 入口,然后再由软件进行判断是哪个中断源触发,获得了相对应的中断服务例程入口地址后,再进行后续的中断处理。ARM7、ARM9 的好处在于,所有中断它们都有统一的入口地址,便于OS 的统一管理。而ARM Cortex-M 系列处理器则恰恰相反,每个中断服务例程必须排列在一起放在统一的地址上(这个地址必须要设置到NVIC 的中断向量偏移寄存器中)。
Linux下的中断机制与FreeRTOS中进行对比
而对于Linux下的异常也是分为同步异常与异步异常,Linux的机制反而与FreeRTOS相反
同步异常(内部事件引起的异常)必须在一条指令执行完毕后才可以发生中断,而不是代码执行期间,如系统调用
通常分为:
- 故障:缺页
- 陷阱:系统调用
- 终止:整数除0
异步中断(外部中断)可以打断指令执行,如键盘中断。常见的如:I/O中断、时钟中断、硬件故障
外部中断也分为两种:
- 可屏蔽中断:打印机中断
- 不可屏蔽中断:提出请求必须无条件相应
那Linux的中断可以嵌套吗?早起的Linux是支持中断嵌套的,分为FIQ和IRQ,IRQ是可以嵌套的,而FIQ是不支持嵌套的。
当ARM处理器收到中断的时候,它进入中断模式,同时ARM处理器的CPSR寄存器的IRQ位(I位)会被硬件设置为屏蔽IRQ。
Linux内核会在如下2个时候重新开启CPSR对IRQ的响应:
- 从IRQ HANDLER返回中断底半部的SOFTIRQ 从IRQ
- HANDLER返回一个线程上下文
也就是现在Linux中的中断上下文机制,主要分为:
- 中断上半部
中断上半部通常执行紧急的事件,不可被中断
- 中断下半部
上半部执行完并清除中断标志后,下半部处理程序就被挂到该设备的下半部执行队列中区,通常下半部完成中断的绝大多数的任务,并且可以被新的中断打断。
中断,中断属于异步异常
FreeRTOS任务部分
任务在内存中的组织方式FreeRTOS为例(TCB-用户栈-用户代码)
准备知识:
空闲任务是FreeRTOS 系统自己启动的一个任务,优先级最低。当整个系统都没有就绪任务的时候,系统必须保证有一个任务在运行,空闲任务就是为这个设计的。当用户任务延时到期,又会从空闲任务切换回用户任务。
在FreeRTOS 系统中,每一个任务都是独立的,他们的运行环境都单独的保存在他们的栈空间当中。
1、那么在定义好任务函数之后,我们还要为任务定义一个栈,
根据栈分配的类型,创建的任务也分两种:
-
一种是静态内存上分配,预先定义好一个全局的静态的栈空间。任务删除时, 内存不能释放。
-
一种是动态内存堆上面运行时分配,按需分配内存,随用随取
2、定义好任务函数和任务栈之后,我们还需要为任务定义一个任务控制块(TCB),通常我们称这个任务控制块为任务的身份证。在C代码上,任务控制块就是一个结构体,里面有非常多的成员,这些成员共同描述了任务的全部信息。
任务控制块是在任务创建的时候分配内存空间创建,任务创建函数会返回一个指针,用于指向任务控制块,所以要预先为任务栈定义一个任务控制块指针,也是我们常说的任务句柄。
3、定义任务函数即任务入口函数
上下文切换时任务在内存中是如何变动,任务调度点
0、首先启动任务
当任务创建好后,是处于任务就绪(Ready),在就绪态的任务可以参与操作系统的调度。但是此时任务仅仅是创建了,还未开启任务调度器,也没创建空闲任务与定时器任务(如果使能了configUSE_TIMERS 这个宏定义),那这两个任务就是在启动任务调度器中实现。每个操作系统,任务调度器只启动一次,之后就不会再次执行了,FreeRTOS 中启动任务调度器的函数是vTaskStartScheduler(),并且启动任务调度器的时候就不会返回,从此任务管理都由FreeRTOS 管理,此时才是真正进入实时操作系统中的第一步。
空闲任务是在vTaskStartScheduler()中创建的
上述代码在启动调度器vTaskStartScheduler这个代码后,启动调度器,如果启动成功,则不再返回。在vTaskStartScheduler中:
PendSV 和 SysTick 的中断优先级为最低。SysTick 和PendSV 都会涉及到系统调度,系统调度的优先级要低于系统的其它硬件中断优先级,即优先相应系统中的外部硬件中断,所以SysTick 和PendSV 的中断优先级配置为最低。
在prvStartFirstTask中开始第一个任务,主要做了两个动作,一个是更新MSP 的值,二是产生SVC 系统调用,然后去到SVC 的中断服务函数里面真正切换到第一个任务。
svc中断响应后执行中断向量表中注册的PortSVCHandler,vPortSVCHandler()函数开始真正启动第一个任务,不再返回
2、任务的切换(可以理解成任务的调度点)
vTaskDelay()函数或者直接调用taskYIELD(); (yield让步、屈服)
vTaskDelay:阻塞延时的阻塞是指任务调用该延时函数后,任务会被剥离CPU 使用权,然后进入阻塞状态,直到延时结束,任务重新获取CPU 使用权才可以继续运行。在任务阻塞的这段时间,CPU 可以去执行其它的任务,如果其它的任务也在延时状态,那么CPU 就将运行空闲任务。
调用tashYIELD()会产生PendSV中断,在PendSV中断服务函数中会调用上下文切换函vTaskSwitchContext(),该函数的作用是寻找最高优先级的就绪任务,然后更新pxCurrentTCB。
任务的切换通过就绪列表来实现,就绪列表pxReadyTasksLists[ configMAX_PRIORITIES ]是一个数组,数组里面存的是就绪任务的TCB(准确来说是TCB 里面的xStateListItem 节点),数据的元素类型也就是一个链表。
任务在创建的时候,会根据任务的优先级将任务插入到就绪列表不同的位置。相同优先级的任务插入到就绪列表里面的同一条链表中,这就是我们下要讲的支持时间片。
支持时间片:
所谓时间片就是同一个优先级下可以有多个任务,每个任务轮流地享有相同的CPU 时间,享有CPU 的时间我们叫时间片。
在RTOS 中,最小的时间单位为一个tick,即SysTick 的中断周期,RT-Thread 和μC/OS可以指定时间片的大小为多个tick,但是FreeRTOS 不一样,时间片只能是一个tick。与其说FreeRTOS 支持时间片,倒不如说它的时间片就是正常的任务调度。
相同优先级的任务采用时间片轮转方式进行调度(也就是通常说的分时调度器)
优先级反转如何解决,任务抢占如何发生,通信机制
通信机制:
-
消息队列:消息队列可以应用于发送不定长消息的场合,包括任务与任务间的消息交换,队列是FreeRTOS 主要的任务间通讯方式,可以在任务与任务间、中断和任务间传送信息,发送到队列的消息是通过拷贝方式实现的,这意味着队列存储的数据是原数据,而不是原数据的引用。
-
二值信号量:二值信号量是任务间、任务与中断间同步的重要手段,将二值信号量看作只有一个消息的队列,二值信号量既可以用于临界资源访问也可以用于同步功能。
-
互斥量又称互斥信号量(本质是信号量),是一种特殊的二值信号量,它和信号量不同的是,它支持互斥量所有权、递归访问以及防止优先级翻转的特性。
二值信号量和互斥信号量(以下使用互斥量表示互斥信号量)非常相似,但是有一些细微差别:互斥量有优先级继承机制,二值信号量则没有这个机制。这使得二值信号量更偏向应用于同步功能(任务与任务间的同步或任务和中断间同步),而互斥量更偏向应用于临界资源的访问。 -
我们先来说使用信号时的优先级翻转问题:
现在有3 个任务分别为H 任务(High)、M 任务(Middle)、L 任务 (Low),3 个任务的优先级顺序为H 任务>M 任务>L任务。正常运行的时候H 任务可以 打断M任务与L 任务,M任务可以打断L 任务,假设系统中有一个资源被保护了,此时该 资源被L任务正在使用中,某一刻,H 任务需要使用该资源,但是L 任务还没使用完,H 任务则因为申请不到资源而进入阻塞态,L任务继续使用该资源,此时已经出现了“优先 级翻转”现象,高优先级任务在等着低优先级的任务执行,如果在L 任务执行的时候刚好 M任务被唤醒了,由于M 任务优先级比L 任务优先级高,那么会打断L 任务,抢占了 CPU 的使用权,直到M任务执行完,再把CPU使用权归还给L 任务,L 任务继续执行, 等到执行完毕之后释放该资源,H 任务此时才从阻塞态解除,使用该资源。这个过程,本来是最高优先级的H 任务,在等待了更低优先级的L 任务与M 任务,其阻塞的时间是M 任务运行时间+L 任务运行时间,这只是只有3个任务的系统,假如很多个这样子的任务打 断最低优先级的任务,那这个系统最高优先级任务岂不是崩溃了,这个现象是绝对不允许出现的,高优先级的任务必须能及时响应。所以,没有优先级继承的情况下,使用资源保护,其危害极大.
优先级翻转示意图
我们再来说优先级继承机制:
优先级继承算法是指, 暂时提高某个占有某种资源的低优先级任务的优先级,使之与在所有等待该资源的任务中优先级最高那个任务的优先级相等,而当这个低优先级任务执行完毕释放该资源时,优先级重新回到初始设定值。因此,继承优先级的任务避免了系统资源被任何中间优先级的任务抢占。
某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问,去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同,这个优先级提升的过程叫做优先级继承。这个优先级继承机制确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”危害降低到最小。
FreeRTOS临界区是怎么实现的(开关中断实现)(与互斥量的区别)
1、临界区介绍
首先临界区的目的就是为了防止全局变量被任意的访问,临界区被打断的方式有:系统调度(PENDSV实现任务的切换,还是中断)、外部中断。因此FreeRTOS对临界段的保护最终还是回到对中断的开关控制上,只是对于中断函数以及普通任务的嵌套有区别而已。
不带返回值的关中断函数,不能嵌套,不能在中断里面使用。不带返回值的意思是:在往BASEPRI 写入新的值的时候,不用先将BASEPRI 的值保存起来,即不用管当前的中断状态是怎么样的,既然不用管当前的中断状态,也就意味着这样的函数不能在中断里面调用。
2、进入临界段的操作
进入和退出临界段的宏分中断保护版本和非中断版本,但最终都是通过开/关中断来实现。
- 不带中断保护版本,不能嵌套
通过变量的自增
- 带中断保护版本,可以嵌套
3、互斥量如何实现的呢
- 函数xSemaphoreCreateCounting()用于创建信号量
/* 初始化有1 个可用资源,当前可用资源为0,此时计数信号量的功能等同二值信号量 */
xSemaphore = xSemaphoreCreateCounting(1, 0);
- 函数xSemaphoreCreateMutex 用于创建互斥信号量。
SemaphoreHandle_t xSemaphoreCreateMutex( void )
/*返回值,如果创建成功会返回互斥信号量的句柄*/
- 递归互斥量创建函数xSemaphoreCreateRecursiveMutex()
- 函数xQueueCreateMutex 的实现是基于消息队列函xQueueGenericCreate 实现的。
#define xSemaphoreCreateMutex() xQueueCreateMutex(queueQUEUE_TYPE_MUTEX )
-
互斥量获取函数xSemaphoreTake()
-
函数xSemaphoreGive 用于在任务代码中释放信号量。
xSemaphoreGive( SemaphoreHandle_t xSemaphore ); /* 信号量句柄 */
流水线冲突与解决以及cache-miss
uboot作用
1、uboot是一个裸机程序,比较复杂。
2、uboot就是一个BootLoader,作用就是引导linux启动。uboot最主要的工作就是初始化DDR里面,因为linux试运行在DDR里面的。
如果linux的镜像编译出来不进行裁剪的话大概4、5M左右大小,一般芯片的RAM是没有这么大的,所以你要运行linux系统就需要提前把DDR初始化好了,这就是Uboot要运行做的事。但是6ULL的DDR初始化是bootROM做的,也就是芯片内部的一小段代码ROMCODE。其他的一些三星的芯片是不支持bootROM初始化DDR的。
一般linux镜像(zImage/uImage)+设备树( .dtb文件,以树的形式存放板子上的硬件信息)存放在SD卡、EMMC、NAND FLASH、SPI FLASH等等外置的存储区域。
因此需要将linux镜像从外置flash拷贝到DDR中,再去启动。
因此uboot的主要目的就是为系统的启动做准备
Uboot,不仅仅能启动Linux,也可以启动其他系统,比如vxworks。
Linux不仅仅能通过uboot启动。
Uboot是个通用的bootloader,他支持多种架构。
为什么要有虚拟内存?
为什么要用到虚拟内存:(P15)
1、需要地址空间隔离
不能随意的修改物理空间的地址的内容,有些程序可能会误操作修改了别的程序的内容
2、内存使用效率太低了
因为程序整个都放入到物理地址中,换入换出特别麻烦
3、程序运行的地址不确定
程序装载运行的时候,内存分配的空间区域是不确定的。因为在程序编写的时候,它访问数据和指令跳转时的目标地址很多都是固定的,涉及程序程序重定位的问题。
因此引出了分段的机制:
分段解决了上面的1、3问题(提一下第三个问题:通过虚拟内存后,就可以解决每次固定都可以重定位到想要的区域,而如果没有虚拟地址的话他们每次都映射同一个重定位的地址,这肯定是不行的),但是还是存在内存使用效率的问题。分段对内存的映射还是按照程序为单位,如果内存不足,还是存在换入换出到磁盘的都是整个程序。
重点来了:根据程序的局部性原理:当一个程序在运行的时候,在某个时间段里面,它只是频繁的用到了一小部分数据,因此人们想到了更小的粒度的内存分割和映射方法就是分页,利用分页机制充分的利用了程序的局部性原理。
程序生成到运行的过程
第一个阶段是预处理:预处理阶段主要的工作是
- 将所有的”#define”删除,并且展开所有宏定义。
- 处理所有条件预编译指令,比如”#if”
- 处理”#include”预编译指令,将包含的文件插入到该预编译指令的位置
- 删除所有注释”//”和”/**/”
- 添加行号和文件
- 保留所有的#pragme编译器指令,因为编译器需要使用它们 经过预编译后的.i文件不包含任何宏定义,因为所有的宏已经被展开。
第二个阶段是进行编译:
- 编译阶段会将展开的程序进行词法及语法分析,并且编译器还会将代码做一定的优化处理,这是.i文件变成.s文件。
第三个阶段是汇编:(汇编部分结合了第三章的内容)
- 汇编会将编译器检查后的代码由高级语言翻译成机器语言,使得执行的程序并不依赖于某一个特定的CPU。这时由.s文件生成.o目标文件,同时编译后生成的不同的信息按照段的形式进行存储,比如程序源代码编译后的机器指令放在代码段(.text段)里、全局变量和局部静态变量数据存放在.data段、为初始化的全局变量和局部静态变量数据存在.bss段,同时还有符号表以及段表其他信息。
第四个阶段是链接:(链接部分结合了第四章的内容)
链接一般分为两步链接:
- 第一步 空间与地址分配 这一步,连接器将能够获得所有输入目标文件的段长度,并且将它们合并,计算出输出文件中各个段合并后的长度与位置,并建立映射关系。
- 第二部 符号解析与重定位
获取上一步的信息,读取输入文件中段的数据,重定位信息,进行符号解析与重定位,调整代码中的地址。因为第一步我们已经确定每个段以及相应的符号的位置,所以我们通过这一步就可以进行重定位确定需要跳转的地址(P103-106介绍了对于外部引用的变量未链接前只是存放一个临时地址,经过分配空间后有了地址,重定位后就可以找到真正的地方了)。这一步是连接过程的核心,特别是重定位过程。
至此,上面是一个程序产生的过程。接下来就是程序装载的过程了。
对于32位的操作系统,一个程序运行起来后,他会有自己独立的虚拟地址空间,对于linux进程,它的0-3G是用户空间,3-4G是内核空间。
如果32位的操作系统物理地址4G空间不够应用程序装载的话怎么办?
答案:操作系统采用窗口映射的方法:例如可以将虚拟地址空间
0x010000000-0x20000000这一段256M的虚拟地址空间作为窗口,应用程序根据选择和需要来选择申请和映射。 Linux下采用mmap来实现
第五个阶段是装载:(第六章的内容)
Linux下程序装载的方式通常为页映射,对程序的虚拟地址空间进行分页4K大小,如果存放在物理内存里的页满了,就需要将之前存放在内存中的页进行换出,这里就有常用的页面置换算法:LRU,FIFO等等。当前,对于程序既有分页也有分段机制,也有段页机制。段式的缺点见上面总结的答案为什么要有虚拟内存?
这里就会有一个问题:我们在第四步链接的时候为所有的目标文件合并后,都分配了相应的地址空间,最后链接到一个可执行文件中。那可执行文件的内存分布关系与虚拟内存中间存放程序的空间是什么关系呢,有什么联系呢?
答案:我们在创建一个进程,然后装载相应的可执行文件并执行之前,需要做三件事情:
1、创建一个独立的虚拟地址空间
我们知道一个虚拟空间由一组页映射函数将虚拟地址空间的各个页映射至相应的物理地址,所以创建一个虚拟地址空间并不是创建空间而是创建映射函数所需要的数据结构。
实际上是一个页目录,甚至不设置页映射关系,映射关系等发生缺页错误的时候再进行设置。
这一步的内容是虚拟空间到物理地址的映射关系!!!
2、读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系⭐⭐⭐⭐⭐(以下内容没有废话)
当程序发生页错误的时候,操作系统将从物理内存中分配一个物理页,然后将该”缺页”从磁盘中读取到内存中,再设置缺页的虚拟页和物理页的映射关系。当操作系统捕获到缺页错误时,它应知道程序当前所需要的页在可执行文件中的哪一个位置。这就是虚拟空间与可执行文件之间的映射关系。Linux中将进程虚拟空间中的一个段叫做虚拟内存区域,在Windows中将这个叫做”虚拟段”。因此这一步也是程序装载的过程终最重要的一步!!!
因此我们可以总结到,可执行文件装载实际上是被映射的虚拟空间。
3、将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
操作系统通过设置CPU的指令寄存器将控制权交给进程,由此进程开始执行。从进程的角度看可以简单的认为操作系统执行了一条跳转指令,直接跳转到可执行文件的入口地址。ELF头中保存有入口地址。
静动态链接的区别
进程间通信方式
- 无名管道:只能用于具有亲缘关系的进程之间的通信。
函数:int pipe(int pipefd[2]); 相当于在内核开辟了一块缓冲区。 - fifo有名管道:任意两个进程间通信。
函数:int mkfifo(const char *pathname, mode_t mode);
1、内核会为fifo文件开辟一块缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写。
2、open函数的注意事项:打开fifo文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另外一端打开,除非对端的OPEN也打开。 - 信号量:实现进程间的同步。
- 信号:用于进程间的异步通信。
通过发送信号事件不同轮询或者中断的去查看事件,只需要事件好了以后通过发送信号通知我,实现的是异步通信。 - 消息队列:数据传输。
- 共享内存:数据传输,开辟一块内存区域,大家都能访问,进程退出后,这块内存会保存下来,后来者还可以继续使用。
- mmap:数据传输,跟共享内存不同的是,这里是在内存开辟一片缓冲区,把文件映射到内存上,你直接去操作内存(操作内存就是快!)就可以了。
- Sokect:套接字缓冲区,不同主机之间不同进程间的通信。
io多路复用
为什么要用io多路复用:
首先我有一个网络有很多客户端连接请求,如果我用多线程去处理的话,会带来上下文切换,如果客户端特别多的话,会带来上下文切换带来的代价特别高。
因此从多线程->单线程。
一、select
select缺点:
1、限制1024位的位图;
2、每次遍历完一次后,要通过FDSET重新置位所有的fds,再重新判断,所以fd_set是不可重用的;
3、每次都要将fd_set从用户态拷贝到内核态,仍有一个开销;
4、从内核态返回后,我并不知道哪些置位了,我又得从0到fd_max O(n)的去遍历一边判断有哪些置位了。
二、poll
poll: poll在select的基础上将位图改为了pollfd
struct pollfd
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */
;
通过建立一个struct pollfd类型的数组。
跟select相比解决了select的缺点1、2,3和4没有解决:
1、select限制为1024,而poll可以自己规定大小,没有事件数量限制;
2、不需要重新置位fd_set,只需要把revents判断后置0即可;
三、epoll
内核会产生一个epoll 实例数据结构并返回一个文件描述符,这个特殊的描述符就是epoll实例的句柄。
内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。
int epfd = epoll_create(0);
epoll和poll很像,都是通过一个结构体。
typedef union epoll_data
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
epoll_data_t;
struct epoll_event
uint32_t events;
epoll_data_t data;
;
events:
- EPOLLIN - 读
- EPOLLOUT - 写
- EPOLLERR - 异常
我们通过对前面epoll_create创建的任务句柄epfd ,通过epoll_ctl对这个对象进行操作(添加、删除、修改),把需要监控的描述符加进去,这些描述符将会以epoll_event结构体的形式组成一颗红黑树(说明查询的复杂度降为logn了);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入一个链表中,返回有事件发生的链表,用户再遍历这个链表查看具体是哪个事件。
int epoll_wait(int epfd, //epoll_wait的返回值返回事件的数目,并将触发的事件写入events数组中
struct epoll_event* events, // 结构体指针,发生改变的文件描述符元素是存在epoll_event结构体里,
//当发生改变,内核会把改变了的epoll_event拷贝到epoll_wait函数的第二个参数里面
int maxevents, //数组的容量
int timeout //函数是否阻塞
);
epoll_wait的返回值是返回事件的数目,并将触发的事件写入events数组中,epoll_wait中的第二个参数就是用户传入的epoll数组因为它是一个结构体指针,最后的结果也会重新放入这个数组中,有事件的数组大小为epoll_wait的返回值。
同时epoll存在两种触发方式:
-
ET模式(边缘触发)只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回;
-
LT 模式(水平触发,默认)只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。
如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边缘触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。
参考链接
跟poll和select相比,epoll的优点:
1、poll和select是通过用户态内存到内核态内存的大量复制,而epoll是将事件复制到mmap共享内存中,通过内核与用户空间mmap同一块内存实现的
2、epoll不需要一个一个遍历查看哪些事件发生了,直接遍历epoll_wait返回的双向链表中产生的事件个数即可。
手撕memcpy
I2C SPI 基础知识速率 工作模式
以上是关于嵌入式面经问题总结的主要内容,如果未能解决你的问题,请参考以下文章