转自:http://gityuan.com/2015/10/30/kernel-memory/
关于Linux的内存管理,本文分别从内核空间和用户空间两个视角来阐述
页(page)是内核的内存管理基本单位。
==> linux/mm_types.h
struct page {
page_flags_t flags; 页标志符
atomic_t _count; 页引用计数
atomic_t _mapcount; 页映射计数
unsigned long private; 私有数据指针
struct address_space *mapping; 该页所在地址空间描述结构指针,用于内容为文件的页帧
pgoff_t index; 该页描述结构在地址空间radix树page_tree中的对象索引号即页号
struct list_head lru; 最近最久未使用struct slab结构指针链表头变量
void *virtual; 页虚拟地址
};
尽管处理器的最小可寻址单位通常为字或字节,但内存管理单元(MMU,把虚拟地址转换为物理地址的硬件设备)通常以页为单位处理。内核用struct page结构体表示每个物理页,struct page结构体占40个字节,假定系统物理页大小为4KB,对于4GB物理内存,1M个页面,故所有的页面page结构体共占有内存大小为40MB,相对系统4G,这个代价并不高。
内核把页划分在不同的区(zone)
总共3个区,具体如下:
区 | 描述 | 物理内存(MB) |
---|---|---|
ZONE_DMA | DMA使用的页 | <16 |
ZONE_NORMAL | 可正常寻址的页 | 16 ~896 |
ZONE_HIGHMEM | 动态映射的页 | >896 |
下面列举所有的页为单位进行连续物理内存分配,也称为低级页分配器:
页分配函数 | 描述 |
---|---|
alloc_pages(gfp_mask, order) | 分配2^order个页,返回指向第一页的指针 |
alloc_pages(gfp_mask) | 分配一页,返回指向页的指针 |
__get_free_pages(gfp_mask, order) | 分配2^order个页,返回指向其逻辑地址的指针 |
__get_free_pages(gfp_mask) | 分配一页,返回指向其逻辑地址的指针 |
get_zeroed_page(gfp_mask) | 分配一页,并填充内容为0,返回指向其逻辑地址的指针 |
页释放函数 | 描述 |
---|---|
__free_pages(page, order) | 从page开始,释放2^order个页 |
free_pages(addr, order) | 从地址addr开始,释放2^order个页 |
free_page(addr) | 释放addr所在的那一页 |
kmalloc,vmalloc分配都是以字节为单位
(1) kmalloc
void * kmalloc(size_t size, gfp_t flags)
该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存在物理内存中连续且保持原有的数据(不清零)
其中部分flags取值说明:
kmalloc内存分配最终总是调用__get_free_pages 来进行实际的分配,故前缀都是GFP_开头。 kmalloc分最多只能分配32个page大小的内存,每个page=4k,也就是128K大小,其中16个字节用来记录页描述结构。kmalloc分配的是常驻内存,不会被交换到文件中。最小分配单位是32或64字节。
kzalloc
kzalloc()
等价于先用 kmalloc()
申请空间, 再用memset()
来初始化,所有申请的元素都被初始化为0。
static inline void *kzalloc(size_t size, gfp_t flags)
{
return kmalloc(size, flags | __GFP_ZERO); //通过或标志位__GFP_ZERO,初始化元素为0
}
(2) vmalloc
void * vmalloc(unsigned long size)
该函数返回的是一个指向内存块的指针,其内存块大小至少为size,所分配的内存是逻辑上连续的。
kmalloc不同,该函数乜有flags,默认是可以休眠的。
小结:
分配函数 | 区域 | 连续性 | 大小 | 释放函数 | 优势 |
---|---|---|---|---|---|
kmalloc | 内核空间 | 物理地址连续 | 最大值128K-16 | kfree | 性能更佳 |
vmalloc | 内核空间 | 虚拟地址连续 | 更大 | vfree | 更易分配大内存 |
malloc | 用户空间 | 虚拟地址连续 | 更大 | free |
slab分配器的作用:
slab层把不同的对象划分为高速缓存组,每个高速缓存组都存放不同类型的对象,每个对象类型对应一个高速缓存。kmalloc接口监理在slab层只是,使用一组通用高速缓存。
每个高速缓存都是用kmem_cache结构来表示
实例分析: 内核初始化期间,/kernel/fork.c的fork_init()中会创建一个名叫task_struct的高速缓存; 每当进程调用fork()时,会通过dup_task_struct()创建一个新的进程描述符,并调用do_fork(),完成从高速缓存中获取对象。
当设置单页内核栈,那么每个进程的内核栈只有一页大小,这取决于编译时配置选项。 好处:
任意函数必须尽量节省栈资源, 方法就是所有函数让局部变量所占空间之和不要超过几百字节。
高端内存中的页不能永久地映射到内核地址空间。
使用每个CPU数据好处:
分配函数选择:
用户空间中进程的内存,往往称为进程地址空间。Linux采用虚拟内存技术
每个进程都有一个32位或64位的地址空间,取决于体系结构。 一个进程的地址空间与另一个进程的地址空间即使有相同的内存地址,也彼此互不相干,对于这种共享地址空间的进程称之为线程。一个进程可寻址4GB的虚拟内存(32位地址空间中),但不是所有虚拟地址都有权访问。对于进程可访问的地址空间称为内存区域。每个内存区域都具有对相关进程的可读、可写、可执行属性等相关权限设置。
内存区域可包含的对象:
这些内存区域不能相互覆盖,每一个进程都有不同的内存片段。
内存描述符由mm_struct
结构体表示,
==> linux/sched.h
struct mm_struct
{
struct vm_area_struct *mmap;
rb_root_t mm_rb;
...
atomic_t mm_users;
atomic_t mm_count;
struct list_head mmlist;
...
};
在进程的进程描述符(<linux/sched.h>中定义的task_struct结构体)中,mm域记录该进程使用的内存描述符。故current->mm代表当前进程的内存描述符。
fork()函数 利用copy_mm函数复制父进程的内存描述符,子进程中的mm_struct结构体通过allcote_mm()从高速缓存中分配得到。通常,每个进程都有唯一的mm_struct结构体,即唯一的进程地址空间。
当子进程与父进程是共享地址空间,可调用clone(),那么不再调用allcote_mm(),而是仅仅是将mm域指向父进程的mm,即 tsk->mm = current->mm。
相反地,撤销内存是exit_mm()函数,该函数会进行常规的撤销工作,更新一些统计量。
内核线程
虚拟内存区域由vm_area_struct结构体描述, 指定地址空间内连续区间的一个独立内存范围。 每个VMA代表不同类型的内存区域。
==> linux/mm_types.h
struct vm_area_struct {
struct mm_struct * vm_mm; //内存描述符
unsigned long vm_start; //区域的首地址
unsigned long vm_end; //区域的尾地址
struct vm_area_struct * vm_next; //VMA链表
pgrot t_vm_page_prot; //访问控制权限
unsigned long vm_flags; //保护标志位和属性标志位
struct rb_node_ vm_rb; //VMA的红黑树结构
...
struct vm_operations_struct * vm_ops; //相关的操作表
struct file * vm_file; //指向被映射的文件的指针
void * vm_private_data; //设备驱动私有数据,与内存管理无关。
}
每个内存描述符对应于进程地址空间的唯一区间,vm_end - vm_start便是内存区间的长度。
VMA操作
struct vm_operations_struct {
void (*open) (struct vm_area_struct * area);
void (*close) (struct vm_area_struct * area);
struct page * (*nopage)(struct vm_area_struct *area, unsigned long address, int write_access);
...
}
查看进程内存空间
cat /proc/<pid>/maps
每行数据格式: 开始-结束 访问权限 偏移 主设备号:次设备号 i节点 文件
也可通过工具pmap
pmap <pid>
find_vma 查看mm_struct所属于的VMA,搜索第一个vm_end大于addr的内存区域
struct vm_area_struct *find_vma(struct mm_struct *mm, usigned long addr)
find_vma_prev 查看mm_struct所属于的VMA,搜索第一个vm_end小于addr的内存区域
struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev)
mmap
应用程序操作的对象时映射到物理内存之上的虚拟内存,而处理器直接操作的是物理内存。故应用程序访问一个虚拟地址时,需要将虚拟地址转换为物理地址,然后处理器才能解析地址访问请求,这个转换工作通过查询页表完成。
Linux使用三级页表完成地址转换。
多数体系结构,搜索页表工作由硬件完成。每个进程都有自己的页表(线程会共享页表)。为了加快搜索,实现了翻译后缓冲器(TLB),作为将虚拟地址映射到物理地址的硬件缓存。还有写时拷贝方式共享页表,当fork()时,父子进程共享页表,只有当子进程或父进程试图修改特定页表项时,内核才创建该页表项的新拷贝,之后父子进程不再共享该页表项。可见,利用共享页表可以消除fork()操作中页表拷贝所带来的消耗。
所有进程都必须占用一定数量的内存,这些内存用来存放从磁盘载入的程序代码,或存放来自用户输入的数据等。内存可以提前静态分配和统一回收,也可以按需动态分配和回收。
对于普通进程对应的内存空间包含5种不同的数据区:
Linux采用虚拟内存管理技术,每个进程都有各自独立的进程地址空间(即4G的线性虚拟空间),无法直接访问物理内存。这样起到保护操作系统,并且让用户程序可使用比实际物理内存更大的地址空间。
进程分配内存,陷入内核态分别由brk和mmap完成,但这两种分配还没有分配真正的物理内存,真正分配在后面会讲。
虚拟内存 转化为 真实物理内存:
虚拟内存与真实物理内存映射关系:
其中物理地址空间中除了896M(ZONE_DMA + ZONE_NORMAL)的区域是绝对的物理连续,其他内存都不是物理内存连续。在虚拟内核地址空间中的安全保护区域的指针都是非法的,用于保证指针非法越界类的操作,vm_struct是连续的虚拟内核空间,对应的物理页面可以不连续,地址范围(3G + 896M + 8M) ~ 4G;另外在虚拟用户空间中 vm_area_struct同样也是一块连续的虚拟进程空间,地址空间范围0~3G。
相关文章
微信公众号 Gityuan | 微博 weibo.com/gityuan | 博客 留言区交流
原文:https://www.cnblogs.com/sky-heaven/p/13676331.html