同步 同步就是发起一个调用后被调用者未处理完请求之前调用不返回。此时需要调用者询问处理结果异步 异步就是发起一个调用后立刻得到被调用者的回应表示已接收到请求但是被调用者并没有返回结果此时调用者来询问处理结果被调用者通常依靠事件回调等机制来通知调用者其返回结果。同步和异步最大的区别在于异步的话调用者不需要等待处理结果被调用者会通过回调等机制来通知调用者其返回结果。阻塞和非阻塞这两个概念是程序级别的。阻塞 阻塞就是发起一个请求调用者一直等待请求结果返回也就是当前线程会被挂起无法从事其他任务只有当条件就绪才能继续。非阻塞 非阻塞调用指在不能立刻得到结果之前该调用不会阻塞当前线程。举个例子理解下同步、阻塞、异步、非阻塞的区别同步就是烧开水要自己来看开没开异步就是水开了然后水壶响了通知你水开了回调通知。阻塞是烧开水的过程中你不能干其他事情必须在旁边等着非阻塞是烧开水的过程里可以干其他事情。从以上概念也可以看出不会存在异步非阻塞的场景Unix IO 模型简介一个输入操作通常包括两个阶段:等待数据准备好从内核向进程复制数据对于一个套接字上的输入操作第一步通常涉及等待数据从网络中到达。当所等待分组到达时它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。Unix 下有五种 I/O 模型:阻塞式 I/O非阻塞式 I/OI/O 复用(select 和 poll)信号驱动式 I/O(SIGIO)异步 I/O(AIO)阻塞式 I/O应用进程被阻塞直到数据复制到应用进程缓冲区中才返回。应该注意到在阻塞的过程中其它程序还可以执行因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行因此不消耗 CPU 时间这种模型的执行效率会比较高。下图中recvfrom 用于接收 Socket 传来的数据并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。cssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);非阻塞式 I/O应用进程执行系统调用之后内核返回一个错误码。应用进程可以继续执行但是需要不断的执行系统调用来获知 I/O 是否完成这种方式称为轮询(polling)。由于 CPU 要处理更多的系统调用因此这种模型是比较低效的。I/O 复用使用 select 或者 poll 等待数据并且可以等待多个套接字中的任何一个变为可读这一过程会被阻塞当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。如果一个 Web 服务器没有 I/O 复用那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接那么就需要创建相同数量的线程。并且相比于多进程和多线程技术I/O 复用不需要进程线程创建和切换的开销系统开销更小。信号驱动 I/O应用进程使用 sigaction 系统调用内核立即返回应用进程可以继续执行也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。相比于非阻塞式 I/O 的轮询方式信号驱动 I/O 的 CPU 利用率更高。异步 I/O进行 aio_read 系统调用会立即返回应用进程继续执行不会被阻塞内核会在所有操作完成之后向应用进程发送信号。异步 I/O 与信号驱动 I/O 的区别在于异步 I/O 的信号是通知应用进程 I/O 完成而信号驱动 I/O 的信号是通知应用进程可以开始 I/OI/O 模型比较同步 I/O 与异步 I/O同步 I/O: 应用进程在调用 recvfrom 操作时会阻塞。异步 I/O: 不会阻塞。阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞但是在之后的将数据从内核复制到应用进程这个操作会阻塞。五大 I/O 模型比较前四种 I/O 模型的主要区别在于第一个阶段而第二个阶段是一样的: 将数据从内核复制到应用进程过程中应用进程会被阻塞。I/O 多路复用一个进程虽然任一时刻只能处理一个请求但是处理每个请求的事件时耗时控制在 1 毫秒以内这样 1 秒内就可以处理上千个请求把时间拉长来看多个请求复用了一个进程这就是多路复用这种思想很类似一个 CPU 并发多个进程所以也叫做时分多路复用。select/poll/epoll 内核提供给用户态的多路复用系统调用进程可以通过一个系统调用函数从内核中获取多个事件。select/poll/epoll 在获取事件时先把所有连接文件描述符传给内核再由内核返回产生了事件的连接然后在用户态中再处理这些连接对应的请求即可。select/pollselect 实现多路复用的方式是将已连接的 Socket 都放到一个文件描述符集合然后调用 select 函数将文件描述符集合拷贝到内核里让内核来检查是否有网络事件产生检查的方式很粗暴就是通过遍历文件描述符集合的方式当检查到有事件产生后将此 Socket 标记为可读或可写 接着再把整个文件描述符集合拷贝回用户态里然后用户态还需要再通过遍历的方法找到可读或可写的 Socket然后再对其处理。所以对于 select 这种方式需要进行2 次「遍历」文件描述符集合一次是在内核态里一个次是在用户态里 而且还会发生2 次「拷贝」文件描述符集合先从用户空间传入内核空间由内核修改后再传出到用户空间中。select 使用固定长度的 BitsMap表示文件描述符集合而且所支持的文件描述符的个数是有限制的在 Linux 系统中由内核中的 FD_SETSIZE 限制 默认最大值为1024只能监听 0~1023 的文件描述符。poll 不再用 BitsMap 来存储所关注的文件描述符取而代之用动态数组以链表形式来组织突破了 select 的文件描述符个数限制当然还会受到系统文件描述符限制。但是 poll 和 select 并没有太大的本质区别都是使用「线性结构」存储进程关注的 Socket 集合因此都需要遍历文件描述符集合来找到可读或可写的 Socket时间复杂度为 O(n)而且也需要在用户态与内核态之间拷贝文件描述符集合这种方式随着并发数上来性能的损耗会呈指数级增长。epoll如下的代码中先用e poll_create 创建一个 epol l对象 epfd再通过 epoll_ctl 将需要监视的 socket 添加到epfd中最后调用 epoll_wait 等待数据。epoll通过epoll_create、epoll_ctl和epoll_wait三个系统调用来实现。cint s socket(AF_INET, SOCK_STREAM, 0); bind(s, ...); listen(s, ...) int epfd epoll_create(...); epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中 while(1) { int n epoll_wait(...); for(接收到数据的socket){ //处理 } }epoll 通过以下内容来解决 select/poll 的问题。epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字把需要监控的 socket 通过epoll_ctl()函数加入内核中的红黑树里红黑树是个高效的数据结构增删改一般时间复杂度是O(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构所以 select/poll 每次操作时都传入整个 socket 集合给内核而 epoll 因为在内核维护了红黑树可以保存所有待检测的 socket 所以只需要传入一个待检测的 socket减少了内核和用户空间大量的数据拷贝和内存分配。epoll 使用事件驱动的机制内核里维护了一个链表来记录就绪事件当某个 socket 有事件发生时通过callback回调函数来实现异步回调此时内核会将已经准备好的连接加入到这个就绪事件列表中当用户调用epoll_wait()函数时只会返回有事件发生的文件描述符的个数不需要像 select/poll 那样需要轮询扫描整个 socket 集合大大提高了检测的效率。
select、poll、epoll 到底有什么区别?一文讲透 I/O 多路复用
同步 同步就是发起一个调用后被调用者未处理完请求之前调用不返回。此时需要调用者询问处理结果异步 异步就是发起一个调用后立刻得到被调用者的回应表示已接收到请求但是被调用者并没有返回结果此时调用者来询问处理结果被调用者通常依靠事件回调等机制来通知调用者其返回结果。同步和异步最大的区别在于异步的话调用者不需要等待处理结果被调用者会通过回调等机制来通知调用者其返回结果。阻塞和非阻塞这两个概念是程序级别的。阻塞 阻塞就是发起一个请求调用者一直等待请求结果返回也就是当前线程会被挂起无法从事其他任务只有当条件就绪才能继续。非阻塞 非阻塞调用指在不能立刻得到结果之前该调用不会阻塞当前线程。举个例子理解下同步、阻塞、异步、非阻塞的区别同步就是烧开水要自己来看开没开异步就是水开了然后水壶响了通知你水开了回调通知。阻塞是烧开水的过程中你不能干其他事情必须在旁边等着非阻塞是烧开水的过程里可以干其他事情。从以上概念也可以看出不会存在异步非阻塞的场景Unix IO 模型简介一个输入操作通常包括两个阶段:等待数据准备好从内核向进程复制数据对于一个套接字上的输入操作第一步通常涉及等待数据从网络中到达。当所等待分组到达时它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。Unix 下有五种 I/O 模型:阻塞式 I/O非阻塞式 I/OI/O 复用(select 和 poll)信号驱动式 I/O(SIGIO)异步 I/O(AIO)阻塞式 I/O应用进程被阻塞直到数据复制到应用进程缓冲区中才返回。应该注意到在阻塞的过程中其它程序还可以执行因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行因此不消耗 CPU 时间这种模型的执行效率会比较高。下图中recvfrom 用于接收 Socket 传来的数据并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。cssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);非阻塞式 I/O应用进程执行系统调用之后内核返回一个错误码。应用进程可以继续执行但是需要不断的执行系统调用来获知 I/O 是否完成这种方式称为轮询(polling)。由于 CPU 要处理更多的系统调用因此这种模型是比较低效的。I/O 复用使用 select 或者 poll 等待数据并且可以等待多个套接字中的任何一个变为可读这一过程会被阻塞当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。如果一个 Web 服务器没有 I/O 复用那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接那么就需要创建相同数量的线程。并且相比于多进程和多线程技术I/O 复用不需要进程线程创建和切换的开销系统开销更小。信号驱动 I/O应用进程使用 sigaction 系统调用内核立即返回应用进程可以继续执行也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。相比于非阻塞式 I/O 的轮询方式信号驱动 I/O 的 CPU 利用率更高。异步 I/O进行 aio_read 系统调用会立即返回应用进程继续执行不会被阻塞内核会在所有操作完成之后向应用进程发送信号。异步 I/O 与信号驱动 I/O 的区别在于异步 I/O 的信号是通知应用进程 I/O 完成而信号驱动 I/O 的信号是通知应用进程可以开始 I/OI/O 模型比较同步 I/O 与异步 I/O同步 I/O: 应用进程在调用 recvfrom 操作时会阻塞。异步 I/O: 不会阻塞。阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞但是在之后的将数据从内核复制到应用进程这个操作会阻塞。五大 I/O 模型比较前四种 I/O 模型的主要区别在于第一个阶段而第二个阶段是一样的: 将数据从内核复制到应用进程过程中应用进程会被阻塞。I/O 多路复用一个进程虽然任一时刻只能处理一个请求但是处理每个请求的事件时耗时控制在 1 毫秒以内这样 1 秒内就可以处理上千个请求把时间拉长来看多个请求复用了一个进程这就是多路复用这种思想很类似一个 CPU 并发多个进程所以也叫做时分多路复用。select/poll/epoll 内核提供给用户态的多路复用系统调用进程可以通过一个系统调用函数从内核中获取多个事件。select/poll/epoll 在获取事件时先把所有连接文件描述符传给内核再由内核返回产生了事件的连接然后在用户态中再处理这些连接对应的请求即可。select/pollselect 实现多路复用的方式是将已连接的 Socket 都放到一个文件描述符集合然后调用 select 函数将文件描述符集合拷贝到内核里让内核来检查是否有网络事件产生检查的方式很粗暴就是通过遍历文件描述符集合的方式当检查到有事件产生后将此 Socket 标记为可读或可写 接着再把整个文件描述符集合拷贝回用户态里然后用户态还需要再通过遍历的方法找到可读或可写的 Socket然后再对其处理。所以对于 select 这种方式需要进行2 次「遍历」文件描述符集合一次是在内核态里一个次是在用户态里 而且还会发生2 次「拷贝」文件描述符集合先从用户空间传入内核空间由内核修改后再传出到用户空间中。select 使用固定长度的 BitsMap表示文件描述符集合而且所支持的文件描述符的个数是有限制的在 Linux 系统中由内核中的 FD_SETSIZE 限制 默认最大值为1024只能监听 0~1023 的文件描述符。poll 不再用 BitsMap 来存储所关注的文件描述符取而代之用动态数组以链表形式来组织突破了 select 的文件描述符个数限制当然还会受到系统文件描述符限制。但是 poll 和 select 并没有太大的本质区别都是使用「线性结构」存储进程关注的 Socket 集合因此都需要遍历文件描述符集合来找到可读或可写的 Socket时间复杂度为 O(n)而且也需要在用户态与内核态之间拷贝文件描述符集合这种方式随着并发数上来性能的损耗会呈指数级增长。epoll如下的代码中先用e poll_create 创建一个 epol l对象 epfd再通过 epoll_ctl 将需要监视的 socket 添加到epfd中最后调用 epoll_wait 等待数据。epoll通过epoll_create、epoll_ctl和epoll_wait三个系统调用来实现。cint s socket(AF_INET, SOCK_STREAM, 0); bind(s, ...); listen(s, ...) int epfd epoll_create(...); epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中 while(1) { int n epoll_wait(...); for(接收到数据的socket){ //处理 } }epoll 通过以下内容来解决 select/poll 的问题。epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字把需要监控的 socket 通过epoll_ctl()函数加入内核中的红黑树里红黑树是个高效的数据结构增删改一般时间复杂度是O(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构所以 select/poll 每次操作时都传入整个 socket 集合给内核而 epoll 因为在内核维护了红黑树可以保存所有待检测的 socket 所以只需要传入一个待检测的 socket减少了内核和用户空间大量的数据拷贝和内存分配。epoll 使用事件驱动的机制内核里维护了一个链表来记录就绪事件当某个 socket 有事件发生时通过callback回调函数来实现异步回调此时内核会将已经准备好的连接加入到这个就绪事件列表中当用户调用epoll_wait()函数时只会返回有事件发生的文件描述符的个数不需要像 select/poll 那样需要轮询扫描整个 socket 集合大大提高了检测的效率。