linuxnote01和linuxnote02中学习了Linux中进程线程的描述,并学习了Linux中如何进行进程管理,学习理解的过程中,我们有收获也有疑惑,比如什么是进程的地址空间这个地址空间存在哪儿,什么是内核栈,页表是什么?等等的问题,其中一些问题都是与Linux中内存管理相关,因此linuxnote03开始学习Linux内存管理相关的知识。本篇及后续所有的Linux学习都基于”LINUX内核修炼之道任桥伟著“和”Linux内核设计与实现“这两本书,作为一个Linux的一个初学者,非常感激前人留下的宝贵经验。)
内存管理概述
内存是CPU能够访问的大容量高速存储区域,是Linux内核所管理的重要资源之一。初识内存这个概念的时候,感觉非常模糊,外加自身一些理解的RAM,ROM,eMMC,UFS,闪存等等相关的概念名词,也许这篇文章”浅谈eMMC,SSD,UFS“可以初步解答我们的一些疑惑。结合上图,本文所介绍的就是中间区域的内存在Linux中管理所涉及到的相关知识。
Page页&Zone区
页,看到这个名词,很容易联想到我们日常所用的书籍,由于对物理内存结构方面的缺乏,暂且就将用到的物理内存比作书籍,物理内存实实在在,在Linux MMU(内存管理单元)内存管理中以页为最小单位管理物理内存,就好比一本书由很多页组成。目前Linux中1page=4kb。Linux内核中以struct page结构体描述一个物理页,我们一定记住该结构体仅与物理页相关,与后续所说的虚拟页无关。该结构体只用来描述一个物理页是否空闲,表明该页是否空闲,以及拥有者。
Linux的内存结构可以很好的支持NUMA的服务器。NUMA下对于SMP系统,每一个CPU对应一个物理存储,一个物理内存结点就称为Node,而通常的单机系统为UMA就是一个Node。每个Node下物理内存分成几个Zone,Zone再对物理叶Page就行管理,因此Linux用了Node->Zone->Page三层结构来描述物理内存。(本文涉及的一些关键名词请参考”Linux中关键名字解释-linuxnote00“中的解释)由于硬件和体系结构的原因,一些在特定地址范围内的物理页在使用时有所限制,为此Linux一般将物理内存划分三个Zone:ZONE_DMA(0~16MB)DMA内存分配的区域,ZONE_NORMAL(16~896MB)正常映射的内存区域,ZONE_HIGHMEM(896~)高端内存区域,其中的页不能永久映射到内核地址空间,只做动态映射。内核中使用struct pglist_data pg_data_t来描述每个Node的内存,使用结构体struct zone来描述Zone。
疑惑点整理:内核空间是什么?
最基础的获取内存页的方法!
核心函数(以页为单位分配内存):
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)//定义于linux/gfp.h
该函数分配2^order(1<<order)个连续的物理页,并返回一个指向地一个物理页page结构体的指针。如果出错,就返回null。使用如下函数把指定页转化成它的逻辑地址:
void *page_address(const struct page *page)//定义于linux/mm.h
返回一个指针,指向给定物理页当前所在的逻辑地址。如果不需要用到结构体struct page,我们可以使用如下函数直接获取一个纸指向第一个页的逻辑地址:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)//定义于mm/page_alloc.c
该函数与alloc_pages作用相同,不同的是alloc_pages返回的是指向第一个物理页结构体的指针,后者是指向第一个页的逻辑地址。对应这两个函数的简化如下:
alloc_page(gfp_t gfp_mask)//定义于linux/gfp.h
__get_free_page(gfp_t gfp_mask)//定义于linux/gfp.h
这两个与之前的区别在于后面的参数order=0,意思只获取一页。
以上分配方法中分配的物理页都是包含一些随机的垃圾信息,因为未对page结构体中的信息做任何初始化,那么我们就可以使用
unsigned long get_zeroed_page(gfp_t gfp_mask)//定义于mm/page_alloc.c
{
return __get_free_pages(gfp_mask | __GFP_ZERO, 0);
}
其实该函数也就对gfp_mask增加一个参数而已,使用该方法,可以将页的所有信息都填充为0,一般如果分配的页是给用户空间的时候,会非常有用。
有分配就有释放!
当不需要页的时候,我们需要释放页,对应的函数如下:
void __free_pages(struct page *page, unsigned int order)
{
if (put_page_testzero(page)) {
if (order == 0)
free_hot_cold_page(page, false);
else
__free_pages_ok(page, order);
}
}
void free_pages(unsigned long addr, unsigned int order)
{
if (addr != 0) {
VM_BUG_ON(!virt_addr_valid((void *)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)
释放页的时候,要谨慎,只能释放我们分配的对应页。如果参数中传递了错误的struct page或地址 ,用了错误的order,很有可能导致系统崩溃。
基于基础分配释放内存方法的包装
kmalloc,该方法定义与slab.h中。
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
if (!(flags & GFP_DMA)) {
int index = kmalloc_index(size);
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace(kmalloc_caches[index],
flags, size);
}
#endif
}
return __kmalloc(size, flags);
}
在前面最基础的分配内存的方式中,我们介绍了,前面分配内存的方式,都是基于物理页的,以页为单位,并且分配的物理页在物理地址上也连续的,对应的逻辑地址上也是连续的。kmalloc是以字节为单位获取一块内核内存。kmalloc返回一个指向内存块的指针,其内存块至少要有size大小,分配的内存区在物理上都是连续的,出错时返回null。一般都会分配成功,除非内存不足时,因此在调用完kmalloc之后,必须检查返回是否为null,并做适当的处理。kmalloc的另一端就是kfree。kfree函数释放有kmalloc分配出来的内存,否则会出错。以下就是一个正确使用kmalloc和kfree的例子:
struct gfm_demo *test;
test = kmalloc(sizeof(test), gfp_mask);
if (!p)
kfree(test);
vmalloc,vmalloc分配的内存逻辑地址是连续的,但物理地址可以不是连续的。vmalloc只能确保逻辑获得内存在虚拟地址空间内是连续的,它通过分配非连续的物理内存块,再”修正“页表,把内存映射到逻辑地址空间的连续区域中,来保证在虚拟地址空间内是连续性。vmalloc定义于vmalloc.c中,对应的释放内存函数是vfree
void *vmalloc(unsigned long size)
{
return __vmalloc_node_flags(size, NUMA_NO_NODE,
GFP_KERNEL | __GFP_HIGHMEM);
}
gfp_t gfp_mask标记
一般内核分配释放内存时,代码中最多的还是使用kmalloc和vmalloc,当然Linux内核中还是很多对基础方法的包装,不管包装成什么样,其原理是一致的。使用这些方法的时候,我们都会使用一个标记gfp_t gfp_mask,这个标记按其作用可以分为三类:行为修饰符,区修饰符和类型。行为修饰符表示内核应该如何分配所需的内存,在某些特定的情况下,我们只能使用某些特定的方法进行内存分配,比如中断程序就要求内核在分配内存分配的时候不能睡眠(因为中断处理程序不能被重新调度)。区修饰符表示从哪儿分配内存,在界面的介绍中,我们说Linux是通过Node->Zone->Page进行管理内存的,Linux内核将内存分成不同的几个区,区修饰符就是指明到底从这些区中的哪一个区进行分配。类型则是行为和区的一个组合,将各种可能用到的组合归纳为不同的类型,简化使用过程。
通过上述的学习,我们知道了什么,有什么疑惑?
通过上述的介绍,我们了解了本文所介绍的是基于物理内存条内存的管理方法,就是文章唯一图片所显示的内存构成中的最中间的那种类型的内存。我们初步了解,Linux内核是通过Node->Zone->Page三层结构来管理内存的,Page对应实实在在的物理页,Zone是对物理内存的进一步划分,以满足不同的需求,而Node在不同的架构和体系中都是不一样的,UMA中SMP系统每个CPU通过总线连接到一块物理连续的内存上,该系统中只有一个Node,NUMA中,每个CPU都对应一个Node节点,意思就是每个CPU都有自己的内存。掌握了最基本的分配释放内存的函数方法,当然Linux内核中还提供很多分配和释放的方法,往往每一种分配和释放的函数方法都是相对应的。如果我们想获取连续的物理页时,我们可以使用最基础的分配方法或者kmalloc。如果你对物理结构上的页是否连续并没有要求,你可以使用vmalloc,它获取的是逻辑地址连续的内存,vmalloc通过修正页表来保证逻辑地址的连续性。我们可以配合不同的gfp_t gfp_mask标记,进行不同类型的内存分配方式,以及规定在不同的区内进行内存分配。前面介绍进程的时候,我们介绍了进程调度的时候,区分不同的进程,对应调用不同的调度器,那么在内存管理的时候以此类推,我们是否存在不同的内存分配器呢?(不同的分配器其实就是不同的策略,不同的算法)当然学习过程中,存在疑惑都是正常,是否和博主一样,对文章中提及的页表这一名词,一直存在疑问?它到底是什么?接下来我们就慢慢解决这些个思考和疑惑。
内存分配的优化之Buddy管理算法
Buddy算法是一种经典的内存管理算法,其作用是减少存储空间中的空洞和碎片,增加利用率。Buddy把内存中所有的页面按照2的幂次放进行分块管理,分配的时候如果没有找到相应大小的块,就把大的块分成小块,释放的时候,回收的块跟相邻的空闲伙伴块又能合并成大块。这就是Buddy的命名的由来。具体的来说,Buddy把所有的物理空闲页分成了11个块链表,每个块链表分别包含了大小1,2,4,8,16,32,64,128,256,512和1024个连续的页。因此限制了最大申请的内存为1024页及对应4M的连续内存块。每个块的第一个页的物理地址是该块大小的整数倍。如,大小为16个页的块,其起始地址是16*4096的倍数。直接介绍的struct zone结构体中的free_area域,他是一个数组大小为11,记录了物理内存上对应大小的空闲块。如下所示
free area[MAX_ORDER]//MAX_ORDER=11 物理页面
.
├─────────┬ .
│ │ .
│ 10 │ .
├─────────├ .
│ │ .
│ 9 │ .
├─────────├ .
│ │ .
│ 8 │ .
├─────────├ .
│ │ .
│ 7 │ .
├─────────├ .
│ │ .
│ 6 │ .
├─────────├ .
│ │ .
│ 5 │ .
├─────────├ .
│ │ .
│ 4 │ .
├─────────├ .
│ │ .
│ 3 │ ├─────────├
├─────────├ ────│ 4 │
│ │ ├────────────────────────────────────│ ├─────────├
│ 2 │ │ └────│ 3 │
├─────────├ ├─────┬ ├─────┬ ├─────────├
│ │──────>│ 3 │──────>│ 1 │ ───│ 2 │
│ 1 │ │ page│ │ page│─────────────────│ ├─────────├
│ │<──────└─────├ <─────└─────├ └───│ 1 │
│ │ ├─────────├
├─────────├ │ 0 │
│ │ ├─────┬───────────────────────────────────├─────────├
│ 0 │──────>│ 0 │
│ │ │ page│
│ │<──────└─────├
├─────────├
数组0指向所有空闲的单页内存链表,数组1指向所有空闲的2页内存链表,依次往上。当需要分配一个页的内存的时候,直接从数组0中获取到0号page,如果分配一个页面的时候不存在0号page,则从数组1中拿到两个页面大小的内存,分裂成两个页面大小为1页的内存块,放于数组0中。同样分配n个(n<11)页面的时候,以此类推。反过来,释放的时候,我们需要合并一些空闲的内存页。因为都是二分的,所以每个页块只有一个buddy,相关索引计算如下:
__find_buddy_index(unsigned long page_idx, unsigned int order)
{
return page_idx ^ (1 << order);
}
假如页面1是空闲的,计算出它的buddy页块是页面0,也是空闲的,则进行合并得到一个order=1(即两个页面组成的页块)的页块。伙伴算法管理的内存管理的内存适合大块的内存,但如果对小内存管理,如几个几十几百个字节的空间。如直接给一个页则导致内存空间的浪费。linux针对小内存的管理,提供了另一个专门的分配器,slab分配器。
疑惑点整理:若干,虽然了解了buddy算法的思想,但对其实现细节,未能完全掌握,计划后续写一个详细介绍该算法的文章。
slab/slob/slub分配器
在linux的不断优化过程中,目前Linux提供配置一组slab/slob/slub的选择,不同的分配器在不同的应用场景下有着不同的应用。我们这里简单的了解一下slab分配器的基本原理,slab分配器申请了一系列的连续物理内存,该内存保存在系统的高速缓存当中,当需要小内存从这些高速缓存中直接申请。slab分配器并不直接释放已分配的内容,而是等到系统的内核线程周期性的扫描高速缓存并释放slab的内容。
疑惑点整理:slab/slob/slub分配器,上同buddy算法,后续专门分析
进程地址空间,是时候解决一下之前的疑惑了!
以32bit的Linux为例,Linux所能管理的虚拟内存最大为2^32(4G=1024*1024*1024*4),Linux将3-4G(0xC0000000~0xFFFFFFFF)的空间分配给内核,将0-3G(0x00000000~0xBFFFFFFF)的空间分配给用户。因此3-4G的空间称为内核空间,0~3G的空间称为用户空间。内核空间是所有进程共享的,系统只存在一份,而用户空间每个进程都有各自独立的3G虚拟地址空间。不管是内核空间还是用户空间,他们都是通过一定的方法将虚拟内存的地址映射到对应的实实在在的物理内存页之上,目前采用的技术就是分页。MMU通过对应的页表将虚拟地址转化成物理地址。在每个进程的虚拟地址空间内,进程根据不同的作用,将地址范围分成了不同的内存区域,包括代码段,数据段,BSS段,堆,栈……数据段中包括了所有静态分配的数据空间,即全局变量和所有申明为static的局部变量。BSS段则是一些为初始化的全局变量,之前的进程管理文章中,我们介绍过Linux内核用task_struct结构体用来描述进程,在task_struct中对应也有进程地址空间信息的域,Linux内核使用struct mm_struct结构体来描述一个进程的地址空间信息,具体我们来看下这个结构体:(删除一些信息)
struct mm_struct {
struct vm_area_struct *mmap; /* list of VMAs */
struct rb_root mm_rb;
u32 vmacache_seqnum; /* per-thread vmacache */
#ifdef CONFIG_MMU
unsigned long (*get_unmapped_area) (struct file *filp,
unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags);
#endif
unsigned long mmap_base; /* base of mmap area */
unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */
unsigned long task_size; /* size of task vm space */
unsigned long highest_vm_end; /* highest vma end address */
pgd_t * pgd;
atomic_t mm_users; /* How many users with user space? */
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */
atomic_long_t nr_ptes; /* PTE page table pages */
#if CONFIG_PGTABLE_LEVELS > 2
atomic_long_t nr_pmds; /* PMD page table pages */
#endif
int map_count; /* number of VMAs */
spinlock_t page_table_lock; /* Protects page tables and some counters */
struct rw_semaphore mmap_sem;
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/
unsigned long hiwater_rss; /* High-watermark of RSS usage */
unsigned long hiwater_vm; /* High-water virtual memory usage */
unsigned long total_vm; /* Total pages mapped */
unsigned long locked_vm; /* Pages that have PG_mlocked set */
unsigned long pinned_vm; /* Refcount permanently increased */
unsigned long shared_vm; /* Shared pages (files) */
unsigned long exec_vm; /* VM_EXEC & ~VM_WRITE */
unsigned long stack_vm; /* VM_GROWSUP/DOWN */
unsigned long def_flags;
unsigned long start_code, end_code, start_data, end_data;
unsigned long start_brk, brk, start_stack;
unsigned long arg_start, arg_end, env_start, env_end;
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */
};
我们暂且先关注这几个字段:mmap和mm_rb都是用来描述进程地址空间内的区域块(如下使用pmap某个进程号和cat 对应进程下的maps输出信息,每一行信息都与一个vm_area_struct结构体对应,代表一个内存区域),不同是mmap用的是链表形式表示,mm_rb用的是红黑树表示,如此空间冗余目的还是以空间换取时间,提高效率,链表可以高效率的遍历,红黑树则方便查找。pgd则对应分页,页表相关映射信息。
pmap 7636
7636: adb logcat -b all
000055655826f000 2212K r-x-- adb
0000556558498000 92K r---- adb
00005565584af000 4K rw--- adb
00005565584b0000 12K rw--- [ anon ]
000055655a2ef000 132K rw--- [ anon ]
00007f725379b000 1792K r-x-- libc-2.23.so
00007f725395b000 2048K ----- libc-2.23.so
00007f7253b5b000 16K r---- libc-2.23.so
00007f7253b5f000 8K rw--- libc-2.23.so
00007f7253b61000 16K rw--- [ anon ]
00007f7253b65000 88K r-x-- libgcc_s.so.1
00007f7253b7b000 2044K ----- libgcc_s.so.1
00007f7253d7a000 4K rw--- libgcc_s.so.1
00007f7253d7b000 28K r-x-- librt-2.23.so
00007f7253d82000 2044K ----- librt-2.23.so
00007f7253f81000 4K r---- librt-2.23.so
00007f7253f82000 4K rw--- librt-2.23.so
00007f7253f83000 1056K r-x-- libm-2.23.so
00007f725408b000 2044K ----- libm-2.23.so
00007f725428a000 4K r---- libm-2.23.so
00007f725428b000 4K rw--- libm-2.23.so
00007f725428c000 96K r-x-- libpthread-2.23.so
00007f72542a4000 2044K ----- libpthread-2.23.so
00007f72544a3000 4K r---- libpthread-2.23.so
00007f72544a4000 4K rw--- libpthread-2.23.so
00007f72544a5000 16K rw--- [ anon ]
00007f72544a9000 12K r-x-- libdl-2.23.so
00007f72544ac000 2044K ----- libdl-2.23.so
00007f72546ab000 4K r---- libdl-2.23.so
00007f72546ac000 4K rw--- libdl-2.23.so
00007f72546ad000 152K r-x-- ld-2.23.so
00007f72548af000 20K rw--- [ anon ]
00007f72548d2000 4K r---- ld-2.23.so
00007f72548d3000 4K rw--- ld-2.23.so
00007f72548d4000 4K rw--- [ anon ]
00007ffc7add9000 136K rw--- [ stack ]
00007ffc7adfb000 12K r---- [ anon ]
00007ffc7adfe000 8K r-x-- [ anon ]
ffffffffff600000 4K r-x-- [ anon ]
pmap信息分为四行:区域其实地址,区域大小,区域属性,内存区域映射的文件。部分信息解释:[anon]一般表示匿名的内存映射,指的是动态生成的内容所占的内存,比如堆。[stack]表示栈区域。一般认为具有rx属性的是程序的代码段,具有w可能是数据段,BSS段等等。观察每段内存区域的大小,不免会发现,其大小都是物理内存页大小(4k)的整数倍。
cat maps
55655826f000-556558498000 r-xp 00000000 fc:00 20467712 /usr/bin/adb
556558498000-5565584af000 r--p 00228000 fc:00 20467712 /usr/bin/adb
5565584af000-5565584b0000 rw-p 0023f000 fc:00 20467712 /usr/bin/adb
5565584b0000-5565584b3000 rw-p 00000000 00:00 0
55655a2ef000-55655a310000 rw-p 00000000 00:00 0 [heap]
7f725379b000-7f725395b000 r-xp 00000000 fc:00 19005614 /lib/x86_64-linux-gnu/libc-2.23.so
7f725395b000-7f7253b5b000 ---p 001c0000 fc:00 19005614 /lib/x86_64-linux-gnu/libc-2.23.so
7f7253b5b000-7f7253b5f000 r--p 001c0000 fc:00 19005614 /lib/x86_64-linux-gnu/libc-2.23.so
7f7253b5f000-7f7253b61000 rw-p 001c4000 fc:00 19005614 /lib/x86_64-linux-gnu/libc-2.23.so
7f7253b61000-7f7253b65000 rw-p 00000000 00:00 0
7f7253b65000-7f7253b7b000 r-xp 00000000 fc:00 19005505 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f7253b7b000-7f7253d7a000 ---p 00016000 fc:00 19005505 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f7253d7a000-7f7253d7b000 rw-p 00015000 fc:00 19005505 /lib/x86_64-linux-gnu/libgcc_s.so.1
7f7253d7b000-7f7253d82000 r-xp 00000000 fc:00 19005637 /lib/x86_64-linux-gnu/librt-2.23.so
7f7253d82000-7f7253f81000 ---p 00007000 fc:00 19005637 /lib/x86_64-linux-gnu/librt-2.23.so
7f7253f81000-7f7253f82000 r--p 00006000 fc:00 19005637 /lib/x86_64-linux-gnu/librt-2.23.so
7f7253f82000-7f7253f83000 rw-p 00007000 fc:00 19005637 /lib/x86_64-linux-gnu/librt-2.23.so
7f7253f83000-7f725408b000 r-xp 00000000 fc:00 19005605 /lib/x86_64-linux-gnu/libm-2.23.so
7f725408b000-7f725428a000 ---p 00108000 fc:00 19005605 /lib/x86_64-linux-gnu/libm-2.23.so
7f725428a000-7f725428b000 r--p 00107000 fc:00 19005605 /lib/x86_64-linux-gnu/libm-2.23.so
7f725428b000-7f725428c000 rw-p 00108000 fc:00 19005605 /lib/x86_64-linux-gnu/libm-2.23.so
7f725428c000-7f72542a4000 r-xp 00000000 fc:00 19005612 /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72542a4000-7f72544a3000 ---p 00018000 fc:00 19005612 /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72544a3000-7f72544a4000 r--p 00017000 fc:00 19005612 /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72544a4000-7f72544a5000 rw-p 00018000 fc:00 19005612 /lib/x86_64-linux-gnu/libpthread-2.23.so
7f72544a5000-7f72544a9000 rw-p 00000000 00:00 0
7f72544a9000-7f72544ac000 r-xp 00000000 fc:00 19005616 /lib/x86_64-linux-gnu/libdl-2.23.so
7f72544ac000-7f72546ab000 ---p 00003000 fc:00 19005616 /lib/x86_64-linux-gnu/libdl-2.23.so
7f72546ab000-7f72546ac000 r--p 00002000 fc:00 19005616 /lib/x86_64-linux-gnu/libdl-2.23.so
7f72546ac000-7f72546ad000 rw-p 00003000 fc:00 19005616 /lib/x86_64-linux-gnu/libdl-2.23.so
7f72546ad000-7f72546d3000 r-xp 00000000 fc:00 19005610 /lib/x86_64-linux-gnu/ld-2.23.so
7f72548af000-7f72548b4000 rw-p 00000000 00:00 0
7f72548d2000-7f72548d3000 r--p 00025000 fc:00 19005610 /lib/x86_64-linux-gnu/ld-2.23.so
7f72548d3000-7f72548d4000 rw-p 00026000 fc:00 19005610 /lib/x86_64-linux-gnu/ld-2.23.so
7f72548d4000-7f72548d5000 rw-p 00000000 00:00 0
7ffc7add9000-7ffc7adfb000 rw-p 00000000 00:00 0 [stack]
7ffc7adfb000-7ffc7adfe000 r--p 00000000 00:00 0 [vvar]
7ffc7adfe000-7ffc7ae00000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
pmap命令和进程maps信息都是一样的,都能查看进程地址空间内存区域的映射情况。比如:第一行pmap命令获取的信息2212k=(556558498000 - 55655826f000)这是十六进制数字= 93893761269760−93893759004672 对应十进制= 2265088 b。虽然我们很直观的了解了进程地址空间的组成,但是这些内存区域是如何创建起来的呢?答案就是通过mmap系统调用,mmap系统调用的最终目的是将进程所对应的程序文件的代码段,数据段等信息(这些信息在程序编译时就已经确定)映射到用户进程的虚拟地址空间。执行完mmap,仅仅是分配了虚拟内存而已,并没有分配对应的物理页面对应的页表也没有创建。当实际访问新映射的页面时,如果没有查找到相应的物理页面,就会抛出一个缺页异常,用户进程陷入内核态,进行页表创建和物理内存的分配。
疑惑点整理:mmap系统调用的时机?页表如何创建?
写在最后
通过本篇的学习,我们最起码掌握了Linux内存管理的一些基本的知识,了解了Linux中一些基本的分配内存的方法,我们知道了,对于一些大块内存,我们使用buddy算法进行分配回收管理,在分配小块内存的时候(比如进程需要很多的几百字节小块内存,我们总不能为了它们每个分配1page),为了避免内存浪费,Linux又增加了slab/slub/slob分配器具专门针对这些小块内存的分配回收,学习过程中我们也解决了前面一直困惑我们的问题,进程的地址空间,页表。当然疑惑也在所难免,本准备一下子将内存相关的知识全部学习一下,但实际遇到很多问题,比如buddy算法的细节实现,slab/slob/slub分配器如何分配内存和buddy之间是否存在一定的关系?Linux通过fork+exec的技术就行进程创建,我们知道这些都是在内核态下完成的,exec就是完成的父子进程可执行程序的替换,那么在mmap系统调用的时机在哪里……长路漫漫,来日方长。
补充:数据结构解释(持续更新)
结构体 | |
---|---|
struct page | 内核对每一个物理页使用page结构体来表示 |
struct zone | 内核把系统的页划分为区 |
struct vm_struct | 表示的虚拟地址是给内核使用的,地址空间范围是(3G + 896M + 8M) ~ 4G(一般小于4G), |
struct vmap_area | 与vm_struct相似,只不过进行一层链表和rb树的包装,便于遍历和查询 |
struct mm_struct | 用于描述一个进程的地址空间信息的内存描述符,地址空间范围是0~3G |
struct vm_area_struct | 进程地址空间里的内存区域块用该结构体描述,通常称为VMA,表示的是一块连续的虚拟地址空间区域 |