Chinaunix首页 | 论坛 | 博客
  • 博客访问: 197587
  • 博文数量: 70
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 412
  • 用 户 组: 普通用户
  • 注册时间: 2013-08-30 11:07
文章分类

全部博文(70)

文章存档

2014年(68)

2013年(2)

我的朋友

分类: LINUX

2014-04-15 09:45:10

内核内存与用户空间内存不同:空间有限;内核一般不睡眠;内核的内存出现错误时也只有靠自己来解决(用户空间的内存错误可以抛给内核来解决)……

一、内存的管理单元
1、页

内核把物理页作为内存管理的基本单位。MMU以page为单位管理页表。
MMU:是中央处理器(CPU)中用来管理虚拟存储器、物理存储器的控制线路,同时也负责虚拟地址映射为物理地址,以及提供硬件机制的内存访问授权(内存保护)。
——地址范围、虚拟地址(线性地址映射为物理地址 以及 分页机制
任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由CPU的位数决定,例如一个32位的CPU,它的地址范围是0~0xFFFFFFFF (4G),而对于一个64位的CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF (16E).这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集。这里举一个最简单的例子直观地说明这两者,对于一台内存为256M的32bit x86主机来说,它的虚拟地址空间范围是0~0xFFFFFFFF(4G),而物理地址空间范围是0x000000000~0x0FFFFFFF(256M)。
在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写;而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到存储器管理单元MMU,把虚拟地址映射为物理地址。
大多数使用虚拟存储器的系统都使用一种称为分页(paging)机制。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页帧(frame).页和页帧的大小必须相同。在这个例子中我们有一台可以生成32位地址的机器,它的虚拟地址范围从0~0xFFFFFFFF(4G),而这台机器只有256M的物理地址,因此他可以运行4G的程序,但该程序不能一次性调入内存运行。这台机器必须有一个达到可以存放4G程序的外部存储器(例如磁盘或是FLASH),以保证程序片段在需要时可以被调用。在这个例子中,页的大小为4K,页帧大小与页相同——这点是必须保证的,因为内存和外围存储器之间的传输总是以页为单位的。对应4G的虚拟地址和256M的物理存储器,他们分别包含了1M个页和64K个页帧。

页的大小:与体系结构有关,在 x86 结构中一般是 4KB或者8KB。

页的结构体头文件是: 位置:include/linux/mm_types.h

复制代码
/* * 页中包含的成员非常多,还包含了一些联合体
 * 其中有些字段我暂时还不清楚含义,以后再补上。。。 */ 
struct page {
    unsigned long flags; /* 存放页的状态,各种状态参见 ,是不是脏的,或者被锁定在内存中*/ 
    atomic_t _count; /* 页的引用计数,调用page_count()函数检查 */ 
    union {
        atomic_t _mapcount; /* 已经映射到mms的pte的个数 */ 
        struct { /* 用于slab层 */ 
            u16 inuse;
            u16 objects;
        };
    };
    union {
     struct {
        unsigned long private; /* 此page作为私有数据时,指向私有数据 */ 
        struct address_space *mapping; /* 此page作为页缓存时,指向关联的address_space */
         };
#if USE_SPLIT_PTLOCKS 
    spinlock_t ptl; 
#endif 
    struct kmem_cache *slab; /* 指向slab层 */ 
    struct page *first_page; /* 尾部复合页中的第一个页 */ 
    };
    union {
        pgoff_t index; /* Our offset within mapping. */ 
        void *freelist; /* SLUB: freelist req. slab lock */
    };
    struct list_head lru; /* 将页关联起来的链表项 */ 
#if defined(WANT_PAGE_VIRTUAL) 
    void *virtual; /* 页的虚拟地址,高端内存不能映射到内核地址空间上 */ 
#endif /* WANT_PAGE_VIRTUAL */ 
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
    unsigned long debug_flags; /* Use atomic bitops on this */ 
#endif 
#ifdef CONFIG_KMEMCHECK /* * kmemcheck wants to track the status of each byte in a page; this
     * is a pointer to such a status block. NULL if not tracked. */ 
    void *shadow; 
#endif 
};
复制代码

物理内存的每个页都有一个对应的 page 结构,看似会在管理上浪费很多内存,其实细细算来并没有多少。

比如上面的page结构体,每个字段都算4个字节的话,总共40多个字节。(union结构只算一个字段)
注:page结构境遇物理页相关,即使页中所包含数据继续存在,由于交换等原因,它们也可能并不再和同一个page结构相关联。
page的拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或者页高速缓存等。

2.区

内核
并不是对于所有的页一视同仁,有些页位于内存中特定的物理地址上。

内核中分的区定义在头文件 位置:include/linux/mmzone.h

内存区的种类参见 enum zone_type 中的定义。


其实一般主要关注的区只有3个:

描述

物理内存

ZONE_DMA DMA使用的页 <16MB
ZONE_NORMAL 正常可寻址的页 16~896MB
ZONE_HIGHMEM 动态映射的页 >896MB

 另:ZONE_DMA32,只能被32位设备访问。
例:ISA设备只能在<16MB的物理内存执行DMA

某些硬件只能直接访问内存地址,不支持内存映射,对于这些硬件内核会分配 ZONE_DMA 区的内存。

某些硬件的内存寻址范围很广,比虚拟寻址范围还要大的多,那么就会用到 ZONE_HIGHMEM 区的内存,

对于 ZONE_HIGHMEM 区的内存,后面还会讨论。

对于大部分的内存申请,只要用 ZONE_NORMAL 区的内存即可。不可跨区分配。

struct zone 定义在

二、内存的获取方法
1.按页获取--最原始的方法,用于底层获取内存的方式

以下分配内存的方法参见:

方法

描述

alloc_page(gfp_mask) 只分配一物理页,返回指向页结构的指针
alloc_pages(gfp_mask, order) 分配 2^order 个页,返回指向第一页页结构的指针
__get_free_page(gfp_mask) 只分配一页,返回指向其逻辑地址的指针
__get_free_pages(gfp_mask, order) 分配 2^order 个页,返回指向第一页逻辑地址的指针
get_zeroed_page(gfp_mask) 只分配一页,让其内容填充为0,返回指向其逻辑地址的指针

 另:void * page_address(struct page * page)

alloc** 方法和 get** 方法的区别在于,一个返回的是内存的物理地址,一个返回内存物理地址映射后的逻辑地址。如果无须直接操作物理页结构体的话,一般使用 get** 方法

相应的释放内存的函数如下:也是在 中定义的

extern void __free_pages(struct page *page, unsigned int order); 
extern void free_pages(unsigned long addr, unsigned int order); 
extern void free_hot_page(struct page *page);

在请求内存时,参数中有个 gfp_mask 标志,这个标志是控制分配内存时必须遵守的一些规则。

gfp_mask 标志有3类:(所有的 GFP 标志都在 中定义)

  1. 行为标志 :控制分配内存时,分配器的一些行为
  2. 区标志   :控制内存分配在那个区(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之类)
  3. 类型标志 :由上面2种标志组合而成的一些常用的场景

 

行为标志主要有以下几种:

行为标志

描述

__GFP_WAIT 分配器可以睡眠
__GFP_HIGH 分配器可以访问紧急事件缓冲池
__GFP_IO 分配器可以启动磁盘I/O
__GFP_FS 分配器可以启动文件系统I/O
__GFP_COLD 分配器应该使用高速缓存中快要淘汰出去的页
__GFP_NOWARN 分配器将不打印失败警告
__GFP_REPEAT 分配器在分配失败时重复进行分配,但是这次分配还存在失败的可能
__GFP_NOFALL 分配器将无限的重复进行分配。分配不能失败
__GFP_NORETRY 分配器在分配失败时不会重新分配
__GFP_NO_GROW 由slab层内部使用
__GFP_COMP 添加混合页元数据,在 hugetlb 的代码内部使用

 

区标志主要以下3种:

区标志

描述

__GFP_DMA 从 ZONE_DMA 分配
__GFP_DMA32 只在 ZONE_DMA32 分配 (注1)
__GFP_HIGHMEM 从 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 (注2)

注1:ZONE_DMA32 和 ZONE_DMA 类似,该区包含的页也可以进行DMA操作。 
         唯一不同的地方在于,ZONE_DMA32 区的页只能被32位设备访问。 
注2:优先从 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 没有多余的页则从 ZONE_NORMAL 分配。

 

类型标志是编程中最常用的,在使用标志时,应首先看看类型标志中是否有合适的,如果没有,再去自己组合 行为标志和区标志。

类型标志

实际标志

描述

GFP_ATOMIC __GFP_HIGH 这个标志用在中断处理程序,下半部,持有自旋锁以及其他不能睡眠的地方
GFP_NOWAIT 0 与 GFP_ATOMIC 类似,不同之处在于,调用不会退给紧急内存池。 
这就增加了内存分配失败的可能性
GFP_NOIO __GFP_WAIT 这种分配可以阻塞,但不会启动磁盘I/O。 
这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,可能会导致递归
GFP_NOFS (__GFP_WAIT | __GFP_IO) 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作。 
这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码中
GFP_KERNEL (__GFP_WAIT | __GFP_IO | __GFP_FS ) 这是常规的分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。 
为了获得调用者所需的内存,内核会尽力而为。这个标志应当为首选标志
GFP_USER (__GFP_WAIT | __GFP_IO | __GFP_FS ) 这是常规的分配方式,可能会阻塞。用于为用户空间进程分配内存时
GFP_HIGHUSER (__GFP_WAIT | __GFP_IO | __GFP_FS )|__GFP_HIGHMEM) 从 ZONE_HIGHMEM 进行分配,可能会阻塞。用于为用户空间进程分配内存
GFP_DMA __GFP_DMA 从 ZONE_DMA 进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志 
通常与以上的某个标志组合在一起使用。

 

以上各种类型标志的使用场景总结:

场景

相应标志

进程上下文,可以睡眠 使用 GFP_KERNEL
进程上下文,不可以睡眠 使用 GFP_ATOMIC,在睡眠之前或之后以 GFP_KERNEL 执行内存分配
中断处理程序 使用 GFP_ATOMIC
软中断 使用 GFP_ATOMIC
tasklet 使用 GFP_ATOMIC
需要用于DMA的内存,可以睡眠 使用 (GFP_DMA|GFP_KERNEL)
需要用于DMA的内存,不可以睡眠 使用 (GFP_DMA|GFP_ATOMIC),或者在睡眠之前执行内存分配

 2. 按字节获取--用的最多的获取方法

这种内存分配方法是平时使用比较多的,主要有2种分配方法:kmalloc()和vmalloc()

kmalloc的定义在

复制代码
/**
 * @size  - 申请分配的字节数
 * @flags - 上面讨论的各种 gfp_mask */ 
static __always_inline void *kmalloc(size_t size, gfp_t flags)
#+end_src

vmalloc的定义在 mm/vmalloc.c 中
#+begin_src C /**
 * @size - 申请分配的字节数 */ void *vmalloc(unsigned long size)
复制代码

kmalloc 和 vmalloc 区别在于:

  • kmalloc 分配的内存物理地址是连续的,虚拟地址也是连续的
  • vmalloc 分配的内存物理地址是不连续的,虚拟地址是连续的

 

因此在使用中,用的较多的还是 kmalloc,因为kmalloc 的性能较好。

因为kmalloc的物理地址和虚拟地址之间的映射比较简单,只需要将物理地址的第一页和虚拟地址的第一页关联起来即可。

而vmalloc由于物理地址是不连续的,所以要将物理地址的每一页都和虚拟地址关联起来,建立专门的页表项。容易产生TLB抖动。vmalloc只有在不得已的情况下使用,为获取大块的内存,例模块被动态的插入到内核中。

 另:硬件设备用到的任何内存区都必须是物理上连续的块。

kmalloc 和 vmalloc 所对应的释放内存的方法分别为:

void kfree(const void *) void vfree(const void *)


3. slab层获取--效率最高的获取方法

频繁的分配/释放内存必然导致系统性能的下降,所以有必要为频繁分配/释放的对象建立缓存。

而且,如果能为每个处理器建立专用的高速缓存,还可以避免 SMP锁带来的性能损耗。

3.1slab层实现原理
对象高速缓存--快速存储频繁使用的对象类型,相当于空闲链表。

linux中的高速缓存是用所谓 slab 层(高速缓存组)来实现的,slab层即内核中管理高速缓存的机制。

整个slab层的原理如下:

  1. 可以在内存中建立各种对象的高速缓存(比如进程描述相关的结构 task_struct 的高速缓存,索引节点struct inode 高速缓存)
  2. 除了针对特定对象的高速缓存以外,也有通用对象的高速缓存
  3. 每个高速缓存中包含多个 slab。slab用于管理缓存的对象
  4. slab中包含多个缓存的对象,物理上由一页或多个连续的页组成
高速缓存、slab及对象之间的关系

3.2slab层的应用

每个高速缓存都使用kmem_cache 结构表示。该结构包含三个链表:slabs_full、slabs_partial、slabs_empty,均放在kmem_list结构中

slab结构体的定义参见:mm/slab.c
slab或是在slab之外另行分配,或slab很小,在slab自身开始的地方分配。

复制代码
struct slab { 
    struct list_head list; /* 存放缓存对象,这个链表有 满,部分满,空 3种状态 */ 
    unsigned long colouroff; /* slab 着色的偏移量 */ 
    void *s_mem; /* 在slab 中的第一个对象 */ 
    unsigned int inuse; /* slab 中已分配的对象数 */ 
    kmem_bufctl_t free; /* 第一个空闲对象(如果有的话) */ 
    unsigned short nodeid; /* 应该是在 NUMA 环境下使用 */ 
};
复制代码

slab层的应用主要有五个方法:
  • 高速缓存的创建
  • 从高速缓存中分配对象
  • 向高速缓存释放对象
  • 高速缓存的销毁
复制代码
/*为高速缓存分配内存(忽略与分配器NUMA相关的代码),高速缓存部分没有空的slab时
    *cachep指向需要很多页的告诉内存
    第二个参数指要给_get
*/
static inline *kmem_getpages(struct kmem_cache *cachep, gfp_t flags)
{
    void *addr;
    flags|=cachep->gfpflags;
    addr=(void*)_get_free_pages(flags,cachep->gfporder);
    return addr;
}
/*释放内存,内存紧张,高速缓存显示的被撤销时*/
kmem_freepages()调用free_pages()
/**
 * 创建高速缓存
 * 参见文件: mm/slab.c
 *name 高速缓存的名字字符串;size 高速缓存中每个元素的大小 ;align slab内第一个对象的偏移,确保页内进行特定的对齐;flags 可选的设置项控制高速缓存的行为
 *flags:SLAB_HWCACHE_ALIGN;SLAB_POSON;SLAB_RED_ZONE;SLAB_PANIC;SLAB_CACHE_DMA
 *ctor 高速缓存的构造函数,新的页追加到高速缓存时调用,
 *函数不可在中断上下文中调用,它可能会引起睡眠*/ 
struct kmem_cache * kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *)) 
    /*撤销一个高速缓存
    使用条件:高速缓存中所有slab都必须为空;调用者确保同步不使用所撤销的高速缓存*/ 
int kmem_cache_destroy(struct kmem_cache *cachep)
/**
 * 从高速缓存中分配对象也很简单
 * 函数参见文件:mm/slab.c
 * @cachep - 指向高速缓存指针
 * @flags  - 之前讨论的 gfp_mask 标志,只有在高速缓存中所有slab都没有空闲对象时,
 *           需要申请新的空间时,这个标志才会起作用。
 *
 * 分配成功时,返回指向对象的指针 */ void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) /**
 * 向高速缓存释放对象
 * @cachep - 指向高速缓存指针
 * @objp   - 要释放的对象的指针 */ void kmem_cache_free(struct kmem_cache *cachep, void *objp) /**
 * 销毁高速缓存
 * @cachep - 指向高速缓存指针 */ void kmem_cache_destroy(struct kmem_cache *cachep)
复制代码

 

复制代码
#include  
#include  
#include "kn_common.h" 
MODULE_LICENSE("Dual BSD/GPL"); 
#define MYSLAB "testslab" 
static struct kmem_cache *myslab; /* 申请内存时调用的构造函数 */ 
static void ctor(void* obj)
{
    printk(KERN_ALERT "constructor is running....\n");
} 
struct student
{ 
    int id; 
    char* name;
}; 
static void print_student(struct student *); 
static int testslab_init(void)
{ 
    struct student *stu1, *stu2; 
    /* 建立slab高速缓存,名称就是宏 MYSLAB */ 
    myslab = kmem_cache_create(MYSLAB, sizeof(struct student), 0, 0,ctor); 
    /* 高速缓存中分配2个对象 */ 
    printk(KERN_ALERT "alloc one student....\n");
    stu1 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);
    stu1->id = 1;
    stu1->name = "wyb1";
    print_student(stu1);
    
    printk(KERN_ALERT "alloc one student....\n");
    stu2 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL);
    stu2->id = 2;
    stu2->name = "wyb2";
    print_student(stu2); /* 释放高速缓存中的对象 */ printk(KERN_ALERT "free one student....\n");
    kmem_cache_free(myslab, stu1);

    printk(KERN_ALERT "free one student....\n");
    kmem_cache_free(myslab, stu2); /* 执行完后查看 /proc/slabinfo 文件中是否有名称为 “testslab”的缓存 */ 
    return 0;
} 
static void testslab_exit(void)
{ /* 删除建立的高速缓存 */ 
    printk(KERN_ALERT "*************************\n");
    print_current_time(0);
    kmem_cache_destroy(myslab);
    printk(KERN_ALERT "testslab is exited!\n");
    printk(KERN_ALERT "*************************\n"); /* 执行完后查看 /proc/slabinfo 文件中是否有名称为 “testslab”的缓存 */
    static void print_student(struct student *stu)
    { if (stu != NULL)
        {
            printk(KERN_ALERT "**********student info***********\n");
            printk(KERN_ALERT "student id   is: %d\n", stu->id);
            printk(KERN_ALERT "student name is: %s\n", stu->name);
            printk(KERN_ALERT "*********************************\n");
        } else printk(KERN_ALERT "the student info is null!!\n");    
    }

module_init(testslab_init);
module_exit(testslab_exit);
复制代码

 

Makefile文件如下:

复制代码
# must complile on customize kernel
obj-m += myslab.o
myslab-objs := testslab.o kn_common.o

#generate the path
CURRENT_PATH:=$(shell pwd)
#the current kernel version number
LINUX_KERNEL:=$(shell uname -r)
#the absolute path
LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
#complie object all: 
    make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules 
    rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
#clean
clean: rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
复制代码

 

执行测试代码:(我是在 centos6.3 x64 上实验的)

复制代码
[root@vbox chap12]# make [root@vbox chap12]# insmod myslab.ko 
[root@vbox chap12]# dmesg | tail -220 # 可以看到第一次申请内存时,系统一次分配很多内存用于缓存(构造函数执行了多次)
[root@vbox chap12]# cat /proc/slabinfo | grep test #查看我们建立的缓存名在不在系统中
testslab 0 0 16 202 1 : tunables 120 60 0 : slabdata 0 0 0 [root@vbox chap12]# rmmod myslab.ko #卸载内核模块
[root@vbox chap12]# cat /proc/slabinfo | grep test #我们的缓存名已经不在系统中了
复制代码

 


三、获取高端内存

高端内存就是之前提到的 ZONE_HIGHMEM 区的内存。高端内存被映射到内核逻辑地址空间的3GB~4GB。

在x86体系结构中,这个区的内存不能映射到内核地址空间上,也就是没有逻辑地址,

为了使用 ZONE_HIGHMEM 区的内存,内核提供了永久映射和临时映射2种手段:

1.永久映射
永久映射的函数是可以睡眠的,所以只能用在进程上下文中。

复制代码
/* 将 ZONE_HIGHMEM 区的一个page永久的映射到内核地址空间
 * 返回值即为这个page对应的逻辑地址(虚拟地址) 
    在<linux/highmem.h>中定义,page对应低端内存中的一页则返回虚拟地址,对应高端内存,则建立永久映射,并返回地址*/ 
static inline void *kmap(struct page *page) 
/* 允许永久映射的数量是有限的,所以不需要高端内存时,应该及时的解除映射 */ 
static inline void kunmap(struct page *page)
复制代码
2.临时映射

临时映射不会阻塞,也禁止了内核抢占,因为映射对每个处理器都是唯一的,所以可以用在中断上下文和其他不能重新调度的地方。

复制代码
/**
 * 将 ZONE_HIGHMEM 区的一个page临时映射到内核地址空间
 * 其中的 km_type 表示映射的目的,
 * enum kn_type 的定义参见: */ 
static inline void *kmap_atomic(struct page *page, enum km_type idx) 
/* 相应的解除映射是个宏 */ 
#define kunmap_atomic(void *kvaddr, enum km_type type)    do { pagefault_enable(); } while (0)
复制代码

以上的函数都在 中定义的。
另:除非激活了内核抢占,否则上2函数基本无事可做,因为只有在下一个临时映射到来前上一个临时映射才有效。

四、内核内存的分配方式

内核的内存分配和用户空间的内存分配相比有着更多的限制条件,同时也有着更高的性能要求。

下面讨论2个和用户空间不同的内存分配方式

1、内核栈上的静态分配

用户空间中一般不用担心栈上的内存不足,也不用担心内存的管理问题(比如内存越界之类的),即使出了异常也有内核来保证系统的正常运行。

 

而在内核空间则完全不一样,不仅栈空间有限,而且为了管理的效率和尽量减少问题的发生,内核栈一般都是小而且固定的。

在x86体系结构中,内核栈的大小一般就是1页或2页,即 4KB ~ 8KB

内核栈可以在编译内核时通过配置选项将内核栈配置为1页,称为单页内核栈。配置为1页的好处是分配时比较简单,只有一页,不存在内存碎片的情况并且减少每个进程的内存消耗,因为一页是本就是分配的最小单位。

当有中断发生时,如果共享内核栈,中断程序和被中断程序共享一个内核栈会可能导致空间不足,于是,每个进程除了有个内核栈之外,还有一个中断栈,中断栈一般也就1页大小。

 减少静态分配避免栈溢出。

查看当前系统内核栈大小的方法:

[xxxxx@localhost ~]$ ulimit -a | grep 'stack' stack size              (kbytes, -s) 8192
2、按CPU分配

与单CPU环境不同,SMP环境下的并行是真正的并行。单CPU环境是宏观并行,微观串行。

真正并行时,会有更多的并发问题。

 

假定有如下场景:

复制代码
void* p; 
if (p == NULL)
{ /* 对 P 进行相应的操作,最终 P 不是NULL了 */
else 
{ /* P 不是NULL,继续对 P 进行相应的操作 */ }
复制代码

在上述场景下,可能会有以下的执行流程:

  1. 刚开始 p == NULL
  2. 线程A 执行到 [if (p == NULL)] ,刚进入 if 内的代码时被线程B 抢占 
      由于线程A 还没有执行 if 内的代码,所以 p 仍然是 NULL
  3. 线程B 抢占到CPU后开始执行,执行到 [if (p == NULL)]时, 发现 p 是 NULL,执行 if 内的代码
  4. 线程B 执行完后,线程A 重新被调度,继续执行 if 的代码 
      其实此时由于线程B 已经执行完,p 已经不是 NULL了,线程A 可能会破坏线程B 已经完成的处理,导致数据不一致

 

在单CPU环境下,上述情况无需加锁,只需在 if 处理之前禁止内核抢占,在 else 处理之后恢复内核抢占即可。

而在SMP环境下,上述情况必须加锁,因为禁止内核抢占只能禁止当前CPU的抢占,其他的CPU仍然调度线程B 来抢占线程A 的执行

 

SMP环境下加锁过多的话,会严重影响并行的效率,如果是自旋锁的话,还会浪费其他CPU的执行时间。

所以内核中才有了按CPU分配数据的接口。

按CPU分配数据之后,每个CPU自己的数据不会被其他CPU访问,虽然浪费了一点内存,但是会使系统更加的简洁高效。

2.1 按CPU分配的优势

按CPU来分配数据主要有2个优点:

  1. 最直接的效果就是减少了对数据的锁,提高了系统的性能
  2. 由于每个CPU有自己的数据,所以处理器切换时可以大大减少缓存失效的几率 (*注1)

 

注1:如果一个处理器操作某个数据,而这个数据在另一个处理器的缓存中时,那么存放这个数据的那个

处理器必须清理或刷新自己的缓存。持续的缓存失效成为缓存抖动,对系统性能影响很大。

新的CPU操作借口:percpu
所有接口操作例程,定义

2.2 编译时分配

可以在编译时就定义分配给每个CPU的变量,其分配的接口参见:

/* 给每个CPU声明一个类型为 type,名称为 name 的变量 */ 
DECLARE_PER_CPU(type, name) 
/* 给每个CPU定义一个类型为 type,名称为 name 的变量 */ 
DEFINE_PER_CPU(type, name)

注意上面两个宏,一个是声明,一个是定义。

其实也就是 DECLARE_PER_CPU 中多了个 extern 的关键字

 

分配好变量后,就可以在代码中使用这个变量 name 了。

DEFINE_PER_CPU(int, name); /* 为每个CPU定义一个 int 类型的name变量 */ 
get_cpu_var(name)++; /* 当前处理器上的name变量 +1 */ 
put_cpu_var(name); /* 完成对name的操作后,激活当前处理器的内核抢占 */
或者
per_cpu(name,cpu)++;/*不会禁止内核抢占,不提供锁保护*/

 

通过 get_cpu_var 和 put_cpu_var 的代码,我们可以发现其中有禁止和激活内核抢占的函数。

相关代码在

#define get_cpu_var(var) (*({                \ 
    extern int simple_identifier_##var(void);    \
 preempt_disable();/* 这句就是禁止当前处理器上的内核抢占 */
    &__get_cpu_var(var); })) 
#define put_cpu_var(var) preempt_enable()  /* 这句就是激活当前处理器上的内核抢占 */

 注:编译时每个CPU数据例子不能在模块内使用,因为链接程序实际上将它们创建在一个唯一的可执行段中。

2.3运行时分配

除了像上面那样静态的给每个CPU分配数据,还可以以指针的方式在运行时给每个CPU分配数据。

动态分配参见:

复制代码
/* 给每个处理器分配一个 size 字节大小的对象,对象的偏移量是 align即分配时按多少字节对齐 */ 
extern void *__alloc_percpu(size_t size, size_t align); 
/* 释放所有处理器上已分配的变量 __pdata */ 
extern void free_percpu(void *__pdata); 
/* 还有一个宏,是按对象类型 type 来给每个CPU分配数据的,
 * 其实本质上还是调用了 __alloc_percpu 函数 */ 
#define alloc_percpu(type)    (type *)__alloc_percpu(sizeof(type), \
                                 __alignof__(type))
复制代码

 

动态分配的一个使用例子如下:

复制代码
void *percpu_ptr;
unsigned long *foo;

percpu_ptr = alloc_percpu(unsigned long); 
if (!percpu_ptr) 
/* 内存分配错误 */ 
foo = get_cpu_var(percpu_ptr); 
/* 操作foo ... */ 
put_cpu_var(percpu_ptr);
复制代码
percpu数据在中断上下文和进程上下文中都很安全,不能在访问percpu数据时睡眠,否则醒来时就已经到其它处理器上了。

五、总结

在众多的内存分配函数中,如何选择合适的内存分配函数很重要,下面总结了一些选择的原则:

应用场景

分配函数选择

如果需要物理上连续的页 选择低级页分配器或者 kmalloc 函数
如果kmalloc分配是可以睡眠 指定 GFP_KERNEL 标志
如果kmalloc分配是不能睡眠 指定 GFP_ATOMIC 标志
如果不需要物理上连续的页 vmalloc 函数 (vmalloc 的性能不如 kmalloc)
如果需要高端内存 alloc_pages 函数获取 page 的地址,在用 kmap 之类的函数进行映射
如果频繁撤销/创建教导的数据结构 建立slab高速缓存


阅读(533) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~