原文链接:https://blog.csdn.net/qq_59084325/article/details/127453099

多路IO转接服务器也叫做多任务IO服务器。该类服务器实现的主旨思想是, 不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件。
通过这种方式在单线程 / 进程的场景下也可以在服务器端实现并发。常见的 IO 多路转接方式有: select、 poll、 epoll。

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

函数参数:

  • nfds: 监听的所有文件描述符中,最大文件描述符+1
  • readfds: 读 文件描述符监听集合,传入传出参数
  • writefds: 写 文件描述符监听集合,传入传出参数
  • exceptfds: 异常 文件描述符监听集合,传入传出参数
  • timeout: 定时阻塞监控时间,3种情况
    NULL,永远等下去,阻塞监听
    设置timeval,等待固定时间
    设置timeval里时间均为0,检查描述字后立即返回,轮询
    函数返回值:
    大于 0:成功,返回集合中已就绪的文件描述符的总个数
    等于 - 1:函数调用失败
    等于 0:超时,没有检测到就绪的文件描述符

d_set 类型的参数还需要使用相关的一些列操作函数,具体如下:

// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);

2.poll
#include <poll.h>
// 每个委托poll检测的fd都对应这样一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 /
short events; /
委托内核检测文件描述符的什么事件 /
short revents; /
文件描述符实际发生的事件 -> 传出 */
};

struct pollfd myfd[100];
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds: 这是一个 struct pollfd 类型的数组,里边存储了待检测的文件描述符的信息,这个数组中有三个成员:

fd:委托内核检测的文件描述符
events:委托内核检测的 fd 事件(输入、输出、错误),每一个事件有多个取值
取值:POLLIN、POLLOUT、POLLERR
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
可能的值:POLLIN、POLLOUT、POLLERR
nfds: 监听数组的,实际有效监听个数。

优点:
自带数组结构。 可以将监听事件集合和返回事件集合分离。
拓展监听上限。 超出1024限制。
缺点:
不能跨平台。 仅限Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大。

3.epoll
​ epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

原文链接:https://blog.csdn.net/qq_59084325/article/details/127453099

epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

#include <sys/epoll.h>
// 创建epoll实例,通过一棵红黑树管理待检测集合
int epoll_create(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

函数参数:

typedef union epoll_data {
 	void        *ptr;
	int          fd;	// 通常情况下使用这个成员, 和epoll_ctl的第三个参数相同即可
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

struct epoll_event {
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};

函数参数size:用来告诉内核监听的文件描述符的个数,跟内存大小有关。
函数返回值:失败:返回 - 1;成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的 epoll 实例了
epfd:epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
op:这是一个枚举值,控制通过该函数执行什么操作
EPOLL_CTL_ADD:往 epoll 模型中添加新的节点
EPOLL_CTL_MOD:修改 epoll 模型中已经存在的节点
EPOLL_CTL_DEL:删除 epoll 模型中的指定的节点
fd:文件描述符,即要添加 / 修改 / 删除的文件描述符
event:epoll 事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
events:委托 epoll 检测的事件
EPOLLIN:读事件,接收数据,检测读缓冲区,如果有数据该文件描述符就绪
EPOLLOUT:写事件,发送数据,检测写缓冲区,如果可写该文件描述符就绪
EPOLLERR:异常事件
data:用户数据变量,这是一个联合体类型,通常情况下使用里边的 fd 成员,用于存储待检测的文件描述符的值,在调用 epoll_wait() 函数的时候这个值会被传出。
events: 传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息

maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,

timeout: 是超时时间

lfd = socket();			监听连接事件lfd
bind();
listen();
int epfd = epoll_create(1024);				epfd, 监听红黑树的树根。
struct epoll_event tep, ep[1024];			tep, 用来设置单个fd属性, ep 是 epoll_wait() 传出的满足监听事件的数组。
tep.events = EPOLLIN;					初始化  lfd的监听属性。
tep.data.fd = lfd
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep);		将 lfd 添加到监听红黑树上。
while (1) {
	ret = epoll_wait(epfd, ep,1024, -1);			实施监听
	for (i = 0; i < ret; i++) {		
		if (ep[i].data.fd == lfd) {				// lfd 满足读事件,有新的客户端发起连接请求
			cfd = Accept();
			tep.events = EPOLLIN;				初始化  cfd的监听属性。
			tep.data.fd = cfd;
			epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &tep);
		} else {						cfd 们 满足读事件, 有客户端写数据来。
			n = read(ep[i].data.fd, buf, sizeof(buf));
			if ( n == 0) {
				close(ep[i].data.fd); 
				epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL);	// 将关闭的cfd,从监听树上摘下。
			} else if (n > 0) {
				小--大
				write(ep[i].data.fd, buf, n);
			}
		}
	}