关于 阻塞和非阻塞 I/O 以及同步和异步 I/O,非常推荐下面这个回答:

IO多路复用到底是不是异步的? - 知乎
简单说一句话,你需要分层看这个事:epoll 这个系统调用,是同步的,也就是必须等待操作系统返回值。而底…

阻塞IO

应用进程接收数据

  1. 应用进程通过系统调用通知系统内核接收数据
  2. 系统内核从硬件接收数据并放入内核进程的某个缓冲区
  3. 内核进程将这个缓冲区的数据复制到应用进程的内存空间中
  4. 应用进程获取输入数据

应用进程发送数据

  1. 应用进程将要发送的数据写入到内存空间的某个缓冲区
  2. 应用进程通过系统调用通知系统内核发送数据
  3. 内核进程将数据从应用进程的内存空间复制到内核进程的内存空间
  4. 内核进程通过指令通过特定 I/O 设备的控制器发送这些数据

整个过程应用进程是阻塞的,在等待内核进程完成操作。

非阻塞 IO

轮询

在应用进程通知系统内核接收数据后,不阻塞进程,用户进程可以通过轮询的方式,定期循环内核进程是否完成数据的接收。通常不应该把这种忙等的逻辑写入处理的逻辑中,而是使用一个单独的线程检查 I/O 是否完成。

信号驱动 I/O

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

异步 I/O

异步 I/O 与信号驱动 I/O 类似,区别在于完成的时机,信号驱动 I/O 在数据复制到内核进程的内存空间时发出 SIGIO 信号。

而异步 I/O 则是在数据复制到应用进程空间后发送信号。

前者还需要完成从内核进程复制到应用进程这个过程,期间应用进程阻塞。

看两个例子(来自:linux下aio异步读写详解与实例_Shreck66的博客-CSDN博客

异步读

#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<aio.h>


#define BUFFER_SIZE 1024

int MAX_LIST = 2;

int main(int argc,char **argv)
{
    //aio操作所需结构体
    struct aiocb rd;

    int fd,ret,couter;

    fd = open("test.txt",O_RDONLY);
    if(fd < 0)
    {
        perror("test.txt");
    }



    //将rd结构体清空
    bzero(&rd,sizeof(rd));


    //为rd.aio_buf分配空间
    rd.aio_buf = malloc(BUFFER_SIZE + 1);

    //填充rd结构体
    rd.aio_fildes = fd;
    rd.aio_nbytes =  BUFFER_SIZE;
    rd.aio_offset = 0;

    //进行异步读操作
    ret = aio_read(&rd);
    if(ret < 0)
    {
        perror("aio_read");
        exit(1);
    }

    couter = 0;
	// 循环等待异步读操作结束,这个过程主线程本可以执行其他操作
    while(aio_error(&rd) == EINPROGRESS)
    {
        printf("第%d次\n",++couter);
    }
    //获取异步读返回值
    ret = aio_return(&rd);

    printf("\n返回值为:%d",ret);
    return 0;
}

异步写

#include<stdio.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<fcntl.h>
#include<aio.h>

#define BUFFER_SIZE 1025

int main(int argc,char **argv)
{
    //定义 aio 控制块结构体
    struct aiocb wr;

    int ret,fd;

    char str[20] = {"hello,world"};

    //置零 wr 结构体
    bzero(&wr,sizeof(wr));

    fd = open("test.txt",O_WRONLY | O_APPEND);
    if(fd < 0)
    {
        perror("test.txt");
    }

    //为 aio.buf 申请空间
    wr.aio_buf = (char *)malloc(BUFFER_SIZE);
    if(wr.aio_buf == NULL)
    {
        perror("buf");
    }

    wr.aio_buf = str;

    // 填充 aiocb 结构
    wr.aio_fildes = fd;
    wr.aio_nbytes = 1024;

    // 异步写操作
    ret = aio_write(&wr);
    if(ret < 0)
    {
        perror("aio_write");
    }

    // 等待异步写完成,此处主线程可进行其他操作
    while(aio_error(&wr) == EINPROGRESS)
    {
        printf("hello,world\n");
    }

    // 获得异步写的返回值
    ret = aio_return(&wr);
    printf("\n返回值为:%d\n",ret);

    return 0;
}

I/O 复用和同步(阻塞的和非阻塞),异步,信号驱动的 I/O 是不同层次的处理

I/O 复用

一个用户进程可以同时的使用多个文件描述符而并发的处理多个 I/O,具体的是使用系统调用(select / poll / epoll)监听多个文件描述符,当其中一个完成 I/O 时将返回应用进程(在等待的过程中仍然是阻塞的,但是依然可以使用非阻塞 I/O 的方式,通过轮询检查而不阻塞)。

select, poll, epoll 比较

3 者共同的作用都是从监听的多个文件描述符中返回数据传输已经完成的那个。

区别在于 poll 和 select 都是线性的遍历文件描述符检查数据传输是否完成,poll 解除了 select 只能监听 1024 个文件描述符的限制。

而 epoll 获取传输完成的文件描述符是通过一棵红黑树,复杂度在 log(n)。

I/O 复用的工作模式

LT (level trigger),水平触发模式

当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
一个事件只要有,就会一直触发。

ET (edge trigger),边缘触发模式

和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。 很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
对于边缘触发模式,只有一个事件从无到有才会触发。