【IO多路复用】:允许同时对多个IO进行控制

本文介绍了IO多路复用的概念及其优点,重点讲解了select函数的工作原理和代码实例,展示了如何在一个线程内处理多个socket的IO请求。虽然存在如文件描述符数量限制和轮询效率问题等缺点,但在处理短作业和减少资源占用方面,IO多路复用模型具有显著优势,尤其适合构建简单的事件驱动服务器。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、多路复用的优点

在这里插入图片描述
2、select()函数—用于探测多个文件句柄的状态变化

#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);
参数:
    nfds:最大文件描述符+1
	readfds:读事件的表
	writefds:写事件的表
	exceptfds:异常事件的表
	timeout:超时检测  NULL: 一直阻塞,直到文件描述符准备好为止
返回值: 
        成功: 准备好的文件描述符的个数 ;失败: -1
        
----------------------------------------------------------
void FD_CLR(int fd, fd_set *set); // 把表中删除一个文件描述符 
int FD_ISSET(int fd, fd_set *set); //检测文件描述符是否准备好了,准备好了返回1,否则返回0 
void FD_SET(int fd, fd_set *set); // 加入到表中 
void FD_ZERO(fd_set *set); // 清空表        
        

基本原理:首先创建一张文件描述符表(fd_set),通过使用特有的函数(select),让内核帮助上层用户循环检测是否有可操作的文件描述符,如果有则告诉应用程序去操作。

代码实例:(有名管道的应用)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/time.h>

int main(int argc, const char *argv[])
{
	int fd1 = open("./f1", O_RDWR);
	int fd2 = open("./f2", O_RDWR);
	int fd3 = open("./f3", O_RDWR);

	//1.创建一张文件描述符表
	fd_set readfds, tmpfds;
	
	FD_ZERO(&readfds); //2.清空表

	FD_SET(fd1, &readfds);
	FD_SET(fd2, &readfds);
	FD_SET(fd3, &readfds);

	int maxfd = fd3;

	tmpfds = readfds;

	char buf[64] = {0};

	int ret, i;

	while(1)
	{
		readfds = tmpfds;

		ret = select(maxfd+1, &readfds, NULL, NULL,NULL);
		if(ret == -1)
		{
			perror("select");
			return -1;
		}

		for(i=fd1; i<maxfd+1; i++)
		{
			if( FD_ISSET(i, &readfds) )//检测是否准备好
			{
				read(i, buf, sizeof(buf));
			
				printf("%s\n", buf);

				memset(buf, 0, sizeof(buf));
			}
		
		}
	}

	close(fd1);
	close(fd2);
	close(fd3);

	return 0;
}

代码运行:
在这里插入图片描述
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个 socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

​ 从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后 最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

​ 这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。我们可以将这种模型归类为“事件驱动模型”。

​ 相比其他模型,使用select的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多CPU,同时能够为多客户端提供服务。

​ 如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。

但这个模型依旧有着很多问题:

​ 当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄; 该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,那么就会导 致后续的事件迟迟得不到处理,并且会影响新的事件轮询,在很大程度上降低了事件探测的及时性。

所以select机制只适用于"短作业" 处理机制. (处理时间短)

select缺点

  • 内核使用轮询的方式来检查文件描述符集合中的描述符是否就绪,文件描述符越多,消耗的时间资源越多。
  • 文件描述符集合使用的数组,有大小限制,1024
  • 每次文件描述符集合更新时,重新拷贝到内核中

多复用tcp服务器:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/time.h>

int main(int argc, const char *argv[])
{
    int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
    if(sockfd<0)
    {
        perror("socket");
        return -1;
    }
    printf("sockfd=%d\n",sockfd);

    //端口复用函数
    int on=1;
    int k=setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));
    if(k<0)
    {
        perror("setsockopt");
        return -1;
    }
    //初始化地址和端口
    struct sockaddr_in seraddr;
    memset(&seraddr,0,sizeof(seraddr));
    int len=sizeof(seraddr);
    seraddr.sin_family=AF_INET;
    seraddr.sin_port=htons(9898);
    //seraddr.sin_addr.s_addr=htonl(INADDR_ANY);//自动路由,寻找ip
    seraddr.sin_addr.s_addr=inet_addr("0");


    int ret=bind(sockfd,(struct sockaddr *)&seraddr,len);
    if(ret<0)
    {
        perror("bind");
        return -1;
    }
    ret=listen(sockfd,5);
    if(ret<0)
    {
        perror("listen");
        return -1;
    }

    fd_set readfds, tmpfds;

    FD_ZERO(&readfds); //2.清空表

    FD_SET(sockfd, &readfds);
    int maxfd=sockfd;
    tmpfds=readfds;
    char buf[64] = {0};

    int i;
    while(1)
    {
        readfds = tmpfds;

        ret = select(maxfd+1, &readfds, NULL, NULL,NULL);
        if(ret == -1)
        {
            perror("select");
            return -1;
        }

        for(i=sockfd; i<maxfd+1; i++)
        {
            if( FD_ISSET(i, &readfds) )//检测是否准备好
            {
                if(i==sockfd)
                {
                    //建立连接,通过accept函数
                    int connfd=accept(i,NULL,NULL);
                    printf("%d is link\n",connfd);

                    FD_SET(connfd,&tmpfds);
                    if(maxfd<connfd)
                    {
                        maxfd=connfd;
                    }
                }

                else
                {
                    ret=recv(i,buf,sizeof(buf),0);
                    if(ret<0)
                    {
                        close(i);
                        perror("recv");
                        return -1;
                    }
                    else if(ret==0)
                    {
                        printf("%d is unlink\n",i);
                        FD_CLR(i,&tmpfds);
                        close(i);
                        break;
                    }
                    else
                    {
                        printf("%d:message=%s\n",i,buf);
                    }
                }


            }



        }
    }

    close(sockfd);

    return 0;
}

运行结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值