📄 linux-+
字号:
<br>
内存分区可以使内核页分配更加合理。当系统物理内存大于1G时,内核不能将所有的物理内存都预先映射到内核空间中,这样就产生了高端内存,高端内存最适于映射到用户进程空间中。预映射的部分可直接用于内核缓冲区,其中有一小块可用于DMA操作的内存,留给DMA操作分配用,一般不会轻易分配。内存分区还可以适应不连续的物理内存分布,是非一致性内存存取体系(NUMA)的基础。<br>
<br>
<br>
</span>先看看代码中的注释:<span class="post"><br>
In <b>linux\include\linux\<font color="#FF0000">mmzone.h</font></b>(version
2.4.16, line 67)<br>
/*<br>
* On machines where it is needed (eg PCs) we divide physical memory<br>
* into multiple physical zones. On a PC we have 3 zones:<br>
*<br>
* ZONE_DMA < 16 MB ISA DMA capable memory<br>
* ZONE_NORMAL16-896 MB direct mapped by the kernel<br>
* ZONE_HIGHMEM > 896 MB only page cache and user processes<br>
*/</span></p>
<p>高端页面的映射<br>
1)<br>
高端物理页面共享一块<font color="#0000FF">4M的映射区域</font>,该区域对齐于4M页边界,并用一张页表(<font color="#800080">pkmap_page_table)</font>来完成映射操作。高端页面的映射地址由其页结构中virtual成员给出。<br>
2)<br>
高端映射区逻辑页面的分配结构用分配表(pkmap_count)来描述,它有1024项,对应于映射区内不同的逻辑页面。当分配项的值等于零时为自由项,等于1时为缓冲项,大于1时为映射项。映射页面的分配基于分配表的扫描,当所有的自由项都用完时,系统将清除所有的缓冲项,如果连缓冲项都用完时,系统将进入等待状态。<br>
3)<br>
页缓冲尽可能地使用高端页面,当通过块结构刷新高端页面时,系统会在提交块设备<br>>
,原请求块,同时中转块被释放。<br>
<br>
还是结合源码看一看, 给我的感觉是这样的:<br>
在 <font color="#FF0000">include/linux/<b>highmem.h </b></font>中没有定义 <font color="#FF0000">CONFIG_HIGHMEM</font>
时, 有 <br>
void *<b><font color="#0000FF">kmap</font></b>(struct page *page) { return <font color="#0000FF">page_address</font>(page); }<br>
#define <font color="#0000FF">page_address</font>(page) ((page)->virtual)<br>
而在定义了<font color="#FF0000">CONFIG_HIGHMEM</font> 时其定义变为:<br>
<font color="#FF0000">include/linux/asm_i386/<b>highmem.h</b></font><br>
static inline void *<b><font color="#0000FF">kmap</font></b>(struct page *page)<br>
{<br>
if (in_interrupt())<br>
BUG();<br>
if (page < <b><font color="#800080">highmem_start_page</font></b>) <font color="#0000FF">//如果是低端内存,我们相信page自己记录的虚拟地址</font><br>
return page_address(page);<br>
return <font color="#00AA00">kmap_high</font>(page); <font color="#0000FF">//高端内存可能需要重新映射</font><br>
}<br>
<br>
void *<b><font color="#0000FF">kmap_high</font></b>(struct page *page)<br>
{<br>
unsigned long vaddr;<br>
<font color="#0000FF">/*<br>
* For highmem pages, we can't trust "virtual" until<br>
* after we have the lock.<br>
*<br>
* We cannot call this from interrupts, as it may block<br>
*/</font><br>
spin_lock(&kmap_lock);<br>
vaddr = (unsigned long) page->virtual;<br>
if (!vaddr)<br>
vaddr = <font color="#00AA00">map_new_virtual</font>(page); <font color="#0000FF">//对于已经被冲掉的页面,需要重映射</font><br>
<b><font color="#800080">pkmap_count</font></b>[PKMAP_NR(vaddr)]++; <font color="#0000FF">//逻辑页面操作</font><br>
if (pkmap_count[PKMAP_NR(vaddr)] < 2)<br>
BUG();<br>
spin_unlock(&kmap_lock);<br>
return (void*) vaddr; <font color="#0000FF">//返回映射好的虚拟地址</font><br>
}
</p>
<hr>
<p><b><font size="6" color="#0000FF"><a name="VM 和 vmalloc">VM 和 vmalloc</a></font></b></p>
<p><span class="post"><br>
先和malloc比一比.</span></p>
<p><span class="post"> malloc和内核无关,事实上内核不知道malloc这回事,它是c
lib做的,c lib在brk-end_code之间用malloc, mfree分配,如果没有地方分配了(比如碎片太多,或者没有空间了),就调用sys_brk,这是个系统调用,内核会分配一个页块,然后将brk上面的空间映射到这个页块,系统调用返回后,malloc会在这新分的空间上做一些细小的分配。<br>
<br>
vmalloc是内核用的,我们知道3 G-4G是内核空间,但是只有3G-3G+mem是被固定映射的,(mem是内存大小或指定的数值),上面还会有一些空间没有映射,vmalloc用来映射这个空间,有点象对用户空间的vma的操作。它一般分配的粒度都是页面。<br>
<br>
如果内存大于(其实接近1G)1G,需要high mem的机制。vmalloc分配的空间中间是有洞的,起到保护的作用,可以看以前jkl的帖子。do_page_fault里有对vmalloc的操作。</span></p>
<p> 而linux中对虚存的概念,linux对虚存的管理其实是用vma进行的.进程的0-4G空间中,user用到0-2G,用户编程序时对虚存线性空间的访问,用MMU对其进行到物理地址的转化,如果发生缺页错误,kernel用vma进行判断此地址是否分配,如果是才将物理页分配,再对此4k的虚存地址操作便不会发生缺页,对vma的操作是do_mmap和do_munmap进行的,象<font color="#00aa00"><span class="post">lucian_lao</span></font>所说malloc
lib c的函数,它并不是每次都调brk,而是现有vm不够用时,调用sys_brk(),而brk为可用堆上限-end_code的值,sys_brk将增大brk值,并调用do_mmap分配vm.
free相反.<br>
<br>
vmalloc是kernel进程用的函数,不同于kmalloc.kmalloc和kfree管理的kernel的内存是真正的物理地址,而vmalloc分配的是kernel的vm,祥见<LINUX内核源代码分析
562页>
,vmalloc是从VMALLOC_START到VMALLOC_END之间的虚存,VMALLOC_START一般是kernel物理地址后8M开始的,其实我自己认为它更和user空间的do_mmap相似,用于分配kernel空间的虚存,而它的操作有一部分通过kmalloc分配一部分不可交换的内存.但是其vmlist并不象user空间一样存在AVL树来增加查找速度.<br>
</p>
<p> 我们来总结一下:<br>
<span class="post">在初始化的时候,3G~3G+mem-->0~mem的映射有了,而且是固定不变的。vmalloc的部分(3G+mem+128M?,
-固定映射区)的映射并不是总被定义,而且会变.vmalloc就是分一个页面,建一个映射,把分到的vm地址返回.vmalloc修改的是</span><font color="#800080"><b>swapper_pg_dir
,</b></font>基准页目录.这样的话也会产生一些问题,举一个例子(<a href="http://www.linuxforum.net/forum/showprofile.php?Cat=&User=lucian_yao&Number=145018&Board=linuxK&what=showthreaded&page=&view=&sb=&o=&fpart=&vc=1"><font color="#00aa00">lucian_yao</font></a><font color="#00aa00">) </font></p>
<p><span class="post">一个问题我没有想清楚:假定在某个时候用__vmalloc将[
3.5G, 3.5G+1M ]映射到[ 2M, 3M ]这个时候进入进程A,然后在中断中访问[3.5G,
3.5G+1M]这段空间,由缺页fault将进程A的页表补上[3.5G, 3.5G+1M]映射到[
2M, 3M ]然后,释放了[ 3.5G, 3.5G+1M ],再次重新分配[3.5G, 3.5G+1M],这个时候[3.5G,
3.5G+1M]映射到[ 5M, 6M ]但是这个时候(仍然使用进程A的页表)访问[3.5G,
3.5G+1M],由于没有出现缺页fault,访问到的实际地址是[ 2M, 3M ],这不是不对了吗?是不是我理解上有什么问题?<br>
<br>
</span><span class="post">(</span><a href="http://www.linuxforum.net/forum/showprofile.php?Cat=&User=lucian_yao&Number=145018&Board=linuxK&what=showthreaded&page=&view=&sb=&o=&fpart=&vc=1"><font color="#00aa00">jkl</font></a><font color="#00aa00">)</font><span class="post"><br>
在进程的内核页目录中补上的是只是页目录项,而页表对所有进程来说是共用的,不管vfree()多大的内存块,在vmalloc()时新分配的页表不会被释放,当重新vmalloc()时,仍旧使用原来的页表。</span><a href="#do_page_fault"><font color="#0000FF">do_</font><span class="post"><b><font color="#0000FF">page_fault</font></b></span></a><span class="post">使得进程的内核页目录项与swapper_pg_dir保持同步,swapper_pg_dir的内核页目录项一旦建立就不再被改变,需要改变的只是共享的页表而已。</span></p>
<p><span class="post"> 说的神些,kmalloc分配连续的物理地址,vmalloc分配连续的虚拟地址.并且有如下结论:<br>
memory.c 主要负责一部分用户空间的虚存映射工作 也就是 0-3G的映射<br>
<b><i>vmalloc</i></b>.c 主要负责内核空间高端的内存分配和映射<br>
<br>
实际上:<br>
<br>
用户空间 0 - 3G do_brk 调用来分配, 还有一些函数处理映射工作(比如memory.c中的函数)<br>
内核空间 3G - 3G + mem kmalloc, __get_free_pages 来分配<br>
内核空间 3G + mem + 隔离带 - 4G <b><i>vmalloc</i></b><br>
<br>
其中 mem
可以看成是内存的大小,会自动检测到,也可以由命令行指定.</span><span class="post"><br>
<br>
memory.c 中的的copy_page_range。clear_page_tables等函数是用于
建立或撤消映射,do_wp_page,do_no_page,do_swap_page等用于页面故障时处理。而vmscan.c则是处理页面交换。<br>
<br>
<br>
并且vmalloc为了捕获越界,vm中间是有洞的.关于这些,看看下面的讨论把:<br>
提问:<br>
vmalloc()函数分配内存的虚拟地址从3G+high_memory+hole_8M开始其中hign_memory为实际物理内存,hole_8M为8M的隔离带为什么要有8M的隔离带?这样岂不是很浪费虚拟地址空间吗?另外分配的内存块之间有一个4K的隔离页,这样是不是也很浪费虚拟地址空间?</span></p>
<p><span class="post"> </span><span class="post">(</span><a href="http://www.linuxforum.net/forum/showprofile.php?Cat=&User=lucian_yao&Number=145018&Board=linuxK&what=showthreaded&page=&view=&sb=&o=&fpart=&vc=1"><font color="#00aa00">jkl</font></a><font color="#00aa00">)<br>
<span class="post">Linux用这些空洞来检测存储器读写越界故障,当然空洞越大出现破坏性故障的可能性就越小,8M的空洞正好用两个页目录项来标记,4K的空洞用一个页表项来标记,由于一般物理内存远小于线性地址空间,因此这种浪费是微不足到的。<br>
<br>
</span></font><span class="post">
谢谢您的回答,第一次请教问题,就得到您的耐心回答,非常感谢我还是有点不明白:<br>
那为何用两个页目录表,用一个不行吗?甚至用一个4k的页表不就够了吗?加一个空洞,是不是利用了页保护的属性?如果越界的话,不论空洞的大小都应该产生异常,对吗?不明白空洞越大越安全的原因另外3G+phymem+8M给vmaloc剩下的空间不多了,如果实际物理内存接近1G的话,是不是vmaloc函数就不能使用了?我感觉空间并不充裕<br>
</span></p>
<p><span class="post"> </span><span class="post">(</span><a href="http://www.linuxforum.net/forum/showprofile.php?Cat=&User=lucian_yao&Number=145018&Board=linuxK&what=showthreaded&page=&view=&sb=&o=&fpart=&vc=1"><font color="#00aa00">jkl</font></a><font color="#00aa00">)</font><span class="post"><font color="#00aa00"><br>
隔离带的大小是任意的,但如果隔离带不够大的话,有可能会被故障代码跨过引起破坏。当内核有代码引用到这些隔离带的地址时,这些地址对应的页目录项或页表项由于被标记为"不存在",就会产生页故障,这样就可以准确定位故障所在。如果物理内存非常大,造成内核虚拟空间不足时,可以减小内核的起始线性地址,通过减小用户程序的虚拟地址空间来增大内核的虚拟空间。如果物理内存超过2G,可通过一个内核补丁<a href="http://oss.sgi.com/projects/bigmem/">big
Physical Memory for IA-32</a>将应用程序与内核的页目录分开,尽管这样还是只能管理3.8G,如果物理内存还要大,就要使用64位的体系了。<br>
<br>
<br>
<br>
<br>
<br>
</font></span><span class="post"><br>
<br>
</span></p>
<hr>
<p><b><font size="6" color="#0000FF"><a name="物理内存管理">物理内存管理</a></font></b></p>
<p><font color="#0000FF">kernel页表</font><br>
kernel的pgd是在开始setup_32时初始化的,它应该使cr3指向swapper_pg_dir(这个变量在arch\i386\kernel.head.s中),它的定义如下:<br>
ENTRY(swapper_pg_dir)<br>
.long 0x00102007<br>
.fill __USER_PGD_PTRS-1,4,0<br>
/* default: 767 entries */<br>
.long 0x00102007<br>
/* default: 255 entries */<br>
.fill __KERNEL_PGD_PTRS-1,4,0<br>
这里面只有两项非空,用户空间的第一项和kernel空间的第一项,它们都指向地址0x00102000,也就是pg0,这个页表中装的是从物理地址0-4M的内容。但是这个pgd并不会一直这样,在start_kernel中会调用paging_init把它完全重新改掉。paging_init将初始化线性空间从start_mem到end_mem的页表项。它做的第一件事就是把swapper_pg_dir的第一项清0,用于捕获null访问。然后它会进入一个循环,这个循环有两种结果,如果你的CPU是Pentium以上,那么它就把分成4M为单位的页。然后把填入pgd中;否则就按4K进行分页,如果pgd中的项为空,就从start_mem开始分配一页作为页表,然后在页表中顺序填入物理页帧的首地址,如果超过了end_mem,就在页表中填入0。<br>
<br>
在swapper_pg_dir中的第一项和第768项都有一个指向pg0的项,因为,初始化的时候,其中有两个工作要做:<br>
1.启动页机制,这个时候从物理地址寻址转为虚地址寻址,为确保平滑过渡,在低端(0到4M)的映射是恒同映射<br>
2 跳到内核,实际上内存仍然保存在物理地址0-4M,这个时候,实际上是通过虚拟地址3G-4G访问的,为了平滑过渡,将3G-3G+4M的虚地址映射和0-4M虚地址一致,这样,开启页面映射后跳到内核时已经运行于3G以上的地址了,从这以后,内核自己的寻址都在3G以上了。<br>
<br>
</p>
<p><font color="#0000FF">内核的内存分配</font>主要会涉及到三组分配函数: <br>
1)页面分配器 <br>
__get_free_pages()/__free_pages() <br>
2)0xc0000000 ~ 0xc0000000+phymem <br>
kmalloc()/kfree() <br>
3) 0xc0000000+phymem+8M_hole ~ 4G <br>
vmalloc()/vfree() <br>
</p>
<p>以下简要的给予介绍:</p>
<p>一:页面分配器: <br>
页面分配器是最底层的内存分配,主要用于物理内存页的分配,在内核初始化时,调用paging_init创建swap_page_dir,使得从PAGE_OFFSET到PAGE_OFFSET+PhyMem的内核虚拟空间与0~PhyMem的物理空间建立起一一对应的关系。所以__get_free_pages返回的是实际物理+PAGE_OFFSET(由ADDRESS宏实现变换)。
页面分配器采用的是“伙伴”算法。主要涉及两个重要全局变量。<br>
1)struct free_area_struct
free_area[NR_MEM_TYPES][NR_MEM_LISTS]; 空闲块数组 <br>
2)mem_map_t * mem_map
逻辑页,标示每个物理页的使用情况,根据系统的实际内存,在内核初启时,由mem_init初始化。
具体实现请阅读源码及参看《UNIX高级教程 系统技术内幕》、<br>
</p>
<p>二:kmalloc/kree kmalloc<br>
分配的是从PAGE_OFFSET~PAGE_OFFSET+PhyMem之间的内核空间,用于分配连续物理空间。将kmalloc返回值减去PAGE_OFFSET就是实际的物理地址。
我现在看的源码(2.2.14)kmalloc实现采用了slab分配器算法。具体的实现请参阅lucian_yao以前的贴子及
《UNIX高级教程 系统技术内幕》。</p>
<p>三:vmalloc/vfree <br>
用于分配内核位于PAGE_OFFSET+PhyMem+8M_hole ~ 4G的虚拟空间,
vmalloc的实现比较简单,主要是维护struct vm_struct vmlist链表
当然vmalloc会调用kmalloc以分配vm_struct,然后为虚拟空间创建页表。 <br>
这里我有一个疑问就是用vmalloc分配的内存似乎不会被swap出去,希望有高手指教。再有就是用malloc分配的位于数据段上端至brk之间的堆。
这部分似乎要用到sys_remap,sys_munmap系统调用,以后看源码再说吧。刚看了点皮毛,写出点心得,就是想暴露一下自己的一些模糊概念,以期有高手指正。 </p>
<p>[jkl]内核要求实时性很高,可加载模块本身就是用vmalloc()分配的内存,它是不能允许极慢的磁盘交换的。</p>
<p> </p>
<hr>
<p><a name="slab 管理"><font face="Arial Black" size="6" color="#0000FF">s</font></a><a name="slab 管理"><font face="Arial Black" size="6" color="#0000FF">lab
</font><font size="6" color="#0000FF"> 管理</font></a></p>
<p><span class="post"><font color="blue">slab分配器在内存分配中起的作用</font><br>
<br>
slab分配器通过页面级分配器获得页块后,做进一步的精细分配,<br>
将这个页块分割成一个个的对象,有点类似c中的malloc<br>
c, mfree的作用。
</span></p>
<p><span class="post"><font color="blue">cache描述符<br>
</font><br>
<font color="green">struct kmem_cache_s {<br>
/* 1) each alloc & free */<br>
/* full, partial first, then free */<br>
struct list_head slabs;<br>
struct list_head *firstnotfull;<br>
unsigned int objsize;<br>
unsigned int flags; /* constant flags */<br>
unsigned int num; /* # of objs per slab */<br>
spinlock_t spinlock;<br>
#ifdef CONFIG_SMP<br>
unsigned int batchcount;<br>
#endif<br>
<br>
/* 2) slab additions /removals */<br>
/* order of pgs per slab (2^n) */<br>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -