前言
写代码想知道某段时间内存够不够用 想更清楚高低水位 清楚虚拟ram和物理ram的关系
CPU通过地址总线可以访问连接在地址总线上的所有外设,包括物理内存、I0设备等等, 但从CPU发出的访问
地址并非是这些外设在地址总线上的物理地址,
而一个虚拟地址,由MMU将虚拟地址转换成物理地址再从地址总线上发出,
MMU上的这种虚拟地址和物理地址的錾关系是需要创建的,并且MMU还可以设备这个物理面是否可以进行写操作,
当没有创建一个虚拟地址到物理地址的映射,或者创建了这样的映射,
但是那个物理面不可写的时候,MMU将会通知CPU产生一个缺页异常。
或者说有时候用户空间或者内核空间使用malloc 这种创建了一块区域 并不是真的在物理ram上分配了页
只有使用到的时候 会发生一个缺页异常 后续才有一系列流程来分配这个页
虚拟内存空间加深印象
抄一下某个博主的图
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统
32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间;
64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。空洞区域
虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
最后到每一个进程中用户空间的分布情况
通过这张图你可以看到,用户空间内存,从低到高分别是 7 种不同的内存段:
程序文件段,包括二进制可执行代码;
已初始化数据段,包括静态常量;
未初始化数据段,包括未初始化的静态变量;
堆段,包括动态分配的内存,从低地址开始向上增长;
文件映射段,包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关);也是文件页(下面有说)
栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;
在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。
VMA
linux内核中,这样的区域被称之为虚拟内存区域(virtual memory areas),简称 VMA。
一个vma就是一块连续的线性地址空间的抽象,它拥有自身的权限(可读,可写,可执行等等) ,
每一个虚拟内存区域都由一个相关的 struct vm_area_struct 结构来描述。
从进程的角度来讲,VMA 其实是虚拟空间的内存块,一个进程的内存资源由多个内存块组成,所以,一个进程的描述结构 task_struct 中首先包含Linux的内存描述符 struct mm_struct 结构。
下面的图为了说明这些结构体都连其他结构体 能看下面的源码
发生缺页异常跑入的函数_do_ page_ fault源码分析
下面的代码会对产生异常的情况进行判断 直到最后才是因为 用户空间或者内核空间
给产生异常的地方分配物理ram页
static noinline voic
do_ page_ fault(struct pt_ regs *regs, unsigned 1ong error_code,
unsigned 1ong address ){
struct vm_ _area_ struct *vma; // 定义结构体指针变量:表示-个独立的虚拟内存区域
struct task_ struct *tsk; //进程描述符
struct mm_ _struct *mm; // 进程内存描述符
int fault, major = 0;
unsigned int flags = FAULT_ _FLAG ALLOW_ RETRY| FAULT_ _FLAG_ _KILLABLE;
//获取当前CPU正在运行的进程的进程描述符
//然后获取进程的内存描述符
tsk = current;
mm = tsk- >mm; .
//缺页地址位于内核空间,并不代表异常发生于内核空间,有可能是用户状态访问了内核空间的地址
if (unlikely(fault_ _in_ _kernel_ space(address))) {
if (!(error. _code & (PF_ _RSVD | PF_ _USER | PF. _PROT))) {
//检查发生缺页地址是否在vmalloc区,是则进行相应的处理,主要是从内核主页表向进程页表同步数据
if (vmalloc_ _fault(address) >= 0)
return;
if (kmemcheck_ fault(regs, address, error. code))
return;
}
//检查是否是TLB (转换后备缓冲区,又称为页表缓存)导致候的性能延迟
if (spurious_ fault(error_ code, address))
//由于异常地址位于内核态,触发内核异常,因为vmalloc区的缺页异常,内核态的缺页异常只能
//发生在vmalloc区,如果不是,那就是内核异常。
bad_ area_ nosemaphore(regs, error_ code, address, NULL);
return;
}
//可以缩短因缺页异常所导致的差中断时长
if (user_mode(regs)) {
1ocal_irq_enable();
error_ code |= PF_ USER;
flags |= FAULT_ FLAG_ USER;
} else{
if (regs->flags & X86_ EFLAGS_ IF)
local_ _irq_ enable();
if (unlikely(!down_ read_ try1ock(&mm- >mmap_ sem))) { .
// 缺页发生在内核.上下文,此情况发生缺页的地址只能位于用户态地址空间
if ((error_ code & PF_ USER) ==日&&
!search_ exception. _tables(regs->ip)) {
bad_area_ nosemaphore(regs, error_ code, address, NULL);
return;
}
retry: //如果发生在用户态或者是有exception talbe, 说明不是内核异常
down_ read( &mm- >mmap_ sem);
} else {
/*
* The above down_ read_ trylock() might have succeeded in
* which case We'll have missed the might_ sleep() from
down_ read():*/
might_ sleep();
}
//在当前进程的地址空间中查找发生异常的地址对应的VMA
vma = find_ vma(mm, address);
//如果没有找到VMA,则释放信号量
if (unlikely(!vma)) {
bad_ _area(regs, error. code, address );
return;
}
//找到VMA,且发生异常的虛拟地址位于VMA的有效范围内,则为正常的缺页异常,请求调页,分配物理内存
if (likely(vma->vm_ _start <= address))
goto good_area;
//如果异常地址不是位于紧挨着堆栈区域,同时又没有相应VMA,则进程访问造成非法地址,进入bad_area处理
if (unlikely(!(vma->Vm_ _flags & VM_ GROWSDOWIN))) {
bad_ _area(regs, error_ code, address);
return;
//表示缺页异常地址位于堆栈区,需要扩展堆栈,说明(堆栈区的虛拟地址空间也是动态分配和扩展的,不是
//一开始就分配好的。)
if (unlikely(expand_ stack(vma, address))) {
bad_ _area(regs, error. code, address);
return;
/*
0k, we have a good Vm_ area for this memory access, so
we can handle it.. .
*/
//说明: 正常的缺页异常, 进行请求调页,分配物理内存
good_area:
if (unlikely(access_ error(error. code, vma))) {
bad_ area_ access_ error(regs, error. code, address, vma);
return;
//从函数handle_ mm_ fault开始的部分,是所有处理器架构共用的部分
//负责处理用户空间的缺页错误异常。
fault = handle_ mm_ faultkvma, address, flags);
major |= fault & VM_ FAULT_ MAJOR;
}
用户空间异常
上面代码执行到用户空间异常会进行判断 反正就是来生成物理ram页
用户空间页错误异常是指进程访问用户虚拟地址生成的页错误异常,可以分为两种情况: .
进程在用户模式下访问用户虚拟地址,生成页错误异常。
进程在内核模式下访问用户虚拟地址,生成页错误异常。
进程通过系统调用进入内核模式,系统调用传入用户空间的缓冲区,进程在内核模式下访问用户空间的缓冲区。
如果虚拟内存区域使用标准巨型页,则调用函数hugetlb_ fault处理标准巨型页的页错误异常。
如果虚拟内存区域使用普通页,则调用_ handle_ mm_ fault处理普通页的页错误异常。
发生页异常处理
应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理。
缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了 ——触发 OOM
(Out of Memory)机制。
匿名页(anonymous pages)
匿名页,没有文件背景的页面(即没有与磁盘文件存在任何映射关系的内存页面),如stack,heap,数据段,共享内存
文件页(file-backed pages)
文件页,即与磁盘文件存在映射关系的内存页(有文件背景的页面),例如进程代码段、文件的映射页等 ,他们有对应的硬盘文件,
因此如果要交换,可以直接和硬盘对应的文件进行交换。内存紧张时,非dirty的文件页可以直接drop掉,所以这个也算作MemAvailable中。
继续回到刚刚的函数
什么情况下会发生匿名页缺页异常呢?
函数的局部变量比较大,或者函数调用的层次比较深,导致当前栈不够用,需要扩大栈;
进程调用malloc,从堆申请了内存块,只分配虚拟内存区域,还没有映射到物理页,第
一次访问时触发缺页异常。
进程直接调用mmap,创建匿名的内存映射,只分配了虚拟内存区域,还没有映射到物
理页,第一次访问时触发缺页异常。
何时会触发文件页的缺页异常呢?
启动程序的时候,内核为程序的代码段和数据段创建私有的文件映射,映射到里程的虚
拟地址空间,第- -访问的时候触发文件页的缺页异常。
进程使用mmap创建文件映射,把文件的一个区间映射到进程的虚拟地址,第一-次访问
的时候触发文件页的缺页异常。
参考文章
https://blog.csdn.net/qq_34827674/article/details/107042163