I/O(Input/Output)输入输出,总体图
一.操作系统与设备之间的IO
简单来说(详细的请看《现代操作系统》),操作系统通过设备驱动程序访问IO设备。方式有:
(1)轮询方式:
CPU主动在各种设备中轮询检查状态,有数据就IO。
(2)中断方式:
设备有数据的时候,发出中断,由CPU决定要不要响应中断,然后中断,去处理设备的IO。CPU不用经常轮询设备状态。被动接收中断就行。
(3)DMA直接存储器访问方式:
如果1个字节的数据中断一次,传1KB的数据得中断1024次,太浪费CPU时间,于是有了DMA方式,CPU只需要把开头和结束的地址告诉DMA,中间由DMA完成数据IO。CPU从字节干预解放到数据块的干预。
(4)通道控制方式:
DMA方式只能控制一个设备的一块数据,多块数据还是要CPU干预多次。于是有了通道来控制IO,它比DMA更强大,能控制多块数据,多个设备的IO,更加解放了CPU参与IO过程。
二.操作系统与用户进程间的IO
(进程中的线程才是CPU基本的执行/调度单元,下面用线程举例,用socket举例)
设备来的数据放在内核cache中,需要用户线程去内核cache中取数据,复制到自己进程的cache中。有5中读取数据方式:
(1)阻塞:
用户线程调用某些系统函数去内核取数据,直到数据到达内核cache前,该线程处于阻塞状态,等待数据到达。
(2)非阻塞
用户线程去取数据,不管内核cache有没有数据,都直接返回,可能拿到数据,也可能拿不到,不会使线程进入阻塞态。
(3)IO多路复用
多路就是一个线程管理多路IO,线程还是被阻塞调用,其中一路或几路IO有数据了就返回。需要线程遍历全部IO,判断是哪个IO有数据。
例如 socket 的 select() 函数,线程调用 select() 进入阻塞态,任何一个IO有数据了,线程就退出阻塞态,获得机会继续执行。
(4)信号驱动IO
给一个IO注册一个信号和信号触发的回调函数,一旦信号被触发,回调函数里读取数据。
例如给 socket 注册一个“可读”的信号,当数据来了,可读的时候,信号被触发,执行回调函数从内核cache复制数据到用户空间。
(5)异步IO
异步IO中,操作系统完成了数据从内核到用户空间的拷贝后,以信号的方式通知用户线程可以下一步操作。省去了用户线程阻塞下来拷贝数据的过程。
IO管理
假设一台服务器需要被1万个客户端连接。方法有:
(1)单路:
最简单的一个线程管理一个客户端的socket IO,那么需要1万的线程,假设每个线程占内存3MB,需要300G内存,单台服务器没那么大的内存,并且操作系统最大线程数有限制,unix下一个进程好像是最多只能开 4096 个线程。
(2)IO 多路复用:
socket一旦多起来,单路IO 就扛不住了,需要一个线程管理多个 socket IO,下面都是在一个线程内的情况。
(2.1)select
一个线程管理多个socket IO,调用 select() 进入阻塞态,任何一个IO有数据则返回,由于不知道是哪个 socket 有数据,需要遍历所有 socket fd 去判断,当1万个 socket 大部分都是有IO的时候,效率较高,如果只是那么几百个有IO,此方法效率较低。
(2.2)epoll 和 kqueue
epoll 是 linux 下的,kqueue 是 unix 下的。
由于 select 需要遍历全部的 socket fd,效率较低,于是有了 epoll, kqueue 方式,kqueue 管理多个IO,阻塞调用等待函数,当有一个或多个IO事件,kqueue 直接返回多个IO事件的 socket fd,不需要遍历全部 socket fd,效率较高。
假设一个 socket 连接的对象是 3 kb,8G的内存可以管理 280w 个连接。
select,epoll,kqueue 原理
已知的情况
内核中注册 socket 的 IO 中断处理的回掉函数,有 IO 了会回调该函数。
select:
select 管理多个 socket,select 收到一个来自网卡 IO 中断就返回,不知道这个中断对应是哪个 socket fd 的。需要用户线程遍历判断。
epoll:
epoll 收到一个 IO 中断,会去查找这个中断对应哪个 socket fd。
epoll 中建立一个红黑树(平衡二叉树的一种),红黑树查找很高效。
用户注册感兴趣的 socket 事件,就是把这个 socket fd 插入到红黑树中,用中断号做key,可以理解为(中断号,socket fd)的二元组。
用户移除事件就是,删除树上的某个节点。
然后收到一个IO中断,epoll 把网卡数据拷贝到内核cache,根据中断号在红黑树中查找对应的 fd,把 fd 加入到就绪链表中,准备返回给用户线程。用户直接得到就绪的 fd。
kqueue:
收到 socket IO 中断去哈希表中查找对应的 socket fd,再把它放到一个链表里,返回。
用户注册一个感兴趣的事件,就是往哈希表中添加一个 fd。
一些优化
Memory Map (mmap)
设备的数据到内核cache,再到用户空间,中间需要把内核数据复制到用户空间,比较耗时,那么直接用 mmap 把用户空间的地址映射到内核空间,省去了内核数据到用户空间的拷贝
if((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fdin, 0)) == MAP_FAILED) err_sys("mmap error for input");
if((dst = mmap(0, statbuf.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fout, 0)) == MAP_FAILED) err_sys("mmap error for output");
memcpy(dst, src, statbuf.st_size);