IO多路复用是一种操作系统技术,旨在提高系统处理多个输入输出操作的性能和资源利用率。与传统的多线程或多进程模型相比,IO多路复用避免了因阻塞IO而导致的资源浪费和低效率问题。它通过将多个IO操作合并到一个系统调用中,允许程序同时等待多个文件描述符(如sockets、文件句柄等)变为可读或可写状态,然后再执行实际的IO操作。
在IO多路复用的实现中,常用的系统调用包括select()
、poll()
和epoll()
。这些机制允许程序监视多个描述符,一旦某个描述符就绪(通常是读就绪或写就绪),程序就会被通知进行相应的读写操作。这个过程通常涉及两个阶段:
尽管select()
、poll()
和epoll()
都是同步IO操作,但它们提供了一种有效的方式来处理并发IO,降低了系统开销,并提高了并发处理能力。与此不同,异步IO(AIO)模型进一步简化了IO操作,因为它允许操作系统自动处理数据从内核到用户空间的复制过程,无需程序显式调用读写操作。这意味着在异步IO模型中,读写操作由操作系统在后台完成,从而进一步提高了应用程序的效率和响应性。
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数 | 说明 |
---|---|
nfds | 是需要监视的最大的文件描述符值+1 |
readfds | 需要检测的可读文件描述符的集合 |
writefds | 需要检测的可写文件描述符的集合 |
exceptfds | 需要检测的异常文件描述符的集合 |
timeout | 当timeout等于NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; 当timeout为0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。 当timeout为特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。 |
返回 | —— |
> 0 | 返回文件描述词状态已改变的个数 |
== 0 | 代表在描述词状态改变前已超过timeout时间,没有返回 |
< 0 | 错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测,错误值可能为: EBADF:文件描述词为无效的或该文件已关闭 EINTR:此调用被信号所中断 EINVAL:参数n 为负值 ENOMEM:核心内存不足 |
输入时:假如我们要关心 0 1 2 3 文件描述符
0000 0000->0000 1111 比特位的位置,表示文件描述符的编号
比特位的内容 0 or 1 表示是否需要内核关心
输出时:
0000 0100->此时表示文件描述符的编号
比特位的内容 0 or 1哪些用户关心的fd 上面的读事件已经就绪了,这里表示2描述符就绪了
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
#include
#include
#include
#include
#include
#include
const static int MAXLINE = 1024;
const static int SERV_PORT = 10001;
int main()
{
int i , maxi , maxfd, listenfd , connfd , sockfd ;
/*nready 描述字的数量*/
int nready ,client[FD_SETSIZE];
int n ;
/*创建描述字集合,由于select函数会把未有事件发生的描述字清零,所以我们设置两个集合*/
fd_set rset , allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr , servaddr;
/*创建socket*/
listenfd = socket(AF_INET , SOCK_STREAM , 0);
/*定义sockaddr_in*/
memset(&servaddr , 0 ,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (struct sockaddr *) & servaddr , sizeof(servaddr));
listen(listenfd , 100);
/*listenfd 是第一个描述字*/
/*最大的描述字,用于select函数的第一个参数*/
maxfd = listenfd;
/*client的数量,用于轮询*/
maxi = -1;
/*init*/
for(i=0 ;i client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for (;;)
{
rset = allset;
/*只select出用于读的描述字,阻塞无timeout*/
nready = select(maxfd+1 , &rset , NULL , NULL , NULL);
if(FD_ISSET(listenfd,&rset))
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd , (struct sockaddr *) & cliaddr , &clilen);
/*寻找第一个能放置新的描述字的位置*/
for (i=0;i {
if(client[i]<0)
{
client[i] = connfd;
break;
}
}
/*找不到,说明client已经满了*/
if(i==FD_SETSIZE)
{
printf("Too many clients , over stack .\n");
return -1;
}
FD_SET(connfd,&allset);//设置fd
/*更新相关参数*/
if(connfd > maxfd) maxfd = connfd;
if(i>maxi) maxi = i;
if(nready<=1) continue;
else nready --;
}
for(i=0 ; i<=maxi ; i++)
{
if (client[i]<0) continue;
sockfd = client[i];
if(FD_ISSET(sockfd,&rset))
{
n = read(sockfd , buf , MAXLINE);
if (n==0)
{
/*当对方关闭的时候,server关闭描述字,并将set的sockfd清空*/
close(sockfd);
FD_CLR(sockfd,&allset);
client[i] = -1;
}
else
{
buf[n]='\0';
printf("Socket %d said : %s\n",sockfd,buf);
write(sockfd,buf,n); //Write back to client
}
nready --;
if(nready<=0) break;
}
}
}
return 0;
}
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
参数 | 说明 |
---|---|
fds | 是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合 |
nfds | 表示fds数组的长度 |
timeout | 表示poll函数的超时时间, 单位是毫秒(ms) |
返回 | —— |
> 0 | 表示poll由于监听的文件描述符就绪而返回 |
== 0 | 表示poll函数等待超时 |
< 0 | 表示出错 |
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024
#define OPEN_MAX 16 //一些系统会定义这些宏
#define SERV_PORT 10001
int main()
{
int i , maxi ,listenfd , connfd , sockfd ;
int nready;
int n;
char buf[MAXLINE];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr , servaddr;
listenfd = socket(AF_INET , SOCK_STREAM , 0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd , (struct sockaddr *) & servaddr, sizeof(servaddr));
listen(listenfd,10);
client[0].fd = listenfd;
client[0].events = POLLRDNORM;
for(i=1;i {
client[i].fd = -1;
}
maxi = 0;
for(;;)
{
nready = poll(client,maxi+1,INFTIM);
if (client[0].revents & POLLRDNORM)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd , (struct sockaddr *)&cliaddr, &clilen);
for(i=1;i {
if(client[i].fd<0)
{
client[i].fd = connfd;
client[i].events = POLLRDNORM;
break;
}
}
if(i==OPEN_MAX)
{
printf("too many clients! \n");
}
if(i>maxi) maxi = i;
nready--;
if(nready<=0) continue;
}
for(i=1;i<=maxi;i++)
{
if(client[i].fd<0) continue;
sockfd = client[i].fd;
if(client[i].revents & (POLLRDNORM|POLLERR))
{
n = read(client[i].fd,buf,MAXLINE);
if(n<=0)
{
close(client[i].fd);
client[i].fd = -1;
}
else
{
buf[n]='\0';
printf("Socket %d said : %s\n",sockfd,buf);
write(sockfd,buf,n); //Write back to client
}
nready--;
if(nready<=0) break; //no more readable descriptors
}
}
}
return 0;
}
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_data_t data;
} EPOLL_PACKED;
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)。
EPOLLOUT : 表示对应的文件描述符可以写。
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)。
EPOLLERR : 表示对应的文件描述符发生错误。
EPOLLHUP : 表示对应的文件描述符被挂断。
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
参数 | 说明 |
---|---|
epfd | epoll_create()的返回值(epoll的句柄) |
op | 表示动作,用三个宏来表示: EPOLL_CTL_ADD :注册新的fd到epfd中 EPOLL_CTL_MOD :修改已经注册的fd的监听事件 EPOLL_CTL_DEL :从epfd中删除一个fd |
fd | 需要监听的fd |
event | 内核需要监听的事件 |
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
参数 | 说明 |
---|---|
epfd | epoll_create()的返回值(epoll的句柄) |
events | 是分配好的epoll_event结构体数组。epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存) |
maxevents | 通知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size |
timeout | 超时时间 (毫秒,0会立即返回,-1是永久阻塞) |
返回 | —— |
> 0 | 返回对应I/O上已准备好的文件描述符数目 |
== 0 | 表示已超时 |
< 0 | 表示失败 |
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 1024
#define OPEN_MAX 16 //一些系统会定义这些宏
#define SERV_PORT 10001
int main()
{
int i , maxi ,listenfd , connfd , sockfd ,epfd, nfds;
int n;
char buf[MAXLINE];
struct epoll_event ev, events[20];
socklen_t clilen;
struct pollfd client[OPEN_MAX];
struct sockaddr_in cliaddr , servaddr;
listenfd = socket(AF_INET , SOCK_STREAM , 0);
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd , (struct sockaddr *) & servaddr, sizeof(servaddr));
listen(listenfd,10);
epfd = epoll_create(256);
ev.data.fd=listenfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
for(;;)
{
nfds=epoll_wait(epfd,events,20,500);
for(i=0; i {
if (listenfd == events[i].data.fd)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd , (struct sockaddr *)&cliaddr, &clilen);
if(connfd < 0)
{
perror("connfd < 0");
exit(1);
}
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
else if (events[i].events & EPOLLIN)
{
if ( (sockfd = events[i].data.fd) < 0)
continue;
n = recv(sockfd,buf,MAXLINE,0);
if (n <= 0)
{
close(sockfd);
events[i].data.fd = -1;
}
else
{
buf[n]='\0';
printf("Socket %d said : %s\n",sockfd,buf);
ev.data.fd=sockfd;
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,connfd,&ev);
}
}
else if( events[i].events&EPOLLOUT )
{
sockfd = events[i].data.fd;
send(sockfd, "Hello!", 7, 0);
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
else
{
printf("This is not avaible!");
}
}
}
close(epfd);
return 0;
}
select
和 poll
是两种传统的 I/O 多路复用技术,它们允许服务器应用程序同时监控多个网络连接,以便在连接准备就绪时进行读写操作。尽管这两种技术在处理大量并发连接时非常有用,但随着连接数的增加,它们的性能会逐渐下降,因为它们需要在每次调用时遍历整个文件描述符集合,这在连接数非常多时会导致效率问题。
为了解决这个问题,epoll
作为 select
和 poll
的一种改进方案,在 Linux 系统中被引入。epoll
提供了一种更为高效的事件驱动模型,它可以显著提高处理大量并发连接的性能。与 select
和 poll
不同,epoll
不会对整个文件描述符集合进行线性遍历,而是使用一组特殊的数据结构来跟踪哪些文件描述符已经准备好 I/O 操作。这种机制使得 epoll
能够快速地通知应用程序哪些连接是活跃的,而无需对所有连接进行不必要的检查。
epoll
的另一个优点是它能够处理大量文件描述符而不会显著增加资源消耗,这使得它非常适合需要处理成千上万甚至更多并发连接的高性能网络服务器。因此,在 Linux 系统上,epoll
常被视为 select
和 poll
的替代方案,特别是在构建高性能网络应用程序时。
- END -
往期推荐:点击图片即可跳转阅读
《YY3568 Debian11+RT-Thread混合内核部署》
《YY3568多核异构(Linux+RT-Thread)--启动流程》
原文标题:Linux--IO多路复用(select,poll,epoll)
文章出处:【微信公众号:Rice 嵌入式开发技术分享】欢迎添加关注!文章转载请注明出处。
全部0条评论
快来发表一下你的评论吧 !