支撑百万并发的“零拷贝”技术,你了解吗?( 二 )

在用户进程和物理内存(磁盘存储器)之间引入虚拟内存主要有以下的优点:

  • 地址空间:提供更大的地址空间,并且地址空间是连续的,使得程序编写、链接更加简单 。
  • 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其他进程造成影响 。
  • 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行等,增加了系统的安全性 。
  • 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件(可执行文件或动态库)到虚拟地址空间 。
这样可以做到物理内存延时分配,只有在需要读相应的文件的时候,才将它真正的从磁盘上加载到内存中来,而在内存吃紧的时候又可以将这部分内存清空掉,提高物理内存利用效率,并且所有这些对应用程序都是透明的 。
  • 共享内存:比如动态库只需要在内存中存储一份,然后将它映射到不同进程的虚拟地址空间中,让进程觉得自己独占了这个文件 。
进程间的内存共享也可以通过映射同一块物理内存到进程的不同虚拟地址空间来实现共享 。
  • 物理内存管理:物理地址空间全部由操作系统管理,进程无法直接分配和回收,从而系统可以更好的利用内存,平衡进程间对内存的需求 。
内核空间和用户空间
操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限 。
为了避免用户进程直接操作内核,保证内核安全,操作系统将虚拟内存划分为两部分,一部分是内核空间(Kernel-space),一部分是用户空间(User-space) 。
在 Linux 系统中,内核模块运行在内核空间,对应的进程处于内核态;而用户程序运行在用户空间,对应的进程处于用户态 。
内核进程和用户进程所占的虚拟内存比例是 1:3,而 Linux x86_32 系统的寻址空间(虚拟存储空间)为 4G(2 的 32 次方),将最高的 1G 的字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)供内核进程使用,称为内核空间 。
而较低的 3G 的字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个用户进程使用,称为用户空间 。
下图是一个进程的用户空间和内核空间的内存布局:
 
支撑百万并发的“零拷贝”技术,你了解吗?

文章插图
 
 
内核空间
内核空间总是驻留在内存中,它是为操作系统的内核保留的 。应用程序是不允许直接在该区域进行读写或直接调用内核代码定义的函数的 。
上图左侧区域为内核进程对应的虚拟内存,按访问权限可以分为进程私有和进程共享两块区域:
  • 进程私有的虚拟内存:每个进程都有单独的内核栈、页表、task 结构以及 mem_map 结构等 。
  • 进程共享的虚拟内存:属于所有进程共享的内存区域,包括物理存储器、内核数据和内核代码区域 。
用户空间
每个普通的用户进程都有一个单独的用户空间,处于用户态的进程不能访问内核空间中的数据,也不能直接调用内核函数的 ,因此要进行系统调用的时候,就要将进程切换到内核态才行 。
用户空间包括以下几个内存区域:
  • 运行时栈:由编译器自动释放,存放函数的参数值,局部变量和方法返回值等 。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存储到栈顶,调用结束后调用信息会被弹出并释放掉内存 。
栈区是从高地址位向低地址位增长的,是一块连续的内在区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小 。
  • 运行时堆:用于存放进程运行中被动态分配的内存段,位于 BSS 和栈中间的地址位 。由卡发人员申请分配(malloc)和释放(free) 。堆是从低地址位向高地址位增长,采用链式存储结构 。
频繁地 malloc/free 造成内存空间的不连续,产生大量碎片 。当申请堆空间时,库函数按照一定的算法搜索可用的足够大的空间 。因此堆的效率比栈要低的多 。