Linux用户空间与内核空间 linux 用户空间
各位老铁们,大家好,今天由我来为大家同享Linux用户空间和内核空间(了解高级内存),以及的相关难题姿势,希望对大家有所帮助。如果可以帮助到大家,还望关注收藏下本站,无论兄弟们的支持是大家最大的动力,谢谢大家了哈,下面大家开始吧!
段页机制如下图。
Linux内核地址空间划分
通常32位Linux内核地址空间分为0~3G作为用户空间,3~4G作为内核空间。注意,这是32位内核地址空间划分,64位内核地址空间划分不同。
Linux内核高级内存的由来
当内核模块代码或线程访问内存时,代码中的内存地址都是逻辑地址,和真正的物理内存地址对应,需要进行地址的一一映射。例如逻辑地址0xc0000003对应的物理地址为03,0xc0000004对应的物理地址为04,……,逻辑地址和物理地址的关系为
物理地址=逻辑地址0xC0000000
假设根据上面简单的地址映射关系,内核逻辑地址空间访问为0xc0000000 ~0xffffffff,那么对应的物理内存范围为00 ~ 040000000,即只能访问1G物理内存。如果机器配置了8G物理内存,内核只能访问前1G物理内存,后面的7G物理内存将无法访问,由于内核的地址空间已经全部映射到物理内存地址范围0 0 ~ 040000000。即使配置了8G物理内存,内核怎样访问物理地址040000001的内存呢?代码必须有壹个内存逻辑地址。0xc0000000到0xffffffff的地址空间已经被用完,因此物理地址040000000之后的内存无法访问。
显然,内核地址空间0xc0000000 ~0xffffffff不能用于简单的地址映射。因此,x86架构将内核地址空间分为三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。 ZONE_HIGHMEM是高级内存,这就是高级内存概念的由来。
在x86架构中,三种类型的区域如下:
ZONE_DMA内存从16MB开始
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~ 结束
Linux内核高级内存的领会
前面大家解释了高级内存的由来。 Linux将内核地址空间分为三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。高级内存HIGH_MEM地址空间范围为0xF8000000到0xFFFFFFFF(896MB到1024MB)。那么内核是怎样利用128MB高级内存地址空间来访问全部物理内存的呢?
当内核要访问物理地址高于896MB的内存时,它会在0xF8000000 ~0xFFFFFFFF地址空间范围内找到相应大致的空闲逻辑地址空间,并借用一段时刻。借用这个逻辑地址空间,并将其映射到想要访问的物理内存(即填写内核PTE页表)。暂时运用一段时刻,用完后归还。这样,其他人也可以借用这个地址空间来访问其他物理内存,实现利用有限的地址空间来访问全部物理内存。如下图所示。
例如,内核要访问从2G开始的1MB物理内存,即物理地址范围为080000000 ~0x800FFFFF。访问之前,首先找到一块1MB的空闲地址空间。假设找到的空闲地址空间为0xF8700000 ~0xF87FFFFF。运用这1MB逻辑地址空间映射到物理地址空间080000000 ~0x800FFFFF中的内存。映射关系如下:
内核访问080000000 ~0x800FFFFF物理内存后,释放0xF8700000 ~0xF87FFFFF内核线性空间。这样,其他进程或代码也可以运用地址0xF8700000 ~0xF87FFFFF来访问其他物理内存。
从上面的描述大家可以了解高级内存最基本的想法:借用一段地址空间,建立临时地址映射,运用完后释放。当到达这个地址空间时,就可以回收并访问全部物理内存。
看到这里,有人不禁要问:如果壹个内核进程或模块一直占用某个逻辑地址空间而不释放如何办?如果这种情况真的发生,内核的高级内存地址空间将会变得越来越紧张。如果被占用而不释放,即使没有映射到物理内存,也无法访问。
在香港尖沙咀的一些写字楼里,厕所和门锁很少。顾客如果想去卫生间,可以给前台领取钥匙,运用后将钥匙归还给前台。这样,虽然只有一间卫生间,但可以满足全部顾客上厕所的需求。如果顾客继续占用卫生间且不归还钥匙,其他顾客将无法运用卫生间。 Linux内核中高级内存管理的思路也类似。
Linux内核高级内存的划分
内核将高级内存分为3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。
对于高级内存,可以通过alloc_page()或者其他函数获取对应的页。然而,如果要访问实际的物理内存,就必须将页转换成线性地址(何故呢?想想MMU是怎样访问物理内存的)。也就是说,大家需要为高级内存对应的页找到壹个线性空间。这个经过称为高级内存映射。
对应高级内存的三部分,高级内存映射有三种方法:
映射到“内核动态映射空间”(非连续内存分配)
这种方式很简单,由于通过vmalloc(),在“内核动态映射空间”申请内存时,有也许从高级内存中获取页面(参见vmalloc的实现),因此有也许高-end内存被映射到“内核动态映射空间”“空间”。
永久内核映射
如果通过alloc_page()获取高级内存对应的页,怎样为其找到线性空间呢?
内核为此专门预留了壹个线性空间,从PKMAP_BASE到FIXADDR_START,用于映射高级内存。在2.6内核上,这个地址范围在4G-8M和4G-4M之间。这个空间称为“内核永久映射空间”或“永久内核映射空间”。该空间运用和其他空间相同的页目录表。对于内核来说,它是swapper_pg_dir。对于普通进程,由CR3寄存器指给。通常情况下,该空间大致为4M,因此只需要一张页表。内核通过pkmap_page_table来查找这个页表。通过kmap(),可以将壹个页面映射到这个空间。由于这个空间大致为4M,因此最多可以同时映射1024个页面。因此,未运用的页面应及时从该空间中释放(即释放映射关系)。通过kunmap(),可以从这个空间释放壹个页面对应的线性地址。
临时内核映射
内核在FIXADDR_START和FIXADDR_TOP之间保留了一些线性空间以供独特需要。这个空间称为“固定映射空间”。在这个空间中,有一部分用于高级内存的临时映射。
这个空间有下面内容多少特征:
(1)每个CPU占用一块空间
(2)将每个CPU占用的空间划分为多个小空间。每个小空间的大致为1 页。每个小空间都有其用途。这些用途在kmap_types.h km_type 中定义。
当需要进行临时映射时,需要指定映射的目的。根据映射目的,可以找到对应的小空间,接着将该空间的地址作为映射地址。这意味着临时映射将导致先前的映射被覆盖。临时映射是通过kmap_atomic()实现的。
可以参考:Linux高级内存映射等。
常见难题:
1、用户空间(进程)有高级内存的概念吗?
用户进程没有高级内存的概念。高级内存仅存在于内核空间中。用户进程最多只能访问3G物理内存,而内核进程可以访问全部物理内存。
2. 64位内核有高级内存吗?
目前的现实情况是,高级内存在64位Linux内核中并不存在,由于64位内核可以支持超过512GB的内存。如果机器上配置的物理内存超过了内核地址空间,就会出现高级内存。
3. 用户进程可以访问几许物理内存?内核代码可以访问几许物理内存?
在32位体系上,用户进程最多可以访问3GB,内核代码可以访问全部物理内存。
在64位体系上,用户进程最多可以访问超过512GB,内核代码可以访问全部物理内存。
4、高级内存和物理地址、逻辑地址、线性地址的关系是啥子?
高级内存只和逻辑地址有关,和逻辑地址和物理地址没有直接关系。
5. 何故不将全部地址空间分配给内核?
如果全部的地址空间都给了内存,那么用户进程怎样运用内存呢?怎样保证内核运用的内存不和用户进程冲突?
(1) 大家忽略Linux对分段内存映射的支持。在保护玩法下,大家了解,无论CPU运行在用户态还是内核态,CPU执行程序访问的地址都是虚拟地址。 MMU必须读取控制寄存器CR3中的值作为当前页目录的指针,接着根据分页内存映射机制(参见相关文档)将虚拟地址转换为真正的物理地址,以便CPU实际上可以访问物理地址。
(2)对于32位Linux来说,每个进程都有4G的寻址空间,然而当壹个进程访问其虚拟内存空间中的某个地址时,怎样才能不和其他进程的虚拟空间混淆呢?每个进程都有自己的页目录PGD,Linux将这个目录的指针存储在进程对应的内存结构task_struct.(struct mm_struct)mm-pgd中。每当壹个进程被调度(schedule())并马上进入运行情形时,Linux内核就会用该进程的PGD指针配置CR3(switch_mm())。
do_fork() --copy_mm() --mm_init() --pgd_alloc() --set_pgd_fast() --get_pgd_slow() --memcpy(PGD + USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t) )
这样,每个进程的页目录就被分为两部分。第一部分是“用户空间”,用于映射其整个进程空间(0x0000 0000-0xBFFF FFFF),一个3G字节的虚拟地址;第二部分是“体系空间”,用于映射(0xC000 0000-0xFFFF FFFF)1G字节虚拟地址。可以看出,Linux体系中每个进程的页目录第二部分都是相同的,因此从进程的角度来看,每个进程都有4G字节的虚拟空间,下面的3G字节是自己的用户空间,顶尖1G字节是全部进程和内核共享的体系空间。
(4) 现在假设大家有下面内容场景:
在进程A中,通过体系调用sethostname(const char *name, seze_t len)配置计算机在网络中的“主机名”。
在这种场景下,大家必然会涉及到将数据从用户空间传输到内核空间的难题。 name是用户空间中的地址,必须通过体系调用将其配置为内核中的地址。大家看一下这个经过中的一些细节: 体系调用的具体实现是将体系调用的参数存放在寄存器ebx、ecx、edx、esi、edi中(最多5个参数,这个场景有两个name和len) ),接着将体系调用号存放在寄存器eax中,接着通过中断指令“int 80”使进程A进入体系空间。由于进程的CPU运行级别小于等于体系调用配置的陷门访问级别3,因此可以畅通无阻地进入体系空间执行为int 80配置的函数指针system_call()。 ()属于内核空间,其运行级别DPL为0,CPU需要将栈切换到内核栈,也就是进程A的体系空间栈。大家了解,当内核为壹个新的进程创建task_struct结构体时进程中,它分配两个连续的page,大致为8K,并运用底部约1k的大致作为task_struct(如#define alloc_task_struct() ((struct task_struct *) __get_free_pages( GFP_KERNEL,1))),而其余的的内存用于体系空间的堆栈空间,即从用户空间转移到体系空间时,堆栈指针esp变成(alloc_task_struct()+8192),这就是何故体系空间通常运用宏的缘故定义current(参见其实现)用于获取当前进程的task_struct地址。进程每次从用户空间进入体系空间时,体系栈已经被压入用户栈SS、用户栈指针ESP、EFLAGS、用户空间反恐精英、EIP,接着system_call()将eax压入,接着调用SAVE_ALL压入依次ES、DS、EAX、EBP、EDI、ESI、EDX、ECX、EBX,接着调用sys_call_table+4*%EAX,本例为sys_sethostname()。
小结
进程寻址空间0~4G 进程在用户态只能访问0~3G,3G~4G只能在进入内核态时访问。每个进程通过体系调用进入内核态时虚拟空间的3G~4G部分是相同的。当进程从用户态进入内核态时,不会引起CR3的变化,但会引起堆栈的变化。 Linux简化了分段机制,使得虚拟地址和线性地址始终保持一致。因此,Linux的虚拟地址空间也是0~4G。 Linux内核将这4G字节空间分为两部分。顶尖的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)由内核运用,称为“内核空间”。低3G字节(从虚拟地址0x00000000到0xBFFFFFFF)被每个进程运用,称为“用户空间”。由于每个进程都可以通过体系调用进入内核,因此Linux内核是由体系中全部进程共享的。因此,从具体进程来看,每个进程可以拥有4G字节的虚拟空间。
Linux采用两级保护机制:0级针对内核,3级针对用户程序。从图中可以看出(这里无法表示图),每个进程都有自己的私有用户空间(0~3G),对于体系中的其他进程是不可见的。虚拟内核空间的顶部1GB 由全部进程和内核共享。
1虚拟内核空间到物理空间的映射
内核代码和数据存储在内核空间中,而用户程序代码和数据存储在进程的用户空间中。无论是内核空间还是用户空间,它们都在虚拟空间中。读者也许会问,体系启动时,内核代码和数据不是加载到物理内存中的吗?何故它们也在虚拟内存中?这和编译器有关,稍后大家会通过详细讨论来领会这一点。
虽然内核空间占据了每个虚拟空间的顶尖1GB字节,但到物理内存的映射总是从最低地址(0x00000000)开始。对于内核空间来说,它的地址映射是一种特别简单的线性映射。0xC0000000是物理地址和线性地址之间的位移,在Linux代码中称为PAGE_OFFSET。
大家看一下include/asm/i386/page.h中关于内核空间地址映射的描述和定义:
源码中的注释指出,如果你的物理内存大于950MB,则需要在编译内核时添加CONFIG_HIGHMEM4G和CONFIG_HIGHMEM64G选项。大家暂时不思考这种情况。如果物理内存小于950MB,那么对于内核空间来说,给定壹个虚拟地址x,它的物理地址为“x- PAGE_OFFSET”,给定壹个物理地址x,它的虚拟地址为“x+ PAGE_OFFSET”。
再次强调,宏__pa() 仅将内核空间中的虚拟地址映射到物理地址,绝不适用于用户空间。用户空间中的地址映射要复杂得多。
2内核映像
在下面的描述中,大家将内核代码和数据称为内核映像。体系启动时,Linux内核映像配置在物理地址0x00100000开始处,即从1MB开始的区间(这1M保留作其他用途)。然而,在正常操作期间,整个内核映像应该位于虚拟内核空间中。因此,在链接内核映像时,链接器会给全部符号地址添加偏移量PAGE_OFFSET。这样,内核映像就从内核空间的开头开始。地址是0xC0100000。
例如,进程的页目录PGD(一种内核数据结构)位于内核空间中。当切换进程时,必须配置寄存器CR3指给新进程的页目录PGD。该目录的起始地址是内核空间中的虚拟地址,但CR3需要物理地址。在这种情况下,必须运用__pa。 () 执行地址转换。 mm_context.h中有这么一行语句:
这是一行嵌入的汇编代码。其含义是通过__pa()将下壹个进程的页目录next_pgd的起始地址转换为物理地址,存储到某个寄存器中,接着运用mov指令写入CR3寄存器。中间。处理完这行语句后,CR3接下来指给新进程的页目录表PGD。