(2) 发生中断或异常。
2.6内核引入了内核抢占,大多数内核模式的代码也可以被抢占。
在Linux内核环境下,申请大块内存的成功率随着系统运行时间的增加而减少,虽然可以通过vmalloc系列调用申请物理不连续但虚拟地址连续的内存,但毕竟其使用效率不高且在32位系统上vmalloc的内存地址空间有限。所以,一般的建议是在系统启动阶段申请大块内存,但是其成功的概率也只是比较高而已,而不是100%。如果程序真的比较在意这个申请的成功与否,只能退用“启动内存”(Boot Memory)
(7)套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
在物理页面管理上实现了基于区的伙伴系统(zone based buddy system)。对不同区的内存使用单独的伙伴系统(buddy system)管理,而且独立地监控空闲页。相应接口alloc_pages(gfp_mask, order),_ _get_free_pages(gfp_mask, order)等。
补充知识:
1.原理说明
Linux内核中采 用了一种同时适用于32位和64位系统的内 存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系 统中,用到了四级页表。
* 页全局目录(Page Global Directory)
* 页上级目录(Page Upper Directory)
* 页中间目录(Page Middle Directory)
* 页表(Page Table)
页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址,每一个页表项指 向一个页框。Linux中采用4KB大小的 页框作为标准的内存分配单元。
多级分页目录结构
1.1.伙伴系统算法
在实际应用中,经常需要分配一组连续的页框,而频繁地申请和释放不同大小的连续页框,必然导致在已分配页框的内存块中分散了许多小块的 空闲页框。这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足。
为了避免出现这种情况,Linux内核中引入了伙伴系统算法(buddy system)。把所有的空闲页框分组为11个 块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连 续页框,对应4MB大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。
假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个 页框的链表中找,找到了则将页框块分为2个256个 页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页 框的链表查找,如果仍然没有,则返回错误。
页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块。
1.2.slab分配器
slab分配器源于 Solaris 2.4 的 分配算法,工作于物理内存页框分配器之上,管理特定大小对象的缓存,进行快速而高效的内存分配。
slab分配器为每种使用的内核对象建立单独的缓冲区。Linux 内核已经采用了伙伴系统管理物理内存页框,因此 slab分配器直接工作于伙伴系 统之上。每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象。根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024个页框构成。出于对齐 等其它方面的要求,slab 中分配给对象的内存可能大于用户要求的对象实际大小,这会造成一定的 内存浪费。
2.常用内存分配函数
2.1.__get_free_pages
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
__get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址。__get_free_pages在实现上只是封装了alloc_pages函 数,从代码分析,alloc_pages函数会分配长度为1<
2.2.kmem_cache_alloc
struct kmem_cache *kmem_cache_create(const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void*, struct kmem_cache *, unsigned long),
void (*dtor)(void*, struct kmem_cache *, unsigned long))
void *kmem_cache_alloc(struct kmem_cache *c, gfp_t flags)
kmem_cache_create/ kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合。首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从 该高速缓存区域中获取新的内存块。kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏定义,在默认的2.6.18内核版本中,该宏定义为5, 于是一次最多能申请1<<5 * 4KB也就是128KB的 连续物理内存。分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()。测试结果验证了分析结果,用kmem_cache_create分 配超过128KB的内存时使内核崩溃。
2.3.kmalloc
void *kmalloc(size_t size, gfp_t flags)
kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/Kmalloc_size.h的 内容来决定,在默认的2.6.18内核版本中,kmalloc一 次最多能申请大小为131702B也就是128KB字 节的连续物理内存。测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过。
2.4.vmalloc
void *vmalloc(unsigned long size)
前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间。但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也 可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。图3-1表 示的是vmalloc分配的内存使用的地址范围。vmalloc对 一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。在测试过程中, 最大能一次分配1GB的空间。
Linux内核部分内存分布
2.5.dma_alloc_coherent
void *dma_alloc_coherent(struct device *dev, size_t size,
ma_addr_t *dma_handle, gfp_t gfp)
DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的 吞吐量。DMA操作中,涉及到CPU高速缓 存和对应的内存数据一致性的问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很 好的解决了这个问题,dma_alloc_coherent和__get_free_pages函数实现差别不大,前者实际是调用__alloc_pages函 数来分配内存,因此一次分配内存的大小限制和后者一样。__get_free_pages分配的内 存同样可以用于DMA操作。测试结果证明,dma_alloc_coherent函 数一次能分配的最大内存也为4M。
2.6.ioremap
void * ioremap (unsigned long offset, unsigned long size)
ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段 物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存 分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试。
2.7.Boot Memory
如果要分配大量的连续物理内存,上述的分配函数都不能满足,就只能用比较特殊的方式,在Linux内 核引导阶段来预留部分内存。
2.7.1.在内核引导时分配内存
void* alloc_bootmem(unsigned long size)
可以在Linux内核引导过程中绕过伙伴系统来分配大块内存。使用方法是在Linux内核引导时,调用mem_init函数之前 用alloc_bootmem函数申请指定大小的内存。如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导 出,然后就可以使用这块内存了。这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统 看不到这部分内存,需要用户自行管理。测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块。
2.7.2.通过内核引导参数预留顶部内存
在Linux内核引导时,传入参数“mem=size”保留顶部的内存区间。比如系统有256MB内 存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存。
3.几种分配函数的比较
分配原理最大内存其他
__get_free_pages直接对页框进行操作4MB适用于分配较大量的连续物理内存
kmem_cache_alloc基于slab机制实现128KB适合需要频繁申请释放相同大小内存块时使用
kmalloc基于kmem_cache_alloc实现128KB最常见的分配方式,需要小于页框大小的内存时可以使用
vmalloc建立非连续物理内存到虚拟地址的映射物理不连续,适合需要大内存,但是对地址连续性没有要求的场合
dma_alloc_coherent基于__alloc_pages实现4MB适用于DMA操 作
ioremap实现已知物理地址到虚拟地址的映射适用于物理地址已知的场合,如设备驱动
alloc_bootmem在启动kernel时,预留一段内存,内核看不见小于物理内存大小,内存管理要求较高6) 通过slab分配器申请内核内存的函数有?
kmalloc和kfree
7) Linux的内核空间和用户空间是如何划分的(以32位系统为例)?8) vmalloc()申请的内存有什么特点?
Kmalloc 是连续的物理内存。
Vmalloc 是非连续的。
vmalloc是一页一页地去获取内存,然后把获取到的这些页映射成连续的虚拟地址;
kmalloc是一次性获取所需要的所有页,并且不需要再做映射;
vmlloc 是否效率远不如 kmalloc
9) 用户程序使用malloc()申请到的内存空间在什么范围?0~0xBFFFFFFF
简单的说就是:内存是分散成一些小块的,malloc管理的内存是通过链表的
方式把这些块串在一起,以这些内存的起始地址排序组织的,相邻的内存块如
果尾首地址连续,那就把它们合并为一块,当你申请一定大小的内存时
以first fit模式,在内存链中找第一个大于你需要大小的内存,返回内存指针
以best fit模式,要遍历整个内存链,找刚好最接近但大于所需要大小的内存
当然这是出于对内存不浪费的考虑,效率是有损失.释放的话相反,把内存放回
内存管理链中,可能的话合并相邻的内存碎片。避免内存过于零散
Linux下malloc函数主要用来在用户空间从heap申请内存,申请成功返回指向所分配内存的指针,申请失败返回NULL。默认情况下,Linux内核使用“乐观的”分配内存策略,首先粗略估计系统可使用的内存数,然后分配内存,但是在使用的时候才真正把这块分配的内存给你。这样一来,即使用malloc申请内存没有返回NULL,你也不一定能完全使用这块内存,特别是在一次或连续多次申请很多内存的时候。如果一直连续用malloc申请内存,而不真正使用,所申请的内存总数可以超过真正可以使用的内存数。但是当真正使用这块内存,比如用memset或bzero函数一次性把所申请到的大块内存“使用掉”,Linux系统就会Out Of Memory,这个时候OOM Killer就会kill掉用户空间的其他进程来腾出更多可使用内存。OOM Killer根据OOM score来决定kill哪个进程,OOM score可以看/proc/
Memory: 2071220k/2097152k available (2122k kernel code, 24584k reserved, 884k data, 228k init, 1179584k highmem)
10) 在支持并使能MMU的系统中,Linux内核和用户程序分别运行在物理地址模式还是虚拟地址模式?虚拟地址模式
在ARM存储系统中,使用内存管理单元(MMU)实现虚拟地址到实际物理地址的映射。利用MMU,可把SDRAM的地址完全映射到0x0起始的一片连续地址空间,而把原来占据这片空间的FLASH或者ROM映射到其他不相冲突的存储空间位置。例如,FLASH的地址从0x0000 0000~0x00ff ffff,而SDRAM的地址范围是0x3000 0000~Ox3lff ffff,则可把SDRAM地址映射为0x0000 0000~Oxlfff ffff而FLASH的地址可以映射到Ox9000 0000~Ox90ff ffff(此处地址空间为空闲,未被占用)。映射完成后,如果处理器发生异常,假设依然为IRQ中断,PC指针指向Oxl8处的地址,而这个时候PC实际上是从位于物理地址的Ox3000 0018处读取指令。通过MMU的映射,则可实现程序完全运行在SDRAM之中。在实际的应用中.可能会把两片不连续的物理地址空间分配给SDRAM。而在操作系统中,习惯于把SDRAM的空间连续起来,方便内存管理,且应用程序申请大块的内存时,操作系统内核也可方便地分配。通过MMU可实现不连续的物理地址空间映射为连续的虚拟地址空间。操作系统内核或者一些比较关键的代码,一般是不希望被用户应用程序访问。通过MMU可以控制地址空间的访问权限,从而保护这些代码不被破坏。
MMU的实现过程,实际上就是一个查表映射的过程。建立页表(translate table)是实现MMU功能不可缺少的一步。页表是位于系统的内存中,页表的每一项对应于一个虚拟地址到物理地址的映射。每一项的长度即是一个字的长度(在ARM中,一个字的长度被定义为4B)。页表项除完成虚拟地址到物理地址的映射功能之外,还定义了访问权限和缓冲特性等。
MMU的映射分为两种,一级页表的变换和二级页表变换。两者的不同之处就是实现的变换地址空间大小不同。一级页表变换支持1 M大小的存储空间的映射,而二级可以支持64 kB,4 kB和1 kB大小地址空间的映射
ARM处理器支持的存储块大小有以下几种:
1)段(section):大小为1MB的存储块。
2)大页(Large Page):大小为64KB的存储块。
3)小页(Small Page):大小为4KB的存储块。
4)极小页(Tiny Page):大小为1KB的存储块。
通过采用适当的访问控制机制,还可以将大页分成大小为16KB的子页,也可将小页分成大小为1KB的子页,但极小页不能再细分,只能以1KB大小的整页为单位。
ARM处理器采用两级页表实现地址映射:
1)一级页表中包含以段为单位的地址变换条目或者指向二级页表的指针,一级页表实现的地址映射粒度较大。
2)二级页表中包含以大页、小页和极小页为单位的地址变换条目。
当以二级分页管理某段存储空间时,要同时设置一级页表项和二级页表项,一个一级页表项对应一段(1section=1MB)虚拟存储空间的映射关系,一个一级页表项对应一张二级页表,这张二级页表中的所有页表项合在一起对应了前面一级页表项所对应的一段虚拟存储空间的映射关系。ARM处理器的二级页表分为粗粒度二级页表和细粒度二级页表,一张粗粒度二级页表的最大容量为1KB,一张细粒度二级页表的最大容量为4KB,不管是粗粒度还是细粒度页表,每张页表都对应一段(1M)虚拟存储空间的映射关系。所以粗粒度二级页表的页表项(页描述符)最小只能描述4KB大小(小页)虚拟存储空间的映射关系,如果再小就没有足够的页表空间来存放一段虚拟存储空间的全部页表项(因为粗粒度二级页表的最大容量为1KB),因为粗粒度二级页表容量最大容量为1K,一个页表项为4个字节,所以1K容量最多有1024/4=256个页表项,所以最小只能描述1M/256=4K大小(小页)虚拟空间的存储映射关系,而细粒度二级页表的页描述符最小可以描述1KB大小(极小页)的虚拟存储空间的映射关系。
综上所述,我们可以归纳出以下6种ARM处理器的地址变换方法:
1)分段地址变换,这种变换只需要一级地址变换,而下面5种都需要两级地址变换;
2)粗粒度大页(64K)地址变换;
3)粗粒度小页(4K)地址变换(Linux通常使用的地址变换方法);
4)细粒度大页(64K)地址变换;
5)细粒度小页(4K)地址变换;
6)细粒度极小页(1K)地址变换。
在讲解以上各种地址变换方法之前我们先介绍一下一级映射描述符和二级映射描述符的定义。
直接调用 延迟调用
切换用户空间
切换内存堆栈
动态优先级----完全公平调度器CFS
实时进程优于普通进程,每次进行进程调度时,会计算出每一个进程占用处理器权利的权重参数weight,优先运行weight值大的进程。
24) TLB中缓存的是什么内容?
25) Linux中有哪几种设备?
字符设备 快设备 网络设备
26) 字符设备驱动程序的关键数据结构是哪个?file_operations
符设备驱动的开始,我们必须了解的是三个很重要的数据结构,他们分别是file_operations、inode、file
27) 设备驱动程序包括哪些功能函数?
模块的注册与注销
设备的打开、关闭、读、写及其他操作函数
设备的中断服务程序
SWI(软中断)
linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。
用户空间的程序无法直接执行内核代码。它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。
新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。
在Linux中,每个系统调用被赋予一个系统调用号。这样,通过这个独一无二的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。进程不会提及系统调用的名称。