一:进程

linux schedule

- 当我谈 scheduling 时我在谈什么?

- 谈谈调度 - Linux O(1)

二:内存

2.1 地址空间

2.1.1 Linux进程地址空间

Linux虚拟内存空间

  1. 代码段 .txt:编译后的机器指令,内存共享;

  2. 数据段 .data:初始化了的全局变量和局部静态变量;

  3. BSS段 .bss:未初始化的全局变量和局部静态变量;(记录大小总和,预留位置,文件中不占空间)

    BSS段在目标文件和可执行文件中不占用空间,但是在装载时占用地址空间,链接时分配虚拟空间。

  4. Heap:程序动态分配的内存区域;

    程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块堆空间,具体来讲,管理堆空间分配的往往是程序的运行时库。

    运行库相当于是向操作系统批发了一块较大的堆空间,然后零售给程序用。

    mmap()作用就是向操作系统申请一段虚拟地址空间,这块虚拟地址空间可以映射到某个文件(最初的作用),如果不将地址映射到某个文件时,这块空间为匿名空间,可以拿来作为堆空间。

    malloc()处理用户的空间请求:对于小于128KB的请求时,在现有堆空间中分配;对于大于128KB的请求时,会使用mmap()函数分配一块匿名空间,然后在这个匿名空间中为用户分配空间。

  5. 动态链接库映射区:用于映射装载的动态链接库;

  6. Stack:维护函数调用的上下文;

  7. 内核区

2.1.2 常见问题

  • 位与寻址空间的关系:为什么32位是4GB?

    请注意32是“地址”总线的宽度,即可以寻址的地址个数是2的32次方个。

    而计算机中以字节位单位存储和解释信息,每个地址(即每个门牌号)指示的空间是一个字节B(即每个门牌号指示的房间大小是一个字节B),那么就是2的32次方个B的房间

    32位机器,物理空间有4GB,但是计算机只装了512MB的内存,那么物理地址的真正有效部分知识一部分地址,0X00000000~0x1FFFFFFF,其他部分是无效的。

2.2 地址翻译

  1. 相关术语

    • MMU(memory management unit):CPU芯片上的硬件,利用存放在内存中的页表来动态翻译虚拟地址。

    • PTE(page table entry):页表条目,有有效位判断是否有物理页分配。

    • TLB( translation lookaside buffer):快表,PTE的缓存,位于MMU。

  2. 页面命中时,CPU硬件的执行步骤。

    页面命中

    页面命中过程

  3. 缺页时,要求硬件和操作系统内核协作完成。

    缺页

    缺页过程

  4. Linux缺页异常处理

深入理解操作系统 P555

2.3 内存分配和回收

  1. 内存分配的原理

    从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:brk和mmap(不考虑共享内存)。

    • brk是将数据段(.data)的最高地址指针_edata往高地址推;

    • mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。

    这两种方式分配的都是虚拟内存,没有分配物理内存在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

  2. Malloc函数实现原理:在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk,mmap,munmap这些系统调用实现的。参考

    • malloc小于128k的内存,使用brk分配内存。将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化)。
    • malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配(对应独立内存,而且初始化为0)

    优缺点:

    • brk() 方式的缓存,可以减少缺页异常的发生,提高内存访问效率。不过,由于这些内存没有归还系统,在内存工作繁忙时,频繁的内存分配和释放会造成内存碎片。
    • 而 mmap() 方式分配的内存,会在释放时直接归还系统,所以每次mmap都会发生缺页异常。在内存工作繁忙时,频繁的内存分配会导致大量的缺页异常,使内核的管理负担增大。这也是malloc只对大块内存使用mmap的原因。
  3. Windows内存管理和linux内存管理

三:IO

3.1 Unix下5种IO模型

  1. 阻塞I/O

  2. 非阻塞I/O

  3. I/O复用(select和poll不知道哪个连接产生事件需要挨个去问,epoll知道哪个连接有I/O流事件产生,然后处理这个连接)

  4. 信号驱动I/O(SIGIO)

  5. 异步I/O(POSIX.1的aio_系列函数)

Linux高性能服务器编程

同步IO:用户自行执行IO操作,将数据从内核缓冲区读入用户缓冲区;

异步IO:内核来执行IO操作,数据在内核缓冲区和用户缓冲区之间的移动是由内核来完成的;

阻塞IO:因为无法立即执行,而被操作系统刮起,直到等待的时间发生;

非阻塞IO:执行的系统调用总是立即返回,而不管事件是否已经发生;

3.2 Linux IO/文件访问方式

  1. 缓存IO

    当应用程序调用 read() 系统调用读取一块数据的时候,如果该块数据已经在内存中了,那么就直接从内存中读出该数据并返回给应用程序;如果该块数据不在内存中,那么数据会被从磁盘上读到页高缓存中去,然后再从页缓存中拷贝到用户地址空间中去。如果一个进程读取某个文件,那么其他进程就都不可以读取或者更改该文件;

    对于写数据操作来说,当一个进程调用了 write() 系统调用往某个文件中写数据的时候,数据会先从用户地址空间拷贝到操作系统内核地址空间的页缓存中去,然后才被写到磁盘上。

    • 同步IO

      如果用户采用的是同步写机制( synchronous writes ), 那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;

    • 标准IO

      如果用户采用的是延迟写机制( deferred writes ),那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了。

  2. 内存映射方式:实现方式mmap

    所谓内存映射文件,就是将文件映射到内存,文件对应于内存中的一个字节数组,对文件的操作变为对这个字节数组的操作,而字节数组的操作直接映射到文件上。这种映射可以是映射文件全部区域,也可以是只映射一部分区域。

  3. 直接IO

    数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,完全不需要页缓存的支持。操作系统层提供的缓存往往会使应用程序在读写数据的时候获得更好的性能,但是对于某些特殊的应用程序,比如说数据库管理系统这类应用,他们更倾向于选择他们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。

零拷贝:zero copy

https://mp.weixin.qq.com/s/-rzyDpckS9CUQblAUac0FQ

3.3 高并发IO:select/poll/epoll

  1. select

    用一个 fd_set 结构体来告诉内核同时监控多个文件句柄,当其中有文件句柄的状态发生指定变化(例如某句柄由不可用变为可用)或超时,则调用返回。之后应用可以使用 FD_ISSET 来逐个查看是哪个文件句柄的状态发生了变化。这样做,小规模的连接问题不大,但当连接数很多(文件句柄个数很多)的时候,逐个检查状态就很慢了。因此,select 往往存在管理的句柄上限(FD_SETSIZE)。同时,在使用上,因为只有一个字段记录关注和发生事件,每次调用之前要重新初始化 fd_set 结构体。

    缺点:句柄上限+重复初始化+逐个排查所有文件句柄状态效率不高。

  2. poll

    poll 主要解决 select 的前两个问题:通过一个 pollfd 数组向内核传递需要关注的事件消除文件句柄上限,同时使用不同字段分别标注关注事件和发生事件,来避免重复初始化。

    缺点:逐个排查所有文件句柄状态效率不高。

  3. epoll

    既然逐个排查所有文件句柄状态效率不高,很自然的,如果调用返回的时候只给应用提供发生了状态变化(很可能是数据 ready)的文件句柄,进行排查的效率不就高多了么。epoll 采用了这种设计,适用于大规模的应用场景。

    执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。

    内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。

    epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。

    问题归纳:依赖特定平台(Linux)。

  4. libevent

    由于epoll, kqueue, IOCP每个接口都有自己的特点,程序移植非常困难,于是需要对这些接口进行封装,以让它们易于使用和移植,其中libevent库就是其中之一。跨平台,封装底层平台的调用,提供统一的 API,但底层在不同平台上自动选择合适的调用。

  5. Select/poll/epoll对比

    • select有句柄上限,而poll没有上限原因?

    ​ undefinedselect使用位域的方式来传递关心的文件描述符,位域就有最大长度,在Unix下是256,在Linux下是1024,好像可以调大,但是不方便;

    ​ undefinedpoll采用链表来进行文件描述符的存储,没有长度限制

    • epoll比select和poll高效的原因主要有两点:

    undefined减少了用户态和内核态之间的文件描述符拷贝

    undefined减少了对就绪文件描述符的遍历

    • epoll的两种模型

    ET (Edge Triggered) 边沿触发

    来了信件,指示板上的灯只闪一下。

    LT (Level Triggered) 水平触发

    来了信件,指示板上的灯一直亮着,直到信箱中的信全部被取走。

    • Epoll和select的区别;

    Select,内核不会为其维护状态。Epoll,内核会为其维护状态。

    因此,每次调用select的时候,都需要重新设置fd_set(数组),然后把整个集合拷贝到内核中。而Epoll,由于内核为它维护了状态(具体是通过创建一个匿名文件,整个匿名文件关联了一个颗红黑树,这颗红黑树保存了之前关注过的epoll_event),因此,如果没有需要新添加或者删除,修改的fd,只需直接epoll_wait就可以了。

    并且,这两个函数调用返回的结果也有所差别。Select会把整个fd_set从内核空间拷贝到用户空间,而epoll只返回就绪事件的列表。

    因此,epoll性能往往会优于select。

四:常见问题

  1. 用户态和内核态的概念区别
  2. CPU上下文切换
  3. 孤儿进程与僵尸进程[总结]

参考资料:

鸟哥Linux在线版本http://cn.linux.vbird.org