# I/O 与网络模型 介绍各种各样的I/O模型,包括以下场景: 1. 阻塞 & 非阻塞 2. 多路复用 3. Signal IO 4. 异步 IO 5. libevent 现实生活中的场景复杂,Linux CPU和IO行为,他们之间互相等待。例如,阻塞的IO可能会让CPU暂停。 I/O模型很难说好与坏,只能说在某些场景下,更适合某些IO模型。其中,1、4 更适合块设备,2、3 更适用于字符设备。 为什么硬盘没有所谓的 多路复用,libevent,signal IO? 因为select(串口), epoll(socket) 这些都是在监听事件,所以各种各样的IO模型,更多是描述字符设备和网络socket的问题。但硬盘的文件,只有读写,没有 epoll这些。这些IO模型更多是在字符设备,网络socket的场景。 ### 为什么程序要选择正确的IO模型? 蓝色代表:cpu,红色代表:io ![](https://box.kancloud.cn/2362bcb8a9159af33c0ec6d3e701c3df_1876x1340.png) 如上图,某个应用打开一个图片文件,先需要100ms初始化,接下来100ms读这个图片。那打开这个图片就需要200ms。 但是 是否可以开两个线程,同时做这两件事? ![](https://box.kancloud.cn/561dc951ee90a91df7d4de84ec3e88ce_1538x1128.png) 如上图,网络收发程序,如果串行执行,CPU和IO会需要互相等待。 为什么CPU和IO可以并行?因为一般硬件,IO通过DMA,cpu消耗比较小,在硬件上操作的时间更长。CPU和硬盘是两个不同的硬件。 再比如开机加速中systemd使用的readahead功能: 第一次启动过程,读的文件,会通过Linux inotify监控linux内核文件被操作的情况,记录下来。第二次启动,后台有进程直接读这些文件,而不是等到需要的时候再读。 ![](https://box.kancloud.cn/7bcb94f8c0d49a964dabf226688164cc_1244x954.png) I/O模型会深刻影响应用的最终性能,阻塞 & 非阻塞 、异步 IO 是针对硬盘, 多路复用、signal io、libevent 是针对字符设备和 socket。 ### 简单的IO模型 ![](https://box.kancloud.cn/97be9252cd80c3254efc34e477cfb28a_1242x742.png) 当一个进程需要读 键盘、触屏、鼠标时,进程会阻塞。但对于大量并发的场景,阻塞IO无法搞定,也可能会被信号打断。 内核程序等待IO,gobal fifo read不到 一般情况select返回,会调用 if signal_pending,进程会返回 ERESTARTSYS;此时,进程的read 返回由singal决定。有可能返回(EINTR),也有可能不返回。 #### demo: ``` #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <sys/types.h> #include <errno.h> #include <string.h> static void sig_handler(int signum) { printf("int handler %d\n", signum); } int main(int argc, char **argv) { char buf[100]; ssize_t ret; struct sigaction oldact; struct sigaction act; act.sa_handler = sig_handler; act.sa_flags = 0; // act.sa_flags |= SA_RESTART; sigemptyset(&act.sa_mask); if (-1 == sigaction(SIGUSR1, &act, &oldact)) { printf("sigaction failed!/n"); return -1; } bzero(buf, 100); do { ret = read(STDIN_FILENO, buf, 10); if ((ret == -1) && (errno == EINTR)) printf("retry after eintr\n"); } while((ret == -1) && (errno == EINTR)); if (ret > 0) printf("read %d bytes, content is %s\n", ret, buf); return 0; } ``` ![](https://box.kancloud.cn/d8477b6d3cf9bcc5804f316ea0246aef_719x709.png) 一个阻塞的IO,在睡眠等IO时Ready,但中途被信号打断,linux响应信号,read/write请求阻塞。 配置信号时,在SA_FLAG是不是加“自动”,SA_RESTART指定 被阻塞的IO请求是否重发,并且应用中可以捕捉。加了SA_RESTART重发,就不会返回出错码EINTR。 没有加SA_RESTART重发,就会返回出错码(EINTR),这样可以检测read被信号打断时的返回。 但Linux中有一些系统调用,即便你加了自动重发,也不能自动重发。man signal. ![](https://box.kancloud.cn/4708e2b53d43c11f9d32c0d590f0711a_2172x938.png) 当使用阻塞IO时,要小心这部分。 ![](https://box.kancloud.cn/c92635e764196130ce6938048bed23f3_2258x1366.png) ### 多进程、多线程模型 当有多个socket消息需要处理,阻塞IO搞不定,有一种可能是多个进程/线程,每当有一个连接建立(accept socket),都会启动一个线程去处理新建立的连接。但是,这种模型性能不太好,创建多进程、多线程时会有开销。 总结:多进程、多线程模型企图把每一个fd放到不同的线程/进程处理,避免阻塞的问题,从而引入了进程创建\撤销,调度的开销。能否在一个线程内搞定所有IO? -- 这就是`多路复用`的作用。 ### 多路复用 ![](https://box.kancloud.cn/4ddb3f8f232beb837f294c27011cc050_2174x1226.png) select :效率低,性能不太好。不能解决大量并发请求的问题。 它把1000个fd加入到fd_set,通过select监控fd_set里的fd,如果有一个fd满足读写事件,select就返回,循环1000次,查找有哪些fd已经满足。 两个问题: 1. select初始化时,要告诉内核,关注1000个fd, 每次初始化都需要重新关注1000个fd。前期准备阶段长。 2. select返回之后,要扫描1000个fd。 后期扫描维护成本大,CPU开销大。 epoll:成功解决了select的两个问题: 1. select的“健忘症”,一返回就不记得关注了多少fd。api 把告诉内核等哪些文件,和最终等哪些文件,都是同一个api。 而epoll,告诉内核等哪些文件 和具体等哪些文件分开成两个api,epoll的“等”返回后,还是知道关注了哪些fd。 2. select在返回后的维护开销很大,而epoll就可以直接知道需要等fd。 ![](https://box.kancloud.cn/ad9da367133ecef6e129578d78f37673_752x434.png) Epoll 分裂出两个api, epoll_ctl:告诉内核epoll关心哪些文件,让内核没有健忘症。 epoll_wait:专门等哪些文件,第2个参数 是输出参数,包含满足的fd,不需要再遍历所有的fd文件。 ![](https://box.kancloud.cn/40b34409c80437841f1706e981809f0a_894x654.png) 如上图,epoll在CPU的消耗上,远低于select,这样就可以在一个线程内监控更多的IO。 ``` #include <stdio.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/epoll.h> #include <sys/stat.h> static void call_epoll(void) { int epfd, fifofd, pipefd; struct epoll_event ev, events[2]; int ret; epfd = epoll_create(2); if (epfd < 0) { perror("epoll_create()"); return; } ev.events = EPOLLIN|EPOLLET; fifofd = open("/dev/globalfifo", O_RDONLY, S_IRUSR); printf("fifo fd:%d\n", fifofd); ev.data.fd = fifofd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fifofd, &ev); pipefd = open("pipe", O_RDONLY|O_NONBLOCK, S_IRUSR); printf("pipe fd:%d\n", pipefd); ev.data.fd = pipefd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, pipefd, &ev); while(1) { ret = epoll_wait(epfd, events, 2, 50000); if (ret < 0) { perror("epoll_wait()"); } else if (ret == 0) { printf("No data within 50 seconds.\n"); } else { int i; for(i=0;i<ret;i++) { char buf[100]; read(events[i].data.fd, buf, 100); printf("%s is available now:, %s\n", events[i].data.fd==fifofd? "fifo":"pipe", buf); } } } _out: close(epfd); } int main() { call_epoll(); return 0; } ``` 总结:epoll是几乎是大规模并行网络程序设计的代名词,一个线程里可以处理大量的tcp连接,cpu消耗也比较低。很多框架模型,nginx, nodejs, 底层均使用epoll实现。 #### signal IO 目前在linux中很少被用到,Linux内核某个IO事件ready,通过kill出一个signal,应用程序在signal IO上绑定处理函数。 ![](https://box.kancloud.cn/89fe7e92530f6e2f8c366b3fe4c1183e_2048x1304.png) kernel发现设备读写事件变化,调用一个 kill fa_sync ,应用程序绑定signal_io上的事件。 ![](https://box.kancloud.cn/984843459472efa52fe4de6299659118_880x392.png) #### 异步IO ![](https://box.kancloud.cn/03f7f8364fe3a3f4db456252f83755b3_1454x906.png) Linux中 不要把aio串起来, 基于epoll等api进行上层的封装,再基于事件编程。某个事件成立了,就开始去做某件事。 # libevent ![](https://box.kancloud.cn/503a1ac368ec755abf62083f1573099a_1150x798.png) 就像MFC一样,界面上的按钮,VC会产生一个on_button,调对应的函数。是一种典型的事件循环。 本质上还是用了epoll,只是基于事件编程。 ![](https://box.kancloud.cn/869403b17eca63bf85ec38d2a99be3b1_1146x812.png)