网络IO
Stevens在网络编程的书中概括了五种网络IO。
- blocking IO
- nonblocking IO
- IO multiplexing
- signal driven IO
- asynchronous IO
通常来说,当一个网络IO发生时候包括到两个对象:用户空间的调用进程和内核空间的内核进程。当我们的用户层触发一个IO操作的时候,需要等待两个阶段:
1.等待数据准备
2.将数据从内核拷贝至用户进程
在这个数据准备和拷贝的过程中,用户空间处于阻塞的状态。
在Linux中,我们可以将socket设置为non-blocking,这样,当我们触发一个IO的时候,则不会有等待的过程,而是直接返回,通过判断调用返回的结果判断。
eg:
fcntl( fd, F_SETFL, O_NONBLOCK ); 设置一个句柄为非阻塞的状态。
多路复用IO
这种IO方式通常被称之为事件驱动IO。其实事件驱动IO的作用主要在于处理多个连接,当我们的连接数目达到一定的数量的时候,可以通过内核来监听所有负责的socket。
- select
int select(int max+1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ;
select返回就绪文件句柄的数量。
=0 为等待超时
<0 发生异常
FD_ZERO( fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
对于fd_set,事实上是一个bit位标记句柄的队列,同时这里的句柄同时为输入输出参数,我们将需要监听的FD标记在集合中输入,调用select后,系统将触发事件的FD标记并返回。
这里也体现了select的问题所在,
1.select需要将全部的IO句柄在用户层和内核层之间复制。
2.轮询。select结果返回之后,需要对集合轮询。
3.select最大句柄数1024。
- poll
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd; //文件描述符
short events; //等待的事件,用户设置域
short revents; //实际发生的事件,内核设置域
};
poll存在的问题与select类似:大量的文件描述符在用户态和内核态之间复制,同时也是进行轮询。只是没有了最大文件描述符的限制,但是根本的问题并没有解决。
- EPOLL
具体参考后续更新EPOLL实现。
事实上,在我们的高并发连接中,每次异步返回活跃链接数量占比不会很高,然而前两种效率不高的原因,是因为每次扫描了大量的不活跃的链接。相对epoll用红黑树(插入lg n)和链表的方式,只对活跃的链接起作用,则大大提高了效率.
事件驱动模型
多路IO复用虽然解决了对多个事件的探测的问题,但是仍然存在着问题。当我们把事件响应加入到事件探测后面时,一旦事件响应的执行比较庞大,则会对整个模型造成灾难性的后果:假如事件1执行导致了响应事件2的执行体迟迟得到执行,则我们事件探测的及时性得不到保障;我们的业务逻辑和网络接入层混合,造成的结构不清晰等等…
Reactor模式 proctor模式
什么是reactor呢。事实上,它就是一个网络接入层,对我们的每一个接入的链接进行管理(组织成一个结构体,这个结构体对每一个链接进行缓冲区,回调,句柄等等的管理)。
一般的事件驱动模型有三个重要的组件:1.多路复用。2.事件分发。3.事件处理。与此相关的模式是Reactor和Proactor.Reactor采用同步IO,多路复用等待文件句柄读写操作准备就绪,然后又用户层完成实际的读写操作。Proactor采用异步IO,IO操作本身由操作系统完成,因此Proactor关注的是io完成事件。通俗来讲,两者的区别在与handle调用的时机:reactor的handle调用在实际的IO操作之前,而Proactor的handler在实际的操作之后。
那么实现了异步操作之后有什么好处呢?异步接口可以利用系统提供的读写并行能力。但是目前Linux上并没有像IOCP这样成熟的异步IO的实现。Linux的AIO经过测试性能并不理想,采用的是POSIX的接口,内部以来应用层的实现,并不支持connect,accept,send,recv,所以Linux还是主要以reactor模型为主。
在不使用系统提供的异步接口情况下,使用reactor来模拟Proactor,在用户态用同步IO模拟Proactor模型:用多线程并发来之处理handle。
reactor的实现可以参考我的GitHub仓库: https://github.com/chuican1/muti_io.git
Reactor与多线程的结合
1.listen线程+recv/send线程
监听单独做一个线程,处理则用多个线程。监听单独做一个线程,这种做法可以使对epollfd句柄的操作只有一条线程,避免多个多线程对同一个epoll句柄的同步互斥问题。这也是memcached的做法。
2.reactor多线程
不管监听或者listen,每个reactor一个线程。这种好处是每个reactor与CPU进行粘合。可以比较好的做到均衡。
3.master线程+recv/send handle线程
recv和send 的handle push到线程池中,当成一个任务处理,可以提高吞吐量。