最近在看网络编程模型,虽然Golang天然高并发的原因很大一部分是因为协程
和channel
,但是这个里面还是离不开底层的网络编程模型的选用 — epoll
。在学习这部分的时候对IO多路复用
做了一些了解。
一些概念
内核空间和用户空间
现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间
,一部分为用户空间
。我们可以简单理解为,一张纸,四分之一给一个叫内核的人用,四分之三给一个叫用户的人用。
fd 文件操作描述符
文件描述符
在形式上是一个非负整数,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。文件描述符这一概念往往只适用于Unix和Linux这样的操作系统。
缓存IO
缓存 I/O 又被称作标准 I/O
,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中
,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。就是从内核空间
到用户空间
需要经过两次复制操作。这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。
Linux的socket 事件wakeup callback机制
Linux(2.6+)内核 会通过sleep_list
等待队列,去管理所有正在等socket事件
的process,会在sleep_list
中为当前正在等待的process构建一个wait_entry
,只到超时或者事件触发,在每个wait_enrey
上会定义一个callback
同时wakeup
机制会异步的唤醒整个sleep_list
上的process,并同时执行callback
,删除sleep_list
上的wait_entry
节点。总体上会涉及两大逻辑:
睡眠机制
select
、poll
、epoll_wait
陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry
节点,然后插入到监控socket的sleep_list
里取。- Linux调用schedule函数进行process的状态转换,shcedule函数是Linux的调度process的函数,这里指的是process进入sleep直到超时或者事件发生。
- 事件触发后,将当前process的
wait_entry
节点从socket的sleep_list
中删除。
唤醒机制
socket的事件
发生了,然后socket
顺序遍历其睡眠队列sleep_list
,依次调用每个wait_entry
节点(对应各个Process)的callback
函数。- 直到完成队列的遍历或遇到某个
wait_entry
节点是排他的才停止。- 一般情况下
callback
包含两个逻辑:wait_entry
自定义的私有逻辑和唤醒的公共逻辑,主要用于将该wait_entry
的process放入CPU的就绪队列,让CPU随后可以调度其执行。
IO模型
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
正式因为这两个阶段,linux系统产生了下面五种网络模式的方案:
- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)(比较少用到)
- 异步 I/O(asynchronous IO)
阻塞 I/O(blocking IO)
用户进程process在Blocking IO读recvfrom
操作的两个阶段都是等待的。在数据没准备好的时候,process原地等待kernel准备数据。kernel准备好数据后,process继续等待kernel将数据copy到自己的buffer。在kernel完成数据的copy后process才会从recvfrom系统调用中返回。
非阻塞 I/O(NonBlocking IO)
process在NonBlocking IO
读recvfrom
操作的第一个阶段是不会block等待的,如果kernel数据还没准备好,那么recvfrom
会立刻返回一个EWOULDBLOCK
错误。当kernel准备好数据后,进入处理的第二阶段的时候,process会等待kernel将数据copy到自己的buffer,在kernel完成数据的copy后process才会从recvfrom系统调用中返回。
IO多路复用(NonBlocking IO)
IO多路复用,就是我们熟知的select
、poll
、epoll
模型。从图上可见,在IO多路复用的时候,process在两个处理阶段都是block住等待的。初看好像IO多路复用没什么用,其实select、poll、epoll的优势在于·可以以较少的代价来同时监听处理多个IO
。