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 零拷贝
什么是零拷贝
在操作系统中,使用传统的方式,数据需要经历几次拷贝,还要经历用户态/内核态
切换
- 从磁盘复制数据到内核态内存;
- 从内核态内存复制到用户态内存;
- 然后从用户态内存复制到网络驱动的内核态内存;
- 最后是从网络驱动的内核态内存复制到网卡中进行传输。
在这里插入图片描述
所以,可以通过零拷贝的方式,减少用户态与内核态的上下文切换和内存拷贝的次数,用来提升I/O
的性能。零拷贝比较常见的实现方式是mmap
,这种机制在Java
中是通过MappedByteBuffer
实现的。
在这里插入图片描述
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
支持的文件描述符数量太小,默认是1024
-
select
返回的是含有整个句柄的数组, 应用程序需要遍历整个数组才能发现哪些句柄发生了事件
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
对象,并传回它的id -
int 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
时立刻返回准备就绪链表里的数据即可
网友评论