CPU Load Balancing

所谓CPU负载均衡就是将进程从繁忙的CPU迁移到比较空闲的CPU上,目的是为了提高系统的整体效率。

进程在不同CPU之间迁移通常会影响cache的命中率,例如:

  1. 支持超线程技术(SMT)的CPU内部的虚CPU之间完全共享cache,在它们之间迁移进程对cache几乎没有影响,所以尽量保证它们之间的进程数均衡。

  2. 单个CPU的多个核有各自的L1缓存,但共享L2和L3缓存,在这些核之间迁移进程会导致L1缓存的失效,但是L2和L3缓存不受影响。

  3. 多CPU,每个CPU有各自独享的缓存,在这些CPU之间迁移进程会导致缓存失效。

  4. 多CPU,多内存节点(NUMA),不同CPU对于不同内存结点访问的效率不同,在这些CPU之间迁移进程不仅会影响缓存,而且会出现内存访问效率的问题。

  5. 多CPU+NUMA组成的阵列。

内核根据迁移进程所造成的影响情况设置了不同的调度域(sched_domain),最小一级的调度域是SMT的情况,SMT中的多个虚拟CPU位于同一调度域内;次小一级是单个CPU的多个核的情况,这些核位于同一调度域内;往上是多个CPU的情况以及多CPU+NUMA的情况,最上面就是阵列情况,自下而上所有的sched_domain形成一棵树形结构,越往上迁移进程导致的影响越大,所以越往上内核会增大迁移的阻力。


Virtual Memory

Linux内核对整个系统的物理内存是通过类型为struct page的数组mem_map来管理的。

Linux的内核地址空间大小为1G 用户空间0~3G,内核空间3G~4G。

如果把这1G线性地址空间全部拿来直接一一映射物理内存的话,在内核态的所有进程能使用的物理内存总共最多只有1G,为了能使在内核态的所有进程能使用更多的物理内存,Linux采取了一种变通的形式:它将1G内核线性地址空间分为几部分,第一部分为1G的前896M,这部分内核线性空间与物理内存的0~896M一一映射,后面128M的线性空间拿来动态映射剩下的所有物理内存,由于动态映射的方法不一样,后面的128M又分成了几个部分。

前面896M线性空间对应的物理内存就是所谓的低端物理内存,剩下的物理内存就是高端物理内存。

进程中使用的所有地址都是虚地址,在linux下这个虚地址就是所谓的线性地址。Linux中进程可运行在用户态和内核态,(典型配置情况下)当进程运行在用户态时,它使用的线性地址只能位于0~3G范围内,当进程运行于内核态时,它使用的线性地址地址范围为3G~4G。

为了把线性地址转化为物理地址,每个进程都有自己私有的页目录和页表。

Linux在建立进程页目录时,把用户地址空间的页目录项(0~767项)清空而将内核页目录表(swapper_pg_dir)的第768项到1023项拷贝到进程的页目录表的第768项到1023项中。

由于内核在初始化时也只映射了物理内存的前896M,我们可以知道内核也目录表只能保证第768项开始的224项中有有效映射。从这里我们可以知道,所有的进程都共享了其内核线性地址空间。

当一个进程在内核空间发生缺页故障的时候,这主要发生在访问内核空间动态映射区线性地址,在其处理程序中,就要通过0号进程的页目录(swapper_pg_dir)来同步本进程的内核页目录,实际上就是拷贝0号进程的内核页目录到本进程中(内核页表与进程0共享,故不需要复制)。如果进程0的该地址处的内核页目录也不存在,则出错,具体代码可以参考vmalloc的实现源码。

当进程运行于用户态时,若其需要申请内存空间,内核首先会在其用户线性空间中分配需要的线性地址空间,再通过伙伴分配系统分配物理内存并把分配的物理内存跟用户空间线性地址映射起来,最后再修改进程的页目录项及页表项写入这些映射关系。


IO

文件描述符用于表述指向文件的引用的抽象化概念。

阻塞 I/O(blocking IO)

Linux 中默认情况下所有的socket都是blocking。

用户进程调用了recvfrom这个系统调用,Linux Kernel就开始了IO的第一个阶段:准备数据。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。当Kernel一直等到数据准备好了,它就会将数据从Kernel中拷贝到用户内存,Kernel返回结果,用户进程解除block状态,重新运行起来。

blocking IO

Linux可以通过设置socket为non-blocking。当对一个non-blocking socket执行读操作时,流程如下:

当用户进程发出Read操作时,如果Kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

epoll是Linux内核的可扩展I/O事件通知机制。设计目的旨在取代既有POSIX select与poll系统函数,让需要大量操作文件描述符的程序得以发挥更优异的性能。epoll 实现的功能与 poll 类似,都是监听多个文件描述符上的事件。epoll 通过使用红黑树搜索被监控的文件描述符。



Reference:

  1. wiki epoll
  2. Linux IO模式及 select、poll、epoll详解
  3. 使用 Linux 系统调用的内核命令