IO多路复用概念
IO多路复用是指通过一种机制,使得单个进程可以监控多个文件描述符的可读、可写和异常等事件。常见的IO多路复用技术包括:select、epoll等。在实际应用中,IO多路复用可以提高程序的运行效率和性能,减少系统开销,降低系统资源的使用率。它广泛应用于网络编程、服务器开发、操作系统等领域,可以帮助开发人员更好地处理大量的网络连接和数据请求。以下主要以select和epoll展开叙述。
IO多路复用之select
select实现原理
IO多路复用select函数是一种实现并行IO的机制,可以同时处理多个socket连接,提高系统的性能和效率。在Linux中,select是一个阻塞函数,把需要管理的文件描述符添加到fd_set集合中,由select统一管理。如果所有的文件描述符都没有数据准备好,那么select会一直阻塞等待,如果有文件描述符的数据准备好,select函数解除阻塞,但是要通过轮循的方式把有响应的文件描述符找出来。处理完后,继续循环到select的位置监听。
select相关知识
1、函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明
nfds:需要监控的最大文件描述符加1。
readfds:读文件描述符集合。
writefds:写文件描述符集合。
exceptfds:异常文件描述符集合。
timeout:select函数的超时时间设置,如果设置为NULL,则一直等待直到事件发生
返回值:
>0 : 表示准备好的文件描述符个数
=0: 超时解除阻塞
-1: 函数出错
2、辅助函数
void FD_CLR(int fd, fd_set *set); //将fd从set集合中移除
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中,如果在返回真,否则返回假
void FD_SET(int fd, fd_set *set); // 将fd加入set集合中
void FD_ZERO(fd_set *set); // 清空set集合中的内容
select应用案例
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
//成功返回监听套接字, 失败返回NULL
int sock_init()
{
int sockfd;
int ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
perror("socket");
return -1;
}
//设置套接字端口复用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in seraddr;
int addrlen = sizeof(struct sockaddr_in);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8001);
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
ret = bind(sockfd, (struct sockaddr*)&seraddr, addrlen);
if(ret<0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 10);
if(ret<0)
{
perror("bind");
return -1;
}
return sockfd;
}
int main()
{
char buff[1024];
int sockfd;
int ret;
int cfd;
sockfd = sock_init();
if(sockfd < 0)
{
return -1;
}
int maxfd;
fd_set set, rset;
FD_ZERO(&set); //清空集合
FD_SET(sockfd, &set); //文件描述符加入集合
maxfd = sockfd; //设置最大文件描述符
while(1)
{
rset = set;//由于select每次都会修改集合中的数据,所有每次都会将set的值拷贝给rset
printf("select..\n");
ret = select(maxfd+1, &rset, NULL, NULL, NULL);//调用select监听rset集合
printf("select over..\n");
if(ret<0)
{
perror("select");
break;
}
if(FD_ISSET(sockfd, &rset)) //有客户端请求连接
{
//1、接受客户端
printf("accept...\n");
cfd = accept(sockfd, NULL, NULL);
printf("accept over...\n");
if(cfd<0)
{
perror("accept");
continue;
}
//2、将cfd加入set集合
FD_SET(cfd, &set);
//3、判断最大值
if(maxfd<cfd)
{
maxfd = cfd;
}
}
for(int i=0; i<=maxfd; i++)
{
if(i == sockfd)
{
continue;
}
if(!FD_ISSET(i, &rset))
{
continue;
}
printf("read...\n");
ret = read(i, buff, 1024 );
printf("read over...\n");
if(ret<0)
{
perror("read");
//1、关闭文件描述符
close(i);
//2、从set集合中移除
FD_CLR(i, &set);
continue;
}
else if(0 == ret)
{
printf("tcp broken...\n");
//1、关闭文件描述符
close(i);
//2、从set集合中移除
FD_CLR(i, &set);
continue;
}
buff[ret] = '\0';
printf("buff: %s\n", buff);
}
}
return 0;
}
select优缺点
Select通过串行模拟并行可以实现服务端的多任务, 但是如果某个任务处理的时间很长,就会影响后面任务的处理。除此之外,select能够监听的文件描述符的个数是有限的,默认是1024。 而且select每次解除阻塞仅仅只是说明有套接字数据准备好了,具体是哪个套接字有数据要通过轮循遍历的方式给找出来。
IO多路复用之epoll
epoll实现原理
Epoll是一种高效的I/O多路复用机制,它可以同时监控多个文件描述符,当其中任意一个文件描述符就绪时,就会通知应用程序进行相应的操作。相比于传统的select和poll机制,epoll具有更高的性能和更好的扩展性。
Epoll的实现原理主要包括三个部分:epoll_create、epoll_ctl和epoll_wait。应用程序需要调用epoll_create函数创建一个epoll实例,该函数返回一个文件描述符,用于标识该实例。在Linux内核中,会为该实例创建一个红黑树和一个双向链表,用于存储待监控的文件描述符和相关的事件信息。接下来,应用程序可以通过epoll_ctl函数向epoll实例中添加、修改或删除待监控的文件描述符和事件。该函数的第一个参数是epoll实例的文件描述符,第二个参数是操作类型(EPOLL_CTL_ADD、EPOLL_CTL_MOD或EPOLL_CTL_DEL),第三个参数是待监控的文件描述符,第四个参数是一个epoll_event结构体,用于指定待监控的事件类型和相关的数据。应用程序需要调用epoll_wait函数等待文件描述符就绪。该函数的第一个参数是epoll实例的文件描述符,第二个参数是一个epoll_event数组,用于存储就绪的文件描述符和相关的事件信息,第三个参数是数组的大小,第四个参数是超时时间(单位为毫秒)。当有文件描述符就绪时,该函数会返回就绪的文件描述符数量,并将相关的事件信息存储在epoll_event数组中。
Epoll的高效性和扩展性主要体现在以下几个方面:
(1)Epoll使用红黑树和双向链表存储待监控的文件描述符和事件信息,可以快速地进行查找和插入操作,而不需要遍历整个文件描述符集合。
(2)Epoll支持边缘触发和水平触发两种模式。边缘触发只在文件描述符状态发生变化时通知应用程序,而水平触发则会在文件描述符处于就绪状态时不断通知应用程序。边缘触发可以减少不必要的通知,提高效率。
(3)Epoll支持EPOLLONESHOT事件,可以确保每个文件描述符只被一个线程处理,避免了多个线程同时处理同一个文件描述符的情况。
(4)Epoll支持EPOLLEXCLUSIVE事件,可以确保每个文件描述符只被一个进程处理,避免了多个进程同时处理同一个文件描述符的情况。
Epoll是一种高效的I/O多路复用机制,可以大大提高应用程序的性能和可扩展性。在实际应用中,应该根据具体的需求选择合适的触发模式和事件类型,以达到最佳的性能和可靠性。
epoll相关知识
1、创建集合空间
int epoll_create(int size);
参数:
size : 指定文件描述符的个数
返回值:
成功:返回与集合关联的文件描述符
失败: -1
2、管理集合空间中的文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
epfd:和集合关联的文件描述符
op : 命令参数
EPOLL_CTL_ADD: 往集合中添加文件描述符
EPOLL_CTL_MOD: 修改集合中文件描述符的信息
EPOLL_CTL_DEL: 删除集合中指定的文件描述符
fd: 操作的文件描述符
event: 如果是删除操作,该参数忽略,直接传NULL
如果是添加操作, 通过该参数告诉内核监听指定文件描述符的指定时间
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(读事件)
3、监听集合中的文件描述符
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
参数说明:
epfd:和集合关联的文件描述符
events:存放准备好文件描述符信息数组的起始地址
maxevents: 数组的最大元素个数
timeout :设置超时时间 (以毫秒为单位的)
>0 : 指定超时时间
=0 : 非阻塞函数
-1 : 永久阻塞
返回值:
>0 : 准备好的文件描述符个数
=0 :超时时间到了
-1 :出错
epoll应用案例
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/epoll.h>
//成功返回监听套接字, 失败返回NULL
int sock_init()
{
int sockfd;
int ret;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd<0)
{
perror("socket");
return -1;
}
//设置套接字端口复用
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in seraddr;
int addrlen = sizeof(struct sockaddr_in);
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(8001);
inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
ret = bind(sockfd, (struct sockaddr*)&seraddr, addrlen);
if(ret<0)
{
perror("bind");
return -1;
}
ret = listen(sockfd, 10);
if(ret<0)
{
perror("bind");
return -1;
}
return sockfd;
}
int main()
{
char buff[1024];
int sockfd;
int ret, count;
int cfd, efd;
efd = epoll_create(100);
if(efd<0)
{
perror("epoll_create");
return -1;
}
sockfd = sock_init();
if(sockfd < 0)
{
return -1;
}
struct epoll_event ev;
struct epoll_event evs[10];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(efd, EPOLL_CTL_ADD, sockfd, &ev);
while(1)
{
printf("wait...\n");
count = epoll_wait(efd, evs, 10, -1);
printf("wait over...\n");
if(count < 0)
{
perror("epoll_wait");
break;
}
for(int i=0; i<count; i++)
{
int temp = evs[i].data.fd;
if(temp == sockfd)// 有客户端请求连接
{
//1、接收客户端
printf("accept...\n");
cfd = accept(sockfd, NULL, NULL);
printf("accept over...\n");
if(cfd<0)
{
perror("accept");
continue;
}
//2、cfd 加入efd关联的集合中
ev.data.fd = cfd;
epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
}
else //已经连接过来的客户端发来数据
{
printf("read..\n");
ret = read(temp, buff, 1024);
printf("read over..\n");
if(ret<0)
{
perror("read");
//1、关闭文件描述符
close(temp);
//2、从集合中移除
epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);
}
else if(ret == 0)
{
printf("tcp broken...\n");
//1、关闭文件描述符
close(temp);
//2、从集合中移除
epoll_ctl(efd, EPOLL_CTL_DEL, temp, NULL);
}
buff[ret] = '\0';
printf("buff: %s\n", buff);
}
}
}
return 0;
}
epoll优缺点
缺点:通过串行模拟并行, 如果处理一个请求的时间过长,会影响后面任务的处理。这是所有多路IO转接的通病。
优点:(1)监听文件描述符的个数由用户指定
(2)如果有套接字的数据准备好, 内核直接告诉你哪个套接字的数据准备就绪,不需要采用轮循的方式去查找