就够了!
Posted JavaCaiy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了就够了!相关的知识,希望对你有一定的参考价值。
前置知识:
- 用户空间和内核空间
- 进程切换
- 进程的阻塞
- 文件描述符
- 缓存 IO
- 中断
如果对以上概念还有不清楚的地方的话,可以先手动百科下再继续往下看~
I/O多路复用
首先我们来了解一下文件描述符:在Unix哲学中:“Everything is a file."Socket、pipe都可以看做文件,并且有自己独一无二的文件描述符(缩写为fd),而文件常常面临着读数据和写数据。
然后我们来了解一下I/O的概念:I/O分别是input和output的说写,也就是读数据和写数据,我们通过write可以往文件中写数据,而通过read从文件中读取数据。
那阻塞I/O是什么意思呢?如果在文件描述符中的数据还没有准备好的时候:例如在还没有任何数据的时候,发送了read()调用,进程就会堵塞,
那有没有什么好办法?我们可以同时监听多个fd,在有fd就绪的时候进行通知,没有的时候就堵塞?有,那就是I/O多路复用!
I/O多路复用就像是公司的收发室小哥,会替你处理中通、顺丰、韵达、申通…等等各家快递公司的快递,等哪家的快递到了,再打电话通知你拿~这个时候你就有很多时间在办公室愉快地摸鱼啦!
那在操作系统里,该如何设计这样的收发室小哥呢?
- 当有一个fd就绪时进行通知(等待小哥给你发短信)
- 都不可用?在有可用的文件描述符之前一直处于堵塞状态(专心在工位摸鱼)
- 唤醒:哪个fd可用了?(收到短信,下楼拿快递)
- 处理完所有的I/O就绪的fd,没有堵塞(拿完所有的快递,没有摸鱼)
- 返回第一步重新开始(上楼,回到工位,继续摸鱼)
UNIX公司里提供了select、poll、epoll、kqueue这几位年纪、性格和能力都有所差异的收发室小哥,让我们分别介绍下这几位小哥的工作特点吧!
select
select是最早出现在UNIX里的多路复用有关的系统调用:
#include<sys/select.h>
int selcet( int n,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
readfds、writefds、exceptfds分别表示select所监视的不同类型的文件描述符,分别等待不同的事件:监视是否有数据可读、监视是否有数据可写、是否有异常etc。第一个参数n呢,等于所有集合中最大的文件描述符的值加一。当selet成功返回时,readfds、writefds、exceptfds都会写改为只包含对应类型的I/O就绪的文件描述。举个例子,readfds对应的是中通快递,而显示有编号1、2、3的快递在发往我们收发室,在select检查快递的时候,只有2到了,因此readfds集合就只返回编号为2的快递。参数timeout是只想timeval结构体的指针:
#include<sys/time.h>
struct timeval
long tv_sec;
long tv_usec;
如果该参数不是NULL,在tv_sec秒tv_usec毫秒过后select系统调用就会返回。
FD_ZERO(fd_set *fdset):清空fdset与所有文件描述符的联系。
FD_SET(int fd, fd_set *fdset):建立文件描述符fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件描述符fd与fdset的联系。
FD_ISSET(int fd, fd_set *fdset):检查fd_set联系的文件描述符fd是否可读写,>0表示可读写。
typedef __kernel_fd_set fd_set;
#undef __FD_SETSIZE
#define __FD_SETSIZE 1024
typedef struct
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
__kernel_fd_set;
select()的机制中提供一种fd_set的数据结构,fd_set是一个struct, 其内部只有一个 由16个元素组成的unsigned long数组,其实就是常见的bitmap,这个数组一共可以表示16 × 64 = 1024位, 每一位用来表示一个 fd, 这也就是 select针对读,定或异常每一类最多只能有 1024个fd 限制的由来。当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。
select机制的问题
1.单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,这个大小是由宏控制的,进行改变很麻烦(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024),并且由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
2.内核 / 用户空间内存拷贝问题:select需要复制大量的句柄数据结构,产生巨大的开销;
3.select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。
poll
早期淘宝购物并不发达,所以收发室的快件并不是很多,select轮询上限1024个快件的工作方式也够用了。但随着网络购物的发展,收发室的快递越来越多,select模式下fd_set的长度限制就开始成为了致命缺陷。
吸取了select的教训,新引入的poll就不再采用定长数组来保存所监控的fd信息了,而是采用可以改变长度的链表,没有了连接数目的限制,poll可以支持高并发的需求。除此以外,poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
poll用来保存事件的数据结构:
#include <poll.h>
struct pollfd
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
;
使用poll函数进行操作:
#include <poll.h>
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
fds:一个pollfd队列的队头指针,我们先把需要监视的文件描述符和他们上面的事件放到这个队列中
nfds:队列的长度
timeout:事件操作,设置指定正数的阻塞事件,0表示非阻塞模式,-1表示永久阻塞。
epoll:
设想一个场景,双十一到了,公司总共有一万份快递要来,而实际上每一段时间只会有几十个或者几百个快递到达收发室,也就是说,每一时刻工作人员只需要处理一万份快递中的一小部分快递,那么如何才能高效的处理这种场景呢?如果继续采用select和poll轮询的方法,收发室工作人员一定会累坏吧。实际上,在Linux 2.4版本以前, unix公司就是这么做的。Linux 2.4版中,引入了epoll,它在Linux内核中申请了一个简易的文件系统,把原先的一个select或poll调用分成了3部分,相当于把select和poll的工作分给三个人来做:
#include <sys/epoll.h>
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对象中添加这100万个连接的套接字;
-
调用epoll_wait收集发生事件的连接。
这样只需要在进程启动时建立1个epoll对象,并在需要的时候向它添加或删除连接就可以了,因此,在实际收集事件时,epoll_wait的效率就会非常高,因为调用epoll_wait时并没有向它传递这100万个连接,内核也不需要去遍历全部的连接。
当进程调用epoll_create方法的时候,Linux内核就会创建一个eventpoll结构体,对于这个结构体我们主要需要关注两个成员:
struct eventpoll
...
struct rb_root rbr;
struct list_head rdllist;
...
;
我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个rdllist双向链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个rdllist双向链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。
在epoll中,对于每一个事件都会建立一个epitem结构体:
struct epitem
...
//红黑树节点
struct rb_node rbn;
//双向链表节点
struct list_head rdllink;
//事件句柄等信息
struct epoll_filefd ffd;
//指向其所属的eventepoll对象
struct eventpoll *ep;
//期待的事件类型
struct epoll_event event;
...
; // 这里包含每一个事件对应着的信息。
因此当我们调用epoll_wait检查是否有就绪的事件时,只是检查eventpoll对象中的rdllist双向链表中是否有epitem, 如果非空的话,就将这里的时间复制到用户态内存中,同时返回时间的数量。因此epoll_wait的效率很高。而epoll_ctl对epoll对象中的事件进行增删查改时,由于使用了红黑树,因此查找事件也是十分高效的。
总结
Linux系统中存在着select、poll、epoll三种系统调用,针对高并发,且任一时间只有少数socket是活跃的场景epoll的性能要好很多。 但是如果在并发量低,socket都比较活跃的情况下,select/poll就不见得比epoll慢了(就像我们常常说快排比插入排序快,但是在特定情况下这并不成立)。并且由于历史原因(先来后到),select因为兼容性比较好,仍然在被大量应用中。
以上是关于就够了!的主要内容,如果未能解决你的问题,请参考以下文章