{ Linux }

  • Zero-Copy

    |

    Linux Zero-Copy

    什么是零拷贝 ?

    Zero Copy 是一种避免 CPU 将数据从一块存储拷贝到另一块存储的技术。 Zero Copy 可以减少数据拷贝和共享总线操作的次数,消除传输数据再存储器之间不必要的拷贝次数,从而有效地提高数据传输地效率。

    Linux IO Copy

    • read() && write()

    当我们在访问一个网页的时候,在 Web Server (Linux) 会调用一下两个 文件读写函数:

    1
    2
    read(fd, buffer, len);
    write(sockfd, buffer, len);

    过程分析:

    1. 调用 read(),将具体的磁盘文件数据读取到 内核(kernel)的文件系统缓冲区中

    2. 接着是将 内核缓存区的数据 拷贝到 用户的缓冲区中

    3. 调用 write(),将用户缓冲区的数据写入到 内核 socket 的发送缓存区中

    4. 在 write() 返回后,内核会将 socket 发送区的数据拷贝到 网卡驱动中

    性能分析

    这个过程中,一共发生了四次 I/O copy, 这期间数据按照 kernel -> user -> kernel -> hard drive 的路线,在 内核用户 白白消耗了一圈的 性能开销,同时除了考虑 I/O 的性能开销,还要考虑系统 context switch 带来的开销,当系统调用 read() 时,系统会从 用户态 切换到 内核态,当 read() 返回时,又需要将 内核态 切换到 用户态,同理,write() 也会导致两次的 context switch,也就是说 read() 和 write() 总共会导致 4次的 I/O copy 和 4次上下文切换。

    • sendfile()

      而采用 sendfile()可减少在 read() & write() 所产生的多次 I/O 拷贝和 context switch

    1
    sendfile(sockfd, fd, NULL, len);

    过程分析:

    1. 将磁盘中的文件数据拷贝到 内核中的文件缓冲区

    2. 向 socket buffer 中 追加 当前的数据在 kernel buffer 中的位置和偏移量

    3. 根据 socket buffer 中的位置和偏移量,将 kernel buffer 中的数据 copy 到 网卡驱动中

    性能分析:

    这次过程中,sendfile() 相比于 read() & write() ,对于将要发送的数据 (socket) ,采用的是记录下对应的 数据在 kernel buffer 中的 位置和偏移量,在最后要发送 socket buffer的数据到网卡设备时,只需通过 位置及偏移量 找到对应 kernel buffer的数据。相比于 read / write, 少了两次 I/O copy,和两次 context switch,性能有了很大的提升。

    总结

    为什么说是 zero-copy 呢? 因为在 sendfile() 调用的过程中,对于内核 kernel ,整个过程中是零拷贝的,不涉及 内核到用户之间的数据拷贝。

  • IO Model

    |

    I / O 模型

    I / O 模型的概念大概有:阻塞 / 非阻塞 / 同步 / 异步

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    一个用户进程发起 I/O 请求的例子:

    Linux内核会将所有的外部设备当作一个文件来操作,与外部设备的交互均可等同于对文件进行操作。
    即文件的读写都是通过系统调用进行的。

    Linux内核通过 file descriptor处理本地文件的读写, socket file descriptor 处理 Socket 网络读写
    那么, I/O将会涉及两个系统对象,一个是调用它的用户线程(or thread),另一个是系统内核 (kernel)

    一个读写操作:
    1. 用户进程调用 read 方法向内核发起读请求b并等待就绪
    2. 内核将要读取的数据复制到文件描述所指向的内核缓存区 (系统准备 IO 数据)
    3. 内核将数据从内核缓存区复制到用户的进程空间

    阻塞 vs 非阻塞

    • 阻塞 ( Blocking IO ):用户发起 I/O 操作后,需要等待其操作完成之后才能继续运行

      • 特点:阻塞式 I/O 模型简单。易于理解,但性能差,会照成用户 CPU 大量闲置
      • 优化:可以采用多线程的方式进行请求调用,但并不能解决根本问题

    • 非阻塞 ( No-Blocking IO ):用户进程发起 I/O 操作后,无需等待操作完成,会直接返回调用结果,即如果数据没有准备好,会直接返回失败,这就需要用户进程要定期轮询 I/O 是否就绪

      • 特点:能立即得到返回结果,当使用一个线程去处理 socket 请求,可以极大减少线程数量。但用户线程会不断轮询会增加额外的 CPU 的资源开销

    • 总结:阻塞 IO 与 非阻塞 IO 的本质区别主要在于 用户程序是否再等待调用结果(继续等待还是得到结果先处理其他事情)

    同步 IO vs 异步 IO

    • 同步 IO ( Synchronous IO ) :当系统内核将处理数据操作准备完毕之后,会主动读取内核数据,用户进程需要等待内核将数据复制到用户进程之后,再进行处理
    • IO 多路复用 ( IO- Multiplexing ):可以监视多个描述符,一旦某个描述符读写操作就绪,就可以通知程序进行相应的读写操作

      应用:Linux中使用的 I/O 多路复用机制:select, poll,epoll ( event driven IO),尽管实现的方式不同,但都属于同步 IO,它们都需要在读写事件就绪后,再自己进行读写的操作,内核向用户进程复制数据的过程仍然是阻塞的。

    • 特点:尽管使用了事件驱动判断就绪,但与 Blocking-IO 并没有什么太大的不同,甚至在读取的过程中,因为会使用到两个 system call ( select, recvfrom),相比于 blocking-io 的一个 recvfrom,可能在连接数不高的情况下,性能会更差。但有了 select 的优势就在于系统可以同时处理多个 connnection,效率更高。

    • 异步 IO ( Asynchronous IO ) : 当用户进程发起 IO 请求后,会直接返回请求成功,等到再接受到内核的 signal 通知的时候, IO 操作已经完成了
    • 非阻塞 ( no-blocking io ) vs 异步io ( asynchronous io )

      no-blocking io:虽然大部分时间都不会 block (loop check data ready),但内核数据准备好之后,还是需要主动调用 recvfrom 系统调用进行数据的复制,这期间 process block

      asynchronous io:整个过程会将任务交由内核处理,直到 IO done,才会向用户进程发送信号通知成功

    • 总结:同步 IO 和 异步 IO的本质区别在于内核数据 复制到 用户空间的时候用户线程是否阻塞等待
    • 大总结