linux 内存看一篇就够了(多图)

目录

0 内存模块

1 linux内存总体布局

2 地址转换

3 内存的分配和回收

4 内存实现

5 内存的读写

6 共享内存

正文

0 内存模块

linux 内存看一篇就够了(多图)

1 linux内存总体布局:内存分成用户态和内核态

4G进程地址空间解析

linux 内存看一篇就够了(多图)

linux 内存看一篇就够了(多图)

内核地址空间

linux 内存看一篇就够了(多图)

linux 内存看一篇就够了(多图)

进程地址空间

linux 内存看一篇就够了(多图)

2 地址转换和页表

2.1 地址转换

虚拟内存是指程序使用的逻辑地址。每个进程4G。所有进程共享物理内存4G,所以逻辑地址和物理地址不是一一对应,需要地址转换.

linux 内存看一篇就够了(多图)

页表由3部分组成:页目录,页面,页内偏移 

32bit只有3级 0 -11位:页内偏移OFFSET 12-21位:页面表偏移PT(PTE 页表项.指向一张具体的物理内存页) 22-31位:页面目录偏移PGD

寻址过程如下:  

1)操作系统从寄存器CR3获得当前页面目录指针(基地址);  

2)基地址+页面目录偏移->页面表指针(基地址);  

3)页面表指针+页面表偏移->内存页基址;

 4)内存页基址+页内偏移->具体物理内存单元。

 页号=逻辑地址/页面大小;页面偏移量=逻辑地址%页面大小 。

页目录保存页表项的地址,页表项保存物理地址,最后1项保存4k页内偏移。

进程页表长这样子:

linux 内存看一篇就够了(多图)

举例说明地址映射:比如要访问线性地址0xBFC0 4FFE(1byte),2进制形式

linux 内存看一篇就够了(多图)

1).mmu先取线性地址高10位(31-22)=767,x4=3068=0xBFC,+cr3=0x3800 0bfc,即页目录项在内存的地址,此地址的内容0x392f f000即页表首地址(页对齐最后12位清0)

2).mmu取线性地址下面10位数(21-12)=4,x4=16=0x10,+0x392f f000=0x392f  f010,即页表项在内存的地址,此地址的内容0x3080 4000即物理页面的首地址(页对齐最后12位清0)

3).mmu取线性地址最后12位数(11-0)=4094=0xffe作为低12位,0x3080 4000的前20位作为高20位,组成一个新的32位数0x3080 4ffe即线性地址0xBFC0 4FFE对应的物理地址

2.2 内核页表和进程页表的区别和联系?

进程页表访问:虚拟内存与物理内存的对应。 内核页表:建立物理内存和disk的对应.address_space。

mmap的时候,vmalloc分配内核内存,更改内核页表,然后拷贝内核页表到进程页表.

无论进程页表还是内核页表都在内核中运行,都由内核修改.

经典问题:两个进程虚拟地址相同,物理地址不同。本质就是进程页表的内容不同。

3 内存的分配

3.1  伙伴系统 and  slab 

伙伴系统分配的最小单位是页(4k). 伙伴系统是一个内存池.

它把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框. 

把空闲的页以2的n次方为单位进行拆分or合并.分配从链表上获取和归还.

linux 内存看一篇就够了(多图)

例如:假设ZONE_NORMAL 有16页内存,此时有人申请一页内存,剩下15页.Buddy算法会把剩下的15页拆分成8+4+2+1,放到不同的链表中去。

slab:内核分配内存通常很小,所以引入了slab的方法. Buddy解决外部内存碎片,slab 解决内部内存碎片.

伙伴系统(buddy system)是以页为单位管理和分配内存.slab是以byte为单位分配内存.

slab分配器是基于对象类型进行内存管理的,每一种对象被划分为一类,例如索引节点对象是一类,进程描述符又是一类,等等 slab分成2种cache,普通cache和专用cache. 每种cache分成3种链表full,paritition,empty.

slab 使用kmalloc分配.

linux 内存看一篇就够了(多图)

3.2 内存分配

内存分配就是向伙伴系统申请内存,然后更改页表形成地址转换.

缺页中断:malloc分配内存即产生缺页中断,缺页中断有两个情况,一种有足够内存,直接分配。另一种情况没有内存,需要先置换后分配.

(1) 进程分配malloc

malloc 分配的最小单位是页.小于128k用brk.malloc 大于128k用mmap.

1): brk原理:brk是堆顶指针,会产生内存空洞.比如:先分配256k,再分配128k,释放前面的256k,因为brk指向的栈顶128k没有回收,所以释放的256k页不会归还os.

brk释放:当brk释放空间少于128k则不会归还os,而是malloc采用链表管理这些空闲内存.

比如:malloc(1) 分配1个字节,os也会分配4k。剩下的4k-1字节全由malloc的空闲内存管理链表管理.

2): mmap

堆和栈之间,独立分配,直接释放. 采用匿名映射分配内存. 缺点在堆栈之间造成内存空洞。

写拷贝:malloc分配内存仅仅分配虚拟内存并没有分配物理内存,根据内存的写拷贝原则,只有访问内存的时候才正式分配物理内存。即malloc分配虚拟内存,memset的时候才真正分配物理内存.

(2) 内核分配内存

1) vmalloc

适用场景:在内核中不需要连续的物理地址,而仅仅需要内核空间里连续的虚拟地址的内存块. 比如:将文件读入内核内存.

适用区域: vmalloc 机制则在高端内存映射区分配物理内存.

通过更改内核页表实现内核内存和物理地址的地址转换. vmalloc是从伙伴系统分配内存.

2) kmalloc

kmalloc是实地址映射,不需要地址转换.

kmalloc基于slab分配器,slab缓冲区建立在一个连续的物理地址的大块内存之上,所以缓冲对象也是物理地址连续的。

3) malloc,vmalloc,kmallc 分配流程

linux 内存看一篇就够了(多图)

3.3 置换和回收

0 两种一种free,一种PFRA(类似jvm 垃圾回收内存机制)

1. 页分类(按有无文件背景页面主要分两种):

文件页(file-backed page):有文件背景页面。可以直接和硬盘对应的文件进行交换。 匿名页(anonymous page):无文件背景页面.如进程堆,栈,数据段使用的页等,无法直接跟磁盘交换,但是可以跟swap区进行交换。

2 哪些内存可以回收

属于内核的大部分页框是不能够进行回收的,比如内核栈、内核代码段、内核数据段以及大部分内核使用的页框。 进程使用的页框可以进行回收的,比如进程代码段,进程数据段、进程堆栈、进程访问文件时映射的文件页、进程间共享内存使用的页。 伙伴系统分配的页面使用者使用free_pages之类的函数主动释放的,页面释放后被直接放归伙伴系统 slab中分配的对象(使用kmem_cache_alloc函数),也是由使用者主动释放的(使用kmem_cache_free函数)。

3  页回收方式

页回写:如果一个很少使用的页的后备存储器是一个块设备(例如文件映射),则可以将内存直接同步到块设备,腾出的页面可以被重用 页交换:如果页面没有后备存储器,则可以交换到特定swap分区,再次被访问时再交换回内存。 页丢弃:如果页面的后备存储器是一个文件,但文件内容在内存不能被修改(例如可执行文件),那么在当前不需要的情况下可直接丢弃。 页面该回收: 磁盘高速缓存的页面,但是如果页面是脏页面,则丢弃之前必须将其写回磁盘 回收匿名映射的页面,只好先把页面上的数据转储到磁盘,这就是页面交换(swap)。 所有的磁盘高速缓存页面都可回收,所有的匿名映射页面都可交换。

4 页回收算法-LRU LRU

磁盘高速缓存页面(包括文件映射页面)的链表、匿名映射页面的链表 当Linux系统内存有盈余时,内核会尽量多地使用内存作为page cache,提高系统性能,page cache会被加入到文件类型的LRU链表中,当系统内存紧张时,会按一定的算法来回收内存,下面简单了解下: LRU链表按zone来配置,每个zone中都有一整套LRU链表.page交换调度策略使用.page可能被调度到active_list或者inactive_list队列里.就是使用lru这个list_head. LRU每个zone有两个链表,一个active,一个non-active 进行页面回收的时候,一是将active链表中最近最少使用的页面移动到inactive链表、二是尝试将inactive链表中最近最少使用的页面回收

5 页回收时机

直接页面回收(主动触发):“内存严重不足”事件的触发。 周期性回收(被动触发):kswapd进程以水线为触发点,按LRU链表来进行回收。系统会调用函数balance_pgdat(),它主要调用的函数是 shrink_zone() 和 shrink_slab()。

6 反向映射(比如共享文件或者内存)

本质是逆向映射。正向映射是建立进程页表和物理内存关系用于访问。逆向映射是回收的时候,查看反向指针rmap,没有页表引用就回收. 多个引用的回收 PFRA处理页面回收的过程中,LRU的inactive链表中的某些页面可能就要被回收了。 如果页面没有被映射,直接回收到伙伴系统即可(对于脏页,先写回、再回收)。否则,还有一件麻烦的事情要处理。因为用户进程的某个页表项正引用着这个页面呢,在回收页面之前,还必须给引用它的页表项一个交待 内核建立了从页面到页表项的反向映射。通过反向映射可以找到一个被映射的页面对应的vma,通过vma->vm_mm->pgd就能找到对应的页表。然后通过page->index得到页面的虚拟地址。再通过虚拟地址从页表中找到对应的页表项。

7 如果操作系统在进行了内存回收操作之后仍然无法回收到足够多的页面以满足上述内存要求,那么操作系统只有最后一个选择,那就是使用 OOM( out of memory )killer,它从系统中挑选一个最合适的进程杀死它,并释放该进程所占用的所有页面

linux 内存看一篇就够了(多图)

4 内存实现

mem_map[]

管理all 物理页,为每个物理页面创建一个page对象.

free_area 

空闲块链表

task_struct

 

进程控制块,内核通过它管理进程.内核为每个进程维护一个单独的 task_struct 任务结构(进程控制块 PCB),包含着内核态运行所需要的信息,如 PID、指向用户栈的指针、可执行目标文件的名字,进程状态、内存、调度、文件系统、时间分配等各种信息.

内核栈的尾端创建新的结构struct thread_info,由struct thread_info指向task_struct

task_struct.mm_struct.pgd指向的就是进程页表。

内核页表init_mm.pgd

 mm_struct

整个虚拟内存描述.

mm_struct中通过mmap链表和mm_rb对vm_area_struct进行管理.

mm_struct :vm_mm域指向该进程的内存描述符 .其中有PGD域,指向该进程地址空间的全局页目录;mmap域指向第一个内存区域描述符vm_area_strut (即页表+mmap)。

mm_struct 中的 mmap 指向 vma 链表.

进程切换,进程所对应的 mm_struct 中的 pgd (页面目录)被写入控制寄存器 CR3 .

进程的task_struct->mm结构中的每一个vma就代表着一个映射

 vm_area_struct

表示进程虚拟内存区域.vm_area_struct不是指向一个段,而是每个访问vss,比如读取一个文件,是一个vm_area_struct。读另一个文件是另一个vm_area_struct.

创建vm_area_struct更新页表。

进程使用到的虚存空间不连续,一个进程的虚存空间需要多个vm_area_struct结构来描述.

vm_area_struct 中的vm_next 将各个内存区描述符连接起来。

每个进程都有一个anon_vma,是用于链接所有vm_area_struct的头结点,通过vm_area_struct的anon_vma_node构成双循环链表.

每个vm_area_struct中存在struct  file* vm_file用于指向该连续地址空间中所打开的文件.

vm_struct

 表示内核虚拟内存区域.

linux 内存看一篇就够了(多图)

5 内存与文件系统

struct file* fd_array[]

用于每个进程打开的文件的指针

struct file* vm _file

用于一个vm_area_struct中all打开文件的指针.vm_file通过struct file中的struct  path与struct  dentry相关联.

address_space

一个文件对应一个address_space,address_space管理这个文件的所有页缓存。通过radix树管理.

innode

一个文件对应唯一inode struct

struct dentry

通过它的inode指针指向inode,inode与address_space一一对应,至此形成了页缓存与文件系统之间的关联;

vm_ops

文件操作 open,close,nopage

页缓存和address_space

linux 内存看一篇就够了(多图)

比如:进程A将文件A读入内核页缓存,然后address_space管理这些页缓存。进程B也要读文件A,它会现在address_space中查找如果存在则直接返回,如果不存在则从文件系统读入. 有了address_space可以多个进程共享读取文件.

linux 内存看一篇就够了(多图)

如果两个进程访问一个文件,其中一个进程写文件,那么采用匿名文件方式。

linux 内存看一篇就够了(多图)

6 共享内存

1 mmap 有2个功能:

1) malloc 底层实现. 参见上文 

2) 映射文件读写. 本质是进程可以访问内核内存,减少了read,write函数所需要的内核拷贝到用户态.请看https://blog.csdn.net/fdsafwagdagadg6576/article/details/107584821

shm*() 函数。就是匿名文件

linux 内存看一篇就够了(多图)