Linux内存管理和应用
[作者:byeyear。首发于cnblogs,转载请注明。联系:east3@163.com]
本文对Linux内存管理使用到的一些数据结构和函数作了简要描述,而不深入到它们的内部。对这些数据结构和函数有了一个总体上的了解后,再针对各项分别作深入了解的时候,也许会简单一些。
Linux内存访问限制(仅针对32位系统)
- 默认情况下Linux的内核空间映射到4G虚拟地址的最高1G(即0xC0000000 - 0xFFFFFFF)
在x86中,这个偏移量表示为TASK_SIZE,也表示为PAGE_OFFSET。
对于其他arch,TASK_SIZE和PAGE_OFFSET之间可能存在HOLE。
- 如果不考虑HIGH_MEMORY,Kernel中内存物理地址和逻辑地址(此时就是虚拟地址)有线性对应关系
有两个函数:virt_to_phys和phys_to_virt用于在内核物理地址和逻辑地址间进行转换。
- 所以,32位系统中,Linux能直接访问的物理内存最大为1G
PHY_ADDR[0] -> LOG_ADDR[3G]
PHY_ADDR[1G] -> LOG_ADDR[4G]
- 除去保留给vmalloc、kmap和其他一些用途的地址空间,可直接访问的物理内存最大为896M
- 进程的虚拟地址空间仍然是4G。0-TASK_SIZE(3G)给user space,各个processor不同;kernel space的1G对每个processor都是一样的。
- 后面我们会说到如何访问1G以上的物理内存。
一些数据结构
- struct vm_area_struct
该结构用来描述进程虚拟地址空间中一段连续的虚拟内存。但是我们知道,进程所占用的虚拟地址空间通常是不连续的(例如要分为代码、Heap和stack;或者考虑经常申请/释放内存的情况),所以一个进程需要多个struct vm_area_struct来描述其virtual memory area。根据进程需要的该结构体数量的大小,这一堆的vm_area_struct将组织成Linear或Linear+AVL的形式。
- struct mm_struct
这个结构用来管理上文所述那一堆的vm_area_struct。在这个结构中有个指针,指向那堆struct vm_area_struct的链表头,另有一个指针指向AVL tree的root(如果有的话)。而mm_struct本身又是task_struct里的一个成员。
- zone和node
在多CPU系统中,每个CPU对应一个node用于描述该CPU所“拥有”的内存区域。这里说的“拥有”是指与该CPU具有最佳亲缘性的内存区域,该CPU访问这些内存时将获得最高的性能。一块CPU也可以访问*非*它所“拥有”的内存区域,但访问“非拥有内存区域”的性能不如访问“拥有的内存区域”的性能。
每个node又拆分成多个zone,不同的zone有不同的属性。例如分配给ISA总线设备进行DMA的内存必须在16M以下(天知道现在的计算机上还有木有ISA设备了),这些内存就归为ZONE_DMA。High Mem归属于ZONE_HIGHMEM。不具有特殊性质的内存都归结到ZONE_NORMAL。
描述node和zone的结构分别为typedef struct pglist_data pg_data_t和struct zone。
每个pg_data_t里有一个struct zone数组,数组的元素个数就是ZONE的类型数。
每个pg_data_t里还有一个struct zonelist结构的数组,数组的每个元素都用来描述一种特定的“分配策略”。所谓“分配策略”,就是在分配内存的时候根据分配需要(kmalloc的第二参数就是用来描述这方面的需要的)确定从哪个zone开始,以及可以使用哪些zone。用于ISA DMA的内存必须在16M以下,所以需要一种“策略”;从High Mem中分配内存必须在1G以上,也需要一种策略;普通的没有特殊要求的内存分配也需要一种策略。有多少种策略,struct zonelist数组中就有多少个元素。
- struct zonelist
该结构用于描述某种特定的分配“策略”,其主要成员是一个数组struct zone *zones[MAX_ZONES_PER_ZONELIST + 1],[...]中的“+1”用来仿照字符串结尾的方式确定数组元素的实际个数。该数组的元素是所有可用于某个特定“策略”的zone们。假定可用于ISA DMA的zone是zoneA,zoneB,zoneC,分配顺序也是从A开始,那么这个数组中的三个元素就是指向zoneA,zoneB,zoneC的指针。
- struct page
Linux会为它所管辖的物理内存的每个Page Frame建立一个struct page数据结构,而不管这个Page Frame是否是HIGH_MEMORY。
- high_memory
这是一个变量,表示能够直接管辖的物理内存尾。如果物理内存小于896M,那么它就是实际物理内存大小;否则就是896M。
vmalloc .vs. kmap
vmalloc用于分配一段虚拟地址空间连续、但是物理地址空间未必连续的内存。vmalloc所能使用的虚拟地址空间在high_memory之上(high_memory + 8M)。由于vmalloc分配得到的内存在虚拟地址和物理地址之间不存在线性关系,所以vmalloc可以从HIGH_MEMORY处分配page frame,而不局限于896M以下。事实上,vmalloc优先考虑HIGH_MEMORY。这里顺便说下,ioremap所得到的虚拟地址也占用vmalloc可用的虚拟地址空间。一般情况下,调用vmalloc能够分配到的内存空间可以远大于一次kmalloc所能得到的。不过vmalloc内部仍然依赖kmalloc逐个分配page frame。
kmap用于在page frame和虚拟地址之间建立映射。kmap本身并不分配物理内存,在使用前需要使用alloc_page先分配page_frame。对于Low memory,kmap将直接返回page frame对应的物理地址。
kmap和vmalloc在应用上的最大区别在于,kmap往往应用于“空间分配”与“地址映射”可以分开的场合。考虑下面的场景:
a) App想要通过网络发送数据;
b) Linux将用户数据复制到内核buffer;
c) NIC driver通过DMA发送数据。
在上述场景的步骤b)中,Linux将执行类似下面的代码(仅作示意,不代表真实代码):
while(more_data_to_copy) {
struct page* page = alloc_page(...);
void* vadr = kmap_atomic(page);
copy_to_kernel(vadr, user_buf, size);
kunmap_atomic(page);
link_page_to_list(page);
}
上面的代码中,一页数据拷贝完成后该页的地址映射就可以立即解除,因为后续的DMA操作是不需要用到内核虚拟地址的。在linux源代码的net目录中的代码大量使用了kmap(_atomic)。这样做(将空间分配和地址映射分离)的好处是可以节约大量的页表(考虑网络服务器中存在大量待收发数据的情况),可以很好的利用HIGH_MEMORY。快速、少量或不可休眠的页映射使用kmap_atomic,其他情况可使用kmap。
vmalloc与page fault
执行vmalloc(包括ioremap)时页表的设置是写在init_mm中的,这样当某个进程试图通过驱动访问该虚拟地址时必然发生page fault。do_page_fault函数判断出page fault发生在vmalloc region,于是从init_mm中将pgd/pud/pmd复制到当前进程,但pte是不复制的——内核地址空间的pte由所有进程共享。如果在将来的某个时刻映射解除,修改的也是init_mm的页表设置。后续对该地址的访问(假设仍为同一进程)在pgd、pud、pmd这三层仍然能够通过(进程有自己的pgd/pud/pmd),但到pte这一层就会发生page fault(init_mm已删除该pte)。类似的情况还有ioremap[注]。
[注] 根据64位atom上安装32位Linux(kernel 2.6.32)的测试结果,页表是3级,各级位宽2/9/9/12,第一级将内存分为4个1G区间,进程pgd的第四条目总是指向init_mm的pmd。这样,只要ioremap已建立好,init_mm对应的pmd就一定有效,也就不会发生page fault。(这种情况下vmalloc的效果未确认)
DirectIO
对于DirectIO来说,数据传输直接发生在设备与应用程序缓冲区之间。因此,一般来说DirectIO往往和DMA配合使用。DirectIO的步骤大体如下:
- 应用程序申请内存
最简单的当然是malloc或new之类;如果你要求获得的内存对齐于页边界,可以用anonymous mmap——使用mmap函数分配虚拟内存,并将mmap函数的flags参数设定为MAP_ANONYMOUS,同时将fd设置为-1(对linux非必须,仅作兼容性考虑)。具体请man mmap。或者使用valloc函数。
- Driver为其分配物理pages
应用程序调用malloc或new或valloc得到的虚拟地址,在初始状态是没有物理页面和其对应的。我们如果要对其做DMA,需要为其分配物理内存,并将其lock,不允许换出。kernel提供get_user_pages函数做这些工作。如果需要的内存空间很大,get_user_pages的执行过程会比较长。
- DMA
一般来说get_user_pages得到的物理页面是不连续的,所以我们需要dma_map_sg。
- 进一步说明
因为DirectIO的数据传输直接发生在设备和应用程序缓冲区之间,所以一般来说Driver是不需要访问缓冲区内容的。如果Driver也需要访问缓冲区内容,需要通过kmap获得这些物理页面对应的内核虚拟地址。
没有对应的put_user_pages函数;你需要自己做个循环调用page_cache_release。
- IBM有篇关于DirectIO的文章写得不错。
应用程序直接访问设备内存
- 应用程序要访问设备内存(例如PCI的memory bar),有两种方法:一种是使用ioremap(或ioremap_nocache),将设备内存物理地址映射到内核虚拟地址,App使用ioctl或read/write通过驱动访问;
- 另一种方法是使用mmap,让App直接访问设备内存。使用这种方法,App需要通过mmap系统调用开辟一块虚拟地址空间,并在驱动中实现file_operations中的mmap函数。App调用mmap时,fd参数设置为打开设备的fd。这块虚拟地址空间大小就是设备内存的大小。驱动在mmap的实现中,调用remap_pfn_range为App开辟的那块虚拟地址空间建立与设备物理内存的映射。和DirectIO相比,由于待映射的物理内存已存在(即设备内存),不需要分配并锁定page,所以速度很快。
- 驱动不需要实现unmap。
- 可能需要对映射得到的内存禁止cache。
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
if(rempa_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vm_start, vma->vm_page_prot))
return - EAGAIN;
- 应用程序调用如下:
void *p = mmap(,
DEV_MEM_LEN, // 设备内存长度
prot,
flags,
dev_fd,
DEV_MEM_PHY); // 设备内存物理地址
注意:如果你需要对mmap映射区进行写操作,在建立设备文件的时候将文件属性设置为rw。
使用系统内存作为设备内存
- 在嵌入式系统中,我们也许对系统有较大的控制权,可以使用mem=启动参数。这个时候我们可以保留一部分系统内存作为设备内存,设备直接将数据DMA到保留下的系统内存中(这块系统内存在mem=启动参数设定的内存大小之外,因此不会被Linux看到)。然后使用上文所说的“应用程序直接访问设备内存”的方法获取数据。
内核物理页面的分配
内核中有多种情况需要分配物理页面,比如处理page fault,比如driver需要临时或永久存储区,等等,一般来说都是通过kmalloc或辅助函数完成(核心数据结构是struct page)。管理内核物理页面的模块并不关心是谁要用物理页面,你要就给你(如果有),你还就收下。至于分配出去后怎么用,那是调用者的事。
相对而言,应用程序中调用malloc仅仅是划出一段虚拟内存,但并不立即分配对应的物理内存空间。只有当有人读或写了这个页面,才会通过page fault为其分配物理页面,并且随时可能被换出。即,应用程序在用户空间所使用的物理页面都是在内核态分配的,纯用户态代码永远无法获得一块实际的物理内存:这是理所当然的事,因为物理页面的使用必须全部由内核所掌控。
物理页面交换
分配到的物理页面只有被挂在某个可换出的链表上,才会被页面换出代码扫描到。所以,如果你的驱动通过kmalloc获得了一些物理页面自用,它们是不可能被换出的——因为没有挂在某个可换出链表上。上文中通过get_user_pages得到的页面也是不会换出的。另一方面,发生page fault后分配到的物理页面是会被内核挂到某个可换出链表上的。
Linux内存管理和应用的更多相关文章
- 浅谈Linux内存管理机制
经常遇到一些刚接触Linux的新手会问内存占用怎么那么多?在Linux中经常发现空闲内存很少,似乎所有的内存都被系统占用了,表面感觉是内存不够用了,其实不然.这是Linux内存管理的一个优秀特性,在这 ...
- linux内存管理
一.Linux 进程在内存中的数据结构 一个可执行程序在存储(没有调入内存)时分为代码段,数据段,未初始化数据段三部分: 1) 代码段:存放CPU执行的机器指令.通常代码区是共享的,即其它执行程 ...
- Linux内存管理原理
本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址又叫线性地址.linux没有采用分段机制,所以逻辑地址和虚拟地址(线性地址)(在用户态,内核态逻 ...
- 了解linux内存管理机制(转)
今天了解了下linux内存管理机制,在这里记录下,原文在这里http://ixdba.blog.51cto.com/2895551/541355 根据自己的理解画了张图: 下面是转载的内容: 一 物理 ...
- Linux内存管理原理【转】
转自:http://www.cnblogs.com/zhaoyl/p/3695517.html 本文以32位机器为准,串讲一些内存管理的知识点. 1. 虚拟地址.物理地址.逻辑地址.线性地址 虚拟地址 ...
- Windows内存管理和linux内存管理
windows内存管理 windows 内存管理方式主要分为:页式管理,段式管理,段页式管理. 页式管理的基本原理是将各进程的虚拟空间划分为若干个长度相等的页:页式管理把内存空间按照页的大小划分成片或 ...
- linux 内存管理——内核的shmall 和shmmax 参数
内核的 shmall 和 shmmax 参数 SHMMAX= 配置了最大的内存segment的大小 ------>这个设置的比SGA_MAX_SIZE大比较好. SHMMIN= 最小的内存seg ...
- linux内存管理子系统
一.Linux内存管理模型 1.虚拟地址与物理地址的映射 2.物理地址的分配二.虚拟地址与物理地址的映射 1.虚拟地址空间分布 32位处理器有32根地址总线,可访问4G的物理空间.其中有0-3G为用户 ...
- Linux内核分析(三)----初识linux内存管理子系统
原文:Linux内核分析(三)----初识linux内存管理子系统 Linux内核分析(三) 昨天我们对内核模块进行了简单的分析,今天为了让我们今后的分析没有太多障碍,我们今天先简单的分析一下linu ...
- Linux内存管理之slab分配器
slab分配器是什么? 参考:http://blog.csdn.net/vanbreaker/article/details/7664296 slab分配器是Linux内存管理中非常重要和复杂的一部分 ...
随机推荐
- C++中的接口继承和实现继承
很多人认为,C++中是不存在接口继承的,只有Java.C#这类语言才提供了相应的语法支持. 但是,如同鲁迅说过的某句名言:世上本没有接口继承,用的人多了,才有了接口继承.C++中依然可以实现接口继承, ...
- 小知识,用myeclipes找jar
有些时候,换台电脑就换了一个框架的版本,找对应的jar是一件非常麻烦的事. 我们可以使用myeclipes快速得到我们想要的jar 首先新建一个项目,然后使用myeclipes的自动生成框架技术,生成 ...
- vee-validate 中文配置报错及自定义规则 报错.updateDictionary/.addlocale is not a function
原因由于vee-validate版本更新的问题导致的. 1. 原因分析: a 新版vee-validate的语言配置方法变化了, Validator.localize('zh_CN', zh_CN) ...
- apache中的RewriteCond、RewriteRule
Rewirte主要的功能就是实现URL的跳转.可基于服务器级的(httpd.conf)和目录级的(.htaccess) 两种方式.如果要想用到rewrite模块,必须先安装或加载rewrite模块. ...
- 如何从ie11降到ie9
如果你是win7,中间没有安装过IE10的话,在系统已安装的更新中找到IE11右键卸载后就会回滚到IE9. ie11浏览器现在win7和win8版本的都已经发布了;但是因为一些12306火车票订票网站 ...
- c++下基于windows socket的单线程服务器客户端程序(基于TCP协议)
今天自己编写了一个简单的c++服务器客户端程序,注释较详细,在此做个笔记. windows下socket编程的主要流程可概括如下:初始化ws2_32.dll动态库-->创建套接字-->绑定 ...
- 配置阿里云ECS支持IPv6,解决苹果app审核失败问题
前几天iOS的App提交给苹果审核没通过,给出的原因是:该应用在 IPv6 的环境下无法使用.检查发现:阿里云优化过的系统没有启用IPv6协议,需要配置启用一下,但是只单独启用IPv6也是无法直接提供 ...
- stm32 DMA配置
DMA就是将一个地址空间复制到另外一个地址空间.DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备直接传送数据,使CPU的效率大大的提高 ...
- Executor 框架
Java的线程既是工作单元,也是执行机制.从JDK5开始,把工作单元与执行机制分离开来.工作单元包括Runnable和Callable,而执行机制由Executor框架提供. Executor 框架简 ...
- Maps.newHashMapWithExpectedSize(2)
☆ Map<String, Object> diffQuota = Maps.newHashMapWithExpectedSize(2); Maps.newHashMapWithExpec ...