网络 IO,会涉及到两个系统对象,一个是用户空间调用 IO 的进程或者线程,另一个是内核空间的内核系统,比如发生 IO 操作 read 时,它会经历两个阶段:
- 等待数据准备就绪
- 将数据从内核拷贝到进程或者线程中。
因为在以上两个阶段上各有不同的情况,所以出现了多种网络 IO 模型
(1) 阻塞IO(blocking IO)
所谓阻塞型接口是指系统调用(一般是 IO 接口)
不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返
回。
(2) 非阻塞 IO(non-blocking IO)
在非阻塞状态下,recv() 接口在被调用后立即返回,返回值代表了不同的含义。如在本例中,
- recv() 返回值大于 0,表示接受数据完毕,返回值即是接受到的字节数;
- recv() 返回 0,表示连接已经正常断开;
- recv() 返回 -1,且 errno 等于 EAGAIN,表示 recv 操作还没执行完成;
- recv() 返回 -1,且 errno 不等于 EAGAIN,表示 recv 操作遇到系统错误 errno。
使用如下的函数可以将某句柄 fd 设为非阻塞状态。
fcntl( fd, F_SETFL, O_NONBLOCK );
(3) 异步 IO(Asynchronous I/O)
异步 IO 是真正非阻塞的,它不会对请求进程产生任何的阻塞,因此对高并发的网络服务器实现至关重要
(4) synchronous IO
blocking 和 non-blocking 的区别在哪?
调用 blocking IO 会一直 block 住对应的进程直到操作完成,而
non-blocking IO 在 kernel 还在准备数据的情况下会立刻返回。
synchronous IO 和asynchronous IO 的区别在哪?
两者的区别就在于 synchronous IO 做”IO operation”的时候会将 process 阻塞。
按照这个定义,之前所述的 blocking IO,non-blocking IO,IO multiplexing 都属于
synchronous IO。有人可能会说,non-blocking IO 并没有被 block 啊。这里有个非常
“狡猾”的地方,定义中所指的”IO operation”是指真实的 IO 操作,就是例子中的 read 这
个系统调用。non-blocking IO 在执行 read 这个系统调用的时候,如果 kernel 的数据没
有准备好,这时候不会 block 进程。但是当 kernel 中数据准备好的时候,read 会将数据
从 kernel 拷贝到用户内存中,这个时候进程是被 block 了,在这段时间内进程是被 block
的。而 asynchronous IO 则不一样,当进程发起 IO 操作之后,就直接返回再也不理睬了,
直到 kernel 发送一个信号,告诉进程说 IO 完成。在这整个过程中,进程完全没有被 block。
多路复用 IO(IO multiplexing)–事件驱动 IO(event driven IO)
- 它的基本原理就是 select/epoll 这个 function会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。
- FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
常见的时间驱动库: libevent,libev
信号驱动 IO(signal driven I/O, SIGIO)
服务器模型 Reactor 与 Proactor
对高并发编程,网络连接上的消息处理,可以分为两个阶段:等待消息准备好、消息处理。
高并发编程方法当然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。
Reactor 模型
Reactor 模型有三个重要的组件:
- 多路复用器:由操作系统提供,在 linux 上一般是 select, poll, epoll 等系统调用。
- 事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中。
- 事件处理器:负责处理特定事件的处理函数