Linux 复习 六
问:什么是I/O复用?
答:I/O复用就是让一个进程/或者线程,操作多个I/O,保证不会阻塞到某个特定的I/O.即一个进程可以处理多个请求,常见的I/O有select,poll,epoll(Linux特有)。
select:
#include <sys/select.h> #include >sys/time.h> int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
返回值:就绪的描述符数目,==0 超时,==-1,出错
int maxfdp1 : 表示指定测试的文件描述符数目+1,(从0开始)
fd_set *readset
fd_set *writeset
fd_set *exceptset
表示我们想让内核帮我们检测的描述字:读事件,写事件,异常,如果对某个事件不敢兴趣,至为NULL ,struct fd_set可以理解为一个集合,这个集合中的文件描述符可以用以下宏进行设置
void FD_ZERO(fd_set *fdset); //清空集合 void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中 void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除 int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写
const struct timeval *timeout
struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds };
select函数调用过程:
- 使用copy__from _user 将fd_set 拷贝到内核空间
- 注册回掉函数_pollwait,
- 遍历所有的fd ,调用对应的poll方法,返回一个bitmask掩码,告诉select是否有读写事件,没有进程睡眠,一只等到有资源可读,或者timeout
- 把fd_set 拷贝到用户空间
select缺点:
- 每次调用select都会将fd_set拷贝到内核空间,这个在fd 很多时开销很大
- 内核需要遍历所有的fd ,当fd很多时,开销也很大
- 找到就绪描述符,又要将fd_set 拷贝到用户空间,又要遍历一遍fd 找出是哪个文件描述符
- select 支持的文件描述符有上限,Linux一般为1024 个
pool 在机制上和select相似,也是进行轮询查找,但是poll 没有最大文件描述符限制
#include <poll.h> int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
pollfd结构体定义如下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 实际发生了的事件 */ } ;
合法的事件如下
- POLLIN 有数据可读。
- POLLRDNORM 有普通数据可读。
- POLLRDBAND 有优先数据可读。
- POLLPRI 有紧迫数据可读。
- POLLOUT 写数据不会导致阻塞。
- POLLWRNORM 写普通数据不会导致阻塞。
- POLLWRBAND 写优先数据不会导致阻塞。
- POLLMSGSIGPOLL 消息可用。
此外,revents域中还可能返回下列事件:
- POLLER 指定的文件描述符发生错误。
- POLLHUP 指定的文件描述符挂起事件。
- POLLNVAL 指定的文件描述符非法。
unsigned int nfds,
nfds_t类型的参数,用于标记数组fds中的结构体元素的总数量
int timeout
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。
timeout指定为负数值表示无限超时,使poll()一直挂起直到一个指定事件发生
timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就像它的名字那样,一旦选举出来,立即返回。
返回值:
成功时,poll()返回结构体中revents域不为0的文件描述符个数。
如果在超时前没有任何事件发生,poll()返回0。
失败时,poll()返回-1,并设置errno为下列值之一:
总结:poll 和 select 本质上一致,也是将用户传入的数组传入到内核空间,轮询遍历,如果有就绪,修改revents ,继续遍历,如果没有就绪,挂起该进程,直到有就绪,或者超时,再次遍历 ,然后拷贝给用户空间
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对象,参数size表示处理的最大描述符数量
- epoll_ctl 可以操作上面的epoll,可以将某个fd 添加到时间表中,或者删除某个文件描述符
- epoll_wait调用时,如果有事件发生,就返回给用户态、
epoll_create()的返回值本身要占用一个fd,因此最后要将这个fd,close(),否则会导致fd被消耗殆尽,
epoll_ctl :的参数
- epfd,就是创建的epoll文件描述符
- op 表示动作,用宏表示
EPOLL_CTL_ADD:注册新的fd到epfd中; EPOLL_CTL_MOD:修改已经注册的fd的监听事件; EPOLL_CTL_DEL:从epfd中删除一个fd;
- 第三个是要监听的fd,
- 第四个告诉内核需要监听什么事
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll_wait():
参数maxevents 告诉内核接收内核能得到事件的集合数量 参数struct epoll_event * 接收内核返回的就绪事件 timeout 超时时间 毫秒。
epoll 实现机制
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket。
这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。
这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层。
简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
epoll的高效就在于,当我们调用epoll_ ctl往里塞入百万个句柄时,epoll_ wait仍然可以飞快的返回,并有效的将发生事件的句柄给我们用户。
这是由于我们在调用epoll_ create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.
当epoll_ wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。
那么,这个准备就绪list链表是怎么维护的呢?
当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。
所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。
如此,一颗红黑树,一张准备就绪句柄链表,少量的内核cache,就帮我们解决了大并发下的socket处理问题。
执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪 链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
ET模式和LT模式
LT:默认水平触发模式,当调用epoll_wait时,返回的socket 未被处理,则下一次调用epoll_wait时,将再次返回这个就绪事件,
ET:沿边触发模式,当调用epoll_wait时,返回的就绪事件未被处理,则下次调用的epoll_wait 不会在返回这个就绪队列,直到这个文件描述符有了新的事件时,才会返回
原理,如果是LT模式,返回后清空了就绪链表,将未处理的再次添加到list链表中,而ET 必须要等到新的中断,才会再次epoll_wait 返回
epoll 的优点:
- 支持的打开的文件描述符很大 跟内存大小有关
- IO效率不会因为fd的增多而下降
- 提高了和用户空间消息传递的效率 (mmap 同一块内存实现)
select 和epoll的使用场景
对于并发量低的场景,select 不会在内核建立起负责的文件系统,所以效率不一定比epoll 慢