文章目录
- 1 select、poll、epoll
- 1.1 引言
- 1.2 IO和Linux内核发展
- 1.2.1 整体概述
- 1.2.2 阻塞IO
- 1.2.3 非阻塞IO
- 1.2.4 select
- 1.2.5 共享空间
- 1.2.6 零拷贝
- 1.3 select
- 1.3.1 简介
- 1.3.2 select缺点
- 1.4 poll介绍
- 1.4.1 与select差别
- 1.4.2 poll缺点
- 1.5 epoll
- 1.5.1 epoll相关函数
- 1.5.2 epoll优点
1 select、poll、epoll
1.1 引言
操作系统在处理io的时候,主要有两个阶段:
- 等待数据传到io设备
- io设备将数据复制到user space
我们一般将上述过程简化理解为:
- 等到数据传到kernel内核space
- kernel内核区域将数据复制到user space(理解为进程或者线程的缓冲区)
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间
1.2 IO和Linux内核发展
1.2.1 整体概述
整体关系流程:

查看进程文件描述符:
获取pid进程号
ps -ef
查看文件描述符
cd /proc/进程号/fd ; ll
或者查看当前进程的fd
$$ 表示 Shell 本身的 PID (ProcessID)
cd /proc/$$/fd ; ll
1.2.2 阻塞IO
计算机是有内核(kernel)的,内核向下连接很多的客户端,内核向上连接进程或线程,早先内核通过read命令读取文件描述符(fd),在这个时期socket是blocking(阻塞的)BIO。
如下图所示:线程通过内核读取文件fd8,读取到用户空间后,在通过内核写入文件fd9,如果fd8阻塞了,它会阻挡后面的操作

1.2.3 非阻塞IO
socket fd nonblock(非阻塞),进程/线程用一个,用循环遍历文件描述符(轮询发生在用户空间),这个时期是同步非阻塞时期NIO;
这是由于内核socket本身就是nio,同步非阻塞IO

1.2.4 select
如果有1000个文件描述符fd,代表用户进程轮询调用1000次内核(kernel),造成成本很大的问题。于是在内核中增加了一个系统调用select,用户空间调用新的系统调用,统一将所有的文件描述符传给select,内核监控文件描述符的完成度,文件描述符完成之后返回,返回之后还有系统调用,再调用read(有数据的文件描述符),这个叫多路复用NIO,在这个时期,文件描述符考来考去成为累赘;

1.2.5 共享空间
共享空间是进程用户空间一部分,也是内核空间的一部分
引入一个共享空间mmap,将文件描述符放在共享空间里,文件描述符放在共享空间的红黑树里,将资源齐全的文件描述符放到链表里

1.2.6 零拷贝
sendfile,是完成零拷贝的命令,两个参数一个写出io,一个读入io
在之前是先读取文件到用户空间,再写到内核中去,有了sendfile后,用这一个命令就可以了,不用读取写入

1.3 select
1.3.1 简介
单个进程就可以同时处理多个网络连接的io请求(同时阻塞多个io操作)。基本原理就是程序呼叫select,然后整个程序就阻塞状态,这时候,kernel内核就会轮询检查所有select负责的文件描述符fd,当找到其中那个的数据准备好了文件描述符,会返回给select,select通知系统调用,将数据从kernel内核复制到进程缓冲区(用户空间)

下图为select同时从多个客户端接受数据的过程
虽然服务器进程会被select阻塞,但是select会利用内核不断轮询监听其他客户端的io操作是否完成

1.3.2 select缺点
select的几大缺点:
- 每次调用
select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 - 同时每次调用
select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 select支持的文件描述符数量太小,默认是1024select返回的是含有整个句柄的数组, 应用程序需要遍历整个数组才能发现哪些句柄发生了事件
1.4 poll介绍
1.4.1 与select差别
poll的原理与select非常相似,差别如下:
文件描述符fd集合的方式不同,poll使用pollfd结构而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。
1.4.2 poll缺点
poll的几大缺点:
- 每次调用
poll,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 - 每次调用
poll都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
1.5 epoll
1.5.1 epoll相关函数
epoll:提供了三个函数:
int epoll_create(int size);
建立一个epoll对象,并传回它的idint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待注册的事件被触发或者timeout发生
1.5.2 epoll优点
epoll解决的问题:
epoll没有fd数量限制
epoll没有这个限制,我们知道每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右
cat /proc/sys/fs/file-max可以查看文件数量epoll不需要每次都从用户空间将fd复制到内核kernel
epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次select和poll都是主动轮询机制,需要遍历每一个fd;
epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。- 虽然
epoll需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。
换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。 - 我们在调用
epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效
这个准备就绪list链表是怎么维护的呢?
当我们执行
epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里;当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了
一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可



















