Linux操作系统和驱动运行在内核空间,应用程序运行在用户空间。两者不能简单的使用指针来传递数据,因为Linux使用的虚拟内存机制,用户空间的数据可能会被换出,而内核空间使用用户空间指针时,对应的数据可能不在内存中。Linux内核地址映射模型x86CPU采用段页地址映射模型。进程代码中的地址是逻辑地址,经过段页地址映射后实际访问的是物理内存。段页机制如下图所示。Linux内核地址空间划分通常32位Linux内核地址空间分为0~3G作为用户空间,3~4G作为内核空间。注意这里是32位内核地址空间划分,和64位内核地址空间划分不同。Linux内核高端内存的由来内核模块代码或线程访问内存时,代码中的内存地址为逻辑地址,与真实的物理内存地址对应,一一地址映射需要,比如逻辑地址0xc0000003对应的物理地址是0×3,0xc0000004对应的物理地址是0×4,...,逻辑地址和物理地址的关系是物理地址=logicaladdress–0xC0000000假设遵循上面简单的地址映射关系,那么内核逻辑地址空间访问为0xc0000000~0xffffffff,则对应的物??理内存范围为0×0~0×40000000,即只有1G物理内存可以被访问。如果机器安装了8G的物理内存,内核只能访问前1G的物理内存,后面的7G的物理内存是访问不到的,因为内核的地址空间已经映射到了物理内存地址范围0×0~0×40000000。即使安装了8G物理内存,内核又该如何访问物理地址为0×40000001的内存呢?代码必须有内存逻辑地址,而0xc0000000~0xffffffff地址空间已经用完,所以不能访问物理地址0×40000000之后的内存。显然,内核地址空间0xc0000000~0xfffffff不能用于简单的地址映射。因此,x86架构将内核地址空间分为三部分:ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_HIGHMEM即高端内存,这就是内存高端内存概念的由来。在x86结构中,三种类型的区域如下:ZONE_DMA内存从16MB开始ZONE_NORMAL16MB~896MBZONE_HIGHMEM896MB~结束Linux内核对高端内存的理解前面我们解释了高端内存的由来。Linux将内核地址空间分为三部分,ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。高端内存HIGH_MEM地址空间范围从0xF8000000到0xFFFFFFFF(896MB到1024MB)。那么内核是如何利用128MB的高端内存地址空间来实现访问所有物理内存的呢?当内核要访问物理地址高于896MB的内存时,会在0xF8000000~0xFFFFFFFF地址空间范围内寻找对应大小的逻辑地址空间。借用一段时间。借用这个逻辑地址空间,创建一个到你要访问的物理内存的映射(即填充内核PTE页表),暂时使用,用完归还。这样其他人也可以使用这个地址空间访问其他的物理内存,实现了利用有限的地址空间访问所有的物理内存。如下所示。例如内核要访问一段从2G开始的物理内存,大小为1MB,即物理地址范围为0×80000000~0x800FFFFF。访问前先找一个1MB的空闲地址空间。假设找到的空闲地址空间为0xF8700000~0xF87FFFFF,用这1MB逻辑地址空间映射到物理地址空间0×80000000~0x800FFFFF中的内存。映射关系如下:内核访问0×80000000~0x800FFFFF物理内存后,释放0xF8700000~0xF87FFFFF内核的线性空间。这样其他进程或代码也可以使用地址0xF8700000~0xF87FFFFF访问其他物理内存。从上面的描述我们可以知道高端内存最基本的思想:借用一个地址空间,建立一个临时的地址映射,用完就释放,这样这个地址空间就可以循环使用,访问所有的物理内存.看到这里,有人不禁要问:如果内核进程或模块一直占用某个逻辑地址空间不释放怎么办?如果发生这种情况,内核的高端内存地址空间就会越来越紧张。如果它们都被占用而没有被释放,那么不映射到物理内存就无法访问它们。在香港尖沙咀的一些写字楼里,厕所很少,而且还有门锁。如果客人想去洗手间,可以到前台领取钥匙,方便后把钥匙还给前台。这样的厕所虽然只有一个,但是可以满足所有顾客如厕的需求。如果一个顾客一直占用浴室,不归还钥匙,那么其他顾客将无法使用浴室。Linux内核中高端内存管理的思路也是类似的。Linux内核中高端内存的划分内核将高端内存分为三部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。对于高端内存,可以通过alloc_page()或者其他函数获取对应的page,但是如果要访问实际的物理内存,就得把page转换成线性地址(为什么?想想MMU是怎么访问的physicalmemory),也就是我们需要为高端内存对应的page找一个线性空间。这个过程称为高端内存映射。对应高端内存的3部分,高端内存有3种映射方式:映射到“内核动态映射空间”(非连续内存分配)这种方式很简单,因为通过vmalloc(),在申请时“内核动态映射空间”中的内存,有可能从高端内存中获取页面(参见vmalloc的实现),所以高端内存可能会映射到“内核动态映射空间”。如果永久内核映射(permanentkernelmapping)通过alloc_page()获取到高端内存对应的page,如何为其寻找线性空间呢?内核为此专门预留了一段线性空间,从PKMAP_BASE到FIXADDR_START,用于映射高端内存。在2.6内核上,这个地址范围在4G-8M和4G-4M之间。这个空间称为“内核永久映射空间”或“永久内核映射空间”。该空间使用与其他空间相同的页目录表。对于内核,它是swapper_pg_dir。对于普通进程,它由CR3寄存器指向。正常情况下,这个空间大小为4M,所以只需要一个页表,内核使用pkmap_page_table来查找这个页表。通过kmap(),可以将一个页面映射到这个空间。由于这个空间大小为4M,最多可以同时映射1024个页面。所以,对于没有被使用的page,应该从这个空间中释放出来(即映射关系是unmapped),通过kunmap()可以将一个page对应的线性地址从这个空间中释放出来。临时内核映射内核在FIXADDR_START和FIXADDR_TOP之间保留一些线性空间以备特殊需要。这个空间称为“固定映射空间”。在这个空间中,有一部分用于高端内存的临时映射。该空间具有以下特点:(1)每个CPU占用一个空间;(2)每个CPU占用的空间被划分为多个小空间,每个小空间大小为1页。小空间用于一个目的,在kmap_types.h的km_type中定义。在执行临时映射时,需要指定映射的目的。根据映射的目的,找到对应的小空间,然后将这个空间的地址作为映射地址。这意味着临时映射会导致以前的映射被覆盖。可以使用kmap_atomic()实现临时映射。常见问题: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.(structmm_struct)mm->pgd中。每当一个进程被调度(schedule()),即将进入运行状态时,Linux内核必须使用进程的PGD指针来设置CR3(switch_mm())。(3)创建新进程时,必须为新进程创建新的页目录PGD,并将内核区间页目录项从内核页目录swapper_pg_dir复制到新创建的进程页目录PGD的对应位置。具体过程如下: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))这样每个进程的页目录就分为两部分,第一部分是“用户空间”,用来映射它的整个进程空间(0x00000000-0xBFFFFFFF),即3G字段的虚拟地址;第二部分是“系统空间”,用来映射(0xC0000000-0xFFFFFFFF)1G字节的虚拟地址。可以看出Linux系统中每个进程的页目录的第二部分是相同的,所以从进程的角度来看,每个进程都有4G字节的虚拟空间,低3G字节是自己的用户空间,最高1G字节是所有进程和内核共享的系统空间。(4)现在假设我们有以下场景:在进程A中,通过系统调用sethostname(constchar*name,seze_tlen)设置网络中计算机的“主机名”。在这种场景下,我们就必须涉及到从用户空间向内核空间传输数据的问题,name是用户空间中的地址,必须通过系统调用设置为内核中的地址。我们来看一下这个过程中的一些细节:系统调用的具体实现是将系统调用的参数存放在寄存器ebx、ecx、edx、esi、edi中(最多5个参数,这个场景有两个name和len),然后将系统调用号存入寄存器eax,然后通过中断指令“int80”使进程A进入系统空间。由于进程的CPU运行级别小于或等于为系统调用设置的陷阱门访问级别3,因此可以畅通无阻地进入系统空间执行为int80设置的函数指针system_call()。由于system_call()属于内核空间,其运行级别DPL为0,CPU需要将栈切换到内核栈,即进程A的系统空间栈。我们知道,当内核为a创建task_struct结构时new进程,分配两个连续的page,即8K的大小,task_struct使用底部1k左右的大小(如#definealloc_task_struct()((structtask_struct*)__get_free_pages(GFP_KERNEL,1))),剩下的内存用于系统空间的栈空间,即当用户空间转移到系统空间时,栈指针esp变为(alloc_task_struct()+8192),这就是为什么系统空间通常是使用宏定义curre的原因nt(见其实现)获取当前进程的task_struct地址。每次进程从用户空间进入系统空间,系统栈已经依次压入用户栈SS、用户栈指针ESP、EFLAGS、用户空间CS、EIP,然后system_call()将eax压入,以及然后调用SAVE_ALL依次按ES、DS、EAX、EBP、EDI、ESI、EDX、ECX、EBX,然后调用sys_call_table+4*%EAX,这个场景就是sys_sethostname()。(5)在sys_sethostname()中,经过一些保护的考虑,调用copy_from_user(to,from,n),其中to指向内核空间system_utsname.nodename,比如0xE625A000,from指向用户空间比如0x8010FE00。现在进程A进入内核,运行在系统空间。MMU根据自己的PGD完成虚拟地址到物理地址的映射,最终完成从用户空间到系统空间的数据拷贝。在准备拷贝之前,内核首先要判断用户空间地址和长度的合法性。至于从用户空间地址开始的一定长度的整个区间是否已经被映射,就不去检查了。如果区间内的某个地址没有被映射或者有读写权限时出现问题,则认为是坏地址,产生页面异常,由页面异常服务程序处理。过程如下:copy_from_user()->generic_copy_from_user()->access_ok()+__copy_user_zeroing()。(6)总结:*进程寻址空间0~4G*进程在用户态只能访问0~3G,只有进入内核态才能访问3G~4G*进程通过系统调用进入内核态*3G~4G部分每个进程的虚拟空间是一样的*进程从用户态进入内核态不会引起CR3的变化但是会引起栈的变化LinuxSimplified分段机制使得虚拟地址和线性地址始终一致。所以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中内核空间地址映射的描述和定义:它向上,很少有人需要*它。**0xC0000000的__PAGE_OFFSET意味着内核具有*1GB的虚拟地址空间,这将*您可以使用的物理内存量限制在950MB左右。**如果您想要比这更多的物理内存然后查看内核配置中的CONFIG_HIGHMEM4G*和CONFIG_HIGHMEM64G选项。*/#define__PAGE_OFFSET(0xC0000000)……#definePAGE_OFFSET((unsignedlong)__PAGE_OFFSET)#define__pa(ignedlong)((unsx)-PAGE_OFFSET)#define__va(x)((void*)((unsignedlong)(x)+PAGE_OFFSET))源码中的注释说明如果你的物理内存大于950MB,需要编译内核添加CONFIG_HIGHMEM4G和CONFIG_HIGHMEM64G选项,我们不会考虑这种情况。如果物理内存小于950MB,那么对于内核空间,给定一个虚拟地址x,它的物理地址就是“x-PAGE_OFFSET”,给定一个物理地址x,它的虚拟地址就是“x+PAGE_OFFSET”。再次强调一下,宏__pa()只是将内核空间的虚拟地址映射到物理地址,并没有应用到用户空间,用户空间的地址映射要复杂得多。2.内核镜像在下面的描述中,我们将内核的代码和数据称为内核镜像(kernelimage)。系统启动时,Linux内核镜像安装在物理地址0x00100000开头,即从1MB开始的区间(1M留作他用)。但是在正常运行时,整个内核镜像应该在虚拟内核空间中,所以链接器在链接内核镜像的时候给所有符号地址加上一个偏移量PAGE_OFFSET,使得内核镜像在内核空间的起始地址是0xC0100000。比如进程的页目录PGD(属于内核数据结构)就在内核空间。进程切换时,必须设置寄存器CR3指向新进程的页目录PGD,目录起始地址是内核空间的虚拟地址,而CR3需要的是物理地址,那么使用__pa()进行地址转换。mm_context.h中有这么一行语句:asmvolatile(“movl%0,%%cr3”::”r”(__pa(next->pgd));这是一行嵌入式汇编代码,意思是将下一个进程的页目录起始地址next_pgd通过__pa()转换为物理地址,存入寄存器,然后用mov指令写入CR3寄存器,处理完这行语句后,CR3指向了next新进程的页目录表是PGD
