select机制
- 单线程内处理多个IO请求,用户线程可根据自身需求,注册自己需要的socket或IO请求,等有数据上来再进行处理,以提高CPU利用率
- 监听上限受文件描述符限制【FD_SETSIZE】,最大 1024
- 解决1024以下客户端时使用select还可,但若链接客户端过多,采用轮询模型的select,会大大降低服务器响应效率
- 理解图示
select函数
头文件
select #include <sys/select.h>
struct timeval #include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
1. nfds 【监听】所有文件描述符中,最大文件描述符 +1
2. readfds 【读】文件描述符监听集合,传入传出参数
3. writefds 【写】文件描述符监听集合,传入传出参数 NULL
4. exceptfds 【异常】文件描述监听集合,传入传出参数 NULL
5. timeout
> 0 设置监听超时时长
= NULL 阻塞监听
= 0 非阻塞监听,轮询
6. 返回值
> 0 所有监听集合[读|写|异常]中, 满足对应事件的总数
= 0 没有满足监听条件的文件描述符
=-1 errno
7. 请注意 |r|w|e| 传入传出参数
8. 传入:各自要求监听的集合
9. 传出:各自实际发生的监听集合
相关函数
1. 清空一个文件描述符集合
void FD_ZERO(fd_set *set);
2. 将待监听的文件描述符,添加到监听集合中
void FD_SET(int fd, fd_set *set);
3. 将一个文件描述符从监听集合中移除
void FD_CLR(int fd, fd_set *set);
4. 判断一个文件描述符是否在监听集合
/* 返回值: 存在:1 | 不存在:0 */
int FD_ISSET(int fd, fd_set *set);
代码部分示例
// main函数
int main(int argc, char *argv[])
{
int i, j, n, maxi;
/* 自定义数组client, 存要监听的文件描述符 */
int nready, client[FD_SETSIZE];
int maxfd, listenfd, connfd, sockfd;
/* #define INET_ADDRSTRLEN 16 */
char buf[BUFSIZ], str[INET_ADDRSTRLEN];
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
/* rset 读事件文件描述符集合 allset暂存 */
fd_set rset, allset;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
/* 端口复用 */
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 填充addrInfo */
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
/* 起初 listenfd 即为最大文件描述符 */
maxfd = listenfd;
/* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
maxi = -1;
/* 用-1初始化client[] */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1;
FD_ZERO(&allset);
/* 构造select监控文件描述符集 */
FD_SET(listenfd, &allset);
while (1) {
/* 每次循环时,更新设置select监控集合 */
rset = allset;
/* [maxfd+1]: lfd [rset]: cfd*/
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
/* 有新的客户端连接请求存在 */
if (FD_ISSET(listenfd, &rset)) {
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
/* 找client[]中没有使用的位置 */
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd;
break;
}
/* 达上限1024,进行错误处理 */
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
/* 向监控文件描述符集合allset添加新的文件描述符connfd */
FD_SET(connfd, &allset);
/* 更新maxfd */
if (connfd > maxfd)
maxfd = connfd;
/* 保证maxi存的总是client[]最后一个元素下标 */
if (i > maxi)
maxi = i;
/* 说明此时只有listenfd,不存在监听的 读事件,没必要继续执行后续,直接continue*/
if (nready == 1)
continue;
}
/* 筛选并查找 【有数据传入的客户端】,进行read读取/write回写处理 */
for (i = 0; i <= maxi; i++) {
if ((sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
/* 当client关闭链接时,服务器端也关闭对应链接 */
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) {
Close(sockfd);
/* 移除select对此文件描述符的监控 */
FD_CLR(sockfd, &allset);
client[i] = -1;
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0)
break;
}
}
}
Close(listenfd);
return 0;
}
poll机制(了解)
相关知识
- poll,半成品,是对select的改进,但相对提升不大
- 最终版本epoll,需重点掌握
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
1. fds:监听的文件描述符【数组】
2. struct pollfd {
int fd; 待监听的文件描述符
short events; 待监听的文件描述符对应的监听事件
取值:POLLIN、POLLOUT、POLLERR
short revnets; 传入时,赋为0
如果满足对应事件的话, 返回非0 => POLLIN、POLLOUT、POLLERR
}
3. nfds: 监听数组的,实际有效监听个数
4. timeout
>0: 超时时长[单位:毫秒]
-1: 阻塞等待
=0: 不阻塞
5. 返回值: 返回满足对应监听事件的文件描述符[总个数]
6. 优点
自带数组结构, 可以将[监听事件集合]和[返回事件集合]分离
拓展监听上限, 超出1024限制
7. 缺点:
不能跨平台, Linux
无法直接定位满足监听事件的文件描述符, 编码难度较大
epoll机制(重点)
epoll相关函数
#include <sys/epoll.h>
1. 创建一棵监听红黑树
int epoll_create(int size)
-size 创建的红黑树的监听节点数量,跟内存大小有关[估摸的预计值,仅供内核参考]
-返回值 成功:指向新创建的红黑树的根节点[epfd]
失败: 返回-1, 置errno
2. 操作监听红黑树,往树上摘结点 ,挂结点
函数及结构体声明:
//返回值:成功 0 | 失败-1 [errno]
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
参数详解:
-epfd 为epoll_create的句柄
-op 表示动作,用3个宏来表示:
EPOLL_CTL_ADD 添加fd到 监听红黑树
EPOLL_CTL_MOD 修改fd在 监听红黑树上的监听事件。
EPOLL_CTL_DEL 将一个fd 从监听红黑树上摘下(取消监听)
-fd 待监听的fd
-event 告之内核【需要监听的事件】
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
1. 本质 struct epoll_event 结构体 地址
2. 成员 events:EPOLLIN(读) / EPOLLOUT(写) / EPOLLERR(异常)
3. 成员 data: 联合体[共用体]
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
1. int fd 对应监听事件的 fd
2. void *ptr
3. 等待所监听的cfd上有事件发生(阻塞监听),类似于select()调用
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
参数值:
-epfd epoll_create()的返回值
-events 传出参数,【数组】,满足监听条件的那些【fd结构体】
-maxevents 数组元素的[总个数]
struct epoll_event events[1024]
告之内核events大小,【maxevents】不能大于epoll_create(size)
-timeout 超时时间:【-1:阻塞 | =0:不阻塞 | >0:超时时间(毫秒)】
返回值: > 0 满足监听的 总个数,可用作循环上限
= 0 没有fd满足监听事件
=-1 失败,置为errno
实现思路
大致步骤:
// 监听 lfd
lfd = socket();
bind();
listen();
// epfd, 监听红黑树的[根节点]
int epfd = epoll_create(1024);
// tep: 用来设置单个fd属性
// ep : epoll_wait() 传出的满足监听事件的数组
struct epoll_event tep, ep[1024];
// 初始化lfd的监听属性
tep.events = EPOLLIN;
tep.data.fd = lfd
// 将lfd 挂到监听红黑树上
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &tep);
while(1) {
// 实施监听
ret = epoll_wait(epfd, ep,1024, -1);
for (i = 0; i < ret; i++) {
// lfd 满足读事件, 有新的客户端发起连接请求
if (ep[i].data.fd == lfd) {
// 创建新的套接字cfd与客户端进行连接
cfd = Accept();
// 初始化 cfd的监听属性
tep.events = EPOLLIN;
tep.data.fd = cfd;
// 将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);
// 将关闭的cfd,从监听树上摘下
epoll_ctl(epfd, EPOLL_CTL_DEL, ep[i].data.fd , NULL);
}
else if(n > 0) {
// 处理操作,然后回写
write(ep[i].data.fd, buf, n);
}
}
}
}
总结select && epoll
用户态与内核态
- 例如:服务端读取文件内容发送至客户端过程:
- 读:调用【系统函数】访问磁盘IO读取【数据】 => 保存数据到内核buf中 => 拷贝数据到用户buf中
- 写:调用【系统函数】拷贝用户数据到 【内核】 => 将数据写入IO设备中
阻塞IO与非阻塞IO
- 阻塞I/O 分为两部分阻塞
- 第一个阶段:等待数据准备完成
- 第二个阶段:等数据从内核buf拷贝到用户内存,内核返回结果,用户进程接触阻塞
- 非阻塞I/O 主动询问内核数据是否准备完毕
- 用户进程系统调用 => 内核buf数据没准备好 =>直接返回error
- 不断询问发送R|W操作,直到直接拷贝用户内存(此时内核buf已备)
多路IO复用(同步I/O)
-例: 用户进程调用select(阻塞等待) => 内核监听fds(需拷贝) => 当有新连接,则返回 =>再调用读写操作 =>数据(内核-用户)
select函数( )存在的问题
- 监听集合lfd限制【1024】| 集合需拷贝(用户 =>内核)
- 当有新连接时,会遍历整个socket集合来收集fds可读列表
epoll的引入解决问题
- (1)epoll_wait()读集合fds拷贝问题
- epoll利用内存映射mmap(用户-内核),减少两者间的数据交换
- epoll使用红黑树来组织监控的fds集合,epoll_ctl()来对监控的fds集合进行增删改操作
- (2)遍历就绪态的集合fds
- epoll中间层(ready_list双向链表+sleep_queue睡眠队列)
- sockfd睡眠被唤醒调用回调函数将当前fd插入ready_list,并执行回调遍历函数收集读事件,通过epoll_wait()传入的事件数组唤醒相应process