linux内核学习笔记之内存管理

本文详细介绍了Linux内核的内存管理机制,包括物理页管理、内存区划分、页操作接口、slab高速缓存以及每CPU变量。讨论了如何通过struct page结构体管理内存,以及kmalloc、vmalloc、slab分配器和per CPU变量的使用。
linux内核把物理页作为内存管理的基本单位。每一页的大小根据系统架构不同有所区别,32位系统下为4KB,64位系统下为8KB。内存管理单元(MMU)以页为单位来管理系统中的页表,负责虚拟地址到物理地址的转换,用户所使用的内存地址一般都是虚拟地址。
内核中页的结构体中比较重要的成员为:
struct page {
    unsigned long flags;        /* 页的状态,包括脏标志、是否锁定 */
    atomic_t _count;              /* 引用计数  */
    void *virtual;                   /* 页的虚拟地址 */     

};
内存中区的划分:
名称
物理内存
描述
ZONE_DMA             
 0~16MB
DMA使用的内存
ZONE_NORMAL
16~896MB
正常可寻址的内存
ZONE_HIGHEM
 896MB之后的内存
动态映射的内存
区的划分实际是和体系结构相关,如x86体系结构下,ISA设备只能在物理内存的前16MB执行DMA。所以在某些体系结构下,ZONE_DMA和ZONE_NORMAL可能合并为ZONE_NORMAL。高端内存ZONE_HIGHEM也是类似。不同的区组成了不同的内存池,内存池的主要作用是减少内存碎片。
内核中区也有对应的结构体(省略部分):
struct zone {
    unsigned long watermark[NR_WMARK];
    unsigned long percpu_drift_mark;
    unsigned long        min_unmapped_pages;
    unsigned long        min_slab_pages;
    spinlock_t        lock;
    struct free_area    free_area[MAX_ORDER];
    /* Fields commonly accessed by the page reclaim scanner */
    spinlock_t        lru_lock;    
    struct zone_lru {
        struct list_head list;
    } lru[NR_LRU_LISTS];
    struct zone_reclaim_stat reclaim_stat;
    unsigned long        pages_scanned;       /* since last reclaim */
    unsigned long        flags;           /* zone flags, see below */
    /* Zone statistics */
    atomic_long_t        vm_stat[NR_VM_ZONE_STAT_ITEMS];
    int prev_priority;
    unsigned int inactive_ratio;
    unsigned long        wait_table_hash_nr_entries;
    unsigned long        wait_table_bits;
    struct pglist_data    *zone_pgdat;
    /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
    unsigned long        zone_start_pfn;
    unsigned long        spanned_pages;    /* total size, including holes */
    unsigned long        present_pages;    /* amount of memory (excluding holes) */
    const char        *name;
}
内核中总共就三个区,所以也只有3个这样的结构,name分别为“DMA”、“Normal”、“HighMem”。
页的操作接口:
    static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)            请求多个内存页,返回指向第一个页的page结构体
    void *page_address(struct page *page)         把给定的页转换成虚拟地址,返回虚拟地址
    unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)      相当于上面两个函数一起执行的结果,返回第一个页的逻辑地址
    unsigned long get_zeroed_page(gfp_t gfp_mask)    请求填充为0的页
    void __free_pages(struct page *page, unsigned int order)          释放页,入参为page
    void free_pages(unsigned long addr, unsigned int order)              释放页,入参为逻辑地址

我们常用的是以字节为单位的分配,字节内存的操作接口:
    void *kmalloc(size_t size, gfp_t flags) 用来分配物理地址连续的内存。
    void kfree(const void *) 用来释放kmalloc函数分配的内存。
    void *vmalloc(unsigned long size)用来分配虚拟地址连续的内存,不要求物理地址连续。
    void vfree(const void *addr)用于释放vmalloc分配的内存

以上的内存分配接口都涉及到gfp_mask标志,这里不进行细说,例举几个常用的:
标志
描述
GFP_KERNEL
常规分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。一般为首选标志。
GFP_ATOMIC
用于中断处理程序、下半部等不能睡眠的地方
GFP_DMA
从ZONE_DMA进行分配,通常与其他标志组合使用
    针对频繁申请和释放数据结构场景(如文件描述符、进程描述符等)实现了slab分配器,slab分配器将不同的对象划分高速缓存组,每个高速缓存组都存放不同的对象,每种对象类型对应一个高速缓存。kmalloc()接口建立在slab层之上,使用了一组通用高速缓存。每个高速缓存又可分为一个或多个slab,通常一个slab由一个页组成。
    

每个slab处于三种状态之一:满、部分满、空。slab在内核中也对应着一个结构体:
struct slab {
    struct list_head list;                    //满、部分满或空链表
    unsigned long colouroff;           //slab着色偏移量
    void *s_mem;                            //在slab中的第一个对象,包含着色偏移
    unsigned int inuse;                    //slab中分配的对象
    kmem_bufctl_t free;                   //slab中第一个空闲的对象
    unsigned short nodeid;             //内存结点ID,非负时分配器尝试从相同同在结点处分配内存,通常用于NUMA系统
};
slab描述有两种存放的选择:一种是存放在slab自身开始的地方,另一种是在slab之外另行分配。slab分配器创建新的slab时,可以kmem_get_pages()。使用kmem_freepages释放内存。
创建高速缓存接口如下:
struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *))
第一个参数为高速缓存名称,第二个参数高速缓存中每个元素的大小,第三个参数为slab内第一个对象的偏移,通常为0。最后一个是高速缓存的构造函数,新页追加到高速缓存时才被调用。成功返回一个指向高速缓存的指针,失败返回 NULL。

void kmem_cache_destroy(struct kmem_cache *);
撤销高速缓存

void *kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags)  
从高速缓存中分配对象,第二个参数通常是GFP_KERNEL或GFP_ATOMIC

void kmem_cache_free(struct kmem_cache *cachep, void *objp)
释放对象,放回slab中。第一个为高速缓存,第二个为需要释放的对象地址

内核中也存在进程的概念,每个内核进程都有两页的内核栈。所以内核进程中尽量避免静态分配,节省栈空间。
高端内存的页不能永久映射到内核地址空间上,使用alloc_pages()+__GFP_HIGHMEM标志来获得的页不具有虚拟地址,还需要使用kmap()将页映射到内核地址空间,通常是3G~4G。该映射函数可以睡眠,只能用于进程上下文。使用kunmap()可以解除映射。在中断上下文,可用kmap_atomic()来映射高端内存,kunmap_atomic()可以解除映射。

最后,内核实现了一种叫"per cpu"变量,我们称为每CPU变量,实际就是定义了一个数组,长度为CPU数量,每一个元素只能由对应的CPU处理。这样可以避免并发访问的问题,处理器可以不加锁的情况下进行安全访问。如:
long my_percpu[NR_CPUS];
int cpu;

cpu=get_cpu();                //获取当前处理器,并禁止内核抢占
my_percpu[cpu]++;
put_cpu()                     //激活内核抢占
可以看到,在CPU访问每CPU变量时,禁止了内核抢占,这是为了防止某些CPU可以接触到其他CPU的数据。

涉及到的相关接口如下:
DEFINE_PER_CPU(type,name)        编译时创建类型为type,名字为name的变量
DECLARE_PER_CPU(type,name)     声明每CPU变量
get_cpu_var()                                返回当前处理器变量,同时禁止抢占,如:get_cpu_var()++;
put_cpu_var()                                激活内核抢占
per_cpu(name,cpu)++                  增加指定CPU上name变量的值

void* alloc_percpu(type)                动态分配type类型的每CPU变量
void* __alloc_percpu(size_t size, size_t align)       增加了字节对齐
void free_percpu(const void *);       释放每CPU变量




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值