文章目录
1、简介
本文重点描述了 IO 模型的 阻塞、同步非阻塞、多路复用、epoll共享空间 这一路的过程,简略介绍了sendfile、 及sendfile和共享空间之间的区别 以下正片,全文阅读完可能需要5 ~ 8分钟时间 ;
2、谈epoll 之前的铺垫
突然一下子还不知道怎么开始,想了想还是从 linux 的 IO模型 演化过程来简单聊聊;看之前不妨大家带着几个问题:
最初 IO模型 是怎样处理的?
IO模型 有哪些演变过程?
每次演化都解决了什么问题?
select 是做什么的?
select和epoll有什么区别?
linux有 AIO吗?
epoll 中的共享空间是零拷贝吗?
共享空间和sendfile有什么不同
3、谈谈 IO 吧
3.1 最初是什么样?
每个计算机是有内核;内核向下有很多连接,这些连接就是客户端,通过文件描述符(fd)连接到内核(一个连接一个文件描述符);
早先 程序的线程/进程 通过 read 命令 读 指定的fd 到内核 ,这个时期socket 就是 blocking (BIO) ------也就是,抛出一个线程,读网卡连接,有数据就会返回数据,如果没有就一直阻塞着,下面的也就执行不了,只能抛出更多的线程,如果只有一颗CPU的话,某一时间片上某一时间点上就只能有一个线程处理,例如: fd1 的数据到了 ,但fd2 的数据还没到 这样就CPU的利用率就不会很高,如果描述的比较饶口,试试下面的图:
3.2 同步非阻塞 (NIO) 时期
这一时期,socket (fd)升级为 NIO,不阻塞只跑一个 线程,通过在线程,尽量不切换,同时内部写个while死循环,先遍历 fd1 当没有程序时在遍历 fd2 ,当有返回时开始处理,处理完后在read fd1,fd1有就返回结果,开始处理,…
以上轮循发生在用户空间,以上就是非阻塞IO的实现,当前由于只有自己去遍历取出来处理,所以还只是同步阶段,所以也是同步非阻塞时期;如图:
3.3 多路复用 NIO
上面提高了 使 CPU 更加繁忙起来了,但是这时就会有个问题: 如果有 1000个 fd 那就代表:
自己的用户空间,查询一回文件表述符,就要进行一次系统调用,得内核,用户态内核态切换,cpu保护现场,恢复现场…,这样就需要1000 次 ,就会带出成本问题,怎么解决? 这种情况下如果不去改变内核,可能也没有办法继续优化;
基于此内核中就增加了一个系统调用,这个系统调用早先就叫做 select,这样用户空间直接调用新的进程调用(select):统一将所有 文件表述符 传给select,然后 内核 统一监控,返回 需要操作的文件表述符,用户空间在拿着返回的文件描述符,找到里面能用的fd(哪些数据到达了,哪些是缓冲区有的) 去调用read,这样就保证 read不会调用 没有数据 的 fd ,看图:
3.4 多路复用 NIO - 2 (伪AIO)
老规矩先说上面的可以优化的地方,首先,线程进程都是在 用户态中,比如里面放着1000个文件表述符 二进制 ,而调内核的时候传参,需要将数据从 用户态拷贝到 内核态,PS:这里的数据是指 内核和用户进程沟通 “IO里是否有数据” 的数据 ; 面对fd 相关数据 需要考来考去有什么解决方案呢,众所周知,目前linux 内核还没实现AIO,只有windows实现了AIO ,但是可以通过伪AIO 实现;
之前,用户态有用户态的内存地址空间,内核有内核的内存地址空间,其实这些都是虚拟地址空间,站在物理内存上无非就两个区域,只不过内核的区域,用户进程没办法访问,所以只能通过传参将数据传过去, 就不可避免的需要考来考去,这时, 内核开辟一个单独的空间,并且空间也属于用户态空间,当用户进程在自己8号位置写了数据相当于间接写在共同空间,在调用 进程调用时,不需要将数据传递拷贝,只需要将对应指针传递,进程调用通过 CUP 解释即可找到自己这边对应的位置如 6号位置,附一点: 例如 公共空间的数据和 用户态,内核态是双向绑定,同一个数据,可能用户态是绑定 7号位置,而内核态绑定的是3号位置;
这样就通过共享空间,免除了数据多次拷贝的问题,而共享空间是通过mmap调用,同时用户程序则在共享空间中 放了一些数据结构; 首先 epoll 是一个大的概念,里面有三个调用,create, read ,用户空间 通过create 给一个 epoll 的文件标识符,epoll会准备一个共享空间,是mmap ,现将所有的fp都注册到共享空间,注意:共享空间的增删改是内核来完成的,查询是用户进程完成,用户进程 可能会 ctl add del 追加或删除文件描述符,和wait 方法,用来等待事件,总结下就是:用户空间先create, epoll 就把 文件描述符 注册到红黑树,当某个文件描述符 数据到了之后会把 它从红黑树放到链表中并维护数据是可写还是可读,只要链表中有了,wait()就可以从阻塞变成不阻塞 取那个链表把实际到达的返回,因为是共享的所以不用拷贝到用户进程,用户进程还需要单独去调wait() read() ,方法所以 epoll也是NIO 而不是 AIO ; 图片如下:
3.5 epoll 怎么样才能算真AIO
如果是AIO: 用户进程 不是直接去调的 read 而是先把 fd 先注册到 read,也不管 数据什么时候到的,整个读写过程都是在内核中完成,最后返回了一个消息或者一个通知,或者用户态有个线程池,回调用户态的方法把 返回 压到线程栈中,关键是这段时间,主线程在做自己的事,两边的事也没有关系;
3.6 谈谈 sendfile
好啦,最后一个 sendfile,首先sendfile 是零拷贝的,而epoll 是共享内存,两个是不一样的,首先sendfile 需要两个参数,一个 输入 fd 一个输出 fd 类似于 阿帕奇中的 out in 一个概念 ,举个实际例子: 有个文件和一个网卡,文件是 文件标识符 fd1,网卡是socket 标识符 fd2,之前 内核先是 把 fd1 数据read 到用户态,用户态在 把数据 write到 内核,在通过内核发出去,中间需要拷贝的过程, 但是有 sendfile则是 用户进程直接调用 sendfile, 内核直接拿着缓冲区 直接读 fd1 ,放到缓冲区,然后直接发出去;就不需要程序拷来拷去了;图片如下:
4、结尾
如有不准确的地方,希望可以留下您宝贵的意见 ,拜谢!!!