目录

IO多路复用实现并发服务器

IO多路复用实现并发服务器

select 的调用注意事项

在使用 select 函数时,需要注意以下几个关键点:

  1. 参数的修改与拷贝

readfds 等参数是结果参数 :

select 函数会直接修改传入的 fd_set(如 readfds、writefds 和 exceptfds)。

为了保留原始监听集合,通常会定义一个备份集合(如 allread_fdset),并将它的拷贝传递给 select。

示例:

fd_set allread_fdset, readfds;

FD_ZERO(&allread_fdset);

FD_SET(fd1, &allread_fdset);

FD_SET(fd2, &allread_fdset);

readfds = allread_fdset; // 拷贝到临时集合

select(…, &readfds, …);

  1. 计算 nfds

nfds 是最大文件描述符值 + 1 :

在新增监听句柄时,更新 nfds 较为简单。

在减少监听句柄时,更新 nfds 较为复杂:

如果需要精确计算,可以通过遍历或维护一个最大堆等数据结构来找到第二大的文件描述符。

或者,可以选择忽略 nfds 的更新,但可能导致性能下降。

  1. 超时参数 timeout

timeout 的含义 :

如果为 NULL,表示阻塞等待,直到有事件发生。

如果指向的时间为 0,表示非阻塞模式。

如果指定超时时间,则 select 会在超时后返回。

注意:Linux 实现中,select 返回时会修改 timeout 为剩余时间 :

如果需要重复使用 timeout,需要重新初始化。

  1. 返回值的处理

返回值的意义 :

-1:表示错误。

0:表示超时时间到,没有事件发生。

正数:表示监听到的事件总数(包括可读、可写和异常事件)。

优化事件处理 :

可以利用返回值避免不必要的检查。例如,如果返回值为 1,并且已经在可读集合中处理了一个事件,则无需再检查可写和异常集合。

select 的缺点

尽管 select 是一种经典的 I/O 多路复用机制,但它存在以下显著缺点:

  1. 文件描述符数量限制

FD_SETSIZE 的限制 :

每个 fd_set 最多只能监听 FD_SETSIZE 个文件描述符(在 Linux 上通常是 1024)。

这一限制使得 select 不适合高并发场景。

  1. 遍历效率低

需要逐一检查文件描述符 :

返回的 fd_set 是一个位图,应用程序需要对所有监听的文件描述符逐一调用 FD_ISSET 来判断是否就绪。

示例:

for (int i = 0; i < nfds; i++) {

if (FD_ISSET(i, &readfds)) {

// 处理可读事件

}

}

  1. nfds 的效率问题

select 的实现方式 :

select 内部会遍历从 0 到 nfds-1 的所有文件描述符,判断每个描述符是否是关心的,并检查是否有事件发生。

即使只监听少数几个文件描述符(如 0 和 1000),select 仍然需要遍历 1001 个描述符,导致效率低下。

总结

优点

简单易用,跨平台支持广泛。

缺点

文件描述符数量受限 :最多只能监听 FD_SETSIZE 个文件描述符。

遍历效率低         :需要逐一检查文件描述符,增加了开销。

nfds 的问题        :即使监听的文件描述符稀疏分布,select 仍需遍历所有小于 nfds 的描述符。

这些缺点促使了更高效的 I/O 多路复用机制(如 poll 和 epoll)的出现,尤其是在高并发场景下,epoll 成为了更优的选择。

https://i-blog.csdnimg.cn/direct/879bb25be5374083bb059be00a44c444.png

https://i-blog.csdnimg.cn/direct/0d8922a252fe4a9087b6fe8fec03da0b.png

https://i-blog.csdnimg.cn/direct/d2ab34b0cc4f40708115e1bd05df2a37.png

https://i-blog.csdnimg.cn/direct/06c3e8ed9501441aae55db51edc6cb1d.png

poll

针对select 做了改进

底层实现 — 用的是数组

poll — 链表

poll 引入了事件机制

  1. 遍历?

  2. poll 需要在 用户空间 和 内核空间 来回拷贝

epoll

三种多路IO操作中最高效

  1. epoll

int epoll_create(int size);

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

typedef union epoll_data {

void *ptr;

int fd;

__uint32_t u32;

__uint64_t u64;

} epoll_data_t;

struct epoll_event {

__uint32_t events;      /* Epoll events */

epoll_data_t data;      /* User data variable */

};

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

epoll 解决了select和poll的几个性能上的缺陷:

① 不限制监听的描述符个数(poll也是),只受进程打开描述符总数的限制;

② 监听性能不随着监听描述 符数的增加而增加,是O(1) 的,

不再是轮询描述符来探测事件,而是由描述符主动上报事件; //事件机制的

③ 使用共享内存的方式,不在用户和内核之间反复传递监听的描述 符信息;

④ 返回参数中就是触发事件的列表,不用再遍历输入事件表查询各个事件是否被触发


epoll显著提高性能的前提是:

监听大量描述符,

并且每次触发事件的描述符文件非常少。

epoll的另外区别是:

①epoll创建了描述符,记得close;

②支持水平触发和边沿触发。

epoll使用注意事项:

//epoll_create

① int epoll_create(int size);    //创建epoll文件描述符

参数size并不是限制了epoll所能监听的描述符最大个数,

只是对内核初始分配内部数据结构的一个建议。

返回是epoll描述符。-1表示创建失败。

//epoll_ctl

② int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);    //epoll文件描述符的控制接口

功能:

epoll_ctl控制对指定描述符fd执行op操作,event是与fd关联的监听事件。

参数:

@epfd — epoll对象

@op

op操作有三种:

添加EPOLL_CTL_ADD,

删除EPOLL_CTL_DEL,

修改EPOLL_CTL_MOD。

分别添加、删除和修改对fd的监听事件。

重复添加fd会怎样(event相同或不相同):

添加失败(errno:17, File exists)

删除和修改不存在的fd会怎样:

删除或修改失败(errno:9,Bad file descriptor)

@fd – 关心的fd

event是与监听的fd相关联的事件信息,event->events描述了要监听的事件类型,有以下类型:

//事件类型:

EPOLLIN        可读

EPOLLOUT       可写

EPOLLRDHUP     套接口对端close或shutdown写,在ET模式下比较有用

EPOLLPRI       紧急数据可读

EPOLLERR       异常条件

EPOLLHUP       挂起,EPOLLERR和EPOLLHUP始终由epoll_wait监听,不需要用户设置

EPOLLET        边沿触发模式,在描述符状态跳变时才上报监听事件。(监听默认都是LT模式)(ET+非阻塞模式)

EPOLLONESHOT   只一次有效,设置oneshot标记,描述符在触发一次事件之后自动失效(fd还被监听),

不会再上报任何事件,直到使用EPOLL_CTL_MOD重新激活,

设置新的监听事件为止(可不可以和之前的事件一样?)。

event->data是个共用体,可以存放和fd绑定的描述符信息,

比如就存放描述符本身fd,或者一个结构体信息,包括fd,ip,port等等。

在epoll_wait返回时,只会返回一个event列表,需要从列表元素中获取fd等信息。

返回值:

返回0表示控制成功,

返回-1表示失败。

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

//等待epfd上的io事件,最多返回maxevents个事件

timeout = -1 的行为是block;

timeout =  0 是立即返回

④ epoll监听ET事件时,fd必须是非阻塞套接口。

比如监听可读事件,当ET上报可读后,需要一直读fd直到遇到EAGAIN错误为止,以免遗留数据在缓冲区中。

如果fd是阻塞的,则会读到阻塞了。

EAGAIN错误对于非阻塞套接口来说不是错误,只是说没有数据可读或者没有空间可写。

EWOULDBLOCK就是EAGAIN,值都是11。

selset/poll/epoll的LT模式监听的fd可以是阻塞模式的。

⑤ 多路复用监听io事件时,如果对某个套接口监听可写事件,总是会返回可写而事实上可能没有数据要写。

处理方法:

①只有在有数据要写时才把要写的套接口加入 监听列表中,数据全部写完之后从监听列表中删除它;

②在有数据写时,首先尝试直接写,当直接写没有把数据全部写入发送缓冲区时再把这个套接口加入可写事件 监听列表。

(这种方式效率较高,需要套接口是非阻塞的,前一种方式可以是阻塞的吗?)

可以是阻塞的。

https://i-blog.csdnimg.cn/direct/e07b5f8dfc1e4a929fcae5a38f13f4f9.png

https://i-blog.csdnimg.cn/direct/896abb49cd9043d797ec8e8942a21021.png