一、基础问题
1、什么是堆内存和栈内存?
答: 我们可以从几个方面来看它们之间的区别
-
分配方式
栈
:由编译器自动分配和释放,一般存放函数的参数、局部变量、临时变量、函数返回地址等
堆
:堆内存是由程序员进行分配和释放的,也称为动态内存分配
。如果没有手动free,在程序结束时有可能由操作系统自动释放(但是仅限于有回收机制的语言, 像C/C++就必须进行手动释放,不然就有可能造成内存泄漏) -
生命周期
栈
:由于栈上的空间是自动分配自动回收的,所以栈上数据的生命周期只是在函数的运行过程中存在,运行后就释放掉了
堆
:堆上的数据,只要程序员不释放空间,就可以一直访问到。 -
空间申请效率
栈
:由系统自动分配,效率比较高
堆
:比较慢 -
分配方向
栈
:用户进程的用户栈从3GB虚拟空间的顶部开始,由顶向下延伸
堆
:分配的空间从数据段的顶部end_data到用户栈的底部。由下往上,每分配一块空间,就把边界往上推进一段。
2、用户层open、read等函数如何调用到对应的驱动ops函数?
3、i2c、uart、spi协议
-
i2c
:物理连接:由SDA、SCL和上拉电阻组成。
当总线空闲时,SDA和SCL两根线一般会被上拉电阻拉高,保持高电平
传输速度: I2C总线数据传输速率在标准模式下可以达到100kbit/s,快速模式下可以达到400kbit/s。一般I2C总线接口可通过编程时钟来实现传输速率的调整,同时也跟所接的上拉电阻的阻值有关
通信协议:I2C协议规定,必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件(总线在空闲状态,SDA和SCL都是高电平,当SCL为高电平,SDA由高变低的时候,则会产生一个起始条件;当SCL为高电平,SDA由低到高时,则会产生一个停止条件)
;I2C总线上的每一个设备都对应一个唯一的地址,主从设备之间的数据传输是建立在地址的基础上的,也就是说,主设备在传输有效数据之前 要先指定从设备的地址,地址指定的过程和上面数据传输的过程一样,只不过大多数从设备的地址是7位的,然后协议规定再给地址添加一个最低位用来表示接下来 数据传输的方向,0表示主设备向从设备写数据,1表示主设备向从设备读数据 -
uart
:物理连接:由一根TX线、一根RX线和一根地线组成
传输速度:传输速度可以由通信双方共同定的波特率来决定
通信协议:UART作为异步串口通信协议
的一种,工作原理是将数据的字节一位接一位地传输。默认情况下,UART处于空闲状态
,此时信号线为高电平;在开始进行数据传输的时候,发送方要先发送一个低电平,代表起始位
;起始位后就是传输的数据位
;数据位发送完成后,一般还要一个奇偶校验
;最后是停止位
,可以是1位、1.5位、2位的高电平。 -
spi
:看这
他们之间更详细的区别介绍,看这
4、说一下volitate和static
https://blog.csdn.net/wyttRain/article/details/117487288
二、Linux内存管理
1、什么是MMU?MMU实现了什么功能?
答: MMU
的中文全称为内存管理单元
,主要实现的功能是将虚拟地址转换为物理地址。MMU
的工作就是一个查询页表的过程,如果页表信息直接存储在内存中,那么查询起来非常费时,所以一般将页表信息存放在MMU
上的一小块访问更快的区域,也就是TLB
中,用于提高查找效率。
2、简述一下现代处理器(如ARM)寻址的过程
答: 现代处理器,由于引入了分页机制,处理器可以直接寻址虚拟地址,这个虚拟地址会发送到内存管理单元(MMU),通过MMU将虚拟地址转换到物理地址。虚拟地址va[31:0]一般分成两部分,一部分代表的是虚拟页面的偏移量,另一部分用来寻找属于哪个页,被称为虚拟页帧号(Virtual Page Frame Number,VPN)。物理地址也同样分为偏移量和物理页帧号(Physical Frame Number,PFN)。处理器通常会使用一张表来存储VPN到PFN的映射关系,这张表称为页表(Page Table,PT),页表中的每一项被称为页表项(Page Table Entry,PTE)(页表项分成两个部分:一部分是PFN,PFN加上页面偏移量就可以得到最终的物理地址PA,另一部分是页表项的属性。)。如果将这张表放到处理器的寄存器中,则会占用很多硬件资源,因此通常的做法是将这张表存放到主存里,然后通过页表基地址寄存器来指向这张页表的起始地址。
实际上的系统中,包含了多级的页表,比如对于ARM64的处理器来说,一般有4级页表。拿二级页表来举个例子,页表基地址寄存器指向的是一级页表的基地址,一级页表的页表项(PTE)中存放了一个指针,指向了二级页表的基地址。
当TLB未命中时,处理器的MMU页表查询的过程如下:
- 处理器根据页表基地址控制寄存器(TTBCR)和虚拟地址来判断使用哪个页表基地址寄存器(TTBR0或者TTBR1)。页表基地址寄存器中存放着一级页表的基地址;
- 处理器根据虚拟地址的Bit[31:20]作为所有制,在一级页表中找到页表项;
- 一级页表的页表项中存放着二级页表的物理基地址。处理器使用虚拟地址的Bit[19:12]作为所有制,在二级页表中找到相应的页表项
- 二级页表的页表项中存放了4KB页的物理基地址。这样,处理器就完成了页表的查询和翻译工作。
2、什么是DMA?为什么需要DMA?DMA的数据传输流程是怎么样的?DMA有什么弊端?
答: DMA
的中文全称是直接访问存储器
,可以让存储器和外部设备直接进行数据交换。 DMA
在进行数据传输的过程中,其实是不需要CPU参与的,也就是说,当DMA
在进行数据传输工作的时候,CPU也可以同步进行其他工作,大大提高了处理器的工作效率。DMA
主要用于需要高速、大批量数据传送的系统中,目的是提高数据的吞吐量,如磁盘存取、图像处理、高速数据采集系统方面应用甚广。通常只有数据量较大的外设才需要支持DMA
能力,比如视频、音频和网络接口。
DMA
的工作过程是由一个DMA控制器
来进行控制的。由DMA控制器
来接收外设的DMA请求
,然后发送一个总线请求信号
给CPU,当CPU接收到总线请求信号后,会在总线状态处于空闲时,反馈一个信号给DMA控制器
,DMA控制器
会获得总线的控制权,然后反馈一个DMA应答信号
给外部设备,表示当前允许进行DMA数据传输。
因为在进行DMA数据传输
的时候,DMA控制器
需要获得总线的控制权,这可能会影响CPU会中断请求的及时响应和处理。因此,在一些系统或速度要求不高,数据传输量不大的系统中,一般并不用DMA方式。
想进一步了解DMA的可以看此链接
3、什么是cache?为什么需要cache?请简述一下cache的工作方式
答: cache
是一块介于CPU和主存之间的高速缓冲存储器。cache
的作用是加快处理器获取数据的速度,可以有效地提高CPU的效率。当主控发起对某个地址(这个地址是虚拟地址
)的访问请求时,会将这个地址同时发送给MMU
和cache
,虚拟地址会在MMU
中的TLB
结构中查找,如果TLB
命中,那么可以直接返回一个物理地址。如果TLB Miss
,那么需要进一步访问MMU
并查询页表,如果还是没有找到页表,那么就会触发一个缺页异常中断
。同时,处理器通过高速缓存编码地址的索引域(Index)
可以很快找到相应的高速缓存行对应的组。但是这里的高速缓存行的数据不一定是处理器所需要的,因此有必要进行一些检查,将高速缓存行中存放的标记域和通过虚实地址转换得到的物理地址的标记域进行比较。如果相同并且状态位匹配,那么就会发生高速缓存命中(cache hit),处理器经过字节选择与对齐(byte select and align)部件,最终就可以获取所需要的数据。如果发生高速缓存 未命中(cache miss)
,处理器需要用物理地址进一步访问主存储器
来获得最终数据,数据也会填充到相应的高速缓存行中。上述描述的是VIPT(Virtual Index Physical Tag)
的高速缓存组织方式。这个也是经典的cache运行模式了。
4、cache有哪几种映射方式?它们各自的优点和缺点是什么?
这里的映射指的是cache
和主存
之间的映射关系。常见的cache
映射方式有三种:直接映射
、全关联
和组相连
-
直接映射
:也就是cache
和主存
是一一对应的关系。但是,cache
的大小是比主存要小得多的,也就是直接映射的话cache只能映射到主存的一小部分,如果处理器想要访问主存的地址A,而cache
正好映射在主存地址A的这个区域,那自然好,但是如果处理器想要访问主存的地址B,而cache并没有映射到主存的这个区域,那么就会发生cache miss
,那么cache
就需要重新映射新的地址,如果处理器频繁地访问主存不同的地址数据,那么cache需要一直不断地重写,这样就会发生cache的颠簸,非常影响处理器效率。 -
全关联
:全相联映射方式比较灵活,主存的各块可以映射到Cache的任一块中,Cache的利用率高,块冲突概率低,只要淘汰Cache中的某一块,即可调入主存的任一块。但是,由于Cache比较电路的设计和实现比较困难,这种方式只适合于小容量Cache采用。 -
组相连
:组相连是现在最常见的一种cache映射方式,是直接映射
和全关联
的折中方法。也就是将一块内存分成了很多组,然后cache也分成了很多组,每组cache中都有多个cache line,例如,主存中的第0块、第8块……均映射于Cache的第0组,但可映射到Cache第0组中的第0块或第1块;主存的第1块、第9块……均映射于Cache的第1组,但可映射到Cache第1组中的第2块或第3块。这样就可以减少cache颠簸的概率。
5、cache的cache line、way和set分别指的都是什么?你能画出它们的结构图吗?
以一个32KB的4路组相连的cache中,其中cache line为32 Byte为例。
什么路(way),set(组)都非常难理解,我们换个通俗点的说法。cache是一个高速缓冲存储块,按照上面描述的,这个存储块有32KB,然后还分4路,也就是这个cache总大小是32块,然后还分成4小块,也就是一小块是32/4=8KB,然后后面又说,每个cache line是32Byte,那么一小块里面就有8KB/32Byte=256个cache line,然后每一个小块的相同位置的cache line,就称为一个组。
6、在多核处理器中,cache的一致性是如何保证的?
cache
的一致性是通过一个叫做MESI
的机制来保证的。
想要进一步了解cache可以看这个链接
7、malloc的实现机制
答: malloc本质上是在维护一条内存空闲链表,每次我们调用mallo分配内存空间时,链表就会从头开始遍历,来寻找一个合适的空闲内存空间,然后把这个空间发分隔开,一部分分配给用户,另一部分标注为空闲。而当没有足够大的内存块时,malloc就会通过系统调用来申请更多的内存块。当我们调用free释放内存块后,该内存块会回到链表中,并且相邻的内存块会合并成一个大内存块。
kmalloc、vmalloc和malloc有什么区别和实现上的差异?
- kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
- kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续
- kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
- 内存只有在要被DMA访问的时候才需要物理上连续
- vmalloc比kmalloc要慢
- kmalloc是基于slab机制来分配内存的
8、下面程序会有怎样的输出呢?
int cnt = 0;
while(1) {
++cnt;
ptr = (char *)malloc(1024*1024*128);
if (ptr == NULL) {
printf("%s\n", "is null");
break;
}
}
printf("%d\n", cnt);
在Linux32位机上输出的结果应该是:
is null
3057
为什么是3057?
因为用户态虚拟内存地址空间是3G,3057大概就是3G。
可见malloc时是在虚拟地址空间申请的,由于代码里并没有对申请的地址进行访问,所以实际上是不会分配物理内存的,直到对地址进行访问,由于缺页中断才开始处理页表映射,然后分配物理内存。
那如果有对地址进行访问,能够分到多少内存呢?
这就取决于Linux的内核参数和目前剩余的内存了!!!!
请描述一下mmap函数的使用
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
addr:
用于指定映射到进程地址空间的起始地址,为了应用程序的可以执行,一般都设置为NULL,让内核来选择一个合适的地址。length:
表示映射到进程地址空间的大小prot:
用于设置内存映射区域的读写属性等flags:
用于设置内存映射的属性,例如共享映射、私有映射等fd:
表示这是一个文件映射offset:
在文件映射时,表示文件的偏移量
私有映射和共享映射有什么区别?
答: 如果是共享映射,那么在内存中对文件进行修改,磁盘中对应的文件也会被修改,相反,磁盘中的文件有了修改,内存中的文件也被修改。如果是私有映射,那么内存中的文件是独立的,二者进行修改都不会对对方造成影响。通过这样的内存共享映射就相当于是进程直接对磁盘中的文件进行读写操作一样,那么如果有两个进程来mmap同一个文件,就实现了进程间的通信。
在一个播放系统中同时打开几十个不同的高清视频文件,发现播放有些卡顿,打开视频文件是用mmap函数,请简单分析原因
答: 使用mmap来创建文件映射时,由于只建立了进程地址空间VMA,并没有马上分配page cache和建立映射关系。因此当播放器真正读取文件时,产生了缺页中断才去读取文件内容到page cache中。这样每次播放器真正读取文件时,会频繁地发生缺页中断,然后从文件中读取磁盘内容到page cache中,导致磁盘读性能比较差,从而造成播放视频的卡顿。对于这个问题,能够有效提高流,媒体服务I/O性能的方法是增大内核的默认预读窗口, 可以通过“blockdev --setra”命令来修改。
9、Linux是如何避免内存碎片的?
答: 使用伙伴系统算法
来避免外部碎片,用slab算法
来避免内部碎片
- 内部碎片:内部碎片就是已经被分配出去的,能明确指出属于哪个进程,缺不能被利用的内存空间(
这个内存块被某一个进程占有,但是这个进程却不用它,然后别的进程也用不了它,这个内存块就被称为内部碎片
) - 外部碎片:外部碎片是指还没被分配出去,但是由于内存块太小了,无法分配给申请内存的进程。
10、伙伴系统算法是怎么实现的?它和slab算法有什么区别?
答: 伙伴系统中包含了多条内存链表,每条链表中的包含了多个内存大小相等的内存块(比如4KB链表,代表这台链表中所有的内存块大小都是4KB),比如我们想要分配一个8KB大小的内存,但是发现对应大小的链表上已经没有空闲内存块, 那么伙伴系统就会从16KB的链表中找到一个空闲内存块,然后分成两个8KB的大小,把其中一个返回给申请者使用,另一块放到8KB对应的链表中进行管理。到这里可能有童靴会有疑问:如果我只需要1KB甚至更小的内存,而伙伴系统链表中只有32KB的链表有空闲块了,那岂不是要切很久?
对于进程描述符这些对象大小比较小的,如果直接采用伙伴系统进行分配和释放,不仅会造成大量的内存碎片,并且在处理速度上也会比较慢,slab机制
的工作就是针对一些 经常分配并释放的对象
,它是基于对象进行管理的,相同类型的对象归为一类
, 每次要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样的大小出去,而当要释放时,将其重新保存到该列表中,而不是直接返回给伙伴系统
。注意:slab分配器最终还是由伙伴系统来分配物理页面的
Linux内核使用伙伴系统管理内存,那么在伙伴系统工作前,如何管理内存?
答: memblock
,memblock
在系统启动阶段进行简单的内存管理,记录物理内存的使用情况
在系统启动的时候,Linux内核如何知道内存多大?
答: 在对应的平台DTS文件中都会配置memory的节点,描述内存信息,在系统启动的时候,会去解析DTS中配置的memory节点,从而获得内存信息。
memory {
device_type = "memory";
reg = <0 0x40000000 0 0x20000000>;
};
物理内存是如何添加到伙伴系统中的?
答:
内核空间的页表存放在什么地方?
页表是如何映射的?
zone
1、系统在启动过程中,解析DTS中的memory节点,获取物理内存信息,并添加到memblock子系统中
2、对页表进行初始化
3、页表初始化完成后,Linux内核就可以开始对物理内存进行管理
4、Linux内核是通过zone来对物理内存进行管理,在Linux内核中,将物理内存分成几个zone来进行管理
11、什么是缺页中断?发生缺页中断后的处理流程是怎么样的?
我们知道用户空间存在虚拟内存,而用户进程访问虚拟内存的时候,正常情况下,虚拟内存和物理内存需要建立映射关系,用户才可以进一步去访问对应的物理内存,而当进程去访问那些还没有建立映射关系的虚拟内存时,CPU就会触发一个缺页中断异常
。
缺页中断有哪几种类型?
12、如果在中断处理函数中出现了缺页中断,会发生什么?
13、Linux内存管理中的分配掩码有什么作用?有哪几个类型?
分配掩码(gfp_mask)
是一个决定内存分配动作的参数,主要分成下面五类:
内存管理区修饰符
:主要决定需要从哪个内存管理区中分配内存,存管理区修饰符使用gfp_mask的最低4个比特位来表示移动修饰符
:用来指示分配出来的页面具有的移动属性水位修饰符
:用来控制是否可以使用系统紧急预留的内存
页面回收描述符
行动修饰符
关于gfp_mask
更加详细的介绍可以看这个链接
三、Linux中断机制
想进一步整体了解学习Linux中断机制的朋友可以看这个链接
ARM处理器中,检测外部硬件中断的任务是由ARM中的中断控制器来检测的。而如果要进一步细分,中断控制器一般分为两个部分,一个是仲裁单元,另一个是CPU Interface模块,所以第一句话可以进一步完善:ARM处理器中,检测外部硬件中断的任务是由ARM中的中断控制器中的仲裁单元来检测的,由中断控制器中的CPU Interface模块来将中断分配给某一个CPU。
1、当发生一个外设中断时,ARM处理器的处理步骤是怎么样的(不涉及下半部的操作)?
当发生一个外设中断后
a:
一个中断M产生,发生了电平变化,被中断控制器中的仲裁单元检测到了(仲裁单元检测中断信号
)b:
然后仲裁单元会把这个中断M的状态设置为pending(等待状态)(仲裁单元将中断信号设置为pending状态
)c:
然后过了一段时间后,中断控制器中的CPU Interface模块会把nFIQCPU[n]信号拉低,目的是向CPU报告中断请求,然后将中断M的硬件中断号存放到GICC_IAR寄存器中(CPU Interface向CPU报告中断请求
)d:
如果这个时候有一个更高优先级的中断N来了,由于中断控制器支持优先级抢占功能,所以这个时候N会变成当前CPU所有pending状态下优先级最高的中断(意味着它会被优先分配)。(发生中断抢占
)e:
然后跟步骤c一样,过了一段时间后,CPU Interface模块会再次去把nFIQCPU[n]信号拉低,由于这个时候nFIQCPU[n]已经是低电平了,所以只需要更新GICC_IAR寄存器的值为中断N的硬件中断号(设置新的优先级较高的中断信号
)f:
然后CPU就会去读取GICC_IAR寄存器,把寄存器中的硬件中断号读出来,也就相当于是响应中断了。这个时候仲裁单元就会把中断N的状态从pending变成了activce and pending。(响应中断信号
)g:
然后后面就是Linux内核处理中断N的中断服务程序了(处理中断
)h:
在Linux中断处理中断N的过程中,CPU Interface模块会重新拉高nFIQCPU[n]信号,当中断N处理完成后,会将N的硬件中断号写入到GICC_EOIR寄存器中,表示完成中断N的全部处理过程。i:
然后中断控制器的仲裁单元,会重新选择该CPU下pending状态的中断中优先级最高的一个,发送该中断请求给CPU Interface模块,继续前面的流程。
2、中断发生后的硬件处理和软件处理
硬件处理: CPU核心在感知到中断发生后,硬件会自动做如下一些事情
- 保存中断发生时CPSR寄存器的内容到SPSR_irq寄存器中
- 修改CPSR寄存器,让CPU进入处理器模式中的IRQ模式
- 硬件自动关闭中断
- 保存返回地址到LR_irq中
- 硬件自动跳转到中断向量表的IRQ向量中
软件处理:软件需要做的事情从中断向量表开始
3、何为中断上下文?为什么中断上下文中不能调用含有睡眠的函数?
当CPU响应一个中断并正在执行中断服务程序,那么内核处于中断上下文中。
当ARM处理器响应中断时,ARM处理器会自动保存终端店的CPSR寄存器和LR寄存器内容,并关闭本地中断,进入IRQ模式。
但在Linux内核中,ARM IRQ模式很短暂(中断上半部执行非常快),很快就退出IRQ模式进入SVC模式,并且把IRQ模式的栈内容负责到SVC模式的栈中,保存中断现场,也就是说中断上下文运行在SVC模式下。 既然中断上下文运行在SVC模式下,并且中断现场已经保存在中断打断的进程的内核栈中,为什么中断上下文不能睡眠呢?
假使现在CPU正在执行一个进程A,这个时候发生了中断,那么中断处理函数会用进程A的内核栈来保存中断上下文。睡眠是什么概念呢?也就是需要调用schedule()函数让当前的进程让出CPU,选择另一个进程继续执行。如果这个时候再发生睡眠或者中断,那么会将中断上下文覆盖了原本进程A的内核栈,当后面这个中断返回的时候,找不到最开始的那个中断的上下文,就再也回不去上一次的中断处理函数中了。
4、硬件中断号和Linux内核的IRQ号是如何映射的?
5、发生硬件中断后,Linux内核是如何响应并处理中断的?
6、介绍一下Linux内核中的中断注册函数
在Linux内核中,注册中断的函数有:request_irq
和request_threaded_irq
,这两个函数调用的其实都是request_threaded_irq
,只是request_irq
函数的第三个参数传的是NULL(request_threaded_irq(irq, handler, NULL, flags, name, dev))
为什么有了request_irq,还要一个request_threaded_irq?
我们都知道,当一个中断发生后,需要等待中断处理完成后才会返回到起初的状态继续执行,这样会造成那些对于实时性要求比较高的任务得不到及时的处理,所以引入了中断线程化
,目的就是将一些工作任务比较繁重的中断处理函数设置成线程看看待,让实时进程能够得到及时的处理。
7、简述一下你对中断上下半部的理解?
答: 我们用一个例子来更好理解中断和中断上下部这两个概念。
什么是中断?
比如你现在肚子饿了,然后叫了个外卖,叫完外卖后你肯定不会傻傻坐在那里等电话响,而是会去干其他事情,比如看电视、看书等等,然后等外卖小哥打电话来,你再接听了电话后才会停止看电视或者看书,进而去拿外卖,这就是一个中断过程。你就相当于CPU,而外卖小哥的电话就相当于一个硬件中断。
什么是中断上下部?
继续参考上面的例子,比如你现在叫的是两份外卖,一份披萨一份奶茶,由两个不同的外卖小哥配送,当第一个送披萨的外卖小哥打电话来的时候(硬件中断来了),你接起电话,跟他聊起了恋爱心得,越聊越欢,但是在你跟披萨小哥煲电话粥的时候,奶茶外卖小哥打电话来了,发现怎么打也打不进来,干脆就把你的奶茶喝了,这样你就痛失了一杯奶茶,这个就叫做中断缺失
。所以我们必须保证中断是快速执行快速结束的。 那有什么办法可以保护好你的奶茶呢?当你接到披萨小哥的电话后,你跟他说,我知道外卖来了,等我下楼的时候我们面对面吹水,电话先挂了,不然奶茶小哥打不进来,这个就叫做 中断上半部处理
,然后等你下楼见到披萨小哥后,面对面吹水聊天,这个就叫做 中断下半部处理
,这样在你和披萨小哥聊天的过程中,手机也不占线,奶茶小哥打电话过来你就可以接到了。所以我们一般在中断上半部处理比较紧急的时间(接披萨小哥的外卖),然后在中断下半部处理执行时间比较长的时间(和披萨小哥聊天),把中断分成上下半部,也可以保证后面的中断过来不会发生中断缺失
上半部通常是完成整个中断处理任务中的一小部分,比如硬件中断处理完成时发送EOI信号给中断控制器等,就是在上半部完成的,其他计算时间比较长的数据处理等,这些任务可以放到中断下半部来执行。Linux内核并没有严格的规则约束究竟什么样的任务应该放到下半部来执行,这是要驱动开发者来决定的。中断任务的划分对系统性能会有比较大的影响。
那下半部具体在什么时候执行呢?
这个没有确定的时间点,一般是从硬件中断返回后的某一个时间点内会被执行。下半部执行的关键点是允许响应所有的中断,是一个开中断的环境。
8、什么是软中断?什么是tasklet?什么是工作队列
软中断
是预留给系统中对时间要求最为严格和最重要的下半部使用的,而且目前驱动中只有块设备和网络子系统使用了软中断。 软中断指的,其实就是我们上面所描述的中断的下半部的处理程序。一般是以内核线程
的方式运行。系统静态定义了若干种软中断的类型。并且Linux内核开发者不希望用户再扩充新的软中断类型,如果需要,建议使用tasklet
tasklet
是基于软中断机制的另一种变种,运行在软中断上下文中。
workqueue
是以内核线程的方式来处理中断下半部事务,是基于进程上下文
的,而软中断和tasklet是基于中断上下文的,优先级要比进程上下文高,所以当软中断或者tasklet的处理时间太长的话,如果在对于实时性要求较高的进程中发生了中断,那么对于系统性能影响会很大。
四、Linux同步机制
1、Linux内核中包含了哪些同步机制?
答: 原子操作
、自旋锁
、信号量
、互斥锁
、读写锁
、RCU
和Per-CPU变量
。
各个同步机制进一步的介绍可以看这个链接
五、Linux进程管理
进程和线程的区别?
进程是资源分配的最小单位,线程是程序执行的最小单位,线程是进程的一个执行单元,是一个比进程要小的独立运行的基本单位,一个程序至少要有一个进程,一个进程至少要有一个线程。更加细致的区别如下:
根本区别
:进程是资源分配的最小单位,线程是程序执行的最小单元;地址空间
:进程拥有自己的独立空间,每创建一个进程,系统都会为其分配地址空间;线程没有独立的地址空间,同一个进程内的所有线程共享本进程的地址空间;- 由于程序执行过程中是执行具体的线程,所以
线程是处理器调度的基本单位
,而进程不是
1、描述进程/线程的结构体
我认为,想要了解和熟悉进程线程,包括它们之间的调度,首先必须要先了解描述它们的结构体
Linux内核中有没有进程?
在Linux内核中,其实是没有进程、线程之分的,它们都是通过task_struct来描述和调度的
1、Linux中的调度类
进程调度依赖于调度策略,Linux内核把相同的调度策略抽象成了调度类
。目前Linux内核中默认实现了五个调度类:stop
、deadline
、realtime
、CFS
和idle
。
用户控件可以使用调度策略API函数来设定用户进程的调度策略(比如sched_setscheduler),其中,SCHED_NORMAL、SCHED_BATCH以及SCHED_IDLE使用完全公平调度器(CFS),SCHED_FIFO和SCHED_RR使用realtime调度器,SCHED_DEADLINE使用deadline调度器。
1、在Linux内核中,使用什么来描述一个进程的所有信息?如何获得当前描述该进程的结构体?
答: 在Linux内核里面采用名为task_struct
的数据结构来描述一个进程,并且利用链表task_list
来存放所有的进程描述符。在内核中,有一个常用的变量current
用来获取当前进程的task_struct结构
,利用了内核栈的特性。
在Linux 4.0的内核中,内核栈(内核栈的大小通常和架构相关,ARM32架构的内核栈为8KB,ARM64架构的为16KB
)里存放了thread_union
数据结构,内核栈的底部存放了thread_info
数据结构。current()宏首先通过ARM的SP寄存器来获取当前内核栈的地址,对其后可以获取thread_info数据结构的指针,最后通过thread_info->task
成员来获取task_struct数据结构。
在Linux 5.4内核中,获取当前进程的内核栈的方式已经发生了巨大的变化。 系统新增了一个配置选项CONFIG_THREAD_INFO_IN_TASK
,目的是允许把thread_info
的数据结构直接存放到task_struct
中。(在ARM64中这个宏是默认打开的)。在ARM64架构中。利用sp_el0寄存器
来存放task_struct
数据结构,current宏
通过sp_el0
寄存器来获取task_struct
数据结构。
2、进程可以分成哪几种类型?
答: 进程可以大致分成两种:I/O消耗型
和CPU消耗型
I/O消耗型:
这类进程很少会完全占用时间片。CPU消耗型:
这类进程会完全占用时间片。
3、进程有那几个状态?
创建态
:创建了新进程就绪态
:进程获得了可以运行的所有资源和准备条件运行态
:进程正在CPU中执行阻塞态
:进程因为等待某项资源而被暂时移出CPU终止态
:进程消亡
4、说一下创建进程的函数fork()、vfork()和clone()还有exec()的区别
答: 在Linux内核中,fork()、vfork()、clone()、exec()以及创建内核线程的函数接口都是通过调用_do_fork()函数来完成的,只是调用的参数不一样。
fork():
如果使用fork()函数来创建子进程,子进程和父进程将拥有各自独立的进程地址空间,但是共享物理内存资源
。fork()函数会返回两次,一次是在父进程,一次是在子进程。如果返回值是0,说明这是子进程,如果返回值为正数,说明是父进程,父进程会返回子进程的ID,如果返回-1表示创建失败。vfork():
上面介绍的fork()函数尽管使用了写时复制技术,但还是需要复制父进程的页表, 在某些场景下会比较慢,所以就有了后面的vfork()
和clone()
。vfork()
的父进程会一直阻塞,直到子进程调用exit()
或execve()
为止。vfork()
比fork()
多了两个标志位,CLONE_VFORK
和CLONE_VM
,CLONE_VFORK
表示进程会被挂起,直到子进程释放虚拟内存资源;CLONE_VM
表示父子进程运行在相同的进程地址空间中。vfork()
的另一个优势是连父进程的页表项复制动作也被省了。clone():
该函数通常用来创建用户线程。Linux内核中没有专门的线程,而是把线程当成普通的进程来看待,在内核中还是以task_struct数据结构来描述。 该函数的功能强大,可以传递众多参数,可以有选择地继承父进程的资源。exec():
一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。
内核线程其实就是运行在内核地址空间中的进程,它和普通用户进程的区别在与内核线程没有独立的进程地址空间,也就是task_struct数据结构中的mm指针被设置为NULL。
想进一步了解fork()函数使用的童鞋可以看看这个链接
5、说一下进程创建函数fork()和exec()的区别
答:
对于fork():
- a、子进程复制父进程的所有进程内存到其内存地址空间中。父、子进程的 “数据段”,“堆栈段”和“代码段”完全相同,即子进程中的每一个字节都和父进程一样。
- b、子进程的当前工作目录、umask掩码值和父进程相同,
fork()
之前父进程 打开的文件描述符,在子进程中同样打开,并且都指向相同的文件表项。 - c、子进程拥有自己的进程ID。
对于exec():
- a、进程调用
exec()
后,将在同一块进程内存里用一个新程序来代替调用exec()
的那个进程,新程序代替当前进程映像,当前进程的“数据段”, “堆栈段”和“代码段”背新程序改写。 - b、新程序会保持调用
exec()
进程的ID不变。 - c、调用
exec()
之前打开打开的描述字继续打开
6、请简述一下Linux内核中的写时复制技术
答: 在传统的UNIX操作系统中,创建新进程的时候会复制父进程拥有的所有资源,这样进程的创建就变得很低效。每次创建子进程时都要把父进程的进程地址空间的内容复制到子进程,但是子进程又不一定需要用到这些资源,出现没必要的复制动作,浪费了资源。
现代操作系统都采用 写时复制技术(COW)
。该技术就是在父进程创建子进程的时候不需要复制进程地址空间的内容给子进程,只需要负责父进程的进程地址空间的页表给子进程
, 这样父子进程就可以共享相同的物理内存了。当父子进程的其中一方需要修改到物理内存时,就会触发 写保护的缺页异常
, 才会把共享页面的内容复制出来,从而让父子进程拥有各自的副本。
7、请简述你对进程调度器的理解?并说明几种经典的Linux内核进程调度器( 包括O(N)和O(1) )的工作原理
进程调度决定了将哪个进程进行执行,以及执行的时间。操作系统进行合理的进程调度,使得资源得到最大化的利用。而进程调度器,就是来干这样的事的东西,每个不同的进程调度器会使用不同的调度算法,使系统呈现出不同的性能。
-
多级反馈队列算法
:
多级反馈队列算法是最早期的一个进程调度算法。它将系统中的进程划分为不同的队列进行管理,相同优先级的进程在同一个队列中,该算法有以下几个原则:
a、如果进程A的优先级大于B,则优先选择A;
b、如果进程A和进程B同属一个优先级链表(优先级相同),这个时候需要通过轮转调度算法来选择;
c、当一个新进程进入调度器,把它放进优先级最高的队列中;
d、当一个进程是一个CPU消耗型,需要将优先级降低一级;
e、当一个进程是一个I/O消耗型,优先级保持不变(会产生饥饿和欺骗CPU行为
);
f、每隔时间周期S之后,把系统中所有进程的优先级都提到最高级别(可以解决e原则产生的饥饿问题
);
g、当一个进程使用完时间片后,不管它是否在时间片的末尾发生I/O请求,都把它的优先级降一级。 -
O(n)调度算法
:
该算法是从就绪队列中比较所有进程的优先级,然后选择一个最高优先级的进程作为下一个的调度对象,每一个进程都有一个固定的时间片,当时间片走完的时候,才会去选择下一个调度进程,这个调度器在选择下一个进程的时候会遍历队列中所有的进程,因此消耗的时间为O(n)。当就绪队列中的进程很多时,选择下一个就绪进程就会变得很慢,导致系统整体性能下降。 -
O(1)调度算法
:
该算法是基于多级反馈队列算法
来实现的,所以也是将相同优先级的进程放到同一个队列中进行管理,每个CPU维护一个自己的就绪队列,就绪队列由两个优先级数组组成,分别是活跃优先级数组和过期优先级数组。这里使用位图来定义给定优先级队列中是否有可运行的进程,如果有,则将位图中对应的位置1。O(1)调度算法在处理某些交互式进程时依然存在问题,对NUMA支持也不完善。 -
CFS调度算法
:
CFS调度算法采用进程权重值的比重来量化和计算实际运行时间。
CFS调度算法引入了虚拟时间(vruntime)
和真实时间(real time)
的概念,CFS调度算法总是优先选择vruntime最小的进程,
8、请简述进程优先级、nice和权重之间的关系
答: nice值小的进程优先级高,权重大;nice值大的进程优先级低,权重小。内核规定nice值为0的权重值为1024,其他nice值对应的权重值可以通过查表的方式来获取,内核预先计算好了一个表sched_prio_to_weight[40]
,表的下表对应nice值[-20~19]。
const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
9、请简述CFS调度器是怎么工作的
答: CFS调度器选择下一个进程的原则非常简单,就是挑选vruntime值最小的进程, CFS使用红黑树来组织就绪队列了,因此可以快速找到vruntime值最小的那个进程,只需要查找树种最左侧的叶子节点即可
。想要进一步了解红黑树的可以点这里。
10、CFS调度器中的vruntime的如何计算的?
答: CFS中有一个用来计算虚拟时间的核心函数calc_delta_fair()
vruntime:
代表虚拟时间;delta_exec:
代表真实运行时间;nice_0_weight:
代表nice为0的权重值;weight:
代表进程的权重值。
11、进程间通信的类型
答: 常见的Linux进程通信(IPC)方式有以下几种:半双工管道
、命名管道
、消息队列
、信号
、信号量
、共享内存
、内存映射文件
,套接字
等等
管道:
管道实际上是一块共享内存,一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道。命名管道(FIFO):
命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,可以允许没有亲缘关系的进程间通信。信号(signal):
信号机制是unix系统中最为古老的进程之间的通信机制,用于一个或者几个进程之间传递异步信号。消息队列:
信号量:
共享内存:
内存映射:
套接字:
六、ARM架构
1、什么是异常向量?ARM架构中的异常向量分成哪几种?
当系统发生异常时,程序跳到一个固定地址继续运行,不同类型的异常都有固定的地址,这个固定的异常地址就叫做异常向量
。在ARM的架构中,异常向量分成7种
1、RESET
:当系统正常运行的过程中,按下了RESET
,就会跳到RESET
异常去进行重启动作2、Undefined instructions
:处理器无法识别指令的异常, 处理器执行的指令是有规范的, 如果 尝试执行 不符合要求的指令, 就会进入到该异常指令对应的地址中3、Software Interrupt
:软中断,软件中需要打断处理器工作,可以使用软中断实现4、Prefetch Abort
:预取指令失败, ARM 在执行指令的过程中, 要先去预取指令准备执行, 如果预取指令失败, 就会产生该异常5、Data Abort”
:获取数据失败6、IQR
:中断7、FIQ
:快速中断
2、ARM的CPU都有哪几种模式
ARM处理器定义多种模式的好处是可以更好地支持操作系统并提高工作效率。