操作系统,揭开钢琴的盖子-2
C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)
本章讲解点
- 1.1 操作系统的来历
- 1.2 操作系统的功能
- 1.3 操作系统的硬件知识
- 1.4 linux下编译程序
- 1.5 Makefile
- 1.6 linux的常用指令
- 1.7 进程的概念
- 1.8 进程的状态转换
- 1.9 进程的创建
- 1.10 守护进程
- 1.11 僵尸进程与孤儿进程
- 1.12 wait()或waitpid()系统调用
- 1.13 进程通信——管道
- 1.14 进程通信——系统IPC
- 1.15 进程通信——socket套接字
- 1.16 线程
- 1.17 线程的创建
- 1.18 线程通信——互斥锁
- 1.19 线程通信——信号量
- 1.20 线程通信——条件变量
- 1.21 线程通信——读写锁
- 1.22 线程池
- 1.23 协程
- 1.24 虚拟内存
- 1.25 页表
- 1.26 缺页中断
- 1.27 缺页置换算法
- 1.28 锁
- 1.29 操作系统资源调度方法
- 1.30 IO模型类型
受众:本教程适合于C/C++已经入门的学生或人士,有一定的编程基础。
本教程适合于互联网、嵌入式软件求职的学生或人士。
故事背景
蒋 豆 芽:小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。
隔壁老李:大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。
导 师:蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。真是感动。
故事引入
隔壁老李:休息好了,豆芽,我们要接着讲啰!
蒋 豆 芽:累死了我,不过还要继续坚持!
1.3 操作系统的硬件知识
隔壁老李:接下来讲I/O设备。(3)CPU和存储器不是操作系统唯一需要管理的资源,IO设备也是其中之一。
如图,I/O设备一般包括两个部分:设备控制器和设备本身。控制器是插在电路板上的一块芯片或一组芯片,这块电路板物理地控制设备。它从操作系统接收命令,例如,从设备读数据。
许多情况下,对设备的控制是非常复杂的,控制器的任务是为操作系统提供一个简单的接口(不过还是很复杂,涉及复杂的硬盘存储映射)。为了完成复杂的硬盘存储映射以及数据读取操作,在控制器中会安装一个小的嵌入式计算机(MCU),该嵌入式计算机会帮助我们完成上面复杂的工作。
而不同的设备的控制器都是不同的,需要不同的软件进行控制。专门与控制器对话,发出命令并接受响应的软件,称为设备驱动程序。使用时,这些设备驱动程序会装入操作系统中,在核心态运行。
一个驱动程序就是一个函数和数据结构的集合,它的目的是建立内核和实际硬件之间的连接,从而提供通过内核访问底层硬件的上层API接口。内核用这个接口请求驱动程序控制设备的I/O操作。
隔壁老李:I/O设备用于数据的输入输出,实现输入和输出的方式有三种。
第一种方式,用户程序发出一个系统调用,设备驱动程序启动I/O并在一个循环中不断检查该设备,看该设备是否完成了工作。当I/O结束后,设备驱动程序把数据送到指定的地方(若有需要),并返回。然后操作系统将控制返回给调用者。
这种方式是忙等待,缺点是要占据CPU,CPU一直轮询设备知道对应的I/O操作完成。这样效率并不高。
第二种方式是设备驱动程序启动设备并且让该设备在操作完成时发出一个中断。
这里我们简单介绍下中断,这也是一个常考面试题。中断是指当出现需要时,CPU暂时停止当前进程的执行,转而执行处理新情况的中断处理程序。当执行完该中断处理程序后,则重新从刚才停下的位置继续当前进程的运行。
为了区分不同的中断,每个设备有自己的中断号。系统有0-255一共256个中断。系统有一张中断向量表,用于存放256个中断的中断服务程序入口地址。每个入口地址对应一段代码,即中断服务程序。
蒋 豆 芽:(困惑)那老李,我想问问,在嵌入式中,中断又是如何的呢?
隔壁老李:行,那我给你讲讲。
ARM有七种运行模式:
模式 | 意义 | 模式 | 模式 |
---|---|---|---|
用户模式(usr,User Mode) | ARM处理器正常的程序执行状态 | 非特权模式 | 普通模式 |
快速中断模式(FIQ,Fast Interrupt Request Mode) | 用于高速数据传输或通道处理。当触发快速中断时进入此模式 | 特权模式 | 异常模式 |
外部中断模式(IRQ,Interrupt Request Mode) | 用于通用的中断处理。当触发外部中断时进入此模式 | 特权模式 | 异常模式 |
管理模式(svc,Supervisor Mode) | 操作系统使用的保护模式。在系统复位或执行软件中断指令SWI时进入 | 特权模式 | 异常模式 |
数据访问中止模式(abt,Abort Mode) | 当数据或指令预取中止时进入该模式,可用于虚拟存储及存储保护 | 特权模式 | 异常模式 |
系统模式(sys,System Mode) | 运行具有特权的操作系统任务 | 特权模式 | 普通模式 |
未定义指令中止模式(und,Undefined Mode) | 当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真 | 特权模式 | 异常模式 |
中断模式是ARM异常模式之一(IRQ模式,FIQ模式,FIQ的优先级高于IRQ),是一种异步事件,如外部按键产生中断,内部定时器产生中断,通信数据口数据收发产生中断等。
隔壁老李:ARM系统 包括两类中断:一类是IRQ中断,另一类是FIQ中断。IRQ是普通中断,FIQ是快速中断,在进行大批量的复制、数据传输等工作时,常使用FIQ中断。FIQ的优先级高于IRQ。
中断向量表如下:
中断向量地址 | 异常中断类型 | 异常中断模式 | 优先级(6最低) |
---|---|---|---|
0x0 | 复位 | 特权模式(SVC) | 1 |
0x4 | 未定义中断 | 未定义指令终止模式(Undef) | 6 |
0x8 | 软件中断(SWI) | 特权模式(SVC) | 6 |
0x0c | 指令预取中止 | 中止模式 | 5 |
0x10 | 数据访问中止 | 中止模式 | 2 |
0x14 | 保留 | 未使用 | 未使用 |
0x18 | 外部中断请求(IRQ) | 外部中断(IRQ)模式 | 4 |
0x1c | 快速中断请求(FIQ) | 快速中断(FIQ)模式 | 3 |
IRQ中断和FIQ中断都属于ARM的异常模式。在ARM系统中,一旦有中断发生,不管是外部中断,还是内部中断,正在执行的程序都会停下来。接下来通常会按照如下步骤处理中断:
保存现场。保存当前的PC值到R14,寄存器R14常用作链接寄存器(LR,Link Register),当进入子程序时,常用来保存PC(Program Counter,程序计数器) 的返回值。保存PC值后,接着保存当前的程序运行状态到SPSR(Storage Program Status Register,程序状态备份寄存器)。
模式切换。根据发生的中断类型,进入IRQ模式或FIQ模式。
获取中断源。以异常向量表保存在低地址处为例,若是IRQ中断,则PC指针跳动0x18处(
0x18:LDR PC, IRQ_ADDR
);若是FIQ中断,则跳到0x1C处(0x1c:LDR PC, FIQ_ADDR
)。IRQ和FIQ的异常向量地址处一般保存的是中断服务子程序的地址,所以接下来PC指针跳入中断服务子程序处理中断。中断处理。
中断返回,恢复现场。当完成中断服务子程序后,将SPSR中保存的程序运行状态恢复到CPSR(Current Program Status Register,当前程序状态寄存器)中,R14中保存的被中断程序的地址恢复到PC中,继续执行被中断的程序。
整个过程如图:
这就是一个嵌入式外部中断完整的执行流程了。
隔壁老李:我们给出一个实例:我们以S3C2410A嵌入式处理器为例,通过定时器1中断控制CPU板上的LED1和LED2实现轮流闪烁。我们给出伪码,
对定时器1初始化,并设定定时器的中断时间为1s
void Timer1_init(void){ rGPCON = rGPGCON & 0xfff0ffff | 0x00050000; //配置GPG口为输出口 rGPGDAT = rGPGDAT | 0x300; // 初始化IO rTCFG0 = 255; // 预分频器的分频值,预分频器0 = 255 rTCFG1 = 0 // 时钟分频器的分频值,选择2分频 rTCNTB1 = 48828; //PCLK=50MHz下,1s的记数值rTCNTB1=50000000/4/256=48828 rTCMPB1 = 0x00; rTCON |= (1 << 22) // 开启自动装载被减数 rTCON |= (1 << 20) // 计数器开始 }
为了使CPU响应中断,在中断服务子程序执行之前,必须打开S3C2410A的CPSR位,以及相应的中断屏蔽寄存器位。打开相应的中断屏蔽寄存器位,是在Timer1INT_Init()函数中实现,具体代码如下,
void Timer1INT_Init(void){ //定时器接口使能 if((rINTPND & BIT_TIMER1)){ rSRCPND |= BIT_TIMER1; } //写入定时器1中断服务子程序的入口地址 pISR_TIMER1 = (int)Timer1_ISR; //开中断 rINTMSK &= ~(BIT_TIMER1); }
等待定时器中断,通过一个死循环如while(1);实现等待过程。
根据设置的定时时间,产生定时器中断。中断发生后,首先进行现场保护,然后转入中断的入口代码处执行。该部分代码通常使用汇编语言编写。在执行中断服务程序之前,要确保HandleIRQ地址处保存中断分发程序IsrIRQ的入口地址,代码如下:
ldr r0,=HandleIRQ ldr r1,=IsrIRQ str r1,[r0]
接下来将执行IsrIRQ中断分发程序,具体代码如下
sub sp,sp,#4 ;为保存PC预留堆栈空间 stmfd sp!,{r8-r9} ldr r9,=INTOFFSET ldr r9,[r9] ;加载INTOFFSET寄存器值到r9 ldr r8,=HandleEINT0 ;加载中断向量表的基地址到r8 add r8,r8,r9,lsl#2 ;获得中断向量 ldr r8,[r8] ;加载中断服务程序的入口地址到r8 str r8,[sp,#8] ;保存sp,将其作为新的pc值 ldmfd sp!,{r8-r9,pc};跳转到新的pc处执行,即跳转到中断服务子程序执行
执行中断服务子程序,该子程序实现将LED1和LED2灯熄灭或点亮。从现象中看到LED1和LED2灯闪烁一次,就说明定时器发生了一次中断
int flag; void __irq Timer1_ISR(void){ if (flag == 0){ rGPGDAT = rGPGDAT & 0xeff | 0x200; flag = 1; } else{ rGPGDAT = rGPGDAT & 0xdff | 0x100; flag=0; } rSRCPND|=BIT_TIMER1; rINTPND|=BIT_TIMER1; }
从中断中返回,恢复现场,跳转到被中断的主程序继续执行,等待下一次中断的到来。
蒋 豆 芽:(笑容邪魅)那这个User模式和Supervisor模式有什么区别?
隔壁老李:小子还想考我?哼!
用户模式user是用户程序的工作模式,它运行在操作系统的用户态,它没有权限去操作其它硬件资源,只能执行处理自己的数据,也不能切换到其它模式下,要想访问硬件资源或切换到其它模式只能通过软中断或产生异常。
管理模式Supervisor是CPU上电后默认模式,因此在该模式下主要用来做系统的初始化,软中断处理也在该模式下。当用户模式下的用户程序请求使用硬件资源时,通过软件中断进入该模式。相比与IRQ和FIQ通过硬件触发,Supervisor优先级最低,而且是通过软件触发。
蒋 豆 芽:(嘻嘻)嗯嗯,老李,我明白了。
隔壁老李:好了,我们就把中断讲清楚了。中断是用户访问IO设备的第二种方式。第二种方式好处就在于,不需要一直轮询设备,在设备中断之前,操作系统可以处理其他事情,当中断来时再处理中断。
隔壁老李:第三种方式是,为I/O使用一种特殊的直接存储器访问(Direct Memory Access,DMA)芯片,它可以直接控制外围设备的数据流,而无需持续的CPU干预。这样效率就很高了,但对应成本就相对高些,因为DMA是由专门的硬件( DMA)控制。
DMA传送主要用于需要高速大批量数据传送的系统中,以提高数据的吞吐量。如磁盘存取、图像处理、高速数据采集系统、同步通信中的收/发信号等方面应用甚广。通常只有数据流量较大(kBps或者更高)的外设才需要支持DMA能力,这些应用方面典型的例子包括视频、音频和网络接口。
因为无需CPU干预,那么DMA要进行数据传输就必须有两个条件:数据从哪传(源地址),数据传到哪里去(目的地址)。通过软件设置,设置好源地址和目的地址。在一个重要的条件就是触发源是什么,就是说什么时候进行DMA数据传输呢?这叫触发信号。也可以通过软件编程设置具体时间,具体条件来触发DMA数据传输。
我们展示一下stm32如何配置DMA:
DMA_InitTypeDef DMA_InitStructure; u16 DMA1_MEM_LEN;//保存DMA每次数据传送的长度 //DMA1的各通道配置 //这里的传输形式是固定的,这点要根据不同的情况来修改 //从存储器->外设模式/8位数据宽度/存储器增量模式 //DMA_CHx:DMA通道CHx //cpar:外设地址 //cmar:存储器地址 //cndtr:数据传输量 void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr){ RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输 DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为缺省值 DMA1_MEM_LEN=cndtr; DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设基地址 DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存读取发送到外设 DMA_InitStructure.DMA_BufferSize = cndtr; //DMA通道的DMA缓存的大小 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位 DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位 DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常模式 DMA_InitStructure.DMA_Priority = DMA_Priority_Mediu***通道 x拥有中优先级 DMA_InitStructure.DMA_M2***_M2M_Disable; //DMA通道x没有设置为内存到内存传输 DMA_Init(DMA_CHx, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器 } //开启一次DMA传输 void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx){ DMA_Cmd(DMA_CHx, DISABLE ); //关闭USART1 TX DMA1 所指示的通道 DMA_SetCurrDataCounter(DMA_CHx,DMA1_MEM_LEN);//DMA通道的DMA缓存的大小 DMA_Cmd(DMA_CHx, ENABLE); //使能USART1 TX DMA1 所指示的通道 }
隔壁老李:好了,我们来讲讲总线(4)任何一个微处理器都要与一定数量的部件和外围设备连接,但如果将各部件和每一种外围设备都分别用一组线路与CPU直接连接,那么连线将会错综复杂,甚至难以实现。为了简化硬件电路设计、简化系统结构,常用一组线路,配置以适当的接口电路,与各部件和外围设备连接,这组共用的连接线路被称为总线。
微机中总线一般有内部总线、系统总线和外部总线。计算机常用总线有PCIe、PCI、USB、SATA和DMI。好了,总线就讲到这里,总线不会影响到我们后面知识点的理解。嵌入式常用总线我们会在以后的章节详细讲解。
蒋 豆 芽:(激动)不错,小笔记本已经写得满满的了。
1.4 linux下编译程序
隔壁老李:(会心一笑)不要急,豆芽,我们后面的内容都会在Linux操作系统上演示,涉及到具体编程,所以还有必要讲讲Linux上的编程和常用指令。
隔壁老李:我们平时习惯了Windows编程,运行一个程序,编译、连接、执行,IDE都帮我们打包好了,确实很方便,但是Linux下就需要我们手动来做了,虽然也有IDE,但是我们接下来并不打算使用IDE,我们就是要手动来学习这个过程,有助于我们理解操作系统。
这里简单介绍下所用的操作系统,我们用的是Ubuntu 16.04版本,Ubuntu是一个以桌面应用为主的Linux操作系统,其内核是Linux。 从前人们认为Linux难以安装、难以使用,在Ubuntu出现后这些都成为了历史。Ubuntu也拥有庞大的社区力量,用户可以方便地从社区获得帮助。Ubuntu常常作为linux爱好者来学习linux应用编程和linux驱动开发的平台。
蒋 豆 芽:老李,你可太贴心了,我之前从来没用过linux操作系统编译程序,啥也不会呀!
隔壁老李:(1)我们首先进入Linux系统中的一个文件夹下,输入命令:touch douya.c 创建一个C文件:
这里提到了touch命令,我们简单介绍一下。
Linux touch命令用于修改文件或者目录的时间属性,包括存取时间和更改时间。若文件不存在,系统会建立一个新的文件。
ls -l 可以显示档案的时间记录
实例:
使用指令"touch"修改文件"douya.c"的时间属性为当前系统时间,输入如下命令:
$ touch douya.c #修改文件的时间属性
首先,使用ls命令查看douya.c文件的属性,如下所示:
$ ls -l douya.c -rw-rw-r-- 1 douya douya 86 Dec 10 19:30 douya.c
可以看到原来的修改时间为:
Dec 10 19:30
然后我们执行touch命令后再来查看:
$ touch douya.c #修改文件的时间属性 $ ls -l douya.c -rw-rw-r-- 1 douya douya 86 Dec 10 22:23 douya.c
可以看到,修改时间已经改变。
使用指令"touch"时,如果指定的文件不存在,则将创建一个新的空白文件。
(2)然后用gedit打开文件,手动输入我们的程序。
#include <stdio.h> int main(){ printf("He
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>