多路选择I/O

Posted 杨静远

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多路选择I/O相关的知识,希望对你有一定的参考价值。

 

多路选择I/O提供另一种处理I/O的方法,相比于传统的I/O方法,这种方法更好,更具有效率。多路选择是一种充分利用系统时间的典型。

1、多路选择I/O的概念

当用户需要从网络设备上读数据时,会发生的读操作一般分为两步。

(1)等待数据准备好,等待数据的到达,并且将其复制到内核的缓冲区,该缓冲区在系统态。

(2)复制数据,将数据从内核缓冲区中复制到用户指定的缓冲区中。

一般的读操作形式为:

 Int nbytes = read(sfd, buf, MAX); 

如果需要的数据没有准备好,例如,数据尚未到达时,read函数发生阻塞,直到所有的数据到达,read函数才将其复制到用户指定的缓冲区,并且返回。如果数据一直未到达,那么read函数将一直阻塞下去,该进程会陷入僵尸状态、这种I/O模型称为阻塞I/O。

为了防止I/O阻塞使进程进入僵死状态,可以使用多路选择I/O。

这种方法的思想是先构造一张需要读取文件描述符的表,调用一个函数轮循这个表中的文件描述符,知道有一个文件描述符可以读写该函数才返回,多路选择I/O需要使用两个系统调用,一个负责检查并返回可用的文件描述符;另一个负责对该文件描述符进行读写。

2、实现多路选择I/O

Linux环境下使用select函数实现多路选择I/O,函数原型如下:

 Int select(int maxfdp1, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict exceptfds, struct timeval *restrict tvptr); 

头文件: #include <sys/select.h> 

参数说明:

第一个参数maxfdp1表示所关心状态的描述符的个数,正确解释是最大描述符加1。如果maxfdp1的值是2,表示用户关心的描述符数为2;最大的文件描述符为1时,描述符0,和1都会被该函数茶韵,大于1则不关心。一个进程最多可以用于1024个文件描述符,因此maxfdp1的值为(0~1023);

第2、3、4这3个参数是readfds、writefds和exceptfds,分别表示用户关心的可读、可写和异常的各个描述符,这3个参数是3个位向量,每一位对应一个文件描述符的状态,每一位对应一个文件描述符的状态。

第5个参数表示用户期望等待的时间。如果tvptr==NULL表示一直等。

返回值:出错返回-1,返回0表示没有设备准备好,返回值大于0表示准备好的设备数目。

在这个函数中第2、3、4这三个参数是特殊的参数,这三个参数是fd_set数据类型的,fd_set本质上市一个位向量,是一个无符号的整型。其中每一位代表一个设备的状态,如果是1表示被设置,如果是0表示没有被设置。上边的的三个参数分别代表的是可读、可写和异常三种状态,这三个位向量中的每一位代表一个状态。比如readfds是“111000”表示前3个文件可读,后三个文件不可读。

最后通过一个实例来演示使用select函数同时处理多个连接请求的服务器端程序。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include "iolib.h"
#define MAX_LINE 80
int port = 8000;

void my_fun(char *p)
{
       if(p == NULL)
              exit(1);
       for(; *p != \0; p++)
              if(*p >= A && *p <= Z)
                     *p = *p - A + a;
}
 
int main(void)
{
       struct sockaddr_in sin;
       struct sockaddr_in cin;
       int lfd;
       int cfd;
       int sfd;
       int rdy;
       int client[FD_SETSIZE];          ///客户端连接的套接字描述符数组
       int maxi;
       int maxfd;                                  ///最大连接数
       fd_set rest;
       fd_set allset;
       socklen_t addr_len;                     ///地址结构的长度
       char buf[MAX_LINE];
       char addr_p[INET_ADDRSTRLEN];
       int i, n;
       int len;
       int opt = 1;                         ///套接字选项
       bzero(&sin, sizeof(sin)); ///填充地址结构
       sin.sin_family = AF_INET;
       sin.sin_addr.s_addr = INADDR_ANY;
       sin.sin_port = hton(port);
        /*创建一个面向连接的套接字*/
       lfd = socket(AF_INET, SOCK_STREAM, 0);
       if(lfd == -1)
       {
              perror("call to sock");
              exit(1);
       }
 
       /*设置套接字选项,使用默认选项*/
       setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
 
       /*绑定套接字到地址结构*/
       n = bind(lfd, (struct sockaddr_in *) &sin, sizeof(opt));
       if(n == -1)
       {
              perror("call to bind");
              exit(1);
       }
 
       /*开始监听连续请求*/
       n = listern(lfd, 20);
       if(n == -1)
       {
              perror("call to listern");
              exit(1);
       }
       printf("Accepting connecting ...\n");
       maxfd = lfd;          ///对最大文件描述符进行初始化
       maxi = -1;
       for(i = 0; i < FD_SETSIZE; i++)     ///初始化客户端连接描述符集合
              client[i] = -1;
       FD_ZERO(&allset);               ///清空文件描述符集合
       FD_SET(lfd, &allset);             ///将监听接字设置在集合内
      
       /*开始服务器程序的死循环*/
       while(1)
       {
              rset = allset;
              /*得到当前可以读的文件描述符*/
              rdy = select(maxfd + 1, &rset, NULL, NULL, NULL);
              if(FD_ISSET(lfd, &rest))
              {
                     addr_len = sizeof(cin);
                     /*创建一个链接描述符*/
                     cfd = accept(lfd, (struct sockaddr_in *) &cin, &addr_len);
                     if(cfd == -1)
                     {
                            perror("fail to accept");
                            exit(1);
                     }
                     /*查找一个空闲的位置*/
                     for(i = 0; i < FD_SETSIZE; i++)
                     {
                            if(client[i] < 0)
                            {
                                   client[i] = cfd;
                                   break;
                            }
                     }
                     /*太多的客户端连接,服务器拒绝连接,跳出循环*/
                     if(i == FD_SETSIZE)
                     {
                            printf("too many clients\n");
                            exit(1);
                     }
                     FD_SET(cfd, &allset);            ///设置连接集合
                     if(cfd > max_fd)
                            maxfd = cfd;
                     if(i > maxi)
                            maxi = i;
                     if(--rdy <= 0)
                            continue;
              }
 
              for(i = 0; i < maxi; i++)        ///对每一个连接描述符做处理

              {

                     if((sfd = client[i] < 0))

                            continue;

                     if(FD_ISSET(sfd, &rset))
                     {
                            n = my_read(sfd, buf, MAX_LINE);      ///读取数据
                            if(n == 0)
                            {
                                   printf("the other side has been closed\n");
                                   fflush(stdout);        ///刷新到输出终端
                                   close(sfd);
                                   FD_CLR(sfd, &allset);    ///清空连接描述符数组
                                   client[i] = -1;
                            }
                           else
                            {
                                   inet_ntop(AF_INET, &cin.sin_addr, addr_p, sizeof(addr_p));
                                   printf("client IP is %s, port is %d\n", addr_p, ntohs(cin.sin_port));
                                   my_fun(buf);
                                   n = my_write(sfd, buf, len + 1);
                                   if(n == -1)
                                          exit(1);

                            }
                            if(--rdy <= 0)
                                   break;
                     }
              }
       }
       close(lfd);
       return 0;
}

 

以上是关于多路选择I/O的主要内容,如果未能解决你的问题,请参考以下文章

详述 Redis 选择单线程模型的原因以及 I/O 多路复用

I/O多路复用是什么?(I/O multiplexing)

Python3标准库:selectors I/O多路复用抽象

select函数与I/O多路转接

select函数与I/O多路转接

什么是Redis I/O 多路复用?