欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

I/O多路复用 - select

程序员文章站 2022-06-13 12:52:51
...
了解select,首先我们先了解一下这两句话
  • select只负责等(即并不数据搬迁,不处理数据)
  • 等待文件描述符的读就绪或者写就绪
select函数

  • select系统调用是用来让我们监视多个程序的文件描述符的变化;
  • 程序会停在select这里等,知道被监视的文件描述符至少有一个达到了就绪状态;
函数原型
I/O多路复用 - select
参数:
  • _nfds : 表示需要监视的最大文件描述符 + 1;
  • __restrict __readfds : 需要检测的可读文件描述符的集合,(输入输出型参数; 输入:用户想关心那些文件描述符上的读事件, 输出: 所关心的文件描述符那些已经读就绪了);
  • __restrict __writefds : 需要检测的可写文件描述符的集合,(输入输出型参数; 输入:用户想关心那些文件描述符上的写事件,输出: 所关心的文件描述符那些已经写就绪了);
  • __restrict __exceptfds : 需要检测的异常文件描述符的集合,,(输入输出型参数; 输入:用户想关心那些文件描述符上的异常事件,输出: 所关心的文件描述符那些已经异常就绪了);
  • __restrict __timeout : 用来设置select等待时间的
timeout的取值:
  • NULL : 表示当前select()没有timeout,select将会阻塞的等待文件描述符就绪;
  • 0 : 仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生; (非阻塞)
  • 一个特定的时间值 : 如果在指定时间内还没有等待到就绪的文件描述符,select()就会超时返回;
fd_set 结构体:
I/O多路复用 - select
其实这个结构体就是一个位图,使用位图中的对应位表示要监视的文件描述符。
fd_set的结构:
void FD_CLR(int fd, fd_set* set);    // 将位图中的某一位置为0
void FD_SET(int fd, fd_set* set);    // 将位图中的某一位置为1
int FD_ISSET(int fd, fd_set* set;    // 检测位图中fd的位置是不是1
void FD_ZERO(fd_set* set);           // 将set中全部位置为0
禁止自己使用与或非方式操作位图!!!

timeval结构体:
这个结构体构建了一个时间, tv_sec是秒数, tv_usec是微妙, 最后时间为 tv_sec+tv_usec; 
I/O多路复用 - select

函数返回值:
  • 执行成功返回已改变状态的文件描述符个数;
  • 0 : 表示在文件描述符状态改变之前就已经超时了,没有返回;
  • -1 : 表示有错误发生,错误原因存在于errno,此时函数的额后四个参数的值变为不可预测;
socket就绪条件

读就绪
  • socket内核中, 接收缓冲区中的字节数, ⼤于等于低⽔位标记SO_RCVLOWAT. 此时可以⽆阻塞的读该⽂件描述符, 并且返回值⼤于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;
写就绪
  • socket内核中, 发送缓冲区中的可⽤字节数(发送缓冲区的空闲位置⼤⼩), ⼤于等于低⽔位标记 SO_SNDLOWAT, 此时可以⽆阻塞的写, 并且返回值⼤于0;
  • socket的写操作被关闭(close或者shutdown). 对⼀个写操作被关闭的socket进⾏写操作, 会触发 SIGPIPE信号;
  • socket使⽤⾮阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;
异常就绪
  • socket收到带外数据(可以联想到TCP中的紧急指针URG);
select特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)的值,我这边是128,每个比特位上一个文件描述符,我的服务器上最大就可表示 128*8 = 1024个;
  • 将fd加入到select监控集的同时,还需要再使用一个数据结构arr来保存select监控集中的fd
一是用于select返回后,arr作为源数据和fd进行FD_ISSET判断;
二是select返回后,会把以前加入的但是没有事件发生的fd清空,则每次开始select前都要重新从arr取得一个fd逐一加入,扫描arr的同时取得fd的maxfd,用于select的第一个参数;

代码编写: select服务器(回显)
// select_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>

// 对fd_set进行一个简单的封装
// 为了能够方便的获取到文件描述符集中的的最大文件描述符
typedef struct FdSet
{
    fd_set fds;
    int max_fd; // 当前文件描述符集中的最大文件描述符
}FdSet;

void InitFdSet(FdSet* set)
{
    if(set == NULL)
    {
        return;
    }
    set->max_fd = -1;
    FD_ZERO(&set->fds);
}

void AddFdSet(FdSet* set,int fd)
{
    FD_SET(fd,&set->fds);
    if(fd > set->max_fd)
    {
        set->max_fd = fd;
    }
}

void DelFdSet(FdSet* set,int fd)
{
    FD_CLR(fd,&set->fds);
    int max_fd = -1;
    int i = 0;
    for(; i <= set->max_fd; ++i)
    {
        if(!FD_ISSET(i,&set->fds))
        {
            continue;
        }
        if(i > max_fd)
        {
            max_fd = i;
        }
    }
    // 循环结束以后,max_fd就是当前最大的文件描述符
    set->max_fd = max_fd;
    return;
}
////////////////////////////////////////////////////  封装完成

int ServerStart(short port)
{
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0)
    {
        perror("socket");
        return -1;
    }
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons(port);
    if(bind(fd,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        return -2;
    }
    if(listen(fd,3) < 0)
    {
        perror("listen");
        return -3;
    }
    return fd;
}

int ProcessRequest(int new_sock)
{
    // 完成对客户端的读写操作
    // 这里不可以向以前一样进行循环读写操作
    // 当前是单进程/线程程序,如果死循环就无法执行别的操作了
    
    // 此处只进行一次读写,因为我们要将所有的等待操作都交给select完成
    // 下一次对该 socket 读取的时机,也是由select来进行通知的
    // 也就是select下一次返回该文件描述符就绪的时候就能进行读数据了

    char buf[1024] = {0};
    ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
    if(read_size < 0)
    {
        perror("read");
        return -1;
    }
    else if(read_size == 0)
    {
        printf("[client %d] : disconnect\n",new_sock);
        return 0;
    }
    buf[read_size] = '\0';
    printf("[client %d] : %s\n",new_sock,buf);
    write(new_sock,buf,strlen(buf));
    return read_size;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s [port]\n",argv[0]);
        return 1;
    }
    // 1.对服务器进行初始化
    int listen_sock = ServerStart(atoi(argv[1]));
    if(listen_sock < 0)
    {
        printf("ServerStart faild\n");
        return 1;
    }
    printf("ServerStart OK\n");
    // 2.进入事件循环
    FdSet read_fds; // 输入参数: 要等待的所有文件描述符都要加到这个位图中
    InitFdSet(&read_fds);
    AddFdSet(&read_fds,listen_sock);
    while(1)
    {
        // 3.使用select完成等待
        //   创建 output_fds 的目的是: 防止select返回,把read_fds的内容给破坏掉,导致期中数据丢失
        //   read_fds 始终表示select监控的文件描述符集的内容

        FdSet output_fds = read_fds; // 输出型参数: 用来保存每一次的输出参数
        int ret = select(output_fds.max_fd+1,&output_fds.fds,NULL,NULL,NULL);
        if(ret < 0)
        {
            perror("select");
            continue;
        }
        // 4.select返回以后进行处理
        //   a) listen_sock 读就绪
        //   b) new_sock 读就绪
        if(FD_ISSET(listen_sock,&output_fds.fds))
        {// 查看listen_sock在位图中,就证明客户端连接上了,可以accept了
         // 调用 accept 获取到连接,将new_sock加入到select之中
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
            if(new_sock < 0)
            {
                perror("accept");
                return 1;
            }
            AddFdSet(&read_fds,new_sock);
            printf("[client : %d] connect \n",new_sock);
        }
        else
        {// 当前就是 new_sock 就绪了
            int i = 0;
            for(; i < output_fds.max_fd + 1; ++i)
            {
                if(!FD_ISSET(i,&output_fds.fds))
                {
                    continue;
                }
                // 到了这里,证明 i 是位图中的一位, 就可以开始读写了
                int ret = ProcessRequest(i);
                if(ret <= 0)
                {
                    // 到这里正明客户端已经断开连接了
                    // close DelFdSet 两步顺序不影响
                    DelFdSet(&read_fds,i);
                    close(i);
                } // end if(ret<=0)
            } // end for
        } // end else
    } // end while(1)
    return 0;
}
客户端的代码和之前实现tcp/udp一样
// select_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        printf("Usage: ./client [ip] [port]\n");
        return 1;
    }
    // 1. 创建 socket
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0)
    {
        perror("socket");
        return 1;
    }
    // 2. 建立连接
    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    int ret = connect(fd,(sockaddr*)&server,sizeof(server));
    if(ret < 0)
    {
        perror("connect");
        return 1;
    }
    // 3. 进入事件循环
    while(1)
    {
        //    a) 从标准输入读数据
        char buf[1024] = {0};
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        if(read_size < 0)
        {
            perror("read");
            return 1;
        }
        if(read_size == 0)
        {
            printf("read done\n");
            return 0;
        }
        buf[read_size] = '\0';
        //    b) 把读入的数据发送到服务器上
        write(fd,buf,strlen(buf));
        //    c) 从服务器读取相应结果
        char buf_resp[1024] = {0};
        read_size = read(fd,buf_resp,sizeof(buf_resp) - 1);
        if(read_size < 0)
        {
            perror("read");
            return 1;
        }
        else if(read_size == 0)
        {
            //对端先断开连接
            printf("server close socket\n");
            return 0;
        }
        buf_resp[read_size] = '\0';
        //    d) 把结果打印到标准输出上
        printf("server respond > %s\n",buf_resp);
    }
    return 0;
}
观察以上代码
如果当前的文件描述符是 3,5,7,9
当前 3 是 listen_sock
返回结果为 3,5
但是只处理了3,这时候 5 会怎么样呢? 会不会处理不到?
水平触发:
如果一个文件描述符就绪了,但是这一次没有处理到这个文件描述符
但是下一次select仍但会获取到这个文件描述符,把它处理掉

select缺点

  • 每次调用select都需要重新手动设置fd集合;
  • 接口使用非常不方便;
  • 每次调用select,都需要把fd集合从用户态拷贝至内核,当fd很多的时候开销会非常大;
  • 每次调用fd都需要在内核中遍历传递进来的所有fd,当fd很多的时候开销也会非常大;
  • select支持的文件描述符太少了;