Linux之IO模型/IO复用模型

7 篇文章 0 订阅

在操作系统中的IO

在此以Linux操作系统为例。Linux是一个将所有的外部设备都看作是文件来操作的操作系统,在它看来:everything is a file,那么我们就把对于外部设备的操作都看作是对文件进行操作。而且我们对一个文件进行读写,都需要通过调用内核提供的系统调用。

而在Linux中,一个基本的IO会涉及到两个系统对象:一个是调用这个IO的进程对象(用户进程),另一个是系统内核。也就是说,当一个read操作发生时,将会经历这些阶段:

  • 通过read系统调用,向内核发送读请求;
  • 内核向硬件发送读指令,并等待读就绪;
  • DMA把将要读取的数据复制到指定的内核缓存区中;
  • 内核将数据从内核缓存区拷贝到用户进程空间中

在此期间会发生几种IO操作:

  • 同步IO:当用户发出IO请求操作后,内核会去查看要读取的数据是否就绪,如果没有,就一直等待。期间用户线程或内存会不断地轮询数据是否就绪。当数据就绪时,再把数据从内核拷贝到用户空间。
  • 异步IO:用户线程只需发出IO请求和接收IO操作完成通知,期间的IO操作由内核自动完成,并发送通知告知用户线程IO操作已经完成。也就是说,在异步IO中,并不会对用户线程产生任何阻塞。
  • 阻塞IO:当用户线程发起一个IO请求操作,而内核要操作的数据还没就绪,则当前线程被挂起,阻塞等待结果返回。
  • 非阻塞IO:如果数据没有就绪,就会返回一个标志信息告知用户线程,当前的数据还没有就绪。当前线程在获得此次请求结果的过程中,还可以做点其他事情。

可能会有读者觉得,怎么同步IO、异步IO和阻塞IO、非阻塞IO的操作好相似,为什么要它们都分出来呢?笔者认为,这同步、异步和阻塞、非阻塞是从不同角度来看待问题的

同步与异步

同步与异步主要是从消息通知的角度来说的。

同步

所谓同步,指的是协同步调。既然叫协同,所以至少要有2个以上的事物存在。同步就是当一个任务A的完成需要依赖另一个任务B时,只有等到B任务完成后,A才能成功地进行,这是一种可靠的任务队列。要么都成功,要么都失败,两个任务的状态可以保持一致。协同的结果就是:

多个事物不能同时进行,必须一个一个的来,上一个事物结束后,下一个事物才开始。

那当一个事物正在进行时,其它事物都在干嘛呢?

严格来讲这个并没有要求,但一般都是处于一种“等待”的状态,因为通常后面事物的正常进行都需要依赖前面事物的结果或前面事物正在使用的资源。

因此,可以认为,同步更希望关注的是从宏观整体来看,多个事物是一种逐个逐个的串行化关系,绝对不会出现交叉的情况。

所以,自然也不太会去关注某个瞬间某个具体事物是处于一个什么状态。

把这个理论应用的出神入化的非“排队”莫属。凡是在资源少需求多的场景下都会用到排队。

比如排队买火车票这件事:

其实售票大厅更在意的是旅客一个一个的到窗口去买票,因为一次只能卖一张票。

即使大家一窝蜂的都围上去,还是一次只能卖一张票,何必呢?挤在一起又不安全。

只是有些人素质太差,非要往上挤,售票大厅迫不得已,采用排队这种形式来达到自己的目的,即一个一个的买票。

至于每个旅客排队时的状态,是看手机呀还是说话呀,根本不用去在意。

除了这种由于资源导致的同步外,还存在一种由于逻辑上的先后顺序导致的同步。

比如,先更新代码,然后再编译,接着再打包。这些操作由于后一步要使用上一步的结果,所以只能按照这种顺序一个一个的执行。

关于同步还需知道两个小的点:

一是范围,并不需要在全局范围内都去同步,只需要在某些关键的点执行同步即可。

比如食堂只有一个卖饭窗口,肯定是同步的,一个人买完,下一个人再买。但吃饭的时候也是一个人吃完,下一个人才开始吃吗?当然不是啦。

二是粒度,并不是只有大粒度的事物才有同步,小粒度的事物也有同步。

只不过小粒度的事物同步通常是天然支持的,而大粒度的事物同步往往需要手工处理。

比如两个线程的同步就需要手工处理,但一个线程里的两个语句天然就是同步的。

异步

所谓异步,就是步调各异。既然是各异,那就是都不相同。异步是不需要等待任务B完成,只是通知任务B要完成什么工作,任务A也立即执行,只要任务A自己执行完了那么整个任务就算完成了。所以结果就是:

多个事物可以你进行你的、我进行我的,谁都不用管谁,所有的事物都在同时进行中。

一言以蔽之,同步就是多个事物不能同时开工,异步就是多个事物可以同时开工。

注:一定要去体会“多个事物”,多个线程是多个事物,多个方法是多个事物,多个语句是多个事物,多个CPU指令是多个事物。等等等等。

异步:有点类似于回调函数,callback,java中的BIO,NIO属于同步模型,AIO属于异步模型

阻塞与非阻塞

阻塞与非阻塞主要是从等待消息通知时的状态角度来说的。

阻塞就是指在调用结果返回之前,当前线程会被挂起,一直处于等待消息通知的状态,不能执行其他业务。只有当调用结果返回之后才能进行其他操作。

非阻塞与阻塞的概念相对应,就是指不能立即得到结果之前,该函数不会阻塞当前线程,而是会立即返回(-1)。虽然非阻塞的方式看上去可以明显提高CPU的利用率,但是也会使系统的线程切换增加,需要好好评估增加的CPU执行时间能不能步长系统的切换成本。

我们继续用上面的栗子,小J无论是在排队还是拿号等通知,如果在这个等待的过程中,小J除了等待消息通知之外就做不了其他的事情,那么该机制就是阻塞的。如果他可以一边打电话一边等待,这个状态就是非阻塞的。

同步、异步与阻塞、非阻塞

其实可能会有其他读者把同步与阻塞等同起来,实际上这两个是不同的。对于同步来说,很多时候当前线程还是在激活状态,只是逻辑上当前函数没有返回而已,此时,线程也会去处理其他的消息。也就是说,同步、阻塞其实是从不同角度对当前线程状态的描述

1 同步阻塞形式

这是效率最低的一种方式,拿上面的栗子来说,就是小J心无旁骛地排队,什么别的事都不做

在这里,同步与阻塞体现在:

  • 同步:小J等待队伍排到他办理业务;
  • 阻塞:小J在等待队伍排到他的过程中,不做其他任务处理。

2 异步阻塞形式

如果小J在银行等待办理业务的时候,领了号,这时候就采用了异步的方式去等待消息被触发(通知),等着柜员喊他的号而不是时刻盯着是不是排到他了。但是在这段时间里,他还是不能离开银行去做其他的事情,那么很显然,他被阻塞在这个等待喊号的操作上了。

在这里,异步与阻塞体现在:

  • 异步:排到小J的话柜员会喊他的号码;
  • 阻塞:等待喊号的过程中,不能做其他事情。

3 同步非阻塞形式

实际上效率也是低下。小J在排队的过程中可以打电话,但是要边打电话边看看还有多久才排到他。如果将打电话和观察排队情况看成是程序中的两个操作的话,这个程序需要在这两个不同的行为之间来回切换。

在这里,同步与非阻塞体现在:

  • 同步:排队等待轮到他办理业务;
  • 非阻塞:可以在排队的过程中打电话,只不过要时不时看看还要多久才排到他办理业务。

4 异步非阻塞形式

这是一个效率更高的模式。小J在拿号之后可以去打电话,只要等待柜员喊号就可以了,在这里打电话是等待着的事情,而通知小J办理业务是柜员的事情。

在这里,异步和非阻塞体现在:

  • 异步:柜员喊小J去办理业务;
  • 非阻塞:在等待喊号的过程中,小J去打电话,只要接收到柜员喊号的通知即可,无需关注是否队伍的进度。

也就是说,同步和异步仅需关注消息如何通知的机制,而阻塞和非阻塞关注的是在等待消息通知的过程中能不能去做别的事。在同步情况下,是由处理者自己去等待消息是否被触发,而异步情况下是由触发机制来通知处理者处理业务。

回到程序里,把它们和线程关联起来:

  • 同步阻塞,相当于一个线程在等待。
  • 同步非阻塞,相当于一个线程在正常运行。
  • 异步阻塞,相当于多个线程都在等待。
  • 异步非阻塞,相当于多个线程都在正常运行。

I/O

IO指的就是读入/写出数据的过程,和等待读入/写出数据的过程。一旦拿到数据后就变成了数据操作了,就不是IO了。拿网络IO来说,等待的过程就是数据从网络到网卡再到内核空间。读写的过程就是内核空间和用户空间的相互拷贝。所以IO就包括两个过程,一个是等待数据的过程,一个是读写(拷贝)数据的过程。而且还要明白,一定能包括操作数据的过程。

阻塞IO和非阻塞IO

应用程序都是运行在用户空间的,所以它们能操作的数据也都在用户空间。按照这样子来理解,只要数据没有到达用户空间,用户线程就操作不了。

如果此时用户线程已经参与,那它一定会被阻塞在IO上。这就是常说的阻塞IO。用户线程被阻塞在等待数据上或拷贝数据上。

非阻塞IO就是用户线程不参与以上两个过程,即数据已经拷贝到用户空间后,才去通知用户线程,一上来就可以直接操作数据了。

用户线程没有因为IO的事情出现阻塞,这就是常说的非阻塞IO。

同步IO和同步阻塞IO

按照上文中对同步的理解,同步IO是指发起IO请求后,必须拿到IO的数据才可以继续执行。

按照程序的表现形式又分为两种:

在等待数据的过程中,和拷贝数据的过程中,线程都在阻塞,这就是同步阻塞IO。

在等待数据的过程中,线程采用死循环式轮询,在拷贝数据的过程中,线程在阻塞,这其实还是同步阻塞IO。

严格来讲,在IO的概念上,同步和非阻塞是不可能搭配的,因为它们是一对相悖的概念。

同步IO意味着必须拿到IO的数据,才可以继续执行。因为后续操作依赖IO数据,所以它必须是阻塞的。

非阻塞IO意味着发起IO请求后,可以继续往下执行。说明后续执行不依赖于IO数据,所以它肯定不是同步的。

因此,在IO上,同步和非阻塞是互斥的,所以不存在同步非阻塞IO。但同步非阻塞是存在的,那不叫IO,叫操作数据了。

所以,同步IO一定是阻塞IO,同步IO也就是同步阻塞IO。

异步IO和异步阻塞/非阻塞IO

按照上文中对异步的理解,异步IO是指发起IO请求后,不用拿到IO的数据就可以继续执行。

用户线程的继续执行,和操作系统准备IO数据的过程是同时进行的,因此才叫做异步IO。

按照IO数据的两个过程,又可以分为两种:

在等待数据的过程中,用户线程继续执行,在拷贝数据的过程中,线程在阻塞,这就是异步阻塞IO。

在等待数据的过程中,和拷贝数据的过程中,用户线程都在继续执行,这就是异步非阻塞IO。

第一种情况是,用户线程没有参与数据等待的过程,所以它是异步的。但用户线程参与了数据拷贝的过程,所以它又是阻塞的。合起来就是异步阻塞IO。

第二种情况是,用户线程既没有参与等待过程也没有参与拷贝过程,所以它是异步的。当它接到通知时,数据已经准备好了,它没有因为IO数据而阻塞过,所以它又是非阻塞的。合起来就是异步非阻塞IO。

IO模型

在我们了解Linux操作系统的IO操作,以及同步与异步、阻塞与非阻塞的概念之后,我们来看看Linux系统中根据同步、异步、阻塞、非阻塞实现的五种IO模型。以Linux下的系统调用recv为例,是一个用于从套接字上接收一个消息,因为是系统调用,所以在调用的时候,会从用户空间切换到内核空间运行一段时间后,再切换回来。在默认情况下recv会等到网络数据到达并复制到用户空间或发生错误时返回。

1 同步阻塞IO模型

从系统调用recv到将数据从内核复制到用户空间并返回,在这段时间内进程始终阻塞

 

JAVA中的BIO中,底层采用的就是这种IO模型

 2 同步非阻塞IO模型

在这里recv不管有没有获得到数据都返回,如果没有数据的话就过段时间再调用recv看看,如此循环。这个模型只有在检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(办理业务),因此它还是同步IO。

 

 

3 IO复用模型

在IO复用模型中,调用recv之前会先调用select或poll,这两个系统调用都可以在内核准备好数据(网络数据已经到达内核了)时告知用户进程,它准备好了,这时候再调用recv时是一定有数据的。因此在这一模型中,进程阻塞于select或poll,而没有阻塞在recv上

4 信号驱动IO模型

此处会通过调用sigaction注册信号函数,在内核数据准备好的时候系统就中断当前程序,执行信号函数(在这里调用recv)

 

5 异步IO模型

调用aio_read令内核把数据准备好,并且复制到用户进程空间后执行事先指定好的函数

 

我们可以看到,前四种模型都是属于同步IO,因为在内核数据复制到用户空间的这一过程都是阻塞的。而最后一种异步IO,通过将IO操作交给操作系统处理,当前进程不关心具体IO的实现,后来再通过回调函数,或信号量通知当前进程直接对IO返回结果进行处理。

select、poll、epoll都是I/O多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个文件描述符,一旦某个描述符就绪(读就绪或写就绪),能够通知程序进行相应的读写操作 。

但是,select,poll,epoll本质还是同步I/O(I/O多路复用本身就是同步IO)的范畴,因为它们都需要在读写事件就绪后线程自己进行读写,读写的过程阻塞的。而异步I/O的实现是系统会把负责把数据从内核空间拷贝到用户空间,无需线程自己再进行阻塞的读写,内核已经准备完成。

IO复用模型

1 Select机制

API简介

linux系统中/usr/include/sys/select.h文件中对select方法的定义如下:

/* fd_set for select and pselect.  */
typedef struct
  { 
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
    #ifdef __USE_XOPEN
        __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    # define __FDS_BITS(set) ((set)->fds_bits)
    #else
        __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
    # define __FDS_BITS(set) ((set)->__fds_bits)
    #endif
  } fd_set;

/* Check the first NFDS descriptors each in READFDS (if not NULL) for read
   readiness, in WRITEFDS (if not NULL) for write readiness, and in EXCEPTFDS
   (if not NULL) for exceptional conditions.  If TIMEOUT is not NULL, time out
   after waiting the interval specified therein.  Returns the number of ready
   descriptors, or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int select (int __nfds, fd_set *__restrict __readfds,
                   fd_set *__restrict __writefds,
                   fd_set *__restrict __exceptfds,
                   struct timeval *__restrict __timeout);

int __nfdsfd_set中最大的描述符+1,当调用select时,内核态会判断fd_set中描述符是否就绪,__nfds告诉内核最多判断到哪一个描述符。

__readfds、__writefds、__exceptfds都是结构体fd_set,fd_set可以看作是一个描述符的集合。 select函数中存在三个fd_set集合,分别代表三种事件,readfds表示读描述符集合,writefds表示读描述符集合,exceptfds表示异常描述符集合。当对应的fd_set = NULL时,表示不监听该类描述符。

timeval __timeout用来指定select的工作方式,即当文件描述符尚未就绪时,select是永远等下去,还是等待一定的时间,或者是直接返回

函数返回值int表示: 就绪描述符的数量,如果为-1表示产生错误 。

运行机制

Select会将全量fd_set从用户空间拷贝到内核空间,并注册回调函数, 在内核态空间来判断每个请求是否准备好数据 。select在没有查询到有文件描述符就绪的情况下,将一直阻塞(I/O多路服用中提过:select是一个阻塞函数)。如果有一个或者多个描述符就绪,那么select将就绪的文件描述符置位,然后select返回。返回后,由程序遍历查看哪个请求有数据。

Select的缺陷

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,fd越多开销则越大;
  • 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量有限,默认是1024。参见/usr/include/linux/posix_types.h中的定义:

# define __FD_SETSIZE 1024

2 Poll机制

API简介

linux系统中/usr/include/sys/poll.h文件中对poll方法的定义如下:

/* Data structure describing a polling request.  */
struct pollfd
  {
    int fd;                     /* File descriptor to poll.  */
    short int events;           /* Types of events poller cares about.  */
    short int revents;          /* Types of events that actually occurred.  */
  };

/* Poll the file descriptors described by the NFDS structures starting at
   FDS.  If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
   an event to occur; if TIMEOUT is -1, block until an event occurs.
   Returns the number of file descriptors with events, zero if timed out,
   or -1 for errors.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int poll (struct pollfd *__fds, nfds_t __nfds, int __timeout);

__fds参数时Poll机制中定义的结构体pollfd,用来指定一个需要监听的描述符。结构体中fd为需要监听的文件描述符,events为需要监听的事件类型,而revents为经过poll调用之后返回的事件类型,在调用poll的时候,一般会传入一个pollfd的结构体数组,数组的元素个数表示监控的描述符个数。

__nfds__timeout参数都和Select机制中的同名参数含义类似

运行机制

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构代替select的fd_set(网上讲:类似于位图)结构,其他的本质上都差不多。所以Poll机制突破了Select机制中的文件描述符数量最大为1024的限制

Poll的缺陷

Poll机制相较于Select机制中,解决了文件描述符数量上限为1024的缺陷。但另外两点缺陷依然存在:

  • 每次调用poll,都需要把fd集合从用户态拷贝到内核态,fd越多开销则越大;
  • 每次调用poll,都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

3 Epoll机制

Epoll在Linux2.6内核正式提出,是基于事件驱动的I/O方式。相对于select来说,epoll没有描述符个数限制;使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,通过内存映射,使其在用户空间也可直接访问,省去了拷贝带来的资源消耗。

API简介

linux系统中/usr/include/sys/epoll.h文件中有如下方法:

/* Creates an epoll instance.  Returns an fd for the new instance.
   The "size" parameter is a hint specifying the number of file
   descriptors to be associated with the new instance.  The fd
   returned by epoll_create() should be closed with close().  */
extern int epoll_create (int __size) __THROW;

/* Manipulate an epoll instance "epfd". Returns 0 in case of success,
   -1 in case of error ( the "errno" variable will contain the
   specific error code ) The "op" parameter is one of the EPOLL_CTL_*
   constants defined above. The "fd" parameter is the target of the
   operation. The "event" parameter describes which events the caller
   is interested in and any associated user data.  */
extern int epoll_ctl (int __epfd, int __op, int __fd,
                      struct epoll_event *__event) __THROW;

/* Wait for events on an epoll instance "epfd". Returns the number of
   triggered events returned in "events" buffer. Or -1 in case of
   error with the "errno" variable set to the specific error code. The
   "events" parameter is a buffer that will contain triggered
   events. The "maxevents" is the maximum number of events to be
   returned ( usually size of "events" ). The "timeout" parameter
   specifies the maximum wait time in milliseconds (-1 == infinite).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int epoll_wait (int __epfd, struct epoll_event *__events,
                       int __maxevents, int __timeout);

epoll_create函数:创建一个epoll实例并返回,该实例可以用于监控__size个文件描述符

epoll_ctl函数:向epoll中注册事件,该函数如果调用成功返回0,否则返回-1。

  • __epfd为epoll_create返回的epoll实例
  • __op表示要进行的操作
  • __fd为要进行监控的文件描述符
  • __event要监控的事件

epoll_wait函数:类似与select机制中的select函数、poll机制中的poll函数,等待内核返回监听描述符的事件产生。该函数返回已经就绪的事件的数量,如果为-1表示出错。

  • __epfd为epoll_create返回的epoll实例
  • __events数组为 epoll_wait要返回的已经产生的事件集合
  • __maxevents为希望返回的最大的事件数量(通常为__events的大小)
  • __timeout和select、poll机制中的同名参数含义相同

运行机制

epoll操作过程需要上述三个函数,也正是通过三个函数完成Select机制中一个函数完成的事情,解决了Select机制的三大缺陷。epoll的工作机制更为复杂,我们就解释一下,它是如何解决Select机制的三大缺陷的。

  1. 对于第一个缺点,epoll的解决方案是:它的fd是共享在用户态和内核态之间的,所以可以不必进行从用户态到内核态的一个拷贝,大大节约系统资源。至于如何做到用户态和内核态,大家可以查一下“mmap”,它是一种内存映射的方法。
  2. 对于第二个缺点,epoll的解决方案不像select或poll一样每次都把当前线程轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把当前线程挂一遍(这一遍必不可少),并为每个fd指定一个回调函数。当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。那么当我们调用epoll_wait时,epoll_wait只需要检查链表中是否有存在就绪的fd即可,效率非常可观
  3. 对于第三个缺点,fd数量的限制,也只有Select存在,Poll和Epoll都不存在。由于Epoll机制中只关心就绪的fd,它相较于Poll需要关心所有fd,在连接较多的场景下,效率更高。在1GB内存的机器上大约是10万左右,一般来说这个数目和系统内存关系很大。

工作模式

相较于Select和Poll,Epoll内部还分为两种工作模式: LT水平触发(level trigger)ET边缘触发(edge trigger)

  • LT模式: 默认的工作模式,即当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;事件会被放回到就绪链表中,下次调用epoll_wait时,会再次通知此事件。
  • ET模式: 当epoll_wait检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应并通知此事件。

由于上述两种工作模式的区别,LT模式同时支持block和no-block socket两种,而ET模式下仅支持no-block socket。即epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个fd的阻塞I/O操作把多个处理其他文件描述符的任务饿死。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。

Epoll的优点

  • 使用内存映射技术,节省了用户态和内核态间数据拷贝的资源消耗;
  • 通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。I/O的效率不会随着监视fd的数量的增长而下降;
  • 文件描述符数量不再受限;

4 Select、Poll、Epoll机制的对比

下图主流I/O多路复用机制的benchmark:

当并发fd较小时,Select、Poll、Epoll的响应效率想差无几,甚至Select和Poll更胜一筹。但是当并发连接(fd)较多时,Epoll的优势便真正展现出来。

总结三种模式的区别:

select/poll 和 epoll 比较

详细内容,参考:

  • select poll epoll三者之间的比较

select/poll 系统调用:

// select 系统调用

int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);

// poll 系统调用

int poll(struct pollfd fds[], nfds_t nfds, int timeout); 

select:

  • 查询 fd_set 中,是否有就绪的 fd,可以设定一个超时时间,当有 fd (File descripter) 就绪或超时返回;
  • fd_set 是一个位集合,大小是在编译内核时的常量,默认大小为 1024

特点:

  • 连接数限制,fd_set 可表示的 fd 数量太小了;
  • 线性扫描:判断 fd 是否就绪,需要遍历一边 fd_set;
  • 数据复制:用户空间和内核空间,复制连接就绪状态信息

poll:

  • 解决了连接数限制:
  • poll 中将 select 中的 fd_set 替换成了一个 pollfd 数组
  • 解决 fd 数量过小的问题
  • 数据复制:用户空间和内核空间,复制连接就绪状态信息

epoll: event 事件驱动

  • 事件机制:避免线性扫描
  • 为每个 fd,注册一个监听事件
  • fd 变更为就绪时,将 fd 添加到就绪链表
  • fd 数量:无限制(OS 级别的限制,单个进程能打开多少个 fd)

select,poll,epoll:

  1. I/O多路复用的机制;
  2. I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
  3. 监视多个文件描述符
  4. 但select,poll,epoll本质上都是同步I/O:
  5. 用户进程负责读写(从内核空间拷贝到用户空间),读写过程中,用户进程是阻塞的;
  6. 异步 IO,无需用户进程负责读写,异步IO,会负责从内核空间拷贝到用户空间;

通过上述的一些总结,希望我们对I/O多路复用的Select、Poll、Epoll机制有一个更深刻的认识。也要明白为什么epoll会成为Linux平台下实现高性能网络服务器的首选I/O多路复用机制。

5 Epoll的使用场景

上面的文章中已经不断介绍了Epoll机制的优势,又提到它是Linux平台下实现高性能网络服务器的首选I/O复用机制。实际工作中,我们在哪里会用到它?怎么用呢?

比如下面代码,就是我们使用高性能网络框架Netty实现IM项目中对于netty的bossGroup和workerGroup以及serverChannel的配置

String os = System.getProperty("os.name");
if(os.toLowerCase().startsWith("win") || os.toLowerCase().startsWith("mac")){
    // 点开NioEventLoopGroup的源码,对于这个类是这么注释的
    // MultithreadEventLoopGroup implementations which is used for NIO Selector based Channel
    bossGroup = new NioEventLoopGroup(1);
    workerGroup = new NioEventLoopGroup(4);
}else{
    // 点开EpollEventLoopGroup的源码,对于这个类是这么注释的
    // EventLoopGroup which uses epoll under the covers. Because of this it only works on linux.
    bossGroup = new EpollEventLoopGroup(1);
    workerGroup = new EpollEventLoopGroup(4);
}
bootStrap = new ServerBootstrap();
bootStrap.group(bossGroup,workerGroup);
if(os.toLowerCase().startsWith("win") || os.toLowerCase().startsWith("mac")) {
    // NioServerSocketChannel implementation which uses NIO selector based implementation to accept new connections.
    bootStrap.channel(NioServerSocketChannel.class);
}else{
    // ServerSocketChannel implementation that uses linux EPOLL Edge-Triggered Mode for maximal performance.
    // 注意看注释中的“linux EPOLL Edge-Triggered Mode”,linux下ET模式的Epoll机制
    bootStrap.channel(EpollServerSocketChannel.class);
}

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值