Linux I/O模式

一、基本概念:

  ①用户空间与内核空间

    现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对Linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。
用户空间与内核空间

  ②进程切换

    为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

    从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
      ●保存处理机上下文,包括程序计数器和其他寄存器。
      ●更新PCB(Process Control Block)信息。
      ●把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
      ●选择另一个进程执行,并更新其PCB。
      ●更新内存管理的数据结构。
      ●恢复处理机上下文。

    可以看出,进程切换很耗资源。

  ③进程阻塞

    正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的。

  ④文件描述符

    文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

    文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

  ⑤缓存I/O

    缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

    缓存 I/O 的缺点:
      ●数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。


二、I/O模式:

  对于一次 I/O 访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
    ●等待数据准备(Waiting for the data to be ready)
    ●将数据从内核拷贝到进程中(Copying the data from the kernel to the process)

  正是因为这两个阶段,Linux系统产生了下面五种网络模式的方案:
    ●阻塞 I/O(blocking I/O)
    ●非阻塞 I/O(nonblocking I/O)
    ●I/O 多路复用( I/O multiplexing)
    ●信号驱动 I/O( signal driven I/O)
    ●异步 I/O(asynchronous I/O)

  ①阻塞 I/O(blocking I/O)

    在Linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
阻塞 I/O模型
    当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

    第一阶段(阻塞):
      ●①进程向内核发起系统调用(recvfrom)。当进程发起调用后,进程开始挂起(进程进入不可中断睡眠状态),进程一直处于等待内核处理结果的状态,此时的进程不能处理其他I/O,亦被阻塞。
      ●②内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核亦不会给进程发送任何消息,直到磁盘中的数据加载至内核缓冲区。
    第二阶段(阻塞):
      ●③内核将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成。
      ●④内核返回成功数据处理完成的指令给进程;进程在收到指令后再对数据包进程处理;处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作。

    特点:I/O执行的两个阶段进程都是阻塞的。

    优点:
      ●①能够及时的返回数据,无延迟。
      ●②程序简单,进程挂起基本不会消耗CPU时间。

    缺点:
      ●①I/O等待对性能影响较大。
      ●②每个连接需要独立的一个进程/线程处理,当并发请求量较大时为了维护程序,内存、线程和CPU上下文切换开销较大,因此较少在开发环境中使用。

  ②非阻塞 I/O(nonblocking I/O)

    Linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
非阻塞 I/O模型
    当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

    第一阶段(非阻塞):
      ●①进程向内核发起IO调用请求,内核接收到进程的I/O调用后准备处理并返回“error”的信息给进程。此后每隔一段时间进程都会向内核发起询问是否已处理完(即轮询),此过程称为为忙等待。
      ●②内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核会给进程发送error信息,直到磁盘中的数据加载至内核缓冲区。
    第二阶段(阻塞):
      ●③内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段,进程阻塞),直到数据复制完成。
      ●④内核返回成功数据处理完成的指令给进程,进程在收到指令后再对数据包进程处理。

    特点:non-blocking I/O模式需要不断的主动询问kernel数据是否已准备好。

    优点:进程在等待当前任务完成时,可以同时执行其他任务。进程不会被阻塞在内核等待数据过程,每次发起的I/O请求会立即返回,具有较好的实时性。

    缺点:不断的轮询将占用大量的CPU时间,系统资源利用率大打折扣,影响性能,整体数据的吞吐量下降。该模型不适用web服务器。

  ③I/O 多路复用( I/O multiplexing)

    I/O multiplexing就是常说的select、poll、epoll,有些地方也称这种I/O方式为event driven I/O(事件驱动式I/O模型)。
    select/epoll的好处就在于单个process就可以同时处理多个网络连接的I/O。它的基本原理就是select、poll、epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
在这里插入图片描述
    当用户进程调用了select,那么整个进程会被block。同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

    第一阶段(阻塞在select/poll之上):
      ●①进程向内核发起select/poll的系统调用,select将该调用通知内核开始准备数据,而内核不会返回任何通知消息给进程,但进程可以继续处理更多的网络连接I/O。
      ●②内核收到进程的系统调用请求后,此时的数据包并未准备好,此时内核亦不会给进程发送任何消息,直到磁盘中的数据加载至内核缓冲区。而后通过select()/poll()函数将socket的可读条件返回给进程。
    第二阶段(阻塞):
      ●③进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom)。
      ●④内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成。
      ●⑤内核返回成功数据处理完成的指令给进程,进程在收到指令后再对数据包进程处理。处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作。

    特点:通过一种机制能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个变为可读就绪状态,select()/poll()函数就会返回。

    优点:可以基于一个阻塞对象,同时在多个描述符上可读就绪,而不是使用多个线程(每个描述符一个线程),即能处理更多的连接。这样可以节省更多的系统资源。

    缺点:如果处理的连接数不是很多的话,使用select/poll的web server不一定比使用multi-threading + blocking I/O的web server性能更好,反而可能延迟还更大。原因在于,处理一个连接需要发起两次system call。

  ④信号驱动 I/O(signal driven I/O)

    信号驱动式I/O是指进程预先告知内核,使得某个文件描述符上发生了变化时,内核使用信号通知该进程。
    在信号驱动式I/O模型,进程使用socket进行信号驱动I/O,并建立一个SIGIO信号处理函数,当进程通过该信号处理函数向内核发起I/O调用时,内核并没有准备好数据报,而是返回一个信号给进程,此时进程可以继续发起其他I/O调用。也就是说,在第一阶段内核准备数据的过程中,进程并不会被阻塞,会继续执行。当数据报准备好之后,内核会递交SIGIO信号,通知用户空间的信号处理程序,数据已准备好;此时进程会发起recvfrom的系统调用,这一个阶段与阻塞式I/O无异。也就是说,在第二阶段内核复制数据到用户空间的过程中,进程同样是被阻塞的。
信号驱动 I/O模型
    第一阶段(非阻塞):
      ●①进程使用socket进行信号驱动I/O,建立SIGIO信号处理函数,向内核发起系统调用,内核在未准备好数据报的情况下返回一个信号给进程,此时进程可以继续做其他事情。
      ●②内核将磁盘中的数据加载至内核缓冲区完成后,会递交SIGIO信号给用户空间的信号处理程序。
    第二阶段(阻塞):
      ●③进程在收到SIGIO信号程序之后,进程向内核发起系统调用(recvfrom)。
      ●④内核再将内核缓冲区中的数据复制到用户空间中的进程缓冲区中(真正执行IO过程的阶段),直到数据复制完成。
      ●⑤内核返回成功数据处理完成的指令给进程,进程在收到指令后再对数据包进程处理。处理完成后,此时的进程解除不可中断睡眠态,执行下一个I/O操作。

    特点:借助socket进行信号驱动I/O并建立SIGIO信号处理函数。

    优点:线程并没有在第一阶段(数据等待)时被阻塞,提高了资源利用率。

    缺点:
      ●①在程序的实现上比较困难。
      ●②信号 I/O 在大量 I/O 操作时可能会因为信号队列溢出导致没法通知。信号驱动 I/O 尽管对于处理 UDP 套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于 TCP 而言,信号驱动的 I/O 方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失。

    因此,信号驱动 I/O 在实际中并不常用

    信号通知机制:
      ●水平触发:指数据报到内核缓冲区准备好之后,内核通知进程后,进程因繁忙未发起recvfrom系统调用;内核会再次发送通知信号,循环往复,直到进程来请求recvfrom系统调用。很明显,这种方式会频繁消耗过多的系统资源。
      ●边缘触发:内核只会发送一次通知信号。

  ⑤异步 I/O(asynchronous I/O)

    Linux下的asynchronous I/O其实用得很少。
异步 I/O模型
    用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先,它会立刻返回,所以不会对用户进程产生任何block;然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

    第一阶段(非阻塞):
      ●①进程向内核请求aio_read(异步读)的系统调用操作,会把套接字描述符、缓冲区指针、缓冲区大小和文件偏移一起发给内核,当内核收到后会返回“已收到”的消息给进程。
      ●②内核将磁盘中的数据加载至内核缓冲区,直到数据报准备好。
    第二阶段(非阻塞):
      ●③内核开始复制数据,将准备好的数据报复制到进程内存空间,直到数据报复制完成。
      ●④内核向进程递交aio_read的返回指令信号,通知进程数据已复制到进程内存中。

    特点:第一阶段和第二阶段都是由内核完成。

    优点:能充分利用DMA的特性,将I/O操作与计算重叠,提高性能、资源利用率与并发能力

    缺点:
      ●①在程序的实现上比较困难。
      ●②要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。而在 Linux 系统下,Linux 2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 复用式I/O模型为主。

  ⑥总结

    1、阻塞式I/O与非阻塞式I/O的区别:

      阻塞式I/O会一直阻塞对应的进程,直至操作完成。
      非阻塞式I/O在内核还在准备数据的情况下,会立即返回“error”给对应的进程。

    2、同步I/O与异步I/O的区别:

      根据POSIX中的定义:
        ●同步I/O操作会导致请求进程被阻塞,直到I/O操作完成为止。(A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes)
        ●异步I/O操作不会导致请求进程被阻止。(An asynchronous I/O operation does not cause the requesting process to be blocked)
      两者的区别就在于,同步I/O在I/O操作时会将进程阻塞。

      这里值得注意的是,非阻塞式I/O在第一阶段并没有阻塞,但是POSIX中定义的“I/O操作”是指的整个I/O操作,即recvfrom这个system call的时候。如果数据报没准备好,不会阻塞进程,一旦数据准备好,recvfrom将内核缓冲区中将数据报拷贝到用户进程时,这个时候进程是被阻塞的。因此,非阻塞式I/O属于同步I/O。

    3、五种I/O模型对比:

五种I/O模型对比
      同步与异步的区别在于调用操作系统的recvfrom函数的时候是否阻塞,可见除了最后的异步I/O,其它都是同步I/O。


三、I/O 多路复用之select、poll、epoll:

  select、poll、epoll都是I/O多路复用的机制。I/O多路复用就是通过某种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
  但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的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

  ②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的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

    1、epoll操作过程

      epoll操作过程需要三个接口,分别如下:

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

        创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议
        当创建好epoll句柄后,它就会占用一个fd值,在Linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

      ⅱ、epoll_ctl

        函数是对指定描述符fd执行op操作。
        - epfd:是epoll_create()的返回值。
        - op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
        - fd:是需要监听的fd(文件描述符)
        - epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

// events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
      ⅲ、epoll_wait

        等待epfd上的io事件,最多返回maxevents个事件。
        参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

    2、工作模式

      epoll对文件描述符的操作有两种模式:LT(level triggered)和ET(edge triggered)。LT模式是默认模式,LT模式与ET模式的区别如下:
        ●LT模式(边沿触发):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
        ●ET模式(水平触发):当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

      ⅰ、LT模式

        LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行I/O操作。如果你不作任何操作,内核还是会继续通知你的。

      ⅱ、ET模式

        ET(edge triggered)是高速工作方式,只支持non-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送、接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作I/O操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。

        ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞I/O操作把处理多个文件描述符的任务饿死。

    3、epoll总结

      在 select/poll 中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符。当进程调用epoll_wait()时便能得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在)

      epoll的优点主要是一下几个方面:
        ●监视的描述符数量不受限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案(Apache就是这样实现的),不过虽然Linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
        ●I/O的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

      如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection(空闲连接),就会发现epoll的效率大大高于select/poll。

  ④总结

在这里插入图片描述

    1、select

      POSIX所规定,目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。

      缺点:
        ●单个进程可监视的fd数量被限制,即能监听端口的数量有限。数值存在如下文件里:

cat /proc/sys/fs/file-max

        ●对socket是线性扫描,即采用轮询的方法,效率较低。
        ●select采取了内存拷贝方法来实现内核将fd消息通知给用户空间,这样一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    2、poll

      本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态。
        ●其没有最大连接数的限制,原因是它是基于链表来存储的。
        ●大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
        ●poll特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
        ●相对“水平触发”还有“边缘触发”:只通知一次。epoll用的就是边缘触发。

    3、epoll

      在Linux2.6内核中提出的select和poll的增强版本。
        ●支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。
        ●使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

      优点:
        ●没有最大并发连接的限制:能打开的fd的上限远大于1024(1G的内存能监听约10万个端口)。
        ●效率提升:非轮询的方式,不会随着fd数目的增加而效率下降;只有活跃可用的fd才会调用callback函数,即epoll最大的优点就在于它只管理“活跃”的连接,而跟连接总数无关。
        ●内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递,即epoll使用mmap减少复制开销。
        ●文件映射内存直接通过地址空间访问,效率更高,把文件映射到内存中。


四、模型的使用:

  ①httpd使用的模型

    prefork模型是复用性I/O模型,以多进程的方式处理请求。
    worker模型是复用性I/O模型,只不过是以多进程多线程的方式处理请求。
    event模型是使用的信号驱动式I/O模型,httpd2.4也实现异步I/O模型。

  ②Nginx使用的模型

    Nginx是基于信号驱动式I/O模型,使用边缘触发来实现,所以并发能力强、性能好,并且支持异步I/O。能完成基于mmap内存映射机制完成数据的发放。

  ③Java使用的模型

    Java出生自带的BIO,Blocking I/O,它使用的是阻塞I/O模型。
    JDK1.4开始支持的NIO,New I/O,它使用的是多路复用的I/O模型。
    JDK1.7开始支持的AIO,Asynchronous I/O,使用的是异步I/O模型,算是一种比较完美的I/O。

  ④Redis使用的模型

    Redis中的I/O模型是多路复用的I/O模型。多路复用API可以同时监听多个客户端的读写操作;多路复用API会检测客户端的请求是读操作还是写操作,往往API会有一个timeout,在这个时间内redis线程会阻塞,进行套接字的监听;redis会给每个客户端套接字匹配一个指令队列,按照队列进行处理,同时也会将操作结果放到输出队列。

  ⑤Netty使用的模型

    Netty中的I/O模型是主从Reactor多线程NIO结构,它使用的是多路复用的I/O模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值