参考文献:
select : https://blog.csdn.net/youyou1543724847/article/details/83445226
原文地址:
https
文章目录
1. select
函数原型:
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
返回值分为三种情况:
- 做好准备的文件描述符的个数
- 返回0:超时;
- 返回-1:错误;
当没有错误、没有超时,返回时,不能直接直到那个文件可以操作,而需要遍历fd_set,对每个描述符进行测试,检查该文件是否ready(通过宏 FD_ISSET进行测试,FD_ISSET(fd, &fds) )。
另外,关于select支持的监控文件数量限制:
select监控的文件描述符都要在fd_set 中设置。但是linux对fd_set 的大小做了限制:#define __FD_SETSIZE 1024
参见:https://blog.csdn.net/youyou1543724847/article/details/83445226
一般使用步骤:
初始化化好需要监听的文件,绑定到 对应的fd_set中,然后调用select等待相关的文件可读、可写或是异常发生。然后对所有监听的文件列表进行一一测试,判定是否有事件发生,如有就处理。直到处理完所有的文件列表
2. poll
函数原型:
int poll(struct pollfd fd[], nfds_t nfds, int timeout);
poll函数与select函数差不多
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
3. epoll
linux2.6 内核正式引入。
3.1 函数原型
3.1.1 创建epoll文件句柄
int epoll_create(int size);
函数参数:
- size: 用来告诉内核要监听的数目一共有多少个
返回值:
- 成功时,返回一个非负整数的文件描述符,作为创建好的epoll句柄
- 调用失败时,返回-1,错误信息可以通过errno获得。
3.1.2 在特定的epoll文件句柄上,注册、修改、删除 要监听文件与事件
int epoll_ctl( int epfd,
int op, int fd,
struct epoll_event *event )
函数参数:
- 参数epfd:epoll_create()函数返回的epoll句柄。
- 参数op:操作选项。指定本次调用是新注册、修改、还是删除已经注册的fd
- 参数fd:要进行操作的目标文件描述符。
- 参数event:struct epoll_event结构指针,将fd和要进行的操作关联起来。
返回值:
- 成功时,返回0,作为创建好的epoll句柄;
- 调用失败时,返回-1,错误信息可以通过errno获得。
说明:epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
参数op的可选值有以下3个:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd;
struct epoll_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 */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
关于epoll events中的 水平触发与边缘触发:
Edge Triggered (ET) :边缘触发(events 设置为EPOLLET),只有数据到来,才触发,不管缓存区中是否还有数据。
Level Triggered (LT): (默认的工作方式)水平触发,只要有数据都会触发(如上一次调用时,数据没有读完,再次调用时,还是再次触发事件)
3.1.2 开始监听,等待事件的产生
int epoll_wait( int epfd,
struct epoll_event * events,
int maxevents,
int timeout);
函数参数:
- 参数epfd:epoll_create()函数返回的epoll句柄。
- 参数events:struct epoll_event结构指针,用来从内核得到事件的集合。
- 参数 maxevents:告诉内核这个events有多大
- 参数 timeout: 等待时的超时时间,以毫秒为单位。
返回值:
- 成功时,返回需要处理的事件数目。具体那些fd可用,通过epoll_event *events获取,有意义的数据长度就是这里的返回值;
- 调用失败时,返回0,表示等待超时。
3.2使用示例
#include<stdio.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<unistd.h>
#include<ctype.h>
int main(int argc,char *argv[])
{
int listenfd = socket(AF_INET,SOCK_STREAM,0);
.........//省略对该socket的初始化共组,包括绑定端口,开始监听client请求
// 创建一个epoll fd
int efd = epoll_create(1024);
struct epoll_event tep.events = EPOLLIN; tep.data.fd = listenfd;
// 把监听socket 先添加到efd中
ret = epoll_ctl(efd,EPOLL_CTL_ADD,listenfd,&tep);
// 循环等待
struct epoll_event ep[MAX_OPEN_FD]; //使用ep保存需要处理的返回的epoll_event
for (;;)
{
// 返回已就绪的epoll_event,-1表示阻塞,没有就绪的epoll_event,将一直等待
size_t nready = epoll_wait(efd, ep ,MAX_OPEN_FD ,-1 );
for (int i = 0; i < nready; ++i) //处理每个ready的fd
{
if (ep[i].data.fd == listenfd ) //server socket可读,即有新client在建立连接
{
connfd = accept(listenfd,(struct sockaddr*)&cliaddr,&clilen);
tep.events = EPOLLIN;
tep.data.fd = connfd;
ret = epoll_ctl(efd,EPOLL_CTL_ADD,connfd,&tep); //和新的client建立连接,将新的confd注册到epoll上。
}
// 否则,读取数据
else //之前的client发送来了数据,需要处理
{
connfd = ep[i].data.fd;
int bytes = read(connfd,buf,MAXLEN);
// 客户端关闭连接
if (bytes == 0){
ret =epoll_ctl(efd,EPOLL_CTL_DEL,connfd,NULL); //处理完这个连接了,在epoll中取消注册
close(connfd); //关闭连接
printf("client[%d] closed\n", i);
}
else
{
for (int j = 0; j < bytes; ++j)
{ buf[j] = toupper(buf[j]); }
// 向客户端发送数据
write(connfd,buf,bytes);
}
}
}
}
return 0;
}
3.3 epoll说明
Linux 内核:epoll系统向内核注册了一个文件系统,用于存储上述的被监控socket。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。
epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的socket(通过epoll_ctl注册),这些socket会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。
通过epoll_ctl函数都会被解释成对相关红黑树上的节点的操作。重复添加是没有用的。
当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。
那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。