Linux C编程(九) 之 I/O复用-----select机制

基础概念

IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。

  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。

  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

  6. 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

  7. LInux下的五种IO模型
    [1] blocking IO - 阻塞IO
    [2] nonblocking IO - 非阻塞IO
    [3] IO multiplexing - IO多路复用
    [4] signal driven IO - 信号驱动IO
    [5] asynchronous IO - 异步IO
    其中前面4种IO都可以归类为synchronous IO - 同步IO,而select、poll、epoll本质上也都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

select函数

该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。函数原型如下:

  #include 
    #include 
    int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
  • 函数参数介绍如下:
  1. 第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此把该参数命名为maxfdp1),描述字0、1、2 maxfdp1-1均将被测试。因为文件描述符是从0开始的。

  2. 中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果对某一个的条件不感兴趣,就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

       void FD_ZERO(fd_set *fdset);           //清空集合
    
       void FD_SET(int fd, fd_set *fdset);   //将一个给定的文件描述符加入集合之中
    
       void FD_CLR(int fd, fd_set *fdset);   //将一个给定的文件描述符从集合中删除
    
       int FD_ISSET(int fd, fd_set *fdset);   // 检查集合中指定的文件描述符是否可以读写 
    
  3. timeout告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。

      struct timeval{
                long tv_sec;   //seconds
                long tv_usec;  //microseconds
    };
    

这个参数有三种可能:

(1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,把该参数设置为空指针NULL。

(2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。

(3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

返回值:就绪描述符的数目,超时返回0,出错返回-1

Linux C编程(九) 之 I/O复用-----select机制_第1张图片

代码示例

server:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXBUF 1024

int main(int argc, char **argv)
{
    int sockfd, new_fd;
    socklen_t len;
    struct sockaddr_in my_addr, their_addr;
    
    unsigned int myport, lisnum;
    char buf[MAXBUF + 1];
    
    fd_set rfds;
    struct timeval tv;
    
    int retval, maxfd = -1;

    if (argv[1])
        myport = atoi(argv[1]);
    else
        myport = 7838;

    if (argv[2])
        lisnum = atoi(argv[2]);
    else
        lisnum = 2;

    // create an new socket
    if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) 
    {
        perror("socket");
        exit(1);
    }

    bzero(&my_addr, sizeof(my_addr));
    my_addr.sin_family = PF_INET;
    my_addr.sin_port = htons(myport);
    
    if (argv[3])
        my_addr.sin_addr.s_addr = inet_addr(argv[3]);
    else
        my_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) 
    {
        perror("bind");
        exit(1);
    }

    if (listen(sockfd, lisnum) == -1) 
    {
        perror("listen");
        exit(1);
    }

    while (1) 
    {
        printf("\n----等待新的连接到来开始新一轮聊天……\n");
	
        len = sizeof(struct sockaddr);
		
		/*接受新连接并打印*/
        if ((new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &len)) == -1) 
		{
            perror("accept");
            exit(errno);
        } 
        else
		{
			printf("server: got connection from %s, port %d, socket %d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);
		}
	
        // 开始处理每个新连接上的数据收发
        printf("\n准备就绪,可以开始聊天了……直接输入消息回车即可发信息给对方\n");
	
        while (1) 
		{
            // 把集合清空  每一次轮询开始都要把fd_set清空
            FD_ZERO(&rfds);
	    
            // 把标准输入(stdin)句柄0加入到集合中
            FD_SET(0, &rfds);
    
            // 把当前连接(socket)句柄new_fd加入到集合中 
            FD_SET(new_fd, &rfds);
	    
			maxfd = 0;
            if (new_fd > maxfd)
			{
					maxfd = new_fd;
			}
	    
            // 设置最大等待时间 
            tv.tv_sec = 5;
            tv.tv_usec = 0;
	    
            // 开始等待 
            retval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
	    
            if (retval == -1) 
			{
                printf("将退出,select出错! %s", strerror(errno));
                break;
            } 
            else if (retval == 0) 
			{
                printf("没有任何消息到来,用户也没有按键,继续等待……\n");
                continue;
            } 
            else 
			{
		 // 判断当前IO是否是stdin
                if (FD_ISSET(0, &rfds))  // 用户按键了,则读取用户输入的内容发送出去
				{                  
                    bzero(buf, MAXBUF + 1);
                    fgets(buf, MAXBUF, stdin);
                    if (!strncasecmp(buf, "quit", 4)) 
					{
                        printf("自己请求终止聊天!\n");
                        break;
                    }
                    len = send(new_fd, buf, strlen(buf) - 1, 0);
                    if (len > 0)
                        printf("消息:%s\t发送成功,共发送了%d个字节!\n", buf, len);
                    else 
					{
                        printf("消息'%s'发送失败!错误代码是%d,错误信息是'%s'\n", buf, errno, strerror(errno));
                        break;
                    }
                }
                
		 // 判断当前IO是否是来自socket
                if (FD_ISSET(new_fd, &rfds)) // 当前连接的socket上有消息到来则接收对方发过来的消息并显示 
				{                 
                    bzero(buf, MAXBUF + 1);
                    // 接收客户端的消息 
                    len = recv(new_fd, buf, MAXBUF, 0);
                    if (len > 0)
					{
                        printf("接收消息成功:'%s',共%d个字节的数据\n", buf, len);
					}
                    else 
					{
                        if (len < 0)
                            printf("消息接收失败!错误代码是%d,错误信息是'%s'\n", errno, strerror(errno));
                        else
                            printf("对方退出了,聊天终止\n");
                        break;
                    }
                }
            }
        }
        
        close(new_fd);
        // 处理每个新连接上的数据收发结束
	
        printf("还要和其它连接聊天吗?(no->退出)");
        fflush(stdout);
	
        bzero(buf, MAXBUF + 1);
        fgets(buf, MAXBUF, stdin);
        if (!strncasecmp(buf, "no", 2))
	{
            printf("终止聊天!\n");
            break;
        }
    }

    close(sockfd);
    return 0;
}

client:

#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	
	#define MAXBUF 1024
	
	int main(int argc, char **argv)
	{
	    int sockfd, len;
	    
	    struct sockaddr_in dest;
	    char buffer[MAXBUF + 1];
	    
	    fd_set rfds;
	    struct timeval tv;
	    
	    int retval, maxfd = -1;
	
	    if (argc != 3)
	    {
	        printf("参数格式错误!正确用法如下:\n\t\t%s IP地址 端口\n\t比如:\t%s 127.0.0.1 80\n此程序用来从某个 IP 地址的服务器某个端口接收最多 MAXBUF 个字节的消息", argv[0], argv[0]);
	        exit(0);
	    }
	    
	    // 创建一个 socket 用于 tcp 通信 
	    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
	    {
	        perror("Socket");
	        exit(errno);
	    }
	
	    // 初始化服务器端(对方)的地址和端口信息
	    bzero(&dest, sizeof(dest));
	    dest.sin_family = AF_INET;
	    dest.sin_port = htons(atoi(argv[2]));
	    
	    if (inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0) 
	    {
	        perror(argv[1]);
	        exit(errno);
	    }
	
	    // 连接服务器 
	    if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0) 
	    {
	        perror("Connect ");
	        exit(errno);
	    }
	
	    printf("\n准备就绪,可以开始聊天了……直接输入消息回车即可发信息给对方\n");
	    
	    while (1) 
	    {
	        // 把集合清空 
	        FD_ZERO(&rfds);
	        // 把标准输入句柄0加入到集合中
	        FD_SET(0, &rfds);
		
	        maxfd = 0;
	        // 把当前连接句柄sockfd加入到集合中
	        FD_SET(sockfd, &rfds);
		
	        if (sockfd > maxfd)
	            maxfd = sockfd;
		
	        // 设置最大等待时间 
	        tv.tv_sec = 3;
	        tv.tv_usec = 0;
		
	        // 开始等待
	        retval = select(maxfd + 1, &rfds, NULL, NULL, &tv);
		
	        if (retval == -1) 
			{
	            printf("将退出,select出错! %s", strerror(errno));
	            break;
	        } 
	        else if (retval == 0) 
			{
	            /* printf("没有任何消息到来,用户也没有按键,继续等待……\n"); */
	            continue;
	        } 
	        else 
			{
	            if (FD_ISSET(sockfd, &rfds)) // 连接的socket上有消息到来则接收对方发过来的消息并显示 
				{               
	                bzero(buffer, MAXBUF + 1);
	                // 接收对方发过来的消息,最多接收 MAXBUF 个字节 
	                len = recv(sockfd, buffer, MAXBUF, 0);
			
	                if (len > 0)
					{
	                    printf("接收消息成功:'%s',共%d个字节的数据\n", buffer, len);
					}
	                else 
					{
	                    if (len < 0)
						{
	                        printf("消息接收失败!错误代码是%d,错误信息是'%s'\n", errno, strerror(errno));
						}
	                    else
						{
	                        printf("对方退出了,聊天终止!\n");
						}
	                    break;
	                }
	            }
	            
	            if (FD_ISSET(0, &rfds)) // 用户按键了,则读取用户输入的内容发送出去
				{              
	                bzero(buffer, MAXBUF + 1);
	                fgets(buffer, MAXBUF, stdin);
	                if (!strncasecmp(buffer, "quit", 4)) 
					{
	                    printf("自己请求终止聊天!\n");
	                    break;
	                }
	                
	                // 发消息给服务器
	                len = send(sockfd, buffer, strlen(buffer) - 1, 0);
	                if (len < 0) 
					{
	                    printf("消息'%s'发送失败!错误代码是%d,错误信息是'%s'\n", buffer, errno, strerror(errno));
	                    break;
	                } 
	                else
					{
						printf("消息:%s\t发送成功,共发送了%d个字节!\n", buffer, len);
					}
	            }
	        }
	    }

	    // 关闭连接 
	    close(sockfd);
	    return 0;
	}

Linux C编程(九) 之 I/O复用-----select机制_第2张图片

select机制的问题

  1. 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
  3. 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)

你可能感兴趣的:(Linux C编程(九) 之 I/O复用-----select机制)