select,poll,epoll详解
小白一枚,欢迎大家批评指正哈!
文章目录
前言
linux网络变编程主要通过select,poll,epoll三种IO多路复用技术,即可以利用select,poll,epoll同时监控多个文件描述符,提高cpu执行效率。即为了避免这里cpu的空转,我们不让这个线程亲自去fd中是否发生变化,而是引进了select,poll,epoll三种IO多路复用技术,它可以同时观察许多流的I/O事件,如果没有事件,代理就阻塞,线程就不会挨个挨个去轮询了,提高了效率。
一、select详解
select是最早出现的多路复用技术,它最多可以同时监控1024个文件描述符。当调用select这个API时,会将监视的文件描述符集合拷贝到内核,由内核顺序遍历是否有文件描述符发生变化,从而返回有变化的文件描述符的个数,但是并不知道到底是哪些文件文件描述符发生了变化,需要一个个去遍历。每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除,因此涉及到两次遍历,并且每次都会把文件描述符集合拷贝到内核,存在开销大,浪费资源的缺点。
1. API介绍
int select(int nfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,struct timeval *timeout);
参数解释:
参数1:-nfds:委托内核检测的最大文件描述符的值+1
参数2:-fd_setreadfds:委托内核检测哪些文件描述符有读的属性
参数3:-fd_setwritefds:委托内核检测哪些文件描述符有写的属性(看看写缓冲区是否已满,如果未满,则可以继续写数据)
参数4:-fd_set*exceptfds:委托内核检测哪些文件描述符发生异常
参数5:-struct timeval *timeout:是一个结构体,用于设置超时时间(NULL代表永久阻塞,0代表不阻塞,大于0代表阻塞对应的时间)
返回值解释:
- -1:失败;
-
0: 检测的文件描述符集合中有n个文件描述符发生了变化;
其他相关函数:
1.将文件描述符fd对应的标志位设置为0
void FD_CLR(int fd,fd_setset);
2.判断fd对应的标志位是0还是1
int FD_ISSET(int fd,fd_setset);
3.将文件描述符fd对应的标志位设置为1
void FD_SET(int fd,fd_setset);
4.初始化文件描述符集合为0
void FD_ZERO(fd_set set);
2. 代码编写
#include<stdio.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/select.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
//创建监听的套接字(文件描述符)
int fd1 = socket(AF_INET,SOCK_STREAM,0);//默认传输层使用tcp协议
if(fd1==-1){
perror("socket");
exit(0);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//默认使用ipv4协议
saddr.sin_addr.s_addr = 0;
saddr.sin_port = htons(9999);
int ret = bind(fd1,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(0);
}
//监听
ret = listen(fd1,128);
if(ret == -1){
perror("listen");
exit(0);
}
//创建数组用于存放多个文件描述符
fd_set reads,temp;
FD_ZERO(&reads);
FD_SET(fd1,&reads);//将监听的文件描述符设置为1
int maxfd = fd1;//将当前只有监听的文件描述符设置为最大的文件描述符值,后续再不断更新
while(1){
temp = reads;//用temp来进行内核区的操作
//调用select函数,让内核帮忙检测哪些文件描述符有数据
int ret = select(maxfd+1,&temp,NULL,NULL,NULL);
if(ret == -1){
perror("select");
exit(0);
}else if(ret == 0){
continue;//如果为0则代表没有检测到文件描述符有数据
}else if(ret>0){
//代表文件描述符对应的缓冲区的数据发生了变化
if(FD_ISSET(fd1,&temp)){
//如果监听的文件描述符发生了变化,则代表有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(fd1,(struct sockaddr*)&cliaddr,&len);
FD_SET(cfd,&reads);
maxfd = maxfd>cfd?maxfd:cfd;
}//遍历看哪些文件描述符发生了变化
for(int i = fd1+1; i<=maxfd; i++){
if(FD_ISSET(i,&temp)) {
//说明客户端有数据发送进来了
char buf[1024] = {0};
int len = read(i,buf,sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}else if(len == 0){
printf("client closed\n");
close(i);
FD_CLR(i,&reads);
}else if(len>0){
printf("read buf: = %s\n",buf);
write(i,buf,strlen(buf)+1);
}
}
}
}
}
close(fd1);
return 0;
}
二、poll详解
poll与select不同之处在于没有文件描述符的限制,并且将需要检测的文件描述符的事件和发生的文件描述符事件分开,但是都需要从内核到用户的相互拷贝,且需要顺序遍历发生变化的文件描述符
1. API介绍
struct pollfd{
int fd;//委托内核检测的文件描述符
short events;//委托内核检测文件描述符的什么事件
short revents;//文件描述符实际发生的事件
};
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
参数解释:
–参数1:-fds 是一个结构体数值,这是一个需要检测的文件描述符集合
–参数2:-nfds 检测的文件描述符的大小
–参数3:-timeout 超时时间
返回值:
成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
2. 代码编写
#include<stdio.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/select.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<poll.h>
int main(){
//创建监听的套接字(文件描述符)
int fd1 = socket(AF_INET,SOCK_STREAM,0);//默认传输层使用tcp协议
if(fd1==-1){
perror("socket");
exit(0);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//默认使用ipv4协议
saddr.sin_addr.s_addr = 0;
saddr.sin_port = htons(9999);
int ret = bind(fd1,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(0);
}
//监听
ret = listen(fd1,128);
if(ret == -1){
perror("listen");
exit(0);
}
//初始化检测的文件描述符组
struct pollfd fds[1024];
for(int i = 0; i<1024; i++){
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = fd1;
int nfds = 0;
while(1){
int ret = poll(fds,nfds+1,-1);//-1代表如果文件描述符不发生改变则阻塞
if(ret == -1){
perror("poll");
exit(0);
}else if(ret == 0){
continue;//如果为0则代表没有检测到文件描述符有数据
}else if(ret>0){
//代表文件描述符对应的缓冲区的数据发生了变化
if(fds[0].revents&POLLIN){
//如果监听的文件描述符发生了变化,则代表有新的客户端连接进来了
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(fd1,(struct sockaddr*)&cliaddr,&len);
for(int i = 1; i<1024; i++){
if(fds[i].fd==-1){
fds[i].fd = cfd;
fds[i].events = POLLIN;
break;
}
}
nfds = nfds>cfd?nfds:cfd;
}//遍历看哪些文件描述符发生了变化
for(int i = 1; i<=nfds; i++){
if(fds[i].revents&POLLIN) {
//说明客户端有数据发送进来了
char buf[1024] = {0};
int len = read(fds[i].fd,buf,sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}else if(len == 0){
printf("client closed\n");
close(fds[i].fd);
fds[i].fd = -1;
}else if(len>0){
printf("read buf: = %s\n",buf);
write(fds[i].fd,buf,strlen(buf)+1);
}
}
}
}
}
close(fd1);
return 0;
}
三、epoll详解
直接封装了三个函数实现,直接在内核中开辟一块内存,用于存放监控的文件描述符(红黑树)和已经就绪的文件描述符(双向链表),不需要顺序遍历全部的文件描述符,只需遍历已经发生变化的文件描述符
1. API介绍
int epoll_create(int size);//在内核中创建一块内存,包含两块数据,用于存放监控的文件描述符(红黑树)和已经就绪的文件描述符(双向链表),返回值为epfd,一个epoll实例
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* events)//对文件描述符进行增删改
int epoll_wait(int epollfd, struct epoll_event* events, int maxevent, int timeout);
//返回就绪的文件描述符个数,具体的发生变化的数据封装在events这个结构体中,第三个参数为指定最多监听多少个事件,其值必须大于0
2. 代码编写
#include<stdio.h>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/select.h>
#include<sys/stat.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/epoll.h>
int main(){
//创建监听的套接字(文件描述符)
int fd1 = socket(AF_INET,SOCK_STREAM,0);//默认传输层使用tcp协议
if(fd1==-1){
perror("socket");
exit(0);
}
//绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;//默认使用ipv4协议
saddr.sin_addr.s_addr = 0;
saddr.sin_port = htons(9999);
int ret = bind(fd1,(struct sockaddr*)&saddr,sizeof(saddr));
if(ret == -1){
perror("bind");
exit(0);
}
//监听
ret = listen(fd1,128);
if(ret == -1){
perror("listen");
exit(0);
}
//创建一个epoll实例
int epfd = epoll_create(100);
//将监听的文件描述符的相关信息加入到epoll实例中
struct epoll_event epev;//创建一个结构体
epev.events = EPOLLIN;//检测读事件
epev.data.fd = fd1;
epoll_ctl(epfd,EPOLL_CTL_ADD,fd1,&epev);
while(1){
//检测哪些有数据
struct epoll_event epevs[1024];
int ret = epoll_wait(epfd,epevs,1024,-1);
if(ret == -1){
perror("epoll_wait");
exit(0);
}
printf("ret : %d\n",ret);//检测到有几个发送了改变
for(int i = 0; i<ret; i++){
if(epevs[i].data.fd==fd1){
//监听的文件描述符有数据到达(有客户端连接)
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int cfd = accept(fd1,(struct sockaddr*)&cliaddr,&len);
//将连接到的客户端的文件描述符添加到实例中去
epev.events = EPOLLIN;
epev.data.fd = cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev);
}else{
//有数据到达
char buf[1024] = {0};
int len = read(epevs[i].data.fd,buf,sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}else if(len == 0){
printf("client closed\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,NULL);
close(epevs[i].data.fd);
}else if(len>0){
printf("read buf: = %s\n",buf);
write(epevs[i].data.fd,buf,strlen(buf)+1);
}
}
}
}
close(fd1);
close(epfd);
return 0;
}
总结
- poll 和select都是轮询的方式,内核每次都扫描整个注册的文件描述符集合;而epoll_wait采用回调方式,内核检测到就绪的文件描述符时,触发回调函数,回调函数将该文件描述符上对应的事件插入内核就绪队列,最后返回给用户。
- 当事件触发比较频繁时,回调函数也会被频繁触发,此时效率就未必比select 或 poll高了。
所以epoll的最佳使用情景是:连接数量多,但活跃的连接数量少。其他情况下epoll的效率较高。