I/O多路转接

一、select

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

select函数就是一个就绪事件的通知机制,就绪事件分为两种:读事件就绪和写事件就绪

读事件就绪

  • read:底层数据从无 -> 有, 有 -> 多
  • select:底层数据只要有,就是读事件就绪

写事件就绪

  • write:底层缓冲区剩余空间从无 -> 有, 有 -> 多
  • select:底层缓冲区剩余空间只要有,就是写事件就绪

select等到事件就绪后,调用read、recv、write、send等不会被阻塞,因为已经有数据或有空间了

select 可以一次等待多个文件描述符

1.1 select函数接口

man 2 select
#include<sys/select.h>

int  select(int nfds, fd_set* readfds, fd_set* writefds, 
            fd_set* exceptfds,  struct timeval* timeout);

参数解释:

  • nfds:select在等待的多个描述符值中,最大的文件描述符+1(对多个文件描述符进行轮询检测 例如:i = 0; i < 10; i++ )
  • fd_set :类似 sigset_t 是一个位图,可以将特定的fd添加(调函数添加和删除)到位图中(0000 0000 1号fd添加到fd_set中变为0000 0010),该参数为输入输出型函数,输入:用户告诉内核哪些文件描述符要监视,输出:内核告诉用户哪些文件描述符事件就绪
  • timeout:用来设置select()的等待时间,设置为NULL 《=》阻塞式等待:没有事件就绪的文件描述符一直阻塞等待; 设置为结构体里的时间为0《=》非阻塞式等待:没有事件就绪立即返回
 struct timeval{
	 long  tv_sec;    //second
	 long  tv_usec;   //microseconds
};

提供了一组操作fd_set的接口, 来比较方便的操作位图

 void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
 int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
 void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
 void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

返回值:

  • 返回所有事件就绪的文件描述符个数
  • 返回0,timeout时间过期了
  • 返回-1,等待出错了

错误值可能为:

  • EBADF文件描述词为无效的或该文件关闭
  • EINTR 被信号中断
  • EINVAL 参数n为负值
  • ENOMEM 核心内存不足

1.2 select具体使用(代码)

IO多路转接之select服务器的具体实现

1.3 select的优缺点和使用场景

从上述具体用法的代码可以看出select是存在不少缺点的:

  1. select能够同时等待的文件描述符是有上限的。因为fd_set是一个具体的数据类型,而在CentOS中sizeof(fd_set)*8 = 1024得出文件描述符集中添加的fd最多只有1024个
  2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  3. select每次调用,都必须重新手动给fd_set重新添加fd,一定会影响程序运行的效率,而且非常麻烦容易出错
  4. 参数maxfd+1:操作系统在检测fd就绪时,需要遍历。所以当有大量的链接时,内核同步select底层遍历,成本会变得越来越高

select的优点:

  • select可以同时等待多个fd,而且只负责等待,有具体的accept,recv,send来完成实际的IO操作,这也任何一个fd就绪的概率增加了,服务器可以在单位时间内,等的比重降低,提高了效率

适应场景:

  • 适合有大量的链接,但是只有少量是活跃的!多路转接适合的场景!(如:聊天工具)

二 、poll

2.1 poll函数接口

#include<poll.h>

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

//pollfd结构
struct pollfd{
	int fd;          
	short events; //用户告知内核要关心哪些事件
	short revents; //内核告知用户哪些fd的哪些事件就绪了
};

参数说明:

  • fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合
  • nfds表示fds数组的长度
  • timeout表示poll函数的超时时间, 单位是毫秒(ms)。0《=》非阻塞式等待, -1《=》阻塞式等待

events和revents的取值:
在这里插入图片描述

返回值与select相同: >0 : 事件就绪的fd个数,=0: 超时无就绪,<0:等待出错

2.2 poll具体使用(代码)

IO多路转接之poll服务器的具体实现

2.3 poll的优缺点

优点:

  1. 解决了select能检测的文件描述符是有上限的这个问题,数组大小取决于内存大小,理论可以无限大
  2. 将event和revent分离,不用在每次调用poll的时候,重新添加fd以及fd要关心的事件

缺点:

  1. 当面临的链接很多,需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  2. 数组的大小越大时,操作系统检测fd就绪还是需要遍历,成本会变高

三、epoll

epoll:是为了处理大批量句柄而做了改进的poll

3.1 epoll的相关系统调用

epoll有3个相关的系统调用

epoll_create —打开一个epoll文件描述符(创建epoll模型,底层包括红黑树、注册回调函数,就绪队列)

#include<sys/epoll.h>

int epoll_create(int size); //成功返回fd,失败返回-1,
                    // 参数只是为了兼容之前的版本,可以忽略

使用完之后必须close()关闭

epoll_ctl —告诉内核epfd模型中的fd的哪些事件(对底层的红黑树节点进行相关操作)

#include<sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

参数:

  • epfd:epoll模型id,epoll_create()的返回值
  • op:表示动作,用三个宏来表示
  • fd:需要监听的fd
  • event:告诉内核需要监听什么事

第二个参数的取值:

  • EPOLL_CTL_ADD:注册新的fd到epfd中
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件
  • EPOLL_CTL_DEL:从epfd中删除一个fd

struct epoll_event结构如下:

在这里插入图片描述

events可以是以下几个宏的集合:

  • EPOLLIN : 表示对应的文件描述符可以读
  • EPOLLOUT : 表示对应的文件描述符可以写
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外来数据)
  • EPOLLERR : 表示对应的文件描述符发生错误
  • EPOLLHUP : 表示对应的文件描述符被挂断
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需再次把这个socket加入到EPOLL队列里

epoll_wait —告诉用户epfd模型中要关心的哪些事件就绪了

#include<sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, 
                 int timeout);

参数:

  • epfd:epoll模型id
  • events:epoll会将发生的事件赋值到events数组中(events不可以是空指针)
  • maxevents:告诉内核这个events有多大,maxevents不能大于epoll_create()的size
  • timeout:超时时间,0立即返回,-1永久阻塞

3.2 epoll的工作机制

在这里插入图片描述

  • 当某一个进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
struct eventpoll{
	... ...
	//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件
	struct rb_root rbr;
	//双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
    ... ...
};
  • 每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进行来的事件
  • 这些事件都会挂在到红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系。也就是说,当响应的事件发生时会调用这个回调方法
  • 这个回调方法在内核叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体
struct epitem{
	struct rb_node rbn; //红黑树节点
	struct list_head rbllink; //双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll* ep //指向其所属的eventpoll对象
	struct epoll_event event; /期待发生的事件类型
};
  • 当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
  • 如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。这个操作时间复杂度尾O(1)

3.3 epoll具体使用(代码)

IO多路转接之epoll服务器的具体实现

3.4 epoll两种工作方式

epoll有2种工作方式-水平触发(LT )和边缘触发(ET)
两种工作方式本质就是数据就绪通知方式的不同

epoll的默认工作方式是LT,水平触发

  • 工作机制:以读事件为例,底层只要有数据,就会一直通知用户拿走数据

epoll的ET工作方式,边缘触发

  • 工作机制:以读事件为例,底层数据从无到有,从有到多会通知一次用户,之后不再通知

ET的通知方式有一个问题,就是如何保证将本次数据全部读完:

  1. 循环读取,直到实际读取的数据小于要读取的数据
  2. 如果实际读取的数据刚好为0,就会发生阻塞,这又要怎么办呢?

所以ET模式下的所有fd,必须将该fd设置为非阻塞!

ET模式倒逼用户一次全部取走数据,tcp的滑动窗口会更新的更大,传输层能传输更多的数据,也减少了内核通知用户事件就绪的重复次数

3.5 epoll的优点(和select的缺点对应)

  • 接口使用方便:接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响
  • 没有数量限制: 文件描述符数目无上限
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值