📄 (ldd) ch07-获取内存(转载).txt
字号:
(LDD) Ch07-获取内存(转载)
7章 获取内存
到目前为止,我们总是用kmalloc和kfree来进行内存分配。当然,只用这些函数的确是
管理内存的捷径。本章将会介绍其他一些内存分配技术。但我们目前并不关心不同的体
系结构实际上是如何进行内存管理的。因为内核为设备驱动程序提供了一致的接口,本
章的模块都不必涉及分段,分页等问题。另外,本章我也不会介绍内存管理的内部细节
,这些问题将留到第13章“Mmap和DMA”的“Linux的内存管理”一节讨论。
kmalloc函数的内幕
kmalloc内存分配引擎功能强大,由于和malloc函数很相似,很容易就可以学会
。这个函数运行得很快-一除非它被阻塞-一它不清零它获得的内存空间;分配给它的
区域仍存放着原有的数据。在下面几节,我会详细介绍kmalloc函数,你可以将它和我后
面要介绍的一些内存分配技术作个比较。
优先权参数
kmalloc函数的第一个参数是size(大小),我留在下个小节介绍。第二个参数,
是优先权,更有意思,因为它会使得kmalloc函数在寻找空闲页较困难时改变它的行为。
最常用的优先权是GFP_KERNEL,它的意思是该内存分配(内部是通过调用get_fre
e_pages来实现的,所以名字中带GFP)是由运行在内核态的进程调用的。也就是说,调用
它的函数属于某个进程的,使用GFP_KERNEL优先权允许kmalloc函数在系统空闲内存低于
水平线min_free_pages时延迟分配函数的返回。当空闲内存太少时,kmalloc函数会使当
前进程进入睡眠,等待空闲页的出现。
新的页面可以通过以下几种途径获得。一种方法是换出其他页;因为对换需要时
间,进程会等待它完成,这时内核可以调度执行其他的任务。因此,每个调用kmalloc(G
FP_KERNEL)的内核函数都应该是可重入的。关于可重入的更多细节可见第5章“字符设备
驱动程序的扩展操作”的“编写可重入的代码”一节。
并非使用GFP_KERNEL优先权后一定正确;有时kmalloc是在进程上下文之外调用
并非使用GFP_KERNEL优先权后一定正确;有时kmalloc是在进程上下文之外调用
的-一比如,在中断处理,任务队列处理和内核定时器处理时发生。这些情况下,curre
nt进程就不应该进入睡眠,这时应该就使用优先权GFP_ATOMIC。原子性(atomic)的内存
分配允许使用内存的空闲位,而与min_free_pages值无关。实际上,这个最低水平线值
的存在就是为了能满足原子性的请求。但由于内核并不允许通过换出数据或缩减文件系
统缓冲区来满足这种分配请求,所以必须还有一些真正可以获得的空闲内存。
为kmalloc还定义了其他一些优先权,但都不经常使用,其中一些只在内部的内
存管理算法中使用。另一个值的注意的优先权是GFP_NFS,它会使得NFS文件系统缩减空
闲列表到min_free_pages值以下。显然,为使驱动程序“更快”而用GFP_NFS优先权取代
GFP_KERNEL优先权会降低整个系统的性能。
除了这些常用的优先权,kmalloc还可以识别一个位域:GFP_DMA。GFP_DMA标志
位要和GFP_KERNEL和GFP_ATOMIC优先权一起使用来分配用于直接内存访问(DMA)的内存页
。我们将在第13章的“直接内存访问”一节讨论如何使用这个标志位。
size参数
系统物理内存的管理是由内核负责的,物理内存只能按页大小进行分配。这就需
要一个面向页的分配技术以取得计算机内存管理上最大的灵活性。类似malloc函数的简
要一个面向页的分配技术以取得计算机内存管理上最大的灵活性。类似malloc函数的简
单的线性的分配技术不再有效了;在象Unix内核这样的面向页的系统中内存如果是线性
分配的就很难维护。空洞的处理很快就会成为一个问题,会导致内存浪费,降低系统的
性能。
Linux是通过维护页面池来处理kmalloc的分配要求的,这样页面就可以很容易地
放进或者取出页面池。为了能够满足超过PAGE_SIZE字节数大小的内存分配请求,fs/kma
lloc.c文件维护页面簇的列表。每个页面簇都存放着连续若干页,可用于DMA分配。在这
里我不介绍底层的实现细节,因为内部的数据结构可以在不影响分配语义和驱动程序代
码的前提下加以改变。事实上,2.1.38版已经将kmalloc重新实现了。2.0版的内存分配
实现代码可以参见文件mm/malloc.c,而新版的实现在文件mm/slab.c中。想了解2.0版实
现的详情可参见第16章“内核代码的物理布局”的“分配和释放”一节。
Linux所使用的分配策略的最终方案是,内核只能分配一些预定义的固定大小的
字节数组。如果你申请任意大小的内存空间,那么很可能系统会多给你一点。
这些预定义的内存大小一般“稍小于2的某次方”(而在更新的实现中系统管理的
这些预定义的内存大小一般“稍小于2的某次方”(而在更新的实现中系统管理的
内存大小恰好为2的各次方)。如果你能记住这一点,就可以更有效地使用内存了。例如
,如果在Linux 2.0上你需要一个2000字节左右的缓冲区,你最好还是申请2000字节,而
不要申请2048字节。在低与2.1.38版的内核中,申请恰好是2的幂次的内存空间是最糟糕
的情况了-内核会分配两倍于你申请空间大小的内存给你。这也就是为什么在示例程序s
cull中每个单元(quantum)要用4000字节而不是4096字节的原因了。
你可以从文件mm/malloc.c(或者mm/slab.c)得到预定义的分配块大小的确切数值
,但注意这些值可能在以后的版本中被改变。在当前的2.0版和2.1版的内核中,都可以
用个小技巧-尽量分配小于4K字节的内存空间,但不能保证这种方法将来也是最优的。
无论如何,Linux2.0中kmalloc函数可以分配的内存空间最大不能超过32个页-A
lpha上的256KB或者Intel和其他体系结构上的128KB。2.1.38版和更新的内核中这个上限
是128KB。如果你需要更多一些空间,那么有下面一些的更好的解决方法。
get_free_page和相关函数
如果模块需要分配大块的内存,那使用面向页的分配技术会更好。请求整页还有
其他一些好处,后面第13章的“mmap设备驱动程序操作”一节将会介绍。
分配页面可使用下面一些函数:
l get_free_page返回指向新页面的指针并将页面清零。
l __get_free_pages和get_free_page类似,但不清零页面。
l __get_free_pages返回一个指向大小为几个页的内存区域的第一个字节位置的
指针,但也不清零这段内存区域。
l __get_dma_pages返回一个指向大小为几个页的内存区域的第一个字节位置的
指针;这些页面在物理上是连续的,可用于DMA传输。
这些函数的原型在Linux2.0中定义如下:
unsigned long get_free_page(int priority);
unsigned long __get_free_page(int priority);
unsigned long __get_dma_pages(int priority, unsigned long order);
unsigned long __get_free_pages(int priority, unsigned long order, int
dma);
实际上,除了__get_free_pages,这些函数或者是宏或者是最终调用了__get_free_page
s的内联函数。
当程序使用完分配给它的页面,就应该调用下面的函数。下面的第一个函数是个宏,其
当程序使用完分配给它的页面,就应该调用下面的函数。下面的第一个函数是个宏,其
中调用了第二个函数:
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
如果你希望代码在1.2版和2.0版的Linux上都能运行,那最好还是不要直接使用函数__ge
t_free_pages,因为它的调用方式在这两个版本间修改过2次。只使用函数get_free_pag
e(和__get_free_page)更安全更可移植,而且也足够了。
至于DMA,由于PC平台设计上的一些“特殊性”,要正确寻址ISA卡还有些问题。我在第1
3章的“直接内存访问”一节中介绍DMA时,将只限于2.0版内核上的实现,以避免引入移
植方面的问题。
分配函数中的priority参数和kmalloc函数中含义是一样的。__get_free_pages函数中的
dma参数是零或非零;如果不是零,那么对分配的页面簇可以进行DMA传输。order是你请
求分配或释放的内存空间相对2的幂次(即log2N)。例如,如果需要1页,order为0;需要
8页,order为3。如果order太大,分配就会失败。如果你释放的内存空间大小和分配得
到的大小不同,那么有可能破坏内存映射。在Linux目前的版本中,order最大为5(相当
于32个页)。总之,order越大,分配就越可能失败。
这里值得强调的是,可以使用类似kmalloc函数中的priority参数调用get_free_pages和
其他这些函数。某些情况下内存分配会失败,最经常的情形就是优先权为GFP_ATOMIC的
时候。因此,调用这些函数的程序在分配出错时都应提供相应的的处理。
我们已经说过,如果不怕冒险的话,你可以假定按优先权GFP_KERNEL调用kmalloc和底层
的get_free_pages函数都不会失败。一般说来这是对的,但有时也未必:我那台忠实可
靠的386,有着4MB的空闲的RAM,但当我运行一个"play-it-dangerous"(冒险)模块时却
象疯了一样。除非你有足够的内存,想写个程序玩玩,否则我建议你总检查检查调用分
配函数的结果。
尽管kmalloc(GFP_KERNEL)在没有空闲内存时有时会失败,但内核总是尽可能满足该内存
分配请求。因此,如果分配太多内存,系统的响应性能很容易就会降下来。例如,如果
往scull设备写入大量数据,计算机可能就会死掉;为满足kmalloc分配请求而换出内存
页,系统就会变得很慢。所有资源都被贪婪的设备所吞噬,计算机很快就变的无法使用
了;因为此时已经无法为你的shell生成新的进程了。我没有在scull模块中提到这个问
题,因为它只是个例子模块,并不能真的在多用户系统中使用。但作为一个编程者,你
必须要小心,因为模块是特权代码,会带来系统的安全漏洞(比如说,很可能会造成DoS(
"denail-of-service")安全漏洞)。
使用一整页的scull: scullp
至此,我们已经较完全地介绍了内存分配的原理,下面我会给出一些使用了页面
分配技术的程序代码。scullp是scull模块的一个变种,它只实现了一个裸(bare)设备-
持久性的内存区域。和scull不同,scullp使用页面分配技术来获取内存;scullp_order
变量缺省为0,也可以在编译时或装载模块时指定。在Linux 1.2上编译的scullp设备在o
rder大于零时会据绝被加载,原因我们前面已经说明过了。在Linux 1.2上,scullp模块
只允许“安全的”单页的分配函数。
尽管这是个实际的例子,但值的在这提到的只有两行代码,因为该设备其实只是
分配和释放函数略加改动的scull设备。下面给出了分配和释放页面的代码行及其相关的
分配和释放函数略加改动的scull设备。下面给出了分配和释放页面的代码行及其相关的
上下文:
/* 此处分配一个单位内存 */
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = (void *)__get_free_pages(GFP_KERNEL,
dptr->order,0);
if (!dptr->data[s_pos])
return -ENOMEM;
memset(dptr->data[s_pos], 0, PAGE_SIZE << dptr->order);
/* 这段代码释放所有分配单元 */
for (i = 0; i < qset; i++)
for (i = 0; i < qset; i++)
if (dptr->data[i])
free_pages((unsigned long)(dptr->data[i]),
dptr->order);
从用户的角度看,可以感觉到的差异就是速度快了一些。我作了写测试,把4M字
节的数据从scull0拷贝到scull1,然后再从scullp0拷贝到scullp1;结果表明内核空间处
理器的使用率有所提高。
但性能提高的并不多,因为kmalloc设计得也运行得很快。基于页的分配策略的
优点实际不在速度上,而是更有效地使用了内存。按页分配不会浪费内存空间,而用kma
lloc函数则会浪费一定数量的内存。事实上,你可能会回想起第5章的“所使用的数据结
构”一节中我们已经提到过select_table用了__get_free_page函数。
使用__get_free_page函数的最大优点是这些分配得到页面完全属于你,而且在
理论上可以通过适当地调整页表将它们合并成一个线性区域。结果就允许用户进程对这
理论上可以通过适当地调整页表将它们合并成一个线性区域。结果就允许用户进程对这
些分配得到的不连续内存区域进行mmap。我将在第13章的“mmap设备驱动程序操作”一
节中讨论mmap调用和页表的实现内幕。
vmalloc和相关函数
下面要介绍的内存分配函数是vmalloc,它分配虚拟地址空间的连续区域。尽管
这段区域在物理上可能是不连续的(要访问其中的每个页面都必须独立地调用函数__get_
free_page),内核却认为它们在地址上是连续的。分配的内存空间被映射进入内核数据
段中,从用户空间是不可见的-这一点上与其他分配技术不同。vmalloc发生错误时返回
0(NULL地址),成功时返回一个指向一个大小为size的线性地址空间的指针。
该函数及其相关函数的原型如下:
void* vmalloc(unsigned long size);
void vfree(void* addr);
void* vremap(unsigned long offset, unsigned long size);
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -