contents
概述
IO多路复用是一种高效的事件驱动编程模型,用于同时监视多个文件描述符的就绪状态(如读就绪、写就绪等),以实现并发处理多个IO操作而无需使用多线程或多进程。
IO多路复用的基本思想是在用户空间中将监听的事件文件描述符添加到事件集合中,调用函数进行判断集合中文件描述符对应的硬件数据是否准备就绪,如果没有一个事件发生,将进程切换到休眠状态(可中断休眠状态)。当有一个或者多个硬件数据准备好了,将休眠的进程唤醒,对准备好的硬件数据进行读写
select
功能:阻塞函数,让内核检测指定文件描述符集合中,是否有文件描述符准备就绪
当文件描述符准备就绪后,该函数解除阻塞。
当事件产生后,集合中会只剩下触发事件的文件描述符。
原型:
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数:
int nfds:所有集合中,最大的文件描述符,加1
fd_set *readfds, fd_set *writefds,
fd_set *exceptfds:读集合,写集合,其他集合。哪个集合没有使用到,则填NULL;
struct timeval *timeout:设置超时时间;
1)如果填NULL,则该函数会一直阻塞,直到集合中有文件描述符准备就绪
2)设置超时时间:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
>0, 成功,返回集合中触发事件的文件描述符个数;
=0,超时
=-1,函数运行失败;
操作集合的函数
void FD_CLR(int fd, fd_set *set); 将fd从集合中剔除
int FD_ISSET(int fd, fd_set *set); 判断fd是否在集合中,在返回真,不在返回假
void FD_SET(int fd, fd_set *set); 将fd加入到集合中
void FD_ZERO(fd_set *set); 清空集合
示意图
核心操作:一颗树、一张表、三个接口
模型实现伪代码
sfd = socket();
bind();
listen();
FD_SET(sfd, &readfds)
while(1)
{
tempfds = readfds;
select(maxfd+1, &tempfds, NULL, NULL, NULL);
for(int i=0; i<maxfd+1; i++)
{
if(FD_ISSET(i, &tempfds) == 0) continue;
if(sfd == i)
{
newfd = accept();
FD_SET(newfd, &readfds);
maxfd = maxfd>newfd?maxfd:newfd;
}
else
{
res = recv(i, buf, sizeof(buf), 0)
if(0 == res)
{
close(i);
FD_CLR(i, &readfds);
for(int j=maxfd; j>sfd; j--)
{
if(FD_ISSET(j, &readfds))
{
maxfd = j;
break;
}
}
}
}
}
}
close(sfd);
poll
poll函数可以同时监视多个文件描述符的就绪状态。
函数签名:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
fds:指向一个pollfd结构体数组的指针,每个结构体描述了一个要监视的文件描述符及其关注的事件。
nfds:表示fds数组中的结构体数量。
timeout:表示等待的超时时间,以毫秒为单位。传递以下值来指定超时时间:
> 0:等待指定的毫秒数。
= 0:立即返回,不进行等待。
-1:无限期等待,直到有事件发生。
返回值:
-1:表示poll函数调用出错。
0:表示超时,没有任何文件描述符就绪。
> 0:表示就绪的文件描述符数量。
使用示例:
#include <stdio.h>
#include <poll.h>
int main() {
struct pollfd fds[1];
int ret;
fds[0].fd = 0; // 监视标准输入
fds[0].events = POLLIN; // 监视读事件
ret = poll(fds, 1, 5000); // 等待5秒钟
if (ret == -1) {
perror("poll");
return 1;
} else if (ret == 0) {
printf("Timeout\n");
} else {
if (fds[0].revents & POLLIN) {
printf("Data is available to read\n");
}
}
return 0;
}
上述示例中,通过创建一个pollfd结构体数组并设置要监视的文件描述符和事件类型,然后调用poll函数等待事件发生。在返回后,可以根据revents字段判断具体哪些文件描述符就绪,并进行相应的处理操作。
poll函数的优点在于它提供了更灵活的事件管理方式,相对于传统的select函数,可以处理更多的文件描述符,并且不受文件描述符数目限制。但在大规模的并发连接场景下,poll的性能可能会受到一定的限制,因为它需要遍历整个监视集合来找到就绪的文件描述符。
epoll
相较于传统的select和poll机制,epoll在处理大量并发连接时具有更好的性能和扩展性。
函数介绍:
#include <sys/epoll.h>
1.int epoll_create(int size);
功能:创建一个新的epoll
参数:
size:大于0的整数
返回值:成功返回用于操作epoll的文件描述符,失败返回错误码
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:进行epoll管理
参数:
epfd:epoll_create生成的文件描述符
op:管理的类型选项
EPOLL_CTL_ADD:用于在epoll上添加文件描述符
EPOLL_CTL_MOD:用于修改文件描述符事件类型
EPOLL_CTL_DEL:从epoll上移除指定的文件描述符
fd:要操作的文件描述符
event:设置文件描述符属性的变量
struct epoll_event {
uint32_t events; /* Epoll events */
【表示事件的类型,它是一个位掩码,可能同时包含多个事件类型的标志位】
//EPOLLIN:读
//EPOLLOUT:写
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;《=======使用这个
uint32_t u32;
uint64_t u64;
} epoll_data_t;
返回值: 成功返回0,失败返回错误码
3.int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能:阻塞等待事件发生
参数:
epfd:epoll_create生成的文件描述符
events: 存放发生的事件的文件描述符数组的首地址
maxevents:监听的文件描述符最大个数
timeout:设置超时事件 毫秒级 -1表示不关注超时 >0超时事件
返回值:
>0:发生事件的文件描述符个数
==0:超时时间到达
<0:失败
示意图:
代码示例
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/epoll.h>
#include <sys/time.h>
int main()
{
char buf[128] = {0};
int fd1, fd2;
int epfd, ret, i;
struct epoll_event event;
struct epoll_event events[20];
fd1 = open("/dev/mycdev0", O_RDWR);
if (fd1 < 0)
{
printf("设备文件打开失败\n");
}
// 创建epoll
epfd = epoll_create(5);
if (epfd < 0)
{
printf("epoll创建失败\n");
exit(-1);
}
// 将文件描述符添加到epoll上
event.data.fd = fd1;
event.events = EPOLLIN;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, &event);
if (ret < 0)
{
printf("在epoll上添加文件描述符失败\n");
exit(-1);
}
while (1)
{
// 阻塞等待事件发生
ret = epoll_wait(epfd, events, 20, -1);
if (ret < 0)
{
printf("epoll失败\n");
exit(-1);
}
// 根据事件的类型进行读写操作
for (i = 0; i < ret; i++)
{
if (events[i].events & EPOLLIN)
/*
events[i].events &&EPOLLIN会判断events[i].events是否为真(非零)
并且EPOLLIN是否为真(非零),而不是进行位运算。
*/
{
read(events[i].data.fd, buf, sizeof(buf));
printf("buf:%s\n", buf);
}
}
}
close(fd1);
return 0;
}
其中,关于 (events[i].events & EPOLLIN) 是否等价于 (events[i].events == EPOLLIN) :
表达式 events[i].events & EPOLLIN 执行的是位与操作,将 events[i].events 和 EPOLLIN 进行按位与运算。它用于检查在 events[i].events 的位掩码中是否设置了 EPOLLIN 标志位。如果位与运算的结果非零,表示 EPOLLIN 标志位被设置。
另一方面,(events[i].events == EPOLLIN) 则是进行相等性比较,判断 events[i].events 是否与 EPOLLIN 相等,即判断两者的值是否完全相同。
因此,这两个表达式的意义是不同的。前者用于检查位掩码中特定位的设置情况,后者用于判断两个值是否相等。
exit 和 return 区别
exit(-1)和return -1在功能上有一些相似之处,但它们在使用和含义上存在一些区别。
exit(-1):exit是一个库函数,用于终止当前进程的执行,并返回一个退出状态给调用进程。在调用exit时,可以传递一个整数参数作为退出状态码,通常用于表示程序的错误状态。exit(-1)表示以非零的状态码退出,常用于表示程序执行失败或出现错误。
return -1:return语句用于函数内部,用于从函数中返回一个值。return -1通常表示函数执行失败或出现错误,并将-1作为函数的返回值。这个返回值可以在函数调用的地方被捕获和处理。
区别:
exit(-1)会立即终止整个进程的执行,并返回一个退出状态码给调用进程,它不仅仅是从当前函数返回,而是终止整个程序的执行。
return -1仅在函数内部使用,表示函数的执行失败或出现错误,并将-1作为函数的返回值,继续执行函数调用的地方的后续代码。
因此,exit(-1)和return -1虽然都可以表示程序执行失败或出现错误,但使用的场景和含义不同。exit(-1)更适用于整个程序的退出,而return -1更适用于函数内部的错误处理。