Linux IO模式及 select、poll、epoll个人理解教程
用户空间和系统空间
32位的操作系统的寻址空间(虚拟内存空间)是2^32次方,内核空间可以直接访问受保护的内存空间,用户空间不能直接操作系统内核。在操作系统中最高位1G字节的虚拟地址为内核空间,较底位的3G虚拟地址为用户空间
标准I/O模式
访问I/O两阶段:
将数据从内核拷贝到进程中:从操作系统内核的缓冲区拷贝到应用程序的地址空间
等待数据准备:数据拷贝到内核缓冲区
阻塞I/O模式:
进程调用recvfrom这个系统调用,kernel准备接受数据,数据拷贝到操作系统内核缓冲区的过程,这是进程会进入阻塞状态。当kernel内核准备好数据,数据从操作系统内核缓冲区拷贝到进程的地址空间。进程解除阻塞状态,重新运行起来
总结:阻塞IO的特点就是两段阻塞阶段,第一段阻塞等待数据,第二段阻塞拷贝数据到用户内存
非阻塞I/O模式:
进程调用revcfrom系统调用ready操作时。当kernel内核没有准备好数据时,不会阻塞进程,返回error结果给进程,进程收到结果是error的时候,不断的轮询调用ready操作.当kernel准备好数据时,用户进程阻塞等待数据拷贝,kernel内核把数据从操作系统内核缓冲区拷贝到进程的地址空间时,可以接受到数据
总结:非阻塞I/O模式,进程不断的轮询kernel直到kernel数据拷贝完毕
I/O多路复用
当进程调用select系统调用,整个进程会阻塞,同时kernel内核会监听所有select负责的socket,当任意一个socket准备好了,select就会返回,当再次进程调用read操作,用户进程进入阻塞状态,kernel内核从操作系统内核缓冲区拷贝数据到用户进程.
总结:I/O多路复用通过一种机制一个进程可以同时等待多个文件描述符,当其中一个文件描述符进入就绪状态,select()函数就会返回
区别:
- select()优势是同时处理多个connection,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好
- 进程阻塞跟阻塞I/O不一样,多路复用阻塞是select()阻塞,阻塞I/O是socket I/O 阻塞了进程
- I/O多路复用是调用了两个系统函数select()和recvfrom(),阻塞I/O只有recvfrom()
异步I/O
用户进程调用aio\_read操作之后,马上返回,不阻塞用户进程。当kernel内核从操作系统内核缓冲区拷贝数据完毕后,通过deliver signal 发送信号,通知用户进程read已完成操作
总结:
- 阻塞与非阻塞:阻塞I/O会阻塞用户进程直到操作完成,非阻塞I/O在kernel内核在准备数据情况下会立刻返回
- 同步I/O与异步I/O:
- 同步I/O在做I/O操作时,会阻塞用户进程,所以阻塞I/O,非阻塞I/O,I/O多路复用都为同步I/O
五种I/O模型对比
通过图对比:用户进程等待kernel内核数据从操作系统内核缓冲区到用户进程是都会进入阻塞状态。
- I/O多路复用的机制Select,Poll,Epoll
Select,Poll,Epoll:
select,poll,epoll都是I/O多路复用的机制.I/O多路复用的机制就是通过一个进程,去监听多个文件描述符的状态(读就绪或者写就绪),通知程序进行相应的操作.当监听的文件描述符状态为就绪状态时,通知程序自行进行读写操作,该状态为阻塞状态.就会阻塞I/O,kernel内核将数据从操作系统内核缓冲区复制到用户空间.
Select:
监听多个文件描述符,程序会处于阻塞状态,直到文件描述符出现就绪状态或者超时.
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select函数监听句柄的状态有,readfds(读状态),writefds(写状态),exceptfds(异常状态).调用select函数就会阻塞直到句柄出现可读状态,可写状态,异常状态或者超时状态。当select函数返回,就会遍历 fdset,找出就绪的描述符
缺点:
1.单个进程能够监听的描述符有限,liunx系统默认是1024,当然这个数值可以提高.因为select是遍历描述符的,所以当描述符越多,性能越差
2.因为select返回结果,就要遍历fdset,由于复制了大量的句柄数据结构。内核将句柄数据拷贝到用户空间,形成巨大的开销
3.遍历所有的句柄 O(N) 的时间复杂度
优点:实现方式简单
Poll:
Poll使用的pollfd结构表示监听的描述符,poll用链表的形式保存多个描述符,没有select监听描述符数量的上限,依然存在轮询的情况
Poll结构
struct pollfd { int fd; /* file descriptor */ short events; /* requested events to watch */ short revents; /* returned events witnessed */ };
fd表示文件描述符,events表示监听的事件,revents表示实际发生的事件。而且也需要函数返回后遍历所有的pollfd的描述符来获取就绪的描述符
Epoll:
epoll实现的机制跟poll和select不一样,epoll是创建一个文件描述符来监听所有的文件描述符.将用户有关联的文件描述符统一放在内核的一个事件表中,这样用户空间跟内核空间就 copy只需一次就好了。
epoll在liunx内核中申请一个简单的文件系统,通过以下函数调用实现
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
调用过程如下:
- 通过epoll\_create建立一个epoll对象(在epoll文件系统为这个句柄对象分配资源)
- 调用epoll\_ctl对epoll对象添加事件
- 调用epoll\_wait收集发生事件的回调
当程序调用epoll\_create时,liunx会创建一个eventpoll的结构体,每个一个epollpoll中都有一个独立的eventpoll结构体,通过epoll\_ctl往epoll对象中添加事件,都会存放于红黑树中,由于红黑树的特点,重复的事件,会高效的识别出来,往epoll里面添加事件会与设备驱动程序建立回调的关系,当事件发生时就会调用回调方法,这个回调方法在内核中叫ep\_poll\_callback,将回调的方法存放于rdlist双向链表里面。
在epoll中,每个事件都有一个epitem结构体,当调用epoll\_wait的时候,只要检查eventpoll里面的rdlist是否有epitem。如果rdlist不为空,将发生的事件复制到用户态里面,同时将事件数量返回给用户
总结:通过红黑树跟双向链表,以及回调方法造就了高效的epoll。进程调用epoll\_create生成epoll对象,通过epoll\_ctl添加或者删除事件,使用epoll\_wait对事件的监听
优点:无需一次性复制所有的句柄到用户空间,也不用遍历所有的句柄