1.什么是多路复用?
多路复用是指通过一个线程同时监听多个IO事件的就绪状态。在传统的阻塞IO模型中,每个IO操作都需要一个独立的线程来处理,当有大量的IO操作时,会导致线程数量的增加,从而带来线程切换和上下文切换的开销。而多路复用通过使用一个线程来监听多个IO事件,避免了线程数量的增加,减少了线程切换和上下文切换的开销。
2.IO事件就绪通知:
IO多路复用机制通过操作系统提供的系统调用(如select、poll、epoll等)来监听多个IO事件的就绪状态。当有任何一个IO事件就绪时,内核会通知,告知哪些IO事件已经准备好可以进行读取或写入操作。
3.应用场景
-
网络编程:在网络编程中,服务器通常需要同时处理多个客户端的连接请求和数据传输。使用IO多路复用可以轻松管理多个网络套接字,监听并处理可读或可写的事件。这种方式可以实现高并发的服务器,提高系统的性能和扩展性。
-
高性能服务器:对于需要处理大量并发连接的服务器,如聊天服务器、实时通信服务器、游戏服务器等,使用IO多路复用可以有效地管理和处理多个客户端连接,提高服务器的吞吐量和响应速度。
-
文件操作:在文件操作中,当需要同时读取或写入多个文件时,可以使用IO多路复用来监听文件描述符的可读或可写事件,从而避免使用多线程或多进程的方式,提高文件操作的效率。
-
定时器和事件调度:IO多路复用可以用于实现定时器和事件调度功能。通过将定时器和事件的触发时间注册到IO多路复用机制中,可以在特定的时间点触发相应的事件,执行相应的操作。
创建方法:
1.创建文件描述符集合
2.添加文件描述符到集合中
3.通知内核开始监测
4.根据返回值做出对应操作
1.select 遵循未被使用原则
使用一个文件描述符集合来监听多个IO事件的就绪状态。应用程序需要将需要监听的文件描述符添加到集合中,然后调用select函数进行监听。当有文件描述符就绪时,select函数会返回,并告知哪些文件描述符已经准备好进行读取或写入操作,将其对应下标置1,然后应用程序可以通过遍历文件描述符集合来处理就绪的IO事件。
函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:监听文件描述符集合
参数:
nfds:监测的文件描述符上限值(最大文件描述符的值+1)
readfds:读文件描述符集合
writefds:写文件描述符集合
exceptfds:异常条件的描述符集合
timeout:设置超时时间
NULL:一直等待
返回值: 成功返回产生事件文件描述符个数
失败返回-1
定时时间到达仍没有事件产生返回0
1. void FD_CLR(int fd, fd_set *set);
将fd从文件描述符集合中清除
2. int FD_ISSET(int fd, fd_set *set);
判断文件描述符fd是否仍在文件描述符集合中
3. void FD_SET(int fd, fd_set *set);
将fd加入文件描述符集合中
4. void FD_ZERO(fd_set *set);
文件描述符集合清0
使用示例:使用select 函数实现tcp并发服务器
int init_tcp_ser(const char *ip ,unsigned short port)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == sockfd)
{
perror("fail socket");
return -1;
}
struct sockaddr_in ser;
ser.sin_family = AF_INET;
ser.sin_port = htons(port);
ser.sin_addr.s_addr = inet_addr(ip);
int ret = bind(sockfd, (struct sockaddr *)&ser, sizeof(ser));
if (-1 == ret)
{
perror("fail bind");
return -1;
}
ret = listen(sockfd, 128);
if (-1 == ret)
{
perror("fail listen");
return -1;
}
return sockfd;
}
int main(int argc, const char *argv[])
{
pid_t pid = 0;
int connfd = 0;
pthread_t tid;
int maxfd = 0;
char buff[1024] = {0};
int sockfd = init_tcp_ser("192.168.1.106", 50000);
if (-1 == sockfd)
{
return -1;
}
fd_set rdfds;
fd_set tmpfds;
FD_ZERO(&rdfds);
FD_SET(sockfd, &rdfds);
maxfd = sockfd > maxfd ? sockfd : maxfd;
while (1)
{
tmpfds = rdfds;
int cnt = select(maxfd+1, &tmpfds, NULL, NULL, NULL);
if (cnt < 0)
{
perror("fail select");
return -1;
}
if (FD_ISSET(sockfd, &tmpfds))
{
int connfd = accept(sockfd, NULL, NULL);
if (connfd < 0)
{
perror("fail accept");
continue;
}
FD_SET(connfd, &rdfds);
maxfd = connfd > maxfd ? connfd : maxfd;
}
for (int i = sockfd+1; i < maxfd; i++)
{
if (FD_ISSET(i, &tmpfds))
{
memset(buff, 0, sizeof(buff));
ssize_t size = recv(i, buff, sizeof(buff), 0);
if (size < 0)
{
perror("fail recv");
FD_CLR(i, &rdfds);
close(i);
continue;
}
else if (0 == size)
{
FD_CLR(i, &rdfds);
close(i);
continue;
}
printf("cli---> %s\n", buff);
strcat(buff, "------ok!");
size = send(i, buff, strlen(buff), 0);
if (size < 0)
{
perror("fail recv");
FD_CLR(i, &rdfds);
close(i);
continue;
}
}
}
}
return 0;
}
2.poll
epoll
epoll模型:
1)epoll_create 创建epoll文件描述符集合
2)epoll_ctl 添加关注的文件描述符
3)epoll_wait 监控io事件
4)epoll_ctl 从事件集合中删除完成的文件描述符
1)epoll_create
函数原型: int epoll_create(int size);
功能:创建一个监听事件表(内核中)
参数:size:监听事件最大个数
返回值:
成功返回非负值:表示epoll事件表对象(句柄)
失败返回-1
2)epoll_ctl
函数原型: 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:文件描述符
events:事件相关结构体
返回值:
成功返回0
失败返回-1
3)epoll_wait
函数原型:int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
功能:监听事件表中的事件,并将产生的事件存放到结构体数组中
参数:
epfd:事件表文件描述符
events:存放结果事件结构体数组空间首地址
maxevents:最多存放事件个数
timeout:超时时间
-1:阻塞等待直到有事件发生
返回值:
成功返回产生事件个数
失败返回-1
超时时间到达没有事件发生返回0