linux用户空间与内核空间关系

A:内核空间和用户空间:
Linux的虚拟地址空间范围为0~4G,Linux内核将这4G字节的空间分为两部分,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为“用户空间。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
Linux使用两级保护机制:0级供内核使用,3级供用户程序使用,每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的,最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。
内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。 虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000),另外,使用虚拟地址可以很好的保护内核空间被用户空间破坏,虚拟地址到物理地址转换过程有操作系统和CPU共同完成(操作系统为CPU设置好页表,CPU通过MMU单元进行地址转换)。

注:多任务操作系统中的每一个进程都运行在一个属于它自己的内存沙盒中,这个沙盒就是虚拟地址空间(virtual address space),在32位模式下,它总是一个4GB的内存地址块。这些虚拟地址通过页表(page table)映射到物理内存,页表由操作系统维护并被处理器引用。每个进程都拥有一套属于它自己的页表。

0-3G 用户空间 0x00000000 ~ 0xbfffffff

3-4G 内核空间 0xc0000000 ~ 0xffffffff

每个用户进程都有独立的用户空间(虚拟地址0-3),而内核空间是唯一的(相当于共享)

用户空间和内核空间均采用分段的地址管理模式,具体的划分方式如下图:
1.
linux用户空间与内核空间关系_第1张图片
2.在3GB~(3GB+896MB)这段线性映射区域,包含了内核初始化页表swapper_pg_dir,内核镜像等。内核也是由一个elf文件(比如vmlinux)加载启动的,加载后也有text段,data段,bss段等。
可通过cat /proc/iomem命令查看kernel的text段,data段和bss段的内存分布。这里给出的地址范围都是小于0xC0000000的,所以可以判断这是物理地址。
在这里插入图片描述
linux用户空间与内核空间关系_第2张图片
3.linux用户空间与内核空间关系_第3张图片
4G地址空间解析图:
linux用户空间与内核空间关系_第4张图片
3G-4G的段分布情况
linux用户空间与内核空间关系_第5张图片
内核空间内存动态申请

主要包括三个函数:kmalloc(), __get_free_pages, vmalloc。

kmalloc(),
__get_free_pages申请的内存位于物理地址映射区,而且在物理上也是连续的,返回的虚拟地址与真实的物理地址(物理地址是连续的,虚拟地址也是连续的)只有一个固定的偏移,因此存在较简单的转换关系。

而vmalloc申请的内存位于vmalloc虚拟内存分配区(这些区都是以线性地址为度量),它在虚拟内存空间给出一块连续的内存区,实质上,这片连续的虚拟内存在物理内存中并不一定连续,而vmalloc申请的虚拟内存和物理内存之间也没有简单的换算关系。因为vmalloc申请的在虚拟内存空间连续的内存区在物理内存中并不一定连续,可以想象为了完成vmalloc,新的页表需要被建立,因此,调用vmalloc来分配少量内存是不妥的。一般来讲,kmalloc用来分配小于128K的内存,而更大的内存块需要用vmalloc来实现。

虚拟地址与物理地址关系

对于内核物理内存映射区的虚拟内存(用kmalloc(),
__get_free_pages申请的),使用virt_to_phys()和phys_to_virt()来实现物理地址和内核虚拟地址之间的互相转换。它实际上,仅仅做了3G的地址移位。上述方法适用于常规内存(内核物理内存映射区),高端内存的虚拟地址与物理地址之间不存在如此简单的换算关系。因为它涉及到了分离物理页的页表控制机制。

B: linux 内核地址映射模型
x86 CPU采用了段页式地址映射模型。进程代码中的地址为逻辑地址,经过段页式地址映射后,才真正访问物理内存。
段页式机制如下图:
linux用户空间与内核空间关系_第6张图片
linux用户空间与内核空间关系_第7张图片
逻辑地址-> 通过段基地址+偏移 指向线性地址空间的某个地址->线性地址中包含有页目录+页表+偏移->映射到真正的物理地址
在这里插入图片描述
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步(如下图):

首先,将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,
其次,再利用其页式内存管理单元,转换为最终物理地址。

这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。

1.进程试图访问用户地址空间中的一个内存地址,利用上面的线性地址去查找页表,确定对应的物理地址,但使用的页表无法确定物理地址(物理内存中没有关联页)
2. 处理器接下来触发一个缺页异常,发送到内核。
3.内核会检查负责缺页区域的进程地址空间数据结构,找到适当的后备存储器,或者确认该访问实际上是不正确的(未映射,未使用)
4.分配物理内存页,并从后备存储器读取所需数据填充。 借助于页表将物理内存页并入到用户进程的地址空间,应用程序恢复执行。

这些操作对用户进程是透明的。换句话说,进程不会注意到页是实际在物理内存中,还是需要通过按需调页加载。
linux用户空间与内核空间关系_第8张图片
在整个过程中可能需要解决以下几个问题:

1)系统如何感知进程当前所需页面不在主存(页表机制);
2)当发现缺页时,如何把所缺页面调入主存(缺页中断机构);
3)在置换页面时,根据什么策略选择欲淘汰的页面(置换算法)。

页表机制

在这里插入图片描述
状态位(中断位):标识该页是否在内存(0或1); 访问位:标识该页面的近来的访问次数或时间(换出); 修改位:标识此页是否在内存中被修改过;
外存地址:记录该页面在外存上的地址,即(外存而非内存的)物理块号。

缺页中断机制

程序在执行时,首先检查页表,当状态位指示该页不在主存时,则引起一个缺页中断发生,其中断执行过程与一般中断相同:
保护现场(CPU环境);
中断处理(中断处理程序装入页面);
恢复现场,返回断点继续执行。

置换算法

FIFO

LRU

LFU

C:Linux内核高端内存的划分

内核将高端内存划分为3部分:VMALLOC_STARTVMALLOC_END、KMAP_BASEFIXADDR_START和FIXADDR_START~4G。

linux用户空间与内核空间关系_第9张图片
linux用户空间与内核空间关系_第10张图片
linux用户空间与内核空间关系_第11张图片
对 于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行(为什么?想想 MMU 是如何访问物理内存的),也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。

对应高端内存的3部分,高端内存映射有三种方式:

映射到”内核动态映射空间”(noncontiguous memory allocation) 这种方式很简单,因为通过 vmalloc()
,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc
的实现),因此说高端内存有可能映射到”内核动态映射空间”中。

持久内核映射(permanent kernel mapping) 如果是通过 alloc_page() 获得了高端内存对应的
page,如何给它找个线性空间? 内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START
,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M
之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是
swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M
大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page
映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的
page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

临时映射(temporary kernel mapping) 内核在 FIXADDR_START 到 FIXADDR_TOP
之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。

D: Linux内核高端内存的理解
在x86结构中,高端内核三种类型的区域如下:

ZONE_DMA 内存开始的16MB

ZONE_NORMAL 16MB~896MB

ZONE_HIGHMEM 896MB ~ 结束
linux用户空间与内核空间关系_第12张图片
内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?
当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。如下图。
linux用户空间与内核空间关系_第13张图片

E:linux虚拟地址内核空间分布
linux用户空间与内核空间关系_第14张图片

1). 在kernel image下面有16M的内核空间用于DMA操作。位于内核空间高端的128M地址主要由3部分组成,分别为vmalloc area,持久化内核映射区,临时内核映射区。

2). 由于ZONE_NORMAL和内核线性空间存在直接映射关系,所以内核会将频繁使用的数据如kernel代码、GDT、IDT、PGD、mem_map数组等放在ZONE_NORMAL里。而将用户数据、页表(PT)等不常用数据放在ZONE_
HIGHMEM里,只在要访问这些数据时才建立映射关系(kmap())。比如,当内核要访问I/O设备存储空间时,就使用ioremap()将位于物理地址高端的mmio区内存映射到内核空间的vmalloc
area中,在使用完之后便断开映射关系。

F: x86的物理地址空间布局:
linux用户空间与内核空间关系_第15张图片

1. 物理地址空间的顶部以下一段空间,被PCI设备的I/O内存映射占据,它们的大小和布局由PCI规范所决定。640K~1M这段地址空间被BIOS和VGA适配器所占据。

2. Linux系统在初始化时,会根据实际的物理内存的大小,为每个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。进一步,针对不同的用途,Linux内核将所有的物理页面划分到3类内存管理区中,如图,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。

3. ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。

4. ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。

G:linux虚拟地址与物理地址映射的关系
linux用户空间与内核空间关系_第16张图片

Linux将4G的线性地址空间分为2部分,0~3G为user space,3G~4G为kernel space。

由于开启了分页机制,内核想要访问物理地址空间的话,必须先建立映射关系,然后通过虚拟地址来访问。为了能够访问所有的物理地址空间,就要将全部物理地址空间映射到1G的内核线性空间中,这显然不可能。于是,内核将0~896M的物理地址空间一对一映射到自己的线性地址空间中,这样它便可以随时访问ZONE_DMA和ZONE_NORMAL里的物理页面;此时内核剩下的128M线性地址空间不足以完全映射所有的ZONE_HIGHMEM,Linux采取了动态映射的方法,即按需的将ZONE_HIGHMEM里的物理页面映射到kernel space的最后128M线性地址空间里,使用完之后释放映射关系,以供其它物理页面映射。虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。

H: 进程的内存组织数据结构
内存管理中有三个重要的数据结构struc vm_area_struct,struct page和struct mm_struct,它们用于表示进程的内存使用,它们与进程的关系如图
linux用户空间与内核空间关系_第17张图片
linux用户空间与内核空间关系_第18张图片

每个进程都有一个struct mm_struct结构体,用来描述一个进程的虚拟内存。在结构体中包含了进程的页表和许多其他的大量信息。

vm_area_struct结构描述进程的一个虚拟内存地址区域。一个VMA(虚拟内存区域)是对页错误处理有同一规则的进程虚拟内存空间的部分,如共享库、运行区域等,代表进程空间的一块单独连续的地址空间。

page结构用来描述一个物理页,系统中每个物理页有一个页结构来保护跟踪。

VMA在/proc文件系统中的显示
VMA是Virtual Memory Aera的缩写,即内存虚拟区域。在每个进程中,一般分为这几个虚拟内存区域:程序的代码区域(即text段);每种类型的数据对应一个区域,其中包括初始化数据(在执行之处已经明确赋值的数据);未初始化的数据(BSS);程序栈等。

vma在程序运行中,不断地由申请、清除、查找、分割、融合等对vma的管理操作,所有vma存在一个双向链表中,另外还用AVL树进行管理,以加速查找。

一个进程的内存区域可以从/proc/pid/maps中看到,/proc/*/maps中的每项都与一个vm_area_struct结构成员对应,可以用一个列表对每个字段进行描述。
linux用户空间与内核空间关系_第19张图片

每行中的字段说明如下:

start-end 虚拟内存区域的起始和结束地址
perm虚拟内存区域的读、写和执行许可的位掩码。最后一个字符p表示是私有,s表示共享。
offset虚拟内存区域在被映射文件中的偏移(以页为单位)。
major:minor主设备号和次设备号。
Image被映射的文件名字
VMA的应用是在页面错误时产生调页或换页操作的。进程的所有VMA以一个排序的双向链表来连接,按虚地址的下降顺序来排列的,每个VMA对应一个相邻的地址空间范围。

大容量对象缓存,vmalloc函数分配的虚拟空间管理结构列出如下:
linux用户空间与内核空间关系_第20张图片
在mm/vmalloc.c中有vmlist链表的全局变量申明struct vm_struct *vmlist,在vmlist链表中,每个虚拟内存块之间都有个4KB大小的“隔离带”,用来检测访问指针的越界错误。第一个节点的地址就是VMALLOC_START。
linux用户空间与内核空间关系_第21张图片

函数vmalloc的功能是分配与size相匹配页面大小的连续虚拟内存,但对应的物理内存仍需经缺页中断后,由缺页中断服务程序分配,分配的物理页是不连续的。函数vmalloc申请大块缓冲区,但当前不用的内容不会调用入到物理内存中。

函数vmalloc列出如下:

void *vmalloc(unsigned long size)
{
    return __vmalloc(size,GFP_KERNEL|__GFP_HIGHMEM,PAGE_KERNEL);
}

函数__vmalloc分配足够的页数与size相配,把它们映射进连续的内核虚拟空间,但分配的内存块不一定连续。在函数中第一步是在vmlist中寻找一个大小合适的虚拟内存块(get_vm_area(size))。第二步检查这个虚拟块是否可用(空闲),建立页目录,找到空闲(虚拟块映射内)分配给调用进程(get_free_page());如果虚拟块是不可用的,那么必须要释放掉这个虚拟块(vfree)。

虚拟地址空间分配及其与物理内存对应图
linux用户空间与内核空间关系_第22张图片
伙伴算法:

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

SLAB算法:

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

VSS -Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
RSS -Resident Set Size 实际使用物理内存(包含共享库占用的内存)
PSS -Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
USS -Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

cat /proc/进程pid/maps :能否读取进程各个虚拟内存地址分段情况.
举例:
1044,1045,1054三个进程,每个进程都有一个页表,对应其虚拟地址如何向real memory上去转换.

process 1044的1,2,3都在虚拟地址空间,所以其VSS=1+2+3。

process 1044的4,5,6都在real memory上,所以其RSS=4+5+6。

分析real memory的具体瓜分情况:

4: libc代码段,1044,1045,1054三个进程都使用了libc的代码段,被三个进程分享。

5: bash shell的代码段,1044,1045都是bash shell,被两个进程分享。

6: 1044独占

所以,上图中4+5+6并不全是1044进程消耗的内存,因为4明显被3个进程指向,5明显被2个进程指向,衍生出了PSS(按比例计算的驻留内存)的概念。进程1044的PSS为4/3 +5/2 +6。

最后,进程1044独占且驻留的内存USS为 6。

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
linux用户空间与内核空间关系_第23张图片

linux 内存地址空间 Linux 内存管理全貌
linux用户空间与内核空间关系_第24张图片
内存地址——MMU 地址转换

MMU 是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件
分段机制把一个逻辑地址转换为线性地址
分页机制把一个线性地址转换为物理地址

linux用户空间与内核空间关系_第25张图片
在 IA32 上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在IA32上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。 但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让 Linux 具有更好的可移植性,我们需要去掉段机制而只使用分页机制。但不幸的是,IA32规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。万般无奈之下,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量<4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。看来,Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

用户进程访问内存分析

用户态进程独占虚拟地址空间,两个进程的虚拟地址可相同
在访问用户态虚拟地址空间时,如果没有映射物理地址,通过系统调用发出缺页异常
缺页异常陷入内核,分配物理地址空间,与用户态虚拟地址建立映射

两个不同的进程可以有相同的虚拟地址空间,cpu调度的时候。cr3寄存器中存储的不同进程的目录页是不同的。所以
不同进程即使虚拟地址空间一样。但是能够映射到不同的物理内存空间中.
linux用户空间与内核空间关系_第26张图片

你可能感兴趣的:(linux,内核)