08内存管理

1、页和区

(1)页

页是虚拟内存的最小单位。内核把物理页作为内存管理的基本单位。处理器的最小可寻址单位通常为字(甚至字节)。但是内存管理单元(将虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。从虚拟内存角度来看,页就是最小单位。内核使用struct page结构(物理页结构)表示系统中的每个物理页:

struct page{
  /* 存放页的状态(是否为脏页)等 */
  unsigned long         flags;
  /* 页被引用的次数统计,为-1时表示内核中没有引用这一页 */
  atomic_t              _count;
  /* 页缓存引用数目 */
  atomic_t              _mapcount;
  /* 私有数据指向 */
  unsigned long         priavte;
  /* 指向页相关的地址空间 */
  struct address_space  *mapping;
  pgoff_t               index;
  struct list_head      lru;
  /* 指向页的虚拟地址 */
  void                  *virtual;
}

物理页结构作用:内核用这一结构来管理系统中所有的页,因为内核需要知道一个页是否空闲(有没有被分配)。如果页已经被分配,内核还需要知道谁拥有这个页。

物理页结构内存空间花费:假定32位系统,系统有4GB物理内存,内存的物理页大小4KB,内存总共有1048576个物理页,每个物理页结构大小大约40B,所以1048576个物理页结构内存空间花费是40MB,大约占总内存空间的0.97%。要管理系统中这么多物理页,这个代价并不算高。

(2)区

分区原因:由于硬件的限制,内核并不能对所有的页一视同仁。所以内核把页划分为不同的区。Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问);另一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。

分区的意义:Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组。

Linux使用了四种区:

  • ZONE_DMA:这个区包含的页能用来执行DMA操作
  • ZONE_DMA32:和DMA类似,该区包含的页面可以用来执行DMA操作,但是只能被32位设备访问
  • ZONE_NORMAL:这个区包含的都是能正常映射的页
  • ZONE_HIGHEM:这个区包含“高端内存”。其中的页并不能永久地映射到内核地址空间。

Linux 内核将这 4GB的空间分为两部分,将最高的 1GB(0xC0000000-0xFFFFFFFF)供内核使用称为内核空间。而将较低的3GB(0x00000000-0xBFFFFFFF)供各个进程使用称为用户空间。

  • 低端内存(ZONE_DMA):3G~3G+16M 用于DMA __pa线性映射
  • 普通内存(ZONE_NORMAL):3G+16M~3G+896M __pa线性映射 (若物理内存<896M,则分界点就在3G+实际内存)
  • 高端内存(ZONE_HIGHMEM):3G+896~4G 采用动态的分配方式

ZONE_DMA+ZONE_NORMAL属于直接映射区:虚拟地址=3G+物理地址 或 物理地址=虚拟地址-3G,从该区域分配内存不会触发页表操作来建立映射关系。

ZONE_HIGHMEM属于动态映射区:128M虚拟地址空间可以动态映射到(X-896)M(其中X位物理内存大小)的物理内存,从该区域分配内存需要更新页表来建立映射关系,vmalloc就是从该区域申请内存,所以分配速度较慢。

直接映射区的作用是为了保证能够申请到物理地址上连续的内存区域,因为动态映射区,会产生内存碎片,导致系统启动一段时间后,想要成功申请到大量的连续的物理内存,非常困难,但是动态映射区带来了很高的灵活性(比如动态建立映射,缺页时才去加载物理页)。

2、内核层内存管理总结

  • kmalloc: 只能在低端内存区域分配(基于ZONE_NORMAL),最大32个PAGE,共128K,kzalloc/kcalloc都是其变种 (slab.h中如果定义了KMALLOC_MAX_SIZE宏,那么可以达到8M或者更大)
  • vmalloc: 只能在高端内存区域分配(基于ZONE_HIGHMEM)
  • alloc_page: 可以在高端内存区域分配,也可以在低端内存区域分配,最大4M(2^(MAX_ORDER-1)个PAGE)
  • __get_free_page: 只能在低端内存区域分配,get_zeroed_page是其变种,基于alloc_page实现
  • ioremap是将已知的一段物理内存映射到虚拟地址空间,物理内存可以是片内控制器的寄存器起始地址,也可以是显卡外设上的显存,甚至是通过内核启动参数“mem=”预留的对内核内存管理器不可见的一段物理内存。

kmalloc和vmalloc申请的内存块大小是以字节为单位(实际上考虑到最小细分度,开辟的可能比申请的多,存在些许浪费),而__get_free_page申请的内存块大小是以PAGE数量为单位。kmalloc和__get_free_page函数返回的地址都是虚拟地址,和物理地址只差一个固定的偏移值3G,无需操作页表。 alloc_page返回的是第一个物理页的page指针,需要经过page_address函数才能取得虚拟地址。 (使用alloc_page在高端内存申请大于1个page的内存,就无法保证物理地址上的连续性)。kmalloc基于slab实现的,slab是为分配小内存提供的一种高效机制,slab会把page再细分成更小的颗粒。

alloc_page和__get_free_page最终都是调用的同一个子函数:

虚拟地址空间分配及其与物理内存对应图:

malloc用户层内存分配:

当开辟的空间小于 128K 时,调用 brk()函数,malloc 的底层实现是系统调用函数 brk(),其主要移动指针 _enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)。

  • malloc分配了这块内存,然后如果从不去访问它,那么物理页是不会被分配的。
  • 当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作

当开辟的空间大于 128K 时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟.

malloc实现原理:

malloc函数用于动态分配内存。为了减少内存碎片和系统调用的开销,malloc其采用内存池的方式,先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。当用户申请内存时,直接从堆区分配一块合适的空闲块。malloc采用隐式链表结构将堆区分成连续的、大小不一的块,包含已分配块和未分配块;同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。

当进行内存分配时,malloc会通过隐式链表遍历所有的空闲块,选择满足要求的块进行分配;当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。

  • 空闲存储空间以空闲链表的方式组织(地址递增),每个块包含一个长度、一个指向下一块的指针以及一个指向自身存储空间的指针。( 因为程序中的某些地方可能不通过malloc调用申请,因此malloc管理的空间不一定连续。)
  • 当有申请请求时,malloc会扫描空闲链表,直到找到一个足够大的块为止(首次适应)(因此每次调用malloc时并不是花费了完全相同的时间)。
  • 如果该块恰好与请求的大小相符,则将其从链表中移走并返回给用户。如果该块太大,则将其分为两部分,尾部的部分分给用户,剩下的部分留在空闲链表中(更改头部信息)。因此malloc分配的是一块连续的内存。
  • 释放时,首先搜索空闲链表,找到可以插入被释放块的合适位置。如果与被释放块相邻的任一边是一个空闲块,则将这两个块合为一个更大的块,以减少内存碎片。

因为brk、sbrk、mmap都属于系统调用,若每次申请内存,都调用这三个,那么每次都会产生系统调用,影响性能;其次,这样申请的内存容易产生碎片,因为堆是从低地址到高地址,如果高地址的内存没有被释放,低地址的内存就不能被回收。
所以malloc采用的是内存池的管理方式(ptmalloc),Ptmalloc 采用边界标记法将内存划分成很多块,从而对内存的分配与回收进行管理。为了内存分配函数malloc的高效性,ptmalloc会预先向操作系统申请一块内存供用户使用,当我们申请和释放内存的时候,ptmalloc会将这些内存管理起来,并通过一些策略来判断是否将其回收给操作系统。这样做的最大好处就是,使用户申请和释放内存的时候更加高效,避免产生过多的内存碎片。

brk和mmap内存分配:

用户态调用malloc()会分配堆内存空间,而实际上则是完成了一次用户态的内存映射,根据分配空间的大小,内存映射对应的系统调用主要有brk()和mmap()(当然我们也可以直接调用mmap()来映射文件)。对小块内存(小于 128KB),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。而大块内存(大于 128KB),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。这两种方式,自然各有优缺点。

  • brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
  • mmap() 方式分配的内存,会在释放时直接归还系统,所以每次 mmap() 都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是 malloc() 只对大块内存使用 mmap() 的原因。

(1)小块内存申请brk()

brk()系统调用为sys_brk()函数,其参数brk是新的堆顶位置,而mm->brk是原堆顶位置。该函数主要逻辑如下:

将原来的堆顶和现在的堆顶按照页对齐地址比较大小,判断是否在同一页中,如果同一页则不需要分配新页,直接跳转至set_brk,设置mm->brk为新的brk即可。如果不在同一页:如果新堆顶小于旧堆顶,则说明不是新分配内存而是释放内存,由此调用__do_munmap()释放;如果是新分配内存,则调用find_vma(),查找vm_area_struct红黑树中原堆顶所在vm_area_struct的下一个结构体,如果在二者之间有足够的空间分配一个页则调用do_brk_flags()分配堆空间。如果不可以则分配失败。

在 do_brk_flags() 中,调用 find_vma_links() 找到将来的 vm_area_struct 节点在红黑树的位置,找到它的父节点、前序节点。接下来调用 vma_merge(),看这个新节点是否能够和现有树中的节点合并。如果地址是连着的,能够合并,则不用创建新的 vm_area_struct 了,直接跳到 out,更新统计值即可;如果不能合并,则创建新的 vm_area_struct,既加到 anon_vma_chain 链表中,也加到红黑树中。

(2)大内存块申请mmap()

大块内存的申请通过mmap系统调用实现,mmap既可以实现虚拟内存向物理内存的映射,也可以映射文件到自己的虚拟内存空间。映射文件时,实际是映射虚拟内存到物理内存再到文件。

这里主要调用ksys_mmap_pgoff()函数,这里逻辑如下:

  • 判断类型是否为匿名映射,如果不是则为文件映射,调用fget()获取文件描述符
  • 如果是匿名映射,判断是否为大页,如果是则进行对齐处理并调用hugetlb_file_setup()获取文件描述符
  • 调用vm_mmap_pgoff()函数找寻可以映射的区域并建立映射

vm_mmap_pgoff()函数调用do_mmap_pgoff(),实际调用do_mmap()函数。这里get_unmapped_area()函数负责寻找可映射的区域,mmap_region()负责映射该区域。

首先来看看寻找映射区的函数get_unmapped_area()。如果是匿名映射,则调用get_umapped_area函数指针,这个函数其实是 arch_get_unmapped_area()。它会调用 find_vma_prev(),在表示虚拟内存区域的 vm_area_struct 红黑树上找到相应的位置。之所以叫 prev,是说这个时候虚拟内存区域还没有建立,找到前一个 vm_area_struct。如果是映射到一个文件,在 Linux 里面每个打开的文件都有一个 struct file 结构,里面有一个 file_operations用来表示和这个文件相关的操作。如果是我们熟知的 ext4 文件系统,调用的也是get_unmapped_area 函数指针。

mmap_region()首先会再次检测地址空间是否满足要求,然后清除旧的映射,校验内存的可用性,在一切均满足的情况下调用vma_link()将新创建的vm_area_struct结构挂在mm_struct内的红黑树上。vma_link()本身是__vma_link()和__vma_link_file()的包裹函数,其中__vma_link()主要是链表和红黑表的插入,这属于基本数据结构操作,不展开讲解。而__vma_link_file()会对文件映射进行处理,在file结构体中成员f_mapping指向address_space结构体,该结构体中存储红黑树i_mmap挂载vm_area_struct。

至此,我们完成了用户态内存的映射,但是此处仅在虚拟内存中建立了新的区域,尚未真正访问物理内存。物理内存的访问只有在调度到该进程时才会真正分配,即发生缺页异常时分配。

(3)缺页异常do_page_fault()

一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就触发缺页中断,调用 do_page_fault()。这里的逻辑如下

  • 判断是否为内核缺页中断fault_in_kernel_space(),如果是则调用vmalloc_fault()
  • 如果是用户态缺页异常,则调用find_vma()找到地址所在vm_area_struct区域
  • 调用handle_mm_fault()映射找到的区域

find_vma()为红黑树查找操作,在此不做展开描述,下面重点看看handle_mm_fault()。这里经过一系列校验之后会根据是否是大页而选择调用hugetlb_fault()或者__handle_mm_fault()。__handle_mm_fault()完成实际上的映射操作。这里涉及到了由pgd, p4g, pud, pmd, pte组成的五级页表,页表索引填充完后调用handle_pte_fault()创建页表项。

handle_pte_fault()处理以下三种情况:页表项从未出现过,即新映射页表项。匿名页映射,则映射到物理内存页,调用do_anonymous_page()。文件映射,调用do_fault()。页表项曾出现过,则为从物理内存换出的页,调用do_swap_page()换回来。

(4)匿名映射

对于匿名页映射,流程如下

  • 调用pte_alloc()分配页表项
  • 通过 alloc_zeroed_user_highpage_movable() 分配一个页,该函数会调用 alloc_pages_vma(),并最终调用 __alloc_pages_nodemask()。该函数是伙伴系统的核心函数,用于分配物理页面,在上文中已经详细分析过了。
  • 调用mk_pte()将新分配的页表项指向分配的页
  • 调用set_pte_at()将页表项加入该页

(5)文件映射

映射文件do_fault()函数调用了fault函数,该函数实际会根据不同的文件系统调用不同的函数,如ext4文件系统中vm_ops指向ext4_file_vm_ops,实际调用ext4_filemap_fault()函数,该函数会调用filemap_fault()完成实际的文件映射操作。

file_map_fault()主要逻辑为:调用find_ge_page()找到映射文件vm_file对应的物理内存缓存页面。如果找到了,则调用do_async_mmap_readahead(),预读一些数据到内存里面。否则调用pagecache_get_page()分配一个缓存页,将该页加入LRU表中,并在address_space中调用。

(6)页交换

前文提到了我们会通过主动回收或者被动回收的方式将物理内存已映射的页面回收至硬盘中,当数据再次访问时,我们又需要通过do_swap_page()将其从硬盘中读回来。do_swap_page() 函数逻辑流程如下:查找 swap 文件有没有缓存页。如果没有,就调用 swapin_readahead()将 swap 文件读到内存中来形成内存页,并通过 mk_pte() 生成页表项。set_pte_at 将页表项插入页表,swap_free 将 swap 文件清理。因为重新加载回内存了,不再需要 swap 文件了。

 

 

3、获得页alloc_pages()

内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所以这些接口都以页为单位分配内存:

/* 分配2^order个连续的物理页,并返回一个指针,指向第一个页的page结构体,如果出错就返回NULL */
struct page *alloc_pages(gfp_t gfp_mask,unsigned int order);
/* 将页的物理地址转换为逻辑地址,返回指向逻辑地址的指针 */
void *page_address(struct page *page);
/* 直接创建并返回第一个页的逻辑地址 */
unsigned long __get_free_pages(gfp_t gfp_mask,unsigned int order);
/* 获取单个页 */
struct page *alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page()gfp_t gfp_mask);
/* 返回页内容全为0 */
unsigned long get_zeroed_page(unsigned int gfp_mask);

释放页时注意只能释放属于自己的页,内核是完全信赖自己的,注意检查错误:

void __free_pages(struct page *page,unsigned int order)
void free_pages(unsigned long addr,unsigned int order)
void free_page(unsigned long addr)

4、kmalloc()

(1)kmalloc

分配页时用alloc_pages(),分配字节为单位的通常用kmalloc()。kmalloc()函数与用户空间的malloc()非常相似,只不过多了一个flags参数,大多数内核分配中kmalloc()接口用的更多。kmalloc()返回一个指向内存块的指针,内存块至少有size大小,所分配的内存区在物理上是连续的,出错时返回NULL。

void *kmalloc(size_t size,gfp_t flags)

(2)gfp_mask标志

页和内存的分配都使用了分配器标志,它有一下三类:

  • 行为修饰符:内核应当如何分配所需的内存,ptr=kmalloc(size,__GFP_WAIT|__GFP_IO|GFP_FS)

  • 区修饰符:从哪儿进行内存分配

注意:

__GFP_HIGHMEM中ZONE_HIGHMEM优先
如果没有任何指定,就优先从ZONE_NORMAL进行分配。
不能给__get_free_pages()或者kmalloc()指定ZONE_HIGHMEM.其返回的是逻辑地址不是page结构,只有alloc_pages()才能分配高端内存

  • 类型标志:组合了行为修饰符和区修饰符

(3)kfree

kfree是kmalloc()的另一端,kfree()函数释放由kmalloc()分配出来的内存块:void kfree(const void *ptr);

(4)kmalloc源码解读

kmalloc/kfree用于划分和回收内核空间低区内存(基于ZONE_NORMAL)的方法。改组方法没有直接通过伙伴系统进行内存的划分,通过slab算法进行分配的。同时也为每个CPU提供一个阵列缓存,用于提高分配效率。

  •  从Slab通用缓存中查找可用的缓存(可以分配的):cachep = __find_general_cachep(size, flags)
  • 接着,就从缓存中分配Slab对象。先从CPU的阵列缓存中申请对象。如果缓存中还有Slab对象:ac = cpu_cache_get(cachep)
  • 如果阵列缓存中没有对象,则需要向Slab缓存申请对象,重填阵列缓存,然后再分配对象。首先尝试从节点的共享缓存中进行分配。接着判断Slab缓存是不是空了,如果空了就需要为缓存注入新的血液。我们知道Slab缓存中有3个链表,分别指向全满,半满和全空的Slab。通过判断这三个链表是不是为空,来决定要不要向伙伴系统申请Slab缓存。如果缓存中仍然存在可用的Slab对象,则分配,填充阵列缓存。如果发现Slab缓存中的对象都已经分配完了,需要从伙伴系统中申请Slab对象,来增长缓存。

5、vmalloc()

vmalloc()函数的工作方式类似于kmalloc(),vmalloc()分配的内存虚拟地址是连续的而物理地址则无须连续。这也是用户空间分配函数的工作的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是,这并不保证它们在物理RAM中也是连续的。

vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。通过vmalloc()获得的页必须一个一个进行映射(因为物理上不连续),这就导致比直接内存映射大得多的TLB抖动。所以vmalloc()在不得已时才会使用,如获得大块内存时。

通过vmalloc申请了3个page大小的内存示例图,由此可见该区域是虚拟地址连续,物理地址不一定连续:

6、slab层

slab层主要作为数据结构高速缓冲层的角色;能方便开发人员随时获取空闲页的链表。分配和释放数据结构是所有内核中最普遍的操作之一。Linux内核提供了slab层,slab分配器扮演了通用数据结构缓存层的角色,slab层的关键是避免频繁分配和释放页,引入Slab分配器的主要目的是为了减少对伙伴系统分配算法的调用次数,频繁分配和回收必然会导致内存碎片,难以找到大块连续的可用内存。

每种对象类型对应一个高速缓存。例如一个高速缓存用于存放进程描述符,另一个高速缓存存放索引节点对象。这些高速缓存又被划分为slab,slab由一个或多个物理上连续的页组成。每个slab都包含一些对象成员,这里的对象是指被缓存的数据结构。每个slab处于三种状态之一:满、部分满、空。一个满的slab没有空闲的对象(slab中的所有对象都已被分配)。如果没有空的slab,就要创建一个slab了。

每个高速缓存都使用kmem_cache结构来表示。这个包含三个链表:slabs_full、slabs_partial和slab_empty;存放在kmem_list:

struct slab{
  /* 链表连接节点 */
  struct list_head list;
  /* slab着色的偏移量 */
  unsigned long colouroff;
  /* slab中的第一个对象 */
  void *s_mem;
  /* slab中已分配的对象数 */
  unsigned int inuse;
  /* 第一个空闲对象(如果有的话) */
  kmem_bufctl_t free;
}

slab分配器的接口:

每个高速缓存通过kmem_cache结构来描述,这个结构中包含了对当前高速缓存各种属性信息的描述。所有的高速缓存通过双链表组织在一起,形成 高速缓存链表cache_chain。

slab分配器所提供的小块连续内存的分配是通过通用高速缓存实现的。通用高速缓存所提供的对象具有几何分布的大小,范围为32到131072字节。内核中提供了kmalloc()和kfree()两个接口分别进行内存的申请和释放。

kmem_cache_create()用于对一个指定的对象创建高速缓存。它从cache_cache普通高速缓存中为新的专有缓存分配一个高速 缓存描述符,并把这个描述符插入到高速缓存描述符形成的cache_chain链表中。kmem_cache_destory()用于撤销一个高速缓存, 并将它从cache_chain链表上删除。

伙伴算法:

一种物理内存分配和回收的方法,物理内存所有空闲页都记录在BUDDY链表中。首选,系统建立一个链表,链表中的每个元素代表一类大小的物理内存,分别为2的0次方、1次方、2次方,个页大小,对应4K、8K、16K的内存,没一类大小的内存又有一个链表,表示目前可以分配的物理内存。例如现在仅存需要分配8K的物理内存,系统首先从8K那个链表中查询有无可分配的内存,若有直接分配;否则查找16K大小的链表,若有,首先将16K一分为二,将其中一个分配给进程,另一个插入8K的链表中,若无,继续查找32K,若有,首先把32K一分为二,其中一个16K大小的内存插入16K链表中,然后另一个16K继续一分为二,将其中一个插入8K的链表中,另一个分配给进程........以此类推。当内存释放时,查看相邻内存有无空闲,若存在两个联系的8K的空闲内存,直接合并成一个16K的内存,插入16K链表中。伙伴算法用于物理内存分配方案。

SLAB算法:

SLAB算法是一种对伙伴算的一种补充,对于用户进程的内存分配,伙伴算法已经够好了,但对于内核进程,还需要存在一类很小的数据(字节大小,比如进程描述符、虚拟内存描述符等),若每次给几个字节的数据分配一个4KB的页,实在太浪费,于是就有了SLBA算法,SLAB算法其实就是把一个页用力劈成一小块一小块,然后再分配。

7、在栈上静态分配

内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。

当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间运行时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。

用户栈和内核栈的相互转换:当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。 进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,每次进程从用户态陷入内核的时候得到的内核栈都是空的,每个进程都用两页的内核栈,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。

中断栈为每个进程提供一个用于中断处理程序的栈。中断处理程序不用再和被中断进程共享一个内核栈。它们可以使用自己的栈了。

注意:必须尽量节省栈资源,让所有局部变量所占空间之和不要超过几百字节。在栈上进行大量的静态分配是很危险的,栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻对战末端的东西,thread_info结构就在内核栈尾。

8、高端内存的映射

内核空间的高端内存如何映射物理内存空间?

内核虚拟地址空间只有1G,也就是说内核只能访问1G物理内存空间,但是如果物理内存是2G,那内核如何访问剩余的1G物理内存空间呢?按照我们刚才说的,这2G的物理内存地址被划分成了3个Zone,物理内存0到896M是内核直接可以访问的,896M到2G这一部分内核要如何访问呢?实际上,当内核想要访问高于896M的物理内存空间时,会从0xF800 0000到0xFFFF FFFF这一块线性地址空间中找一段,然后映射到想要访问的那一块超过896M部分的物理内存,用完之后就归还。由于0xF800 0000~0xFFFF FFFF这一块没有和固定的物理内存空间进行映射,也就是说,这128M的线性地址空间可以和高于896M的物理内存空间短暂的、任意的建立映射,循环使用者128M线性地址空间,这样内核就可以访问剩下的高于896M的物理内存空间了。

永久映射:使用void *kmap(struct page *page)函数映射给定的page到内核地址空间。如果是低端则会返回该页的虚拟地址;如果是高端则会建立一个永久映射,再返回地址。函数可以睡眠。可以使用void kunmap(struct page *page)解除对应的映射。

暂时映射:当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射(原子映射)。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。void *kmap_atomic(struct page *page, enum km_type type) //映射    void kunmap_atomic(void *kvaddr, enum km_type type) //取消映射

  • 动态内存映射区:vmalloc Region 该区域由内核函数vmalloc来分配,特点是:线性空间连续,但是对应的物理地址空间不一定连续。vmalloc 分配的线性地址所对应的物理页可能处于低端内存,也可能处于高端内存。
  • 永久内存映射区:Persistent Kernel Mapping Region 该区域可访问高端内存。访问方法是使用 alloc_page (_GFP_HIGHMEM) 分配高端内存页或者使用kmap函数将分配到的高端内存映射到该区域。
  • 固定映射区:Fixing kernel Mapping Region 该区域和 4G 的顶端只有 4k 的隔离带,其每个地址项都服务于特定的用途,如 ACPI_BASE 等。

Linux内核空间虚拟内存分布:

9、每个CPU的分配

每个CPU上的数据,对于给定的处理器其数据是唯一的。每个CPU的数据存放在一个数组中。数组中的每一项对应着系统上一个存在的处理器。按照当前处理器号确定这个数组的当前元素。

10、新的percpu接口

内核为了方便创建和操作每个CPU数据,引进了新的操作接口,称为percpu。内核实现每个CPU数据的动态分配方法类似于kmalloc()。

11、使用每个CPU数据的原因

  • 减少数据锁定。因为按照每个处理器访问每个CPU数据的逻辑,你可以不再需要任何锁。不过这只是一个编程约定,你需要确保本地处理器只会访问它自己的唯一数据。还需要禁止内核抢占。
  • 使用每个CPU数据可以大大减少缓存失效。如果一个处理器操作某个数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新自己的缓存。持续不断的缓存失效称为缓存抖动。

12、分配函数的选择

  • 需要连续的物理页,就可以使用某个低级页分配器或kmalloc()。这是内核中内存分配的常用方式。
  • 从高端内存进行分配,就是用alloc_pages()。alloc_pages()函数返回一个指向struct page结构的指针。
  • 不需要物理上连续的页,而仅仅需要虚拟地址上连续的页,就使用vmalloc()。vmalloc()函数分配的内存虚地址是连续的,但不保证物理上的连续。
 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值