您的位置:首页 > 运维架构 > Linux

Linux内存管理基本概念(2)

2013-06-14 22:18 489 查看



(一)Linux内存管理基本概念

1. 基本概念

1.1 地址

(1)逻辑地址:指由程序产生的与段相关的偏移地址部分。在C语言指针中,读取指针变量本身值(&操作),实际上这个值就是逻辑地址,它是相对于你当前进程数据段的地址。
(2)线性地址:段中的偏移地址(逻辑地址),加上相应段的基地址就生成了一个线性地址。
(3)物理地址:
放在寻址总线上的地址。
(4)虚拟地址:保护模式下段和段内偏移量组成的地址,而逻辑地址就是代码段内偏移量,或称进程的逻辑地址。

1.2
内存

(1)虚拟内存:计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上(swap
disk),在需要时进行数据交换。
与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。

(2)
    物理内存:实际的内存。物理地址被分成离散的单元,成为页(page)。目前大多数系统的页面大小都为4k。

1.3
地址转换

Linux采用段页式管理机制,有两个部件用于地址转换:分段部件和分页部件。

(1) 分段部件:将逻辑地址转换为线性地址。分段提供了隔绝各个代码、数据和堆栈区域的机制,因此多个程序(任务)可以运行在同一个处理器上而不会互相干扰。

(2) 分页部件:将线性地址转换为物理地址(页表和页目录),若没有启用分页机制,那么线性地址直接就是物理地址。



2.
内存分配

Malloc,kmalloc
和vmalloc区别?
(1) kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存。
(2) kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续。
(3) kmalloc申请的内存比较小,一般小于128
K。它是基于slab(内存池)的,以加快小内存申请效率。

3.
常见问题

(1)
调用malloc函数后,OS会马上分配实际的内存空间吗?

答:不会,只会返回一个虚拟地址,待用户要使用内存时,OS会发出一个缺页中断,此时,内存管理模块才会为程序分配真正的内存。

(2)
段式管理和页式管理的优缺点?

在段式存储管理中,将程序的地址空间划分为若干个段(segment),这样每个进程有一个二维的地址空间,相互独立,互不干扰。程序通过分段划分为多个模块,如代码段、数据段、共享段。这样做的优点是:可以分别编写和编译源程序的一个文件,并且可以针对不同类型的段采取不同的保护,也可以按段为单位来进行共享。段式存储管理的优点是:没有内碎片,外碎片可以通过内存紧缩来消除;便于实现内存共享。
在页式存储管理中,将程序的逻辑地址空间划分为固定大小的页(page),而物理内存划分为同样大小的页框(pageframe)。程序加载时,可将任意一页放人内存中任意一个页框,这些页框不必连续,从而实现了离散分配。这种管理方式的优点是,没有外碎片,且一个程序不必连续存放。这样就便于改变程序占用空间的大小。
页式和段式系统有许多相似之处。比如,两者都采用离散分配方式,且都通过地址映射机构来实现地址变换。但概念上两者也有很多区别,主要表现在:
[1] 页是信息的物理单位,分页是为了实现离散分配方式,以减少内存的外零头,提高内存的利用率。或者说,分页仅仅是由于系统管理的需要,而不是用户的需要。段是信息的逻辑单位,它含有一组其意义相对完整的信息。分段的目的是为了更好地满足用户的需要。
[2] 页的大小固定且由系统决定,把逻辑地址划分为页号和页内地址两部分,是由机器硬件实现的。段的长度不固定,且决定于用户所编写的程序,通常由编译系统在对源程序进行编译时根据信息的性质来划分。
[3] 页式系统地址空间是一维的,即单一的线性地址空间,程序员只需利用一个标识符,即可表示一个地址。分段的作业地址空间是二维的,程序员在标识一个地址时,既需给出段名,又需给出段内地址。

(3) Malloc在什么情况下调用mmap?

从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。brk是将数据段(.data)的最高地址指针_edata往高地址推,mmap是在进程的虚拟地址空间中(一般是堆和栈中间)找一块空闲的。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。
默认情况下,malloc函数分配内存,如果请求内存大于128K(可由M_MMAP_THRESHOLD选项调节),那就不是去推_edata指针了,而是利用mmap系统调用,从堆和栈的中间分配一块虚拟内存。这样子做主要是因为brk分配的内存需要等到高地址内存释放以后才能释放(例如,在B释放之前,A是不可能释放的),而mmap分配的内存可以单独释放。

(4) 32位系统,通常情况下,最大虚拟地址和物理地址空间为多少?

 
 不使用PAE情况下,最大虚拟地址和物理地址空间均为4G,若果使用PAE,最大虚拟地址仍为4G,而物理地址空间可变为64G(x86, 32为变36位)。
(5) 怎样实现malloc和free?

Malloc实现可考虑采用buddy算法+slob算法,free类似。


Linux 内存基础

地址类型

用户虚拟地址 用户空间看到的常规地址,通过页表可以将虚拟地址和物理地址映射起来
物理地址 用在cpu和内存之间的地址叫做物理地址
总线地址 外围总线和内存之间的地址叫做总线地址。通常他们和物理地址相同
内核逻辑地址 内核的常规地址空间,必定有对应的物理内存与之映射。kmalloc返回的就是内核逻辑地址
内核虚拟地址 内核虚拟地址和内核逻辑地址的相同之处在于,他们都将内核空间的地址映射到物理内存上。但是内核虚拟地址不一定是线性的和一对一的。vmalloc返回的是虚拟地址。

x86系统内存子系统初始化
首先设置页表(可能有多级)
完成内核内存映射(内核中的物理内存和逻辑地址只有一个固定的OFFSET,PAGE_OFFSET)

用户空间的内存管理
struct mm_struct 进程内存空间的最高级别管理结构
struct vm_area_struct 内存区域,组成进程内存
pgd_t *pgd 进程页表指针

物理地址和页

高端和低端内存

高端内存
HighMemory

应对高端内存
高端物理内存在需要使用时会被临时映射到内核虚拟内存上
内核经常访问的数据被放在低端内存上
内核偶尔访问的数据最好放在高端内存上
不同内存区域的内存分配和换页应该有一个平衡

临时映射
kmap和kunmap,产生一个永久的内存映射,但他们有一个全局锁,不适合SMP系统
kmap_atomic和kunmap_atomic,常用于SMP系统,产生的映射地址是每个CPU私有的

内存映射和结构
struct page *virt_to_page(void *kaddr);
struct page *pfn_to_page(int pfn);
void *page_addr(struct page *page);

分页

页表

demand paging

Shared virtual memory

虚拟内存区
程序可执行代码区域(text)
数据区(bss,stack,data)
与每个活动的内存映射区对应的区域

内存映射处理

mmap设备操作
像串口这样面向流的设备就不能
mmap的另一个限制:必须以
PAGE_SIZE
为单位进行映射,因为内核只能在页表一级上对虚拟地址进行管理。

内存映射的方法
重新映射特定的I/O区域
重新映射ram
重新映射内核虚拟地址
执行直接I/O访问

分配内存

Kmalloc函数
flags参数 以多种方式控制kmalloc的行为
size参数

高速缓存

内存池

get_free_pages和相关函数

页分配核心 alloc_pages

vmalloc以及相关函数

per-CPU变量
DEFINE_PER_CPU(type, name);
动态分配: void *alloc_percpu(type); void *__alloc_percpu(size_t size, size_t align);
per_cpu_ptr(void *per_cpu_var, int cpu_id);返回给定cpu_id的per_cpu_var指针

DMA

DMA数据传输概览
进程调用read,驱动程序分配一个DMA缓冲,然后让硬件把数据传输到这里,此时进程睡眠
硬件把数据写入到DMA缓冲,写完之后产生一个中断
中断处理程序获取输入的数据,应答中断并唤醒进程,进程即可读取DMA缓冲里面的数据
硬件产生中断,宣告有数据到来
中断处理程序分配缓冲区,告诉硬件向哪里传输数据
外设将数据写入缓冲,完成后产生另一个中断
处理程序分发新数据,唤醒相关进程,然后执行清理工作

分配DMA缓冲区
驱动作者必须谨慎的为DMA分配正确的内存类型,并不是所有的内存区间都适合DMA操作。外设不能使用高端内存。
对于有限制的设备,应使用GFP_DMA标志调用内存分配函数。

DIY分配

总线地址

通用DMA层

确定设备的DMA能力

内存分配实现
页框分配,zoned buddy算法
内存区分配,slab 分配器
非连续内存区管理,虚拟内存映射

zoned buddy

核心函数
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned
int order);

struct page * alloc_pages(gfp_t gfp_mask, unsigned
int order);

void __free_pages(struct page *page, unsigned int
order);

struct page * __alloc_pages_nodemask(gfp_t gfp_mask,
unsigned int order, struct zonelist *zonelist, nodemask_t *nodemask);


TODOS
paging
segmentation
swaping
demand paging
buddy system & slab allocator(ULK3)

linux内核中有许多种不同的地址类型
虚拟内存

虚拟内存是用来描述一种不直接映射计算机物理内存的方法。分页是在虚拟内存与物理内存转换时用到的。请参阅intel手册了解更多分页系统的知识。

低于896MB的每页内存都被直接映射到内核空间。高于896的内存,又称高端内存,不会一直映射到内存空间,而是使用kmap和kmap_atomic来临时映射。剩余的126MB内存的一部分用于映射高端内存。

内核内存从PAGE_OFFSET开始,在x86架构中它的值是0xc0000000(3G),高于PAGE_OFFSET的虚拟内存用于内核空间,低于的用于用户空间。

阅读ULK3学习更多内存管理的细节

物理地址被分成离散的单元,成为页。目前大多数系统的页面大小都为4k。实际使用的时候应该使用指定体系架构下的页面大小PAGE_SIZE。PAGE_SHIFT可以将地址转换为页帧。

系统中逻辑地址和虚拟地址不一致的情况产生了高端内存和低端内存的说法。

通常linux x86内核将4GB的虚拟地址分割为用户空间和内核空间;在二者的上下文中使用相同的映射。一个典型的分配是将低地址3GB分给用户空间,将剩下的高地址1GB分给内核空间。这样由于内核只能直接操作已经映射了物理内存的虚拟地址,所以内核在大内存系统中就不能直接访问所有的物理内存。这样就产生了高端内存和低端内存的说法。

高端内存是没有直接映射到物理内存的内核逻辑地址

在访问特定的高端内存之前,内核必须建立明确的虚拟映射,使该页可以在内核地址空间被访问。

总的来说高端内存就是没有逻辑地址的内存,反之就是低端内存。

由于高端内存中无法使用逻辑地址,所以内核中处理内存的函数趋向于使用指向page结构的指针。该结构保存了内核需要知道的所有物理内存的信息。系统中的每个物理页都和一个page结构对应。

page结构和虚拟地址之间转换的函数和宏:

In a virtual memory system all of these addresses are virtual addresses and not physical addresses. These virtual addresses are converted into physical addresses by the processor based on information held in a set of tables maintained by the operating system.在虚拟内存系统中,所有的地址都是虚拟地址而不是物理地址。这些虚拟地址可以通过操作系统维护的一系列的表转换为物理地址。

To make this translation easier, virtual and physical memory are divided into handy sized chunks called pages. These pages are all the same size, they need not be but if they were not, the system would be very hard to administer. Linux on Alpha AXP systems
uses 8 Kbyte pages and on Intel x86 systems it uses 4 Kbyte pages. Each of these pages is given a unique number; the page frame number (PFN).为了使这个转换更加简单,虚拟地址和物理地址都被分成叫做内存页面小的内存块。所有的页面都是同样大小。每页内存都有一个唯一的编号,这种编号叫做页帧号。

In this paged model, a virtual address is composed of two parts; an offset and a virtual page frame number. If the page size is 4 Kbytes, bits 11:0 of the virtual address contain the offset and bits 12 and above are the virtual page frame number. Each time
the processor encounters a virtual address it must extract the offset and the virtual page frame number. The processor must translate the virtual page frame number into a physical one and then access the location at the correct offset into that physical page.
To do this the processor uses page tables.在这种分页模式下,虚拟地址由两部分组成;页帧内的偏移和虚拟页帧号。如果页面大小是4KB,11:0这些位就是页帧内偏移,12位以上的叫做页帧号。每当处理器遇到虚拟内存地址,它就会把地址中的页内偏移和页帧号解出来。处理器通过页表把虚拟帧号转换成物理帧号,然后在加上页内偏移就可以找到对应的物理地址了。

现代系统中,处理器需要使用某种机制将虚拟地址转换成物理地址。这种机制被成为页表;它基本上是一个多层树形结构,结构化的数组中包含了虚拟地址到物理地址的映射和相关的标志位。

Linux uses demand paging to load executable images into a processes virtual memory. Whenever a command is executed, the file containing it is opened and its contents are mapped into the processes virtual memory. This is done by modifying the data structures
describing this processes memory map and is known as memory mapping. However, only the first part of the image is actually brought into physical memory. The rest of the image is left on disk. As the image executes, it generates page faults and Linux uses the
processes memory map in order to determine which parts of the image to bring into memory for execution.Linux使用按需分页来将可执行镜像载入到进程的虚拟内存空间。每当命令执行时,命令的文件被打开,内容被映射到进程的虚拟内存上。这里是通过修改进程的内存映射相关结构体来实现的,这个过程也叫做内存映射。不过,只有镜像的开头部分被真正的放进了物理内存。余下部分还在磁盘上。镜像执行的时候,它将持续的产生页面异常,linux通过进程的内存映射表来确定镜像的哪个部分需要被载入物理内存执行。

Virtual memory makes it easy for several processes to share memory. All memory access are made via page tables and each process has its own separate page table. For two processes sharing a physical page of memory, its physical page frame number must appear
in a page table entry in both of their page tables. 虚拟内存使得多个进程共享内存更加简单。所有的内存访问都要通过页表来实现。对于共享一个物理页的两个进程来说,这个物理页面必须同时在两个进程的页表中都有相应的页表项。

VMA是用于管理进程地址空间中不同区域的内核数据结构。

进程的内存映射至少包含下面这些区域:

可以
cat /proc/<pid/maps>
来查看具体进程的内存映射。

当用户空间进程调用mmap时,系统会创建一个新的VMA来相应它。

注意vm_area_struct这个重要的数据结构(定义在中)。

系统中每个进程(除了内核空间的辅助线程)都有一个struct mm_struct结构(定义在中),其中包含了大量的内存管理信息。多个进程可以共享内存管理结构,linux就是使用这种方法实现线程的。

mmap可以将用户空间的内存和设备内存映射起来,这样在访问分配地址范围内的内存时就相当于访问设备内存了。

并非所有的设备都能进行mmap抽象:

为了执行mmap,驱动程序只需要为该地址范围建立合适的页表,并将
vma->vm_ops
替换为一系列的新操作就可以了。有两种建立页表的方法:使用
remap_pfn_range
函数一次全部建立;通过VMA的fault方法一次建立一个新页表。

这里我们来看看内核为设备驱动程序提供的内存管理接口。

kmalloc内存分配工具和malloc的使用方法很接近。 它的原型是:

\#include <linux/slab.h> void *kmalloc(size_t size, int flags);


最常用的标志是GFP_KERNEL(GFP的来源是因为kmalloc最终会调用get_free_pages函数),这个标志允许kmalloc在页面不足的情况下休眠。

如果在进程上下文之外使用kmalloc,比如中断处理例程中就需要使用GFP_ATOMIC标志,不会休眠

其他标志都定义在文件中,请阅读该文件后使用他们

内核中使用基于页面的方式管理内存,因此和用户空间的基于堆的简单内存管理有很大的差别

由于slab分配器(即kmalloc的底层实现)最大分配的内存单元是128KB,所以如果分配的内存过大,最好不要使用kmalloc方法

内核实现了一些内存池,内核驱动程序通过使用它们可以减少内存分配的次数。

它的api在中,类型为kmem_cache_t。

内存池其实是某种形式的高速缓冲,它试图始终保持空闲的状态,方便那些要求内存分配不能失败的代码使用。

它的api在中,类型为mempool_t。

如果驱动使用较大块的内存,则适合使用面向页的分配技术。

get_zeroed_page(unsigned int flags); 返回指向新页面的指针并清零

__get_free_page(unsigned int flags); 返回指针但不清零

__get_free_pages(unsigned int flags, unsigned int order); 分配2^order个连续页面,不清零

alloc_pages用来分配描述用struct page描述的页面内存,使用这种结构描述的内核内存在某些地方使用起来非常方便。

struct page *alloc_pages_node(int nid, unsigned int flags, unsigned int order);

vmalloc分配虚拟地址空间的连续内存。尽管可能这段内存在物理上可能不是连续的。

通过vmalloc获得的内存使用起来效率不高,如果可能,应该直接和单个的页面打交道,也就是使用前面的函数来处理而不是使用vmalloc。vmalloc分配的虚拟地址上可能没有物理内存对应。

kmalloc和__get_free_pages返回的虚拟地址内存范围与物理内存的范围是一一对应的。但vmalloc和ioremap使用的地址范围则是完全虚拟的,每次分配都需要适当的设置页表来建立内存区域。

ioremap也和vmalloc一样建立新页表,但它不会分配内存。它更多用于映射设备缓冲到虚拟内核空间。值得注意的是不能把ioremap返回的指针直接当作内存使用,应该使用I/O函数来访问。

vmalloc的一个小缺点是它不能在原子上下文中使用。

相关的函数定义在中。

当建立一个per-CPU变量时,系统的每个处理器都会拥有该变量的副本。对于per-CPU变量的访问几乎不需要锁定,因为每个处理器有自己的副本。

注意当处理器在修改某个per-CPU变量的临界区中间时,它可能被抢占,需要避免这种情况发生。所以我们应该显式地调用get_cpu_var访问某给定变量的当前处理器副本,结束后调用put_cpu_var。

使用方法:

DMA(Direct Memory Access)是一种高级的硬件机制,它允许外设直接和主内存之间进行I/O传输而不用CPU的干预。

有两种方式可以引发DMA数据传输:软件对数据的请求;硬件异步地把数据传给系统。

第一种情况:

第二种情况:

可以看出,高效的DMA传输依赖于中断报告。

DMA缓冲区的主要问题是:当大于一页时,它必须占用连续的物理页,这是因为多数外设总线都使用物理地址。

使用get_free_pages分配大于128KB内存的时候很容易失败返回-ENOMEM。此时的办法是在引导时分配内存或者为缓冲区保留顶部物理内存。

如果要为DMA分配一大块内存,最好考虑分散聚集I/O。

硬件和程序代码使用不同的地址,所以需要有一个地址转换。

由于多种系统对缓存和DMA的处理不同,内核提供了一个通用DMA层,建议在用到DMA时使用该层。struct device隐藏了描述设备的总线细节,在使用通用DMA层时需要使用到该结构的指针。

接下来的DMA函数都需要包含文件

int dma_set_mask(struct device *dev, u64 mask); 可以用来确定设备是否支持DMA。

下面将介绍linux内核中针对不同的应用场景实现的不同内存分配算法。

在支持NUMA的linux内核当中,系统的物理内存被分为多个节点,在单独节点内,任一给定cpu访问页面所需要的时间都是相同的。每个节点的物理内存又分成多个内存区(zone)。x86下内存区有
ZONE_DMA
ZONE_NORMAL
ZONE_HIGHMEM
。x86_32系统上,
ZONE_HIGHMEM
中的内存没有直接映射到内核线性地址上,在每次使用之前都需要先设置页表映射内存。每个zone下面的内存都是以页框为单位来管理的。

每个zone的内存页面是通过buddy算法来管理的。

页框分配算法需要解决external fragmentation的内存管理问题。linux内核使用buddy算法来解决这个问题。把所有的空闲页分组为2^(order-1)大小的块链表。order的最大值为11,所以一共有11个这样的链表。链表的元素最小的为4k(1一个页面大小),最大的为4M(2^10个页面大小)。请求内存时,内核首先从最接近请求大小的链表中查询,如果有这样的空闲单元,则直接使用。如果没有则一次递增到更大块的内存链表中查询,如果有则将内存分出最接近请求大小的块,在把余下的内存拆分添加到较小的内存链表中。

核心接口

核心实现



参考:http://vmlinz.is-programmer.com/posts/26540.html
        
         大规模web服务开发技术
      
         unix高级环境编程
         
         http://dongxicheng.org/os/linux-memory-management-basic/
   
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: