网络IO与IO模型
网络IO
典型的一次IO两个阶段是:数据准备和数据读写
在数据准备阶段,根据系统IO操作的准备状态,分为两种
- 阻塞:比如
int size = recv(sockfd, buf, 1024, 0);
如果sockfd没有数据的话,当前线程会阻塞在此处,数据可读时唤醒 - 非阻塞:比如以上的语句把sockfd设置为no_block,通过size == -1&&errno = EAGAIN判断有没有数据,不会改变当前线程的状态
在数据读写阶段,根据应用程序和内核的交互方式,分为两种(!= 并发里的同步和异步)
-
同步:当操作系统准备好了数据放在TCP接收缓冲区中时,需要应用程序调用recv接口自行从缓冲区取出数据,在取完数据之前,应用程序是卡在recv系统调用上的
包括send和recv两个都是同步的IO接口
-
异步:使用异步IO接口时(比如aio_read,aio_write),让操作系统内核自己把缓冲区的数据给到buf里,然后通过一种通知方式(sigio通知, 回调等等)通知应用程序说buf的数据准备好了,是异步就必有通知机制
在处理IO的时候,阻塞和非阻塞都是同步IO,只有使用了特殊的API才是异步IO(所以epoll是一个同步IO接口)
而在业务层面上的逻辑处理判断同步和异步:同步就是A等待B做完后返回值才继续处理;异步就是A告诉B它所感兴趣的事件和通知方式,A继续操作直到B通知
面试指南:
阻塞、非阻塞、同步、异步描述的都是一个IO的状态,一个典型的IO包含两个阶段:数据准备和数据读写,比如recv传sockfd,buf和大小,当sockfd工作在阻塞模式下,如果数据没有准备就绪,线程会阻塞在此;如果是非阻塞模式,通过返回的值大小就能判断数据的准备状态
- socket是以文件的方式存在的拥有文件描述符,双端进行网络通信,即读写socket,即读写文件
- 最初:同步阻塞的方式进行网络I/O,但一个服务端只能服务一个客户端
- 改进:多进程模型:父进程只关心监听socket,一旦有连接socket调用accept()从全连接队列取出,就会通过fork()创建一个子进程,为每个客户端分配一个进程处理请求,在父进程中会调用wait()或waitpid()等待子进程退出回收资源,但进程间上下文切换代价太高
- 改进:多线程模型:通过pthread_create()创建多个线程作为线程池,同时accept()将连接从全连接队列里取出,把已建立连接的socket放进一个全局队列,线程池里的线程从队列里取出socket连接加锁后处理请求
- 改进:I/O多路复用:一个进程处理多个socket的请求,类似于CPU并发多个进程,由三个系统调用select/poll/epoll,将socket的文件描述符传给内核,内核返回产生了事件的连接,在用户态处理这些连接的时间
linux下的5种IO模型
通过setsockopt()来设置套接字的非阻塞
IO复用
最大的特点是:监听这些sock是否发生事件,只需要很少的线程,就能监听很多个sock,然后给应用返回那些就绪的可读写的sock列表;而不需要向之前那样一个线程监听一个sock
内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需 要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率
通过aiocb结构体可以传入参数,定义异步io接口的配置,比如传入应用程序所感兴趣的事件fd等等
Reactor模型
对IO多路复用的一种封装,有单reactor单线程,单reactor多线程,多reactor多线程模式
关键组件:event事件,reactor反应堆,demultiplex事件分发器(其实本质就是epoll),eventhandler事件处理器
Reactor设计模式是一种用于处理同时由一个或多个输入并发传递给服务处理程序的服务请求的事件处理模式。服务处理程序对传入的请求进行多路复用,并将它们同步分派到相应的请求处理程序
- 单Reactor单线程
- select监听事件,dispatch将连接事件分给acceptor,acceptor通过accept获取连接并创建一个handler处理后续,将其他事件分给handler
- 单Reactor多线程
- handler不负责实际的业务处理,它将数据发送给子线程中的processor处理完后再用send将处理结果发给client
- 并发好,可以发挥多核优势,但需要在操作共享资源前加上互斥锁,不能有多个线程同时操作同一片数据
- 多Reactor多线程(muduo库所采用的模型,multi-reactor)
- mainractor将连接分配给子线程里的subreactor继续监听,并在子线程里处理完请求
- 分工明确:主线程只负责接收新连接,子线程负责完成后续的业务处理
- 交互简单:主线程只负责将连接给子线程,子线程会自己将结果返回给client
以Redis里的事件驱动框架为例
Reactor模型:网络服务器端用来处理高并发网络IO请求的编程模型,特征是两个3
- 三类关键处理事件,连接事件,写事件,读事件,三类事件对应了客户端和服务端交互过程中不同类请求引发的事件
- 三个关键角色:reactor,acceptor,handler
- acceptor处理连接事件,负责接受连接,在接受连接后创建handler,用于后续处理
- handler处理读写事件
- reactor监听和分配事件,处理高并发下连接、读写事件同时发生的情况
事件驱动框架:实现reactor模型时,需要实现的代码整体控制逻辑,包括两部分
-
事件初始化:在服务器启动时注册需要监听的事件类型以及对应的handler
-
事件捕获分发和处理主循环:在while主循环中捕获发生的事件判断事件类型,根据不同类型调用不同角色处理
Redis中事件驱动框架的实现
事件数据结构aeFileEvent
redis的事件驱动框架定义了两类事件:IO事件(对应客户端发送的网络请求)和时间事件(redis自身的周期性操作)不同类型事件的数据结构定义是不一样的
以IO事件aeFileEvent为例
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) 事件类型掩码,主要有三种类型*/
aeFileProc *rfileProc; //指向AE_READABLE的处理函数
aeFileProc *wfileProc; //指向AE_WRITEABLE的处理函数,即reactor中的handler
void *clientData; //指向客户端私有数据
} aeFileEvent;
事件注册aeCreateFileEvent
在Redis启动后的初始化过程中,initServer会调用该函数注册要监听的事件以及相应的事件处理函数
它会根据启动的IP端口数量,为每个IP端口上的网络事件调用aeCreateFileEvent,创建对AE_READABLE事件的监听并注册handler即acceptTcpHandler,所以AE_READABLE事件就是客户端的网络连接事件,对应处理函数就是接受TCP连接请求
然后aeCreateFileEvent会调用aeApiAddEvent,然后它会调用epoll_ctl,通过封装epoll来实现注册希望监听的事件和相应处理函数
主循环:aeMain
用一个循环不断判断事件循环的停止标记,在服务器程序初始化完成之后被调用开始执行
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
…
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
由aeProcessEvent函数完成事件的捕获和分发
事件捕获与分发:aeProcessEvent
主要有三种情况
- 情况一:既没有时间事件,也没有网络事件;直接返回
- 情况二:有 IO 事件或者有需要紧急处理的时间事件;调用aeApiPoll捕获事件
- 情况三:只有普通的时间事件;调用专门处理时间事件的函数processTimeEvents
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* 若没有事件处理,则立刻返回*/
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/*如果有IO事件发生,或者紧急的时间事件发生,则开始处理*/
if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
//通过调用aeApiPoll来捕获发生的网络事件
…
}
/* 检查是否有时间事件,若有,则调用processTimeEvents函数处理 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
/* 返回已经处理的文件或时间*/
return processed;
}
在第二种情况下调用的aeApiPoll就是封装了epoll_wait用于检测内核中发生的网络IO事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
…
//调用epoll_wait获取监听到的事件
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
if (retval > 0) {
int j;
//获得监听到的事件数量
numevents = retval;
//针对每一个事件,进行处理
for (j = 0; j < numevents; j++) {
#保存事件信息
}
}
return numevents;
}