select/poll/epoll

11 篇文章 3 订阅
1 篇文章 0 订阅

概述

什么是多路I/O转接技术
多路IO转接的字面意思:原本使用socket套接字编程时,是服务器(应用程序)一直在阻塞等待客户端的连接,这样服务器端(应用程序)的压力太大。于是服务器请来了助手,即select、poll、epoll等,这几个函数借助内核来替服务器监视有无客户端的连接请求,当有客户端的连接请求时,再经select、poll、epoll等助手转接给服务器端处理,这样可以有效减轻服务器的压力。

1.select()

相应代码地址:https://github.com/qingyiz/client-server/tree/master/04_%E5%A4%9A%E8%B7%AFIO%E8%BD%AC%E6%8E%A5/select_mode

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout)
功 能:监听多个文件描述符的状态变化
参 数:
nfds:      
    监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds:   
    监控有读数据到达文件描述符集合,传入传出参数  fd_set是一个结构体,可以理解为文件描述符的集合,理解为位图
writefds: 
    监控写数据到达文件描述符集合,传入传出参数
exceptfds: 
    监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout:   
    定时阻塞监控时间,3种情况
               1.NULL,永远等下去,即阻塞监听
               2.设置timeval,等待固定时间
               3.设置timeval里时间均为0,检查描述字后立即返回,轮询,即非阻塞监听
    struct timeval {
        long tv_sec; /* seconds */
        long tv_usec; /* microseconds */
    };
返回值:
    >0:所有监听集合中(即读、写、异常3个集合),满足对应事件的总数
    0:没有满足监听条件的文件描述符
    -1:失败,并设置errno    

值得一提的是 fd_set *readfds,该参数是传入传出参数,传入的是原始表,传出的是修改表,那么修改了什么?当有客户端建立连接时,相应的标志位会置1,
在这里插入图片描述
如上图所示,左边为原始表,一般默认为1024个标志位,所有的标志位都为0,当经过select函数传参回时,某些标志位会发生变化,一个客户端对应一个标志位,当有客户端进行连接时,相应标志位的值由0变成1.

文件描述符集类型: fd_set rdset;void FD_ZERO(fd_set *set);
- 全部清空
○ void FD_CLR(int fd, fd_set *set);
- 从集合中删除某一项
○ void FD_SET(int fd, fd_set *set);
- 将某个文件描述符添加到集合
○ int FD_ISSET(int fd, fd_set *set);

使用select函的优缺点:

  • 优点:
    ○ 跨平台
  • 缺点:
    ○每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
    ○同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大,越到后面的fd,遍历的越多,速度就越慢。
    ○select支持的文件描述符数量太小了,默认是1024,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及 了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,所以默认只能监视1024个socket。

2.poll()

相应代码地址:https://github.com/qingyiz/client-server/tree/master/04_%E5%A4%9A%E8%B7%AFIO%E8%BD%AC%E6%8E%A5/poll_mode

poll结构体:
struct pollfd {
 int fd;		/* 文件描述符 */
 short events; 	/* 等待的事件 */
 short revents; /* 实际发生的事件 */
};

在这里插入图片描述

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds,int timeout);
fds:
	存放需要被检测状态的套接字描述符,与select不同(select在调用之后会清空这个数组),
	每当调用这个数组,系统不会清空这个数组,而是存放revents状态变化描述符变量,这样才做起来很方便。
nfds:
	 数组的最大长度, 数组中最后一个使用的元素下标+1
	 内核会轮询检测fd数组的每个文件描述符
timeout:
	poll函数调用阻塞时间,单位是毫秒(ms)
	 -1: 永久阻塞
	 =0: 调用完成立即返回
	 >0: 等待的时长毫秒
返回值: IO发送变化的文件描述符的个数

3.epoll()

这里有一篇好文章,详细解释了epoll相关理论,可以参考下 epoll本质,这里将不在详细介绍相关理论。

相应代码地址https://github.com/qingyiz/client-server/tree/master/04_%E5%A4%9A%E8%B7%AFIO%E8%BD%AC%E6%8E%A5/epoll_mode
epoll相关API

1.epoll_create()

创建一个epoll句柄(可以理解为树),参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关,可以自己设置。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。本代码中最后一句 Close(efd);

#include <sys/epoll.h>
	int epoll_create(int size)		size:监听数目

2.epoll_ctl()

控制某个epoll监控的文件描述符上的事件:注册、修改、删除。

#include <sys/epoll.h>
	int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
		epfd:	为epoll_creat的句柄
		op:		表示动作,用3个宏来表示:
			EPOLL_CTL_ADD (注册新的fd到epfd)EPOLL_CTL_MOD (修改已经注册的fd的监听事件)EPOLL_CTL_DEL (从epfd删除一个fd);
		event:	告诉内核需要监听的事件

		struct epoll_event {
			__uint32_t events; /* Epoll events */
			epoll_data_t data; /* User data variable */
		};
		typedef union epoll_data {
			void *ptr;
			int fd;
			uint32_t u32;
			uint64_t u64;
		} epoll_data_t;

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

3.epoll_wait()

等待所监控文件描述符上有事件的产生,类似于select()调用。
该函数返回需要处理的事件数目,如返回0表示已超时。
返回的事件集合在events数组中,数组中实际存放的成员个数是函数的返回值。返回0表示已经超时。

该函数用于轮询I/O事件的发生;

#include <sys/epoll.h>
	int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
		events:	用来存内核得到事件的集合,
		maxevents:	告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
		timeout:	是超时时间
			-1:	阻塞
			 0:	立即返回,非阻塞
			>0:	指定毫秒
		返回值:		成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
		

4 epoll进阶

事件模型
EPOLL事件有两种模型:
Level Triggered (LT) 水平触发
Edge Triggered (ET) 边缘触发

1.Level Triggered (LT) 水平触发:

  1. Level Triggered (LT) 水平触发只要有数据都会触发.,也就是epoll_wait就会返回
  2. 返回的次数与发送数据的次数没有关系
  3. 这是epoll默认的工作模式

epoll的工作模式默认为水平触发模式(LT),为了更直观的体现,我们先复制一份epoll.c改名为lt_epoll.c,只需要修改一些代码就可以有直观的体现
第一处就是把buf数组 缓冲区的内存改成5,这样每次只能接收五个字符
在这里插入图片描述
第二处就是在epoll_wait 函数下增加一个printf,这样可以直观的看到eopll_wait的调用次数
在这里插入图片描述
第三处主要是把printf(),改成write(),因为printf()函数没有/0时,可能会出现乱码的情况,而有/0时,会等待缓冲区满时再刷新,才输出到屏幕上,这样 可能现象不明显。
在这里插入图片描述
接下来 就是测试了,gcc lt_epoll.c wrap.c -o lt_epoll
在这里插入图片描述
当有几个客户端连接就会调用几次epoll_wait(),当客户端发送一段很长的数据时,你会发现epoll_wait触发了很多次,但我只发了一次数据,这既是水平触发模式,但是要知道的是,epoll_wait 调用次数越多, 系统的开销越大,所以这并不是一个明智的选择。
在这里插入图片描述
在这里插入图片描述

2.Edge Triggered (ET) 边缘触发:

相应代码地址:https://github.com/qingyiz/client-server/blob/master/04_%E5%A4%9A%E8%B7%AFIO%E8%BD%AC%E6%8E%A5/epoll_mode/et_epoll.c

Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
边缘触发模式可以细分为两种模式,一个是边缘阻塞触发模式,一个是边缘非阻塞触发模式
默认为阻塞属性

1. 边缘阻塞触发模式

当客户端给server发数据时,发一次数据server 的 epoll_wait返回一次,不在乎数据是否读完,
当然也可以读完,使用一个 while(recv()){};但是这会出现一个很严重的问题,
因为fd文件描述符默认为阻塞属性,当读完一次数据时,recv阻塞,而且我们只有一个进程,此时就会一直阻塞在while(recv()){};该循环中,无法回到外层的循环中,也就是无法调用epoll_wait函数,问题出现了,那就是可以要解决问题,解决该问题就是阻塞问题,设置属性为非阻塞就行,这就出现了边缘非阻塞触发模式。

2. 边缘非阻塞触发模式

相应代码地址: https://github.com/qingyiz/client-server/blob/master/04_%E5%A4%9A%E8%B7%AFIO%E8%BD%AC%E6%8E%A5/epoll_mode/nonblock_et_epoll.c

该模式是效率最高的
那要怎么设置该模式?
我们只需要修改fd的属性就可以了,这里会涉及一个函数fcntl();由于该函数功能较多,本节只介绍本节使用的功能

#include <unistd.h>
#include <fcntl.h> 
int fcntl(int fd, int cmd); 
int fcntl(int fd, int cmd, long arg); 
int fcntl(int fd, int cmd, struct flock *lock);

/* 设置文件cfd为非阻塞模式 */
  int flag = fcntl(cfd, F_GETFL);
  flag |= O_NONBLOCK;
  fcntl(cfd, F_SETFL, flag);
  
  cfd:文件描述符
  F_SETFL:设置文件描述符标志
  F_GETFD:读取文件描述符标志
  O_NONBLOCK:非阻塞模式

效果图:
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值