原文转载自:https://mp.weixin.qq.com/s/_Eh7eZLtcUjrp9QiCvLmrw
武侠小说里有很多的“心法”和“招式”。计算机技术里的“心法”和“招式”呢,我们可以简称为“道”和“术”;
“道” 最基础的计算机理论,隐藏于表象之下,非常抽象、晦涩难懂,需要用具象化的事物加以理解;
“术” 具体的技艺,它有可能是一门语言,比如:python 出手见效快;
一、混乱的 IO 概念
二、用户空间和内核空间
包括和我们熟知的和IO相关的CPU、内存、磁盘和网卡几个硬件;
计算机开机后首先会运行内核程序,内核程序占用的一块私有的空间就是内核空间,并且可支持访问CPU所有的指令集(ring0 – ring3)以及所有的内存空间、IO及硬件设备;
每个普通的用户进程都有一个单独的用户空间,用户空间只能访问受限的资源(CPU的“保护模式”)也就是说用户空间是无法直接操作像内存、网卡和磁盘等硬件的;
三、IO模型
1、 BIO(Blocking IO)
ServerSocket serverSocket = new ServerSocket(8080); // step1: 创建一个ServerSocket,并监听8080端口 while(true) { // step2: 主线程进入死循环 Socket socket = serverSocket.accept(); // step3: 线程阻塞,开启监听 BufferedReader reader = new BufferedReader(nwe InputStreamReader(socket.getInputStream())); System.out.println("read data: " + reader.readLine()); // step4: 数据读取 PrintWriter print = new PrintWriter(socket.getOutputStream(), true); print.println("write data"); // step5: socket数据写入 }
- 创建ServerSocket,并监听8080端口;
- 主线程进入死循环,用来阻塞监听客户端的连接,socket.accept();
- 数据读取,socket.read();
- 写入数据,socket.write();
2、“C10K”问题
“C10K”即“client 10k”用来指代数量庞大的客户端;
- 严重依赖线程,线程还是比较耗系统资源的(一个线程大约占用1M的空间);
- 频繁地创建和销毁代价很大,因为涉及到复杂的系统调用;
- 线程间上下文切换的成本很高,因为发生线程切换前,需要保留上一个任务的状态,以便切回来的时候,可以再次加载这个任务的状态。如果线程数量庞大,会造成线程做上下文切换的时间甚至大于线程执行的时间,CPU负载变高;
3、NIO非阻塞模型
// 循环遍历 while(1) { // 遍历fd集合 for (fdx in range(fd1, fdn)) { // 如果fdx有数据 if (null != fdx.data) { // 进行读取和处理 read(fdx)&handle(fdx); } } }
4、IO多路复用模型
4.1 select()
/** * select()系统调用 * * 参数列表: * nfds - 值为最大的文件描述符+1 * *readfds - 用户检查可读性 * *writefds - 用户检查可写性 * *exceptfds - 用于检查外带数据 * *timeout - 超时时间的结构体指针 */ int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *ti
DESCRIPTION
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become “ready” for some class of I/O operation (e.g.,input possible). A file descriptor is considered ready if it is possible to perform the corresponding I/O operation (e.g., read(2)) without blocking.select()允许程序监控多个fd,阻塞等待直到一个或多个fd到达”就绪”状态。
- FD_ZERO(&set); // 初始化,清空的作用,使集合中不含任何fd
- FD_SET(fd, &set); // 将fd加入set集合,给某个位置赋值的操作
- FD_CLR(fd, &set); // 将fd从set集合中清除,去掉某个位置的值
- FD_ISSET(fd, &set); // 校验某位置的fd是否在集合中
- 复杂度O(n),轮询的任务交给了内核来做,复杂度并没有变化,数据取出后也需要轮询哪个fd上发生了变动;
- 用户态还是需要不断切换到内核态,直到所有的fds数据读取结束,整体开销依然很大;
- fd_set有大小的限制,目前被硬编码成了1024;
- fd_set不可重用,每次操作完都必须重置;
4.2 poll()
/** * poll()系统调用 * * 参数列表: * *fds - pollfd结构体 * nfds - 要监视的描述符的数量 * timeout - 等待时间 */ int poll(struct pollfd *fds, nfds_t nfds, int *timeout); ### pollfd的结构体 struct pollfd{ int fd;// 文件描述符 short event;// 请求的事件 short revent;// 返回的事件 }
DESCRIPTION poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O. poll() 非常像select(),它也是阻塞等待直到一个或多个fd到达”就绪”状态。
- 没有了bitmap大小1024的限制;
- 通过结构体中的revents置位;
/**
* 返回专用的文件描述符
*/
int epoll_create(int size);
/** * epoll_ctl()系统调用 * * 参数列表: * epfd - 由epoll_create()返回的epoll专用的文件描述符 * op - 要进行的操作例如注册事件,可能的取值:注册-EPOLL_CTL_ADD、修改-EPOLL_CTL_MOD、删除-EPOLL_CTL_DEL * fd - 关联的文件描述符 * event - 指向epoll_event的指针 */ int epoll_ctl(int epfd, int op, int fd , struce epoll_event *event );
- 注册 – EPOLL_CTL_ADD
- 修改 – EPOLL_CTL_MOD
- 删除 – EPOLL_CTL_DEL
/** * epoll_wait()返回n个可读可写的fds * * 参数列表: * epfd - 由epoll_create()返回的epoll专用的文件描述符 * epoll_event - 要进行的操作例如注册事件,可能的取值:注册-EPOLL_CTL_ADD、修改-EPOLL_CTL_MOD、删除-EPOLL_CTL_DEL * maxevents - 每次能处理的事件数 * timeout - 等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞。一般用-1即可 */ int epoll_wait(int epfd, struce epoll_event *event , int maxevents, int timeout);
- 没有了频繁的用户态到内核态的切换;
- O(1)复杂度,返回的”nfds”是一个确定的可读写的数量,相比于之前循环n次来确认,复杂度降低了不少;
四、同步、异步
- 阻塞和非阻塞:读写没有就绪或者读写没有完成,函数是否要一直等待还是采用轮询;
- 同步和异步:同步是读写由应用程序完成。异步是读写由操作系统来完成,并通过回调的机制通知应用程序。
这边顺便提两种大家可能会经常听到的模式:Reactor和Preactor。
- Reactor 模式:主动模式。应用程序需要不断轮询,询问操作系统IO是否就绪。select()、poll()、epoll()就是这种主动模式;
- Preactor 模式:被动模式。应用程序把读写操作全交给操作系统,IO就绪后由操作系统完成后再回调给应用程序,“异步IO”就是这种模式;