linux下的网络I/O——转载

Posted Icedzzz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux下的网络I/O——转载相关的知识,希望对你有一定的参考价值。

转载于——腾讯技术工程

网络IO的发展

网络 IO 的发展过程是随着 linux 的内核演变而变化,因此网络 IO 大致可以分为如下几个阶段:

  1. 阻塞 IO(BIO)
  2. 非阻塞 IO(NIO)
  3. IO 多路复用第一版(select/poll)
  4. IO 多路复用第二版(epoll)
  5. 异步 IO(AIO)

网络的两个阶段

在网络中,我们通常可以将其广义上划分为以下两个阶段:

  • 第一阶段:硬件接口到内核态
  • 第二阶段:内核态到用户态
    本人理解:我们通常上网,大部分数据都是通过网线传递的。因此对于两台计算机而言,要进行网络通信,其数据都是先从应用程序传递到传输层(TCP/UDP)到达内核态,然后再到网络层、数据链路层、物理层,接着数据传递到硬件网卡,最后通过网络传输介质传递到对端机器的网卡,然后再一步一步数据从网卡传递到内核态,最后再拷贝到用户态。

堵塞IO和非堵塞IO的区别

网络中的数据传输从网络传输介质到达目的机器,需要如上两个阶段。此处我们把从硬件到内核态这一阶段,是否发生阻塞等待,可以将网络分为阻塞 IO和非阻塞 IO。 如果用户发起了读写请求,但内核态数据还未准备就绪,该阶段不会阻塞用户操作,内核立马返回,则称为非阻塞 IO。如果该阶段一直阻塞用户操作。直到内核态数据准备就绪,才返回。这种方式称为阻塞 IO。

同步 IO 和异步 IO 的区别

从前面我们知道了,数据的传递需要两个阶段,在此处只要任何一个阶段会阻塞用户请求,都将其称为同步 IO,两个阶段都不阻塞,则称为异步 IO。 在目前所有的操作系统中,linux 中的 epoll、mac 的 kqueue 都属于同步 IO,因为其在第二阶段(数据从内核态到用户态)都会发生拷贝阻塞。 而只有 windows 中的 IOCP 才真正属于异步 IO,即 AIO。

堵塞IO

阻塞 IO 英文为 blocking IO,又称为 BIO。阻塞 IO 主要指的是第一阶段(硬件网卡到内核态)。当用户发生了系统调用后,如果数据未从网卡到达内核态,内核态数据未准备好,此时会一直阻塞。直到数据就绪,然后从内核态拷贝到用户态再返回。


堵塞IO的缺点:

在一般使用阻塞 IO 时,都需要配置多线程来使用,最常见的模型是阻塞 IO+多线程每个连接一个单独的线程进行处理
我们知道,一般一个程序可以开辟的线程是优先的,而且开辟线程的开销也是比较大的。也正是这种方式,会导致一个应用程序可以处理的客户端请求受限。面对百万连接的情况,是无法处理。既然发现了问题,分析了问题,那就得解决问题。既然阻塞 IO 有问题,本质是由于其阻塞导致的,因此自然而然引出了下面即将介绍的主角:非阻塞 IO

非堵塞IO

非阻塞 IO:见名知意,就是在第一阶段(网卡-内核态)数据未到达时不等待,然后直接返回。因此非阻塞 IO 需要不断的用户发起请求,询问内核数据好了没,好了没。


非堵塞IO的优点:
非阻塞 IO 解决了阻塞 IO每个连接一个线程处理的问题,所以其最大的优点就是 一个线程可以处理多个连接

非堵塞IO的缺点:

但这种模式,也有一个问题,就是需要用户多次发起系统调用。频繁的系统调用是比较消耗系统资源的。
因此,既然存在这样的问题,那么自然而然我们就需要解决该问题:保留非阻塞 IO 的优点的前提下,减少系统调用

IO多路复用

IO 多路复用:
**多路复用主要复用的是通过有限次的系统调用来实现管理多个网络连接。**最简单来说,我目前有 10 个连接,我可以通过一次系统调用将这 10 个连接都丢给内核,让内核告诉我,哪些连接上面数据准备好了,然后我再去读取每个就绪的连接上的数据。因此,IO 多路复用,复用的是系统调用。通过有限次系统调用判断海量连接是否数据准备好了

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,**一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。**但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select/poll

select

select 的 api

// readfds:关心读的fd集合;writefds:关心写的fd集合;excepttfds:异常的fd集合
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的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。

poll

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

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。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

select/poll IO 多路复用

IO 多路复用,主要在于复用,通过 select()或者 poll()将多个 socket fds 批量通过系统调用传递给内核,由内核进行循环遍历判断哪些 fd 上数据就绪了,然后将就绪的 readyfds 返回给用户。再由用户进行挨个遍历就绪好的 fd,读取或者写入数据。
所以通过 IO 多路复用+非阻塞 IO,一方面降低了系统调用次数,另一方面可以用极少的线程来处理多个网络连接。
缺点: 用户需要每次将海量的 socket fds 集合从用户态传递到内核态,让内核态去检测哪些网络连接数据就绪了,但这个地方会出现频繁的将海量 fd 集合从用户态传递到内核态,再从内核态拷贝到用户态。 所以,这个地方开销也挺大。

select 和 poll 的区别:

  • select 能处理的最大连接,默认是 1024 个,可以通过修改配置来改变,但终究是有限个;而 poll 理论上可以支持无限个
  • select 和 poll 在管理海量的连接时,会频繁的从用户态拷贝到内核态,比较消耗资源。

epoll

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

api:

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待epfd上的io事件,最多返回maxevents个事件。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  • **LT模式:**当epoll_wait检测到描述符事件发生并将此事件通知应用程序,**应用程序可以不立即处理该事件。**下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • **ET模式:**当epoll_wait检测到描述符事件发生并将此事件通知应用程序,**应用程序必须立即处理该事件。**如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

epoll的优点:

一开始就在内核态分配了一段空间,来存放管理的 fd,所以在每次连接建立后,交给 epoll 管理时,需要将其添加到原先分配的空间中,后面再管理时就不需要频繁的从用户态拷贝管理的 fd 集合。通通过这种方式大大的提升了性能。

异步IO

前面介绍的所有网络 IO 都是同步 IO,因为当数据在内核态就绪时,在内核态拷贝用用户态的过程中,仍然会有短暂时间的阻塞等待。而异步 IO 指:内核态拷贝数据到用户态这种方式也是交给系统线程来实现,不由用户线程完成,目前只有 windows 系统的 IOCP 是属于异步 IO。

reactor模型

1. 单reactor模型

此种模型,通常是只有一个 epoll 对象,所有的接收客户端连接、客户端读取、客户端写入操作都包含在一个线程内。该种模型也有一些中间件在用,比如 redis

但在目前的单线程 Reactor 模式中,不仅 I/O 操作在该 Reactor 线程上,连非 I/O 的业务操作也在该线程上进行处理了,这可能会大大延迟 I/O 请求的响应。所以我们应该将非 I/O 的业务逻辑操作从 Reactor 线程上卸载,以此来加速 Reactor 线程对 I/O 请求的响应。

2. 单reactor多线程模型

该模型主要是通过将,前面的模型进行改造,将读写的业务逻辑交给具体的线程池来实现,这样可以显示 reactor 线程对 IO 的响应,以此提升系统性能。
在工作者线程池模式中,虽然非 I/O 操作交给了线程池来处理,但是所有的 I/O 操作依然由 Reactor 单线程执行,在高负载、高并发或大数据量的应用场景,依然较容易成为瓶颈。所以,对于 Reactor 的优化,又产生出下面的多线程模式。

3. multi-reactor 多线程模型

在这种模型中,主要分为两个部分:mainReactor、subReactors。 mainReactor 主要负责接收客户端的连接,然后将建立的客户端连接通过负载均衡的方式分发给 subReactors,subReactors 来负责具体的每个连接的读写对于非 IO 的操作,依然交给工作线程池去做,对逻辑进行解耦

以上是关于linux下的网络I/O——转载的主要内容,如果未能解决你的问题,请参考以下文章

linux下的网络I/O——转载

linux下的网络I/O——转载

从操作系统层面理解Linux下的网络IO模型

程序员必备:linux网络I/O+Reactor模型

[]转帖] 浅谈Linux下的五种I/O模型

Linux下的I/O与管道