我对网络IO的明白

我对网络IO的明白

华三防火墙发布内网服务器配置案例

Unix/linux系统下IO主要分为磁盘IO,网络IO,我今天主要说一下对网络IO的明白,网络IO主要是socket套接字的读(read)、写(write),socket在Linux系统被抽象为流(stream)。

网络IO模子

在Unix/Linux系统下,IO分为两个差别阶段:

  • 守候数据准备好
  • 从内核向历程复制数据

壅闭式I/O

壅闭式I/O(blocking I/O)是最简朴的一种,默认情况下,socket 套接字的系统挪用都是壅闭的,我以recv/recvfrom 明白一下网络IO的模子。当应用层的系统挪用recv/recvfrom时,开启Linux的系统挪用,最先准备数据,然后将数据从内核态复制到用户态,然后通知应用程序获取数据,整个历程都是壅闭的。两个阶段都市被壅闭。

我对网络IO的明白

壅闭I/O模子

图片来源于《Unix网络编程卷1》

壅闭I/O下开发的后台服务,一样平常都是通过多历程或者线程取出来请求,然则开拓历程或者线程是异常消耗系统资源的,当大量请求时,由于需要开拓更多的历程或者线程有可能将系统资源耗尽,因此这种模式不适合高并发的系统。

非壅闭式I/O

非壅闭IO(non-blocking I/O)在挪用后,内核马上返回给历程,若是数据没有准备好,就返回一个error ,历程可以先去干其他事情,一会再次挪用,直到数据准备好为止,循环往返的系统挪用的历程称为轮询(pool),然后在从内核态将数据拷贝到用户态,然则这个拷贝的历程照样壅闭的。

我照样以recv/recvfrom为例说一下,首选需要将socket套接字设置成为非壅闭,历程最先挪用recv/recvfrom,若是内核没有准备好数据时,马上返回给历程一个error码(在Linux下是EAGINE的错误码),历程接到error返回后,先去干其他的事情,进入了轮询,只等到数据准备好,然后将数据拷贝到用户态。

需要通过ioctl 函数将socket套接字设置成为非壅闭

ioctl(fd, FIONBIO, &nb);

我对网络IO的明白

非壅闭I/O模子

图片来源于《Unix网络编程卷1》

非壅闭I/O的第一阶段不会壅闭,然则第二个阶段将数据从内核态拷贝到用户态时会有壅闭。在开发后台服务,由于非壅闭I/O需要通过轮询的方式去知道是否数据准备好,轮询的方式稀奇耗CPU的资源。

I/O多路复用

在Linux下提供一种I/O多路复用(I/O multiplexing)的机制,这个机制允许同时监听多个socket套接字描述符fd,一旦某个fd停当(停当一样平常是有数据可读或者可写)时,能够通知历程举行响应的读写操作。

在Linux下有三个I/O多路复用的函数Select、Poll、Epoll,然则它们都是同步IO,由于它们都需要在数据准备好后,读写数据是壅闭的。

我对网络IO的明白

I/O多路复用模子

图片来源于《Unix网络编程卷1》

I/O多路复用是Linux处置高并发的手艺,Epoll比Select、Poll性能更优越,后面会讲到它们的区别。优异的高并发服务例如Nginx、redis都是接纳Epoll+Non-Blocking I/O的模式。

信号驱动式I/O

信号驱动式I/O是通过信号的方式通知数据准备好,然后再讲数据拷贝到应用层,拷贝阶段也是壅闭的。

我对网络IO的明白

信号驱动式I/O

图片来源于《Unix网络编程卷1》

异步I/O
 

异步I/O(asynchronous I/O或者AIO),数据准备通知和数据拷贝两个阶段都在内核态完成,两个阶段都不会壅闭,真正的异步I/O。

历程挪用read/readfrom时,内核马上返回,历程不会壅闭,历程可以去干其他的事情,当内核通知历程数据已经完成后,历程直接可以处置数据,不需要再拷贝数据,由于内核已经将数据从内核态拷贝到用户态,历程可以直接处置数据。

我对网络IO的明白

异步I/O模子

图片来源于《Unix网络编程卷1》

Linux对AIO支持欠好,因此使用的不是太普遍。

同步和异步区别、壅闭和非壅闭的区别

同步和异步区别

对于这两个器械,POSIX实在是有官方界说的。A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;An asynchronous I/O operation does not cause the requesting process to be blocked;

一个同步I/O操作会引起请求历程壅闭,只到这个I/O请求竣事。

一个异步I/O操作不会引起请求历程壅闭。

超详细的Socket通信原理和实例讲解

从这个官方界说中,不管是Blocking I/O照样Non-Blocking I/O,实在都是synchronous I/O。由于它们一定都市壅闭在第二阶段拷贝数据那里。只有异步IO才是异步的。

我对网络IO的明白

同步异步对比

图片来源于知乎

壅闭和非壅闭的区别

壅闭和非壅闭主要区别实在是在第一阶段守候数据的时刻然则在第二阶段,壅闭和非壅闭实在是没有区别的。程序必须守候内核把收到的数据复制到历程缓冲区来。换句话说,非壅闭也不是真的一点都不”壅闭”,只是在不能马上获得效果的时刻不会傻乎乎地等在那里而已。

IO多路复用

Select、Poll、Epoll的区别

Select、poll、epoll都是I/O多路复用的机制,I/O多路复用就是通过一种机制,一个历程可以监视多个文件描述符fd,一旦某个描述符停当(一样平常是读停当或者写停当),能够通知程序举行响应的读写操作。但select,poll,epoll本质上都是同步I/O,由于他们都需要在读写事宜停当后自己卖力举行读写,也就是说这个读写历程是壅闭的,而异步I/O则无需自己卖力举行读写,异步I/O的实现会卖力把数据从内核拷贝到用户空间。

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。挪用后select函数会壅闭,直到有描述符停当(有数据 可读、可写、或者有except),或者超时(timeout指定守候时间,若是马上返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到停当的描述符。

select支持险些所有的平台,跨平台是它的优点。

select瑕玷是:1)单个历程支持监控的文件描述符数目有限,Linux下一样平常是1024,可以修改提升限制,然则会造成效率低下。2)select通过轮询方式通知新闻,效率比较低。

poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

差别于select使用三个位图来示意三个fdset的方式,poll使用一个pollfd的指针实现。

struct pollfd {
 int fd; /* file descriptor */
 short events; /* requested events to watch */
 short revents; /* returned events witnessed */
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”通报的方式。同时,pollfd并没有最大数目限制(然则数目过大后性能也是会下降)。和select函数一样,poll返回后,需要轮询pollfd来获取停当的描述符。

从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经停当的socket。事实上,同时毗邻的大量客户端在一时刻可能只有很少的处于停当状态,因此随着监视的描述符数目的增进,其效率也会线性下降。

epoll

epoll是在2.6内核中提出的,是之前的select和poll的增强版本,是Linux特有的。相对于select和poll来说,epoll加倍天真,没有描述符限制。epoll使用一个文件描述符治理多个描述符,将用户关系的文件描述符的事宜存放到内核的一个事宜表中,这样在用户空间和内核空间的copy只需一次。

int epoll_create(int size);//建立一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

执行epoll_create时,建立了红黑树和停当list链表;执行epoll_ctl时,若是增添fd,则检查在红黑树中是否存在,存在则马上返回,不存在则添加到红黑树中,然后向内核注册回调函数,用于当中断事宜到来时向准备停当的list链表中插入数据。执行epoll_wait时马上返回准备停当链内外的数据即可。

事情模式

1. LT模式

LT(level triggered)是缺省的事情方式,而且同时支持block和no-block socket,在这种做法中,内核告诉你一个文件描述符是否停当了,然后你可以对这个停当的fd举行IO操作。若是你不做任何操作,内核照样会继续通知你的。

2. ET模式

ET(edge-triggered)是高速事情方式,只支持no-block socket。在这种模式下,当描述符从未停当变为停当时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经停当,而且不会再为谁人文件描述符发送更多的停当通知,直到你做了某些操作导致谁人文件描述符不再为停当状态了(好比,你在发送,吸收或者吸收请求,或者发送吸收的数据少于一定量时导致了一个EWOULDBLOCK/EAGAIN 错误)。然则请注意,若是一直纰谬这个fd作IO操作(从而导致它再次酿成未停当),内核不会发送更多的通知(only once),因此必须把缓存区buff数据读取完毕,否则就可能会丢数据。

ET模式在很大程度上减少了epoll事宜被重复触发的次数,因此效率要比LT模式高。epoll事情在ET模式的时刻,必须使用非壅闭套接口,以制止由于一个文件句柄的壅闭读/壅闭写操作把处置多个文件描述符的义务饿死。

详细对比

我对网络IO的明白

三种I/O多路复用对比

Nginx中Epoll+非壅闭IO
 

Nginx高并发主要是通过Epoll模式+非壅闭I/O

Nginx对I/O多路复用举行封装,封装在结构体struct ngx_event_s,同时将事宜封装在ngx_event_actions_t结构中。

typedef struct {
 ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
 ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

 ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
 ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

 ngx_int_t (*add_conn)(ngx_connection_t *c);
 ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);

 ngx_int_t (*notify)(ngx_event_handler_pt handler);

 ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
 ngx_uint_t flags);

 ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
 void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;

初始化epoll句柄

static ngx_int_t
ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
 ngx_epoll_conf_t *epcf;

 epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module);

 if (ep == -1) {
 ep = epoll_create(cycle->connection_n / 2);

 if (ep == -1) {
 ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
 "epoll_create() failed");
 return NGX_ERROR;
 }
 ...
 }
}

将fd设置为非壅闭

(ngx_nonblocking(s) == -1) #nginx将fd设置非壅闭

设置事宜触发

static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
 int op;
 uint32_t events, prev;
 ngx_event_t *e;
 ngx_connection_t *c;
 struct epoll_event ee;

 c = ev->data;

 events = (uint32_t) event;

 if (event == NGX_READ_EVENT) {
 e = c->write;
 prev = EPOLLOUT;
#if (NGX_READ_EVENT != EPOLLIN|EPOLLRDHUP)
 events = EPOLLIN|EPOLLRDHUP;
#endif

 } else {
 e = c->read;
 prev = EPOLLIN|EPOLLRDHUP;
#if (NGX_WRITE_EVENT != EPOLLOUT)
 events = EPOLLOUT;
#endif
 }

 if (e->active) {
 op = EPOLL_CTL_MOD;
 events |= prev;

 } else {
 op = EPOLL_CTL_ADD;
 }

#if (NGX_HAVE_EPOLLEXCLUSIVE && NGX_HAVE_EPOLLRDHUP)
 if (flags & NGX_EXCLUSIVE_EVENT) {
 events &= ~EPOLLRDHUP;
 }
#endif

 ee.events = events | (uint32_t) flags;
 ee.data.ptr = (void *) ((uintptr_t) c | ev->instance);

 ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
 "epoll add event: fd:%d op:%d ev:%08XD",
 c->fd, op, ee.events);

 if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
 ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
 "epoll_ctl(%d, %d) failed", op, c->fd);
 return NGX_ERROR;
 }

 ev->active = 1;
#if 0
 ev->oneshot = (flags & NGX_ONESHOT_EVENT) ? 1 : 0;
#endif

 return NGX_OK;
}

处置停当的事宜

static ngx_int_t
ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
 int events;
 uint32_t revents;
 ngx_int_t instance, i;
 ngx_uint_t level;
 ngx_err_t err;
 ngx_event_t *rev, *wev;
 ngx_queue_t *queue;
 ngx_connection_t *c;

 /* NGX_TIMER_INFINITE == INFTIM */

 ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
 "epoll timer: %M", timer);

 events = epoll_wait(ep, event_list, (int) nevents, timer);
 ...
}

 引用

深入解读同步/异步 IO 编程模子

关于同步/异步 VS 壅闭/非壅闭的一点体会

怎样明白壅闭非壅闭与同步异步的区别?

华三防火墙外网使用固定IP地址方式上网配置方法

分享到 :
相关推荐

发表评论

登录... 后才能评论