共计 5168 个字符,预计需要花费 13 分钟才能阅读完成。
前言
网络 I /O,能够了解为网络上的数据流。通常咱们会基于 socket 与远端建设一条 TCP 或者 UDP 通道,而后进行读写。单个 socket 时,应用一个线程即可高效解决;然而如果是 10K 个 socket 连贯,或者更多,咱们如何做到高性能解决?
- 基本概念介绍
- 网络 I / O 的读写过程
- linux 下的五种网络 I / O 模型
- 多路复用 I / O 深刻了解一波
- Reactor 模型
- Proacotr 模型
关注公众号,一起交换 : 潜行前行
github 地址,感激 star
基本概念介绍
-
过程 (线程) 切换
* 所有零碎都有调度过程的能力,它能够挂起一个以后正在运行的过程,并复原之前挂起的过程
-
过程 (线程) 的阻塞
* 运行中的过程,有时会期待其余事件的执行实现,比方期待锁,申请 I / O 的读写;过程在期待过程会被零碎主动执行阻塞,此时过程不占用 CPU
-
文件描述符
* 在 Linux,文件描述符是一个用于表述指向文件援用的抽象化概念,它是一个非负整数。当程序关上一个现有文件或者创立一个新文件时,内核向过程返回一个文件描述符
-
linux 信号处理
* Linux 过程运行中能够承受来自零碎或者过程的信号值,而后依据信号值去运行相应捕获函数;信号相当于是硬件中断的软件模仿
在零拷贝机制篇章已介绍过 用户空间和内核空间 和缓冲区,这里就省略了
网络 IO 的读写过程
- 当在用户空间发动对 socket 套接字的读操作时,会导致上下文切换,用户过程阻塞(R1)期待网络数据流到来,从网卡复制到内核;(R2)而后从内核缓冲区向用户过程缓冲区复制。此时过程切换复原,解决拿到的数据
- 这里咱们给 socket 读操作的第一阶段起个别名 R1,第二阶段称为 R2
- 当在用户空间发动对 socket 的 send 操作时,导致上下文切换,用户过程阻塞期待(1)数据从用户过程缓冲区复制到内核缓冲区。数据 copy 实现,此时过程切换复原
linux 五种网络 IO 模型
阻塞式 I /O (blocking IO)
ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);
- 最根底的 I / O 模型就是阻塞 I / O 模型,也是最简略的模型。所有的操作都是程序执行的
- 阻塞 IO 模型中,用户空间的应用程序执行一个零碎调用(recvform),会导致应用程序被阻塞,直到内核缓冲区的数据筹备好,并且将数据从内核复制到用户过程。最初过程才被零碎唤醒解决数据
- 在 R1、R2 间断两个阶段,整个过程都被阻塞
非阻塞式 I /O (nonblocking IO)
- 非阻塞 IO 也是一种同步 IO。它是基于轮询(polling)机制实现,在这种模型中,套接字是以非阻塞的模式关上的。就是说 I / O 操作不会立刻实现,然而 I / O 操作会返回一个错误代码(EWOULDBLOCK),提醒操作未实现
- 轮询查看内核数据,如果数据未筹备好,则返回 EWOULDBLOCK。过程再持续发动 recvfrom 调用,当然你能够暂停去做其余事
- 直到内核数据筹备好,再拷贝数据到用户空间,而后过程拿到非错误码数据,接着进行数据处理。须要留神,拷贝数据整个过程,过程依然是属于阻塞的状态
- 过程在 R2 阶段阻塞,尽管在 R1 阶段没有被阻塞,然而须要一直轮询
多路复用 I /O (IO multiplexing)
- 个别后端服务都会存在大量的 socket 连贯,如果一次能查问多个套接字的读写状态,若有任意一个筹备好,那就去解决它,效率会高很多。这就是“I/ O 多路复用”,多路是指多个 socket 套接字,复用是指复用同一个过程
- linux 提供了 select、poll、epoll 等多路复用 I / O 的实现形式
- select 或 poll、epoll 是阻塞调用
- 与阻塞 IO 不同,select 不会等到 socket 数据全副达到再解决,而是有了一部分 socket 数据筹备好就会复原用户过程来解决。怎么晓得有一部分数据在内核筹备好了呢?答案:交给了零碎零碎解决吧
- 过程在 R1、R2 阶段也是阻塞;不过在 R1 阶段有个技巧,在多过程、多线程编程的环境下,咱们能够只调配一个过程(线程)去阻塞调用 select,其余线程不就能够解放了吗
信号驱动式 I /O (SIGIO)
- 须要提供一个信号捕获函数,并和 socket 套接字关联;发动 sigaction 调用之后过程就能解放去解决其余事
- 当数据在内核筹备好后,过程会收到一个 SIGIO 信号,继而中断去运行信号捕获函数,调用 recvfrom 把数据从内核读取到用户空间,再解决数据
- 能够看出用户过程是不会阻塞在 R1 阶段,但 R2 还是会阻塞期待
异步 IO (POSIX 的 aio_系列函数)
- 绝对同步 IO,异步 IO 在用户过程发动异步读(aio_read)零碎调用之后,无论内核缓冲区数据是否筹备好,都不会阻塞以后过程;在 aio_read 零碎调用返回后过程就能够解决其余逻辑
- socket 数据在内核就绪时,零碎间接把数据从内核复制到用户空间,而后再应用信号告诉用户过程
- R1、R2 两阶段时过程都是非阻塞的
多路复用 IO 深刻了解一波
select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 1)应用 copy_from_user 从用户空间拷贝 fd_set 到内核空间
- 2)注册回调函数__pollwait
- 3)遍历所有 fd,调用其对应的 poll 办法(对于 socket,这个 poll 办法是 sock_poll,sock_poll 依据状况会调用到 tcp_poll,udp_poll 或者 datagram_poll)
- 4)以 tcp_poll 为例,其外围实现就是__pollwait,也就是下面注册的回调函数
- 5)\__pollwait 的次要工作就是把 current(以后过程)挂到设施的期待队列中,不同的设施有不同的期待队列,对于 tcp_poll 来说,其期待队列是 sk->sk_sleep(留神把过程挂到期待队列中并不代表过程曾经睡眠了)。在设施收到一条音讯(网络设备)或填写完文件数据(磁盘设施)后,会唤醒设施期待队列上睡眠的过程,这时 current 便被唤醒了
- 6)poll 办法返回时会返回一个形容读写操作是否就绪的 mask 掩码,依据这个 mask 掩码给 fd_set 赋值
- 7)如果遍历完所有的 fd,还没有返回一个可读写的 mask 掩码,则会调用 schedule_timeout 是调用 select 的过程(也就是 current)进入睡眠
- 8)当设施驱动产生本身资源可读写后,会唤醒其期待队列上睡眠的过程。如果超过肯定的超时工夫(timeout 指定),还是没人唤醒,则调用 select 的过程会从新被唤醒取得 CPU,进而从新遍历 fd,判断有没有就绪的 fd
- 9)把 fd_set 从内核空间拷贝到用户空间
select 的毛病
- 每次调用 select,都须要把 fd 汇合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
- 同时每次调用 select 都须要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- select 反对的文件描述符数量太小了,默认是 1024
epoll
int epoll_create(int 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,会在内核 cache 里建个 红黑树 用于存储当前 epoll_ctl 传来的 socket,同时也会再建设一个 rdllist 双向链表 用于存储准备就绪的事件。当 epoll_wait 调用时,仅查看这个 rdllist 双向链表数据即可
- epoll_ctl 在向 epoll 对象中增加、批改、删除事件时,是在 rbr 红黑树中操作的,十分快
- 增加到 epoll 中的事件会与设施 (如网卡) 建设回调关系,设施上相应事件的产生时会调用回调办法,把事件加进 rdllist 双向链表中;这个回调办法在内核中叫做 ep_poll_callback
epoll 的两种触发模式
-
epoll 有 EPOLLLT 和 EPOLLET 两种触发模式,LT 是默认的模式,ET 是“高速”模式(只反对 no-block socket)
* LT(程度触发)模式下,只有这个文件描述符还有数据可读,** 每次 epoll_wait 都会触发它的读事件 ** * ET(边缘触发)模式下,检测到有 I / O 事件时,通过 epoll_wait 调用会失去有事件告诉的文件描述符,对于文件描述符,如可读,则必须将该文件描述符始终读到空(或者返回 EWOULDBLOCK),** 否则下次的 epoll_wait 不会触发该事件 **
epoll 相比 select 的长处
-
解决 select 三个毛病
* ** 对于第一个毛病 **:epoll 的解决方案在 epoll_ctl 函数中。每次注册新的事件到 epoll 句柄中时(在 epoll_ctl 中指定 EPOLL_CTL_ADD),会把所有的 fd 拷贝进内核,而不是在 epoll_wait 的时候反复拷贝。epoll 保障了每个 fd 在整个过程中只会拷贝一次(epoll_wait 不须要复制) * ** 对于第二个毛病 **:epoll 为每个 fd 指定一个回调函数,当设施就绪,唤醒期待队列上的期待者时,就会调用这个回调函数,而这个回调函数会把就绪的 fd 退出一个就绪链表。epoll_wait 的工作实际上就是在这个就绪链表中查看有没有就绪的 fd(不须要遍历) * ** 对于第三个毛病 **:epoll 没有这个限度,它所反对的 FD 下限是最大能够关上文件的数目,这个数字个别远大于 2048,举个例子,在 1GB 内存的机器上大概是 10 万左右,一般来说这个数目和零碎内存关系很大
-
epoll 的高性能
* epoll 应用了红黑树来保留须要监听的文件描述符事件,epoll_ctl 增删改操作疾速 * epoll 不须要遍历就能获取就绪 fd,间接返回就绪链表即可 * linux2.6 之后应用了 mmap 技术,数据不在须要从内核复制到用户空间,零拷贝
对于 epoll 的 IO 模型是同步异步的疑难
-
概念定义
* 同步 I / O 操作:导致申请过程阻塞,直到 I / O 操作实现 * 异步 I / O 操作:不导致申请过程阻塞,异步只用解决 I / O 操作实现后的告诉,并不被动读写数据,由零碎内核实现数据的读写 * 阻塞,非阻塞:过程 / 线程要拜访的数据是否就绪,过程 / 线程是否须要期待
- 异步 IO 的概念是要求无阻塞 I / O 调用。后面有介绍到 I / O 操作分两阶段:R1 期待数据筹备好。R2 从内核到过程拷贝数据。尽管 epoll 在 2.6 内核之后采纳 mmap 机制,使得其在 R2 阶段不须要复制,然而它在 R1 还是阻塞的。因而归类到同步 IO
Reactor 模型
Reactor 的中心思想是将所有要解决的 I / O 事件注册到一个核心 I / O 多路复用器上,同时主线程 / 过程阻塞在多路复用器上;一旦有 I / O 事件到来或是准备就绪,多路复用器返回,并将当时注册的相应 I / O 事件散发到对应的处理器中
相干概念介绍:
- 事件 :就是状态;比方: 读就绪事件 指的是咱们能够从内核读取数据的状态
- 事件分离器:个别会把事件的期待产生交给 epoll、select;而事件的到来是随机,异步的,所以须要循环调用 epoll,在框架里对应封装起来的模块就是事件分离器(简略了解为对 epoll 封装)
-
事件处理器:事件产生后须要过程或线程去解决,这个解决者就是事件处理器,个别和事件分离器是不同的线程
Reactor 的个别流程
- 1)应用程序在 事件分离器 注册 读写就绪事件 和读写就绪事件处理器
- 2)事件分离器期待读写就绪事件产生
- 3)读写就绪事件产生,激活事件分离器,分离器调用读写就绪事件处理器
- 4)事件处理器先从内核把数据读取到用户空间,而后再解决数据
单线程 + Reactor
多线程 + Reactor
多线程 + 多个 Reactor
Proactor 模型的个别流程
- 1)应用程序在事件分离器注册 读实现事件 和读实现事件处理器,并向零碎收回异步读申请
- 2)事件分离器期待读事件的实现
- 3)在分离器期待过程中,零碎利用并行的内核线程执行理论的读操作,并将数据复制过程缓冲区,最初告诉事件分离器读实现到来
- 4)事件分离器监听到 读实现事件 ,激活 读实现事件的处理器
-
5)读实现事件处理器间接解决用户过程缓冲区中的数据
Proactor 和 Reactor 的区别
- Proactor 是基于异步 I / O 的概念,而 Reactor 个别则是基于多路复用 I / O 的概念
- Proactor 不须要把数据从内核复制到用户空间,这步由零碎实现
欢送指注释中谬误
参考文章
- 聊聊 Linux 五种 IO 模型
- 网络 io 模型
- 网络 IO
- 5 种网络 IO 模型
- epoll 原理详解及 epoll 反应堆模型
正文完