一、相关概念

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

  • 同步和异步:用户程序通过recv()进行系统调用后,如果最后还需要调用read(),则是同步的;否则就是异步的。

  • 阻塞和非阻塞:用户程序通过recv()进行系统调用后,如果还可以做其它事情,则是非阻塞的,反之则是阻塞的。

概念有些抽象,可以通过以下的例子来理解一下这组概念:

假设小明需要在网上下载一个软件:

  • 同步阻塞:如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成;
  • 异步阻塞(比较罕见):如果小明点击下载按钮之后,不用瞪着进度条但也不做其他任何事情直到软件下载完成,软件下载完成是会「叮」的一声通知的。
  • 同步非阻塞:如果小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了
  • 异步非阻塞:如果小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件

1.2 用户态和内核态

1.2.1 用户态和内核态的基本概念

  • 用户态:就是提供应用程序运行的空间,为了使应用程序访问到内核管理的资源例如CPU,内存,I/O。
  • 内核态:控制计算机的硬件资源,例如协调CPU资源,分配内存资源,并且提供稳定的环境供应用程序运行。

用户空间和内核空间

1.2.2 用户态切换到内核态的方式

  • 中断:当外设完成用户的请求时,会向CPU发送中断信号。
  • 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
  • 系统调用:本质上也是中断,属于软中断的一种。

1.3 文件描述符

内核(kernel)利用文件描述符(file descriptor)来访问文件。文件描述符是非负整数。打开现存文件或新建文件时,内核会返回一个文件描述符。读写文件也需要使用文件描述符来指定待读写的文件。

二、IO模型

2.1 同步阻塞IO

  • 应用进程向内核发起recfrom读取数据。

  • 准备数据报(应用进程阻塞)。

  • 将数据从内核负责到应用空间。

  • 复制完成后,返回成功提示。

同步阻塞IO

2.2 同步非阻塞IO

  • 应用进程向内核发起recvfrom读取数据。
  • 没有数据报准备好,即刻返回EWOULDBLOCK错误码。
  • 应用进程向内核发起recvfrom读取数据。
  • 已有数据包准备好就进行一下 步骤,否则还是返回错误码。
  • 将数据从内核拷贝到用户空间。
  • 完成后,返回成功提示。

同步非阻塞IO

2.3 IO多路复用

定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力,linux有三种实现方式,分别为select,poll和epoll。

IO多路复用

2.3.1 select

  • 源码
/* According to POSIX.1-2001, POSIX.1-2008 */
    #include <sys/select.h>
    /* According to earlier standards */
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>

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

    int pselect(int nfds, fd_set *readfds, fd_set *writefds,
                fd_set *exceptfds, const struct timespec *timeout,
                const sigset_t *sigmask);

    void FD_CLR(int fd, fd_set *set);
    int  FD_ISSET(int fd, fd_set *set);
    void FD_SET(int fd, fd_set *set);
    void FD_ZERO(fd_set *set);

select的调用会阻塞到有文件描述符可以进行IO操作或被信号打断或者超时才会返回。

select将监听的文件描述符分为三组,每一组监听不同的需要进行的IO操作。readfds是需要进行读操作的文件描述符,writefds是需要进行写操作的文件描述符,exceptfds是需要进行异常事件处理的文件描述符。这三个参数可以用NULL来表示对应的事件不需要监听。

当select返回时,每组文件描述符会被select过滤,只留下可以进行对应IO操作的文件描述符。

FD_xx系列的函数是用来操作文件描述符组和文件描述符的关系。

FD_ZERO用来清空文件描述符组。每次调用select前都需要清空一次。

select可同时监听的文件描述符数量是通过FS_SETSIZE来限制的,在Linux系统中,该值为1024。

2.3.2 poll

  • 源码
#include <poll.h>

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

    #include <signal.h>
    #include <poll.h>

    int ppoll(struct pollfd *fds, nfds_t nfds,
            const struct timespec *tmo_p, const sigset_t *sigmask);

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

poll和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件,revents是所有内核监测到的事件。

poll在linux下没有监听的文件描述符数量限制,但是如果过多的话,速度也会变慢。

2.3.3 epoll

  • 源码
#include <sys/epoll.h>

    int epoll_create(int size);
    int epoll_create1(int flags);

    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);
    int epoll_pwait(int epfd, struct epoll_event *events,
                int maxevents, int timeout,
                const sigset_t *sigmask);

epoll_create用于创建一个epoll实例,而epoll_ctl用于往epoll实例中增删改要监测的文件描述符,epoll_wait则用于阻塞的等待可以执行IO操作的文件描述符直到超时。

epoll优于select&poll在下面几点:

  • 在需要同时监听的文件描述符数量增加时,select&poll是O(N)的复杂度,epoll是O(1),在N很小的情况下,差距不会特别大,但如果N很大的前提下,一次O(N)的循环可要比O(1)慢很多,所以高性能的网络服务器都会选择epoll进行IO多路复用。
  • epoll内部用一个文件描述符挂载需要监听的文件描述符,这个epoll的文件描述符可以在多个线程/进程共享,所以epoll的使用场景要比select&poll要多。

2.4 异步IO

进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。  
这个模型工作机制是:告诉内核启动某个操作,并让内核在整个操作(包括第二阶段,即将数据从内核拷贝到进程缓冲区中)完成后通知我们。

异步IO模型