人间还是仙界?聊一聊linux系统的用户空间和内核空间
以32 位Linux系统为例,虚拟地址的大小是4GB (0x0000_0000 ~ 0xffff_ffff)。
Linux 用户空间和 内核空间的大小可以通过设置宏 PAGE_OFFSET 来配置,默认PAGE_OFFSET = 0xc000_0000。
当PAGE_OFFSET = 0xc000_0000 时用户空间为 0-3G,内核空间为3-4G。
应用进程地址空间:
在Linux 中一个进程的空间大小就是4GB,0-3G 是用户空间,3-4G 是内核空间。
这3G 的虚拟空间就包含代码段、数据段、bss 段、堆栈等等。
内核使用task_struct 来描述一个用户进程,每个进程都需要分配内存空间资源 用task_struct->mm_struct来描述一个进程的内存资源。
mm_struct->vm_area_struct 用来描述进程中各部分的虚拟地址空间(代码段、数据段等等);mm_struct->pgd 用来描述虚拟地址与物理地址之间的关系。
不同进程之间的内存资源是不可共享的,因为它们都有自己唯一的页表,mm_struct->pgd 表示一个进程的页目录表地址,通过页表就可以找到对应的内存物理地址。所以有时候发现不同进程使用相同的虚拟地址,依然能正常工作,这是因为它们页表中实际映射的物理地址不同。(因为如此不同进程之间通信要使用进程间通信)
其实并不是说一个进程真的拥有 4GB 的内存空间,内核对进程屏蔽了内存空间的管理,表面上给他画出4个G 的大饼,但是只有在进程真正访问一个内存地址的时候,内核才会将这个虚拟地址映射到实际存在的物理地址上。进程用到一点就给他分配一点,所以内核实际上是将物理内存东一块、西一块的分配给不同进程。
内核空间:内核中所有的线程,共享1GB 内核空间,因此内核中不同模块之间通信只需要全局变量即可。
用户进程也可以通过调用c库中的函数,执行系统调用产生0x80 号中断来陷入内核空间。
Linux为什么要划分用户空间 和 内核空间?
① 处理器模式不同、权限不同
对于x86 体系的CPU,用户空间的代码运行在Ring3 模式,内核空间的代码运行在Ring0 模式;
对于 arm体系的CPU,用户空间的代码运行在usr 模式,内核空间的代码运行在svc 模式;
②安全考虑
内核空间主要用于内核,内核负责管理系统中的各种资源,CPU资源、内存资源和外设资源等等。
Linux是多用户、多进程的系统,用户进程访问这些资源是受限的,空间隔离可以保证系统内核的稳定性,即使一个用户进程奔溃,或是恶意的破坏也不会导致内核奔溃。
③软件设计思想
内核代码偏向于系统管理,用户空间代码偏向于业务逻辑实现,二者分工不同更利于管理。
在程序中 CPU 使用的是虚拟地址,需要通过MMU 的转换得到物理地址,在总线上发出物理地址才能访问到内存。
MMU:地址映射单元,完成虚拟地址到物理地址转换的计算。
TLB:页表缓存,是一个高速ram。由于在地址换算过程中MMU 需要经常访问页表,那么如果把页表放在相对低速的内存中,那么转换效率将会变得低下。
linux内核页表映射机制:线性地址如何转为物理地址?
二级页表映射:
以32位 系统为例虚拟地址的大小一共是4GB (0x0000_0000 ~ 0xffff_ffff)。
在程序中想要表达一个虚拟地址就是 用32位长度,比如0xffff_0011,要完成对内存上的数据的访问就要做到 虚拟地址–> 线性地址–> 物理地址的转换,物理地址才是真实存放数据的地址。
在Linux 内核中虚拟地址与线性地址的值是相等的。
下图是一个二级页表的结构图:
线性地址由页目录表索引、页表索引和页表偏移组成,一共32位。
Linux 将一个物理页大小设置为4KB,那么一个4GB 的物理内存就有 4GB / 4KB = 1024* 1024页;
页目录表(Page directory)用来存放页表地址,每一个地址条目的大小4 byte,一个页目录表一个共可以描述1024个页表;
页表(page Table)用来描述一个物理页的基地址,每一个条目需要大小为4 bype,一个页表可以描述1024个页地址;
(1024项页表 x 1024条页地址 x 4KB(页大小) = 4GB )如此刚好能描述4GB 的地址大小。
虚拟地址到物理地址的转换过程(页表映射):
① 从寄存器CR3 得到页目录表基地址。
② 取出线性地址高 10位 作为页目录表索引,通过索引找到页目录表中对应的页表项,得到页表基地址。
③ 取出线性地址中间 10位 作为页表索引,通过页表索引找到页表中描述的物理页项,得到页基地址。
④ 取出线性地址低 12位 作为物理页中的偏移地址,页地址 + 偏移地址 = 物理地址。
每一个进程都有自己唯一的页表,Linux 内核使用mm_struct->pgd 来描述一个进程的页目录表地址,在切换进程时只需要将pgd 存入寄存器CR3 即可完成进程页表的切换。
ARMv8 使用的是4级页表映射,4级页表映射参考
4级页表与2级页表的基本原理是一样的,只不过多了几层:全局页目录(pgd)、上级页目录(pud)、中间页目录(pmd)和页表(pte)。
页的大小还是相同的 4KB,但是多级页目录能描述的地址空间也就更广泛了。
一个pgd 一共能描述512项pud,一个pud 一功能描述512项pmd,一个pmd 一共能描述512项pte,一页大小4KB。
(512 x 512 x 512 x4KB = 256TB) 目前64位系统的寻址能力就是256TB。
使用参考链接中的测试代码可以查看ARMv8 的页表映射情况、和虚拟地址到物理地址的换算:
下图是进程hello 中变量a 地址的虚拟地址->物理地址转换。
ARMv8 对pgd_t、pud_t、pmd_t、pte_t 等类型定义在:arch\arm64\include\asm\pgtable-types.h (不同架构定义不同)