IO多路转接之select

五种IO模型

  1. 阻塞IO,在内核将数据准备好之前,系统会一直在等待,所有的套接字,默认都是阻塞方式
  2. 非阻塞IO:如果内核还没有将数据准备好,系统会调用仍然直接返回,并且返回EWOULDBLOCK错误码,非阻塞IO需要程序员循环的方式反复尝试读写文件描述符,看数据是否准备好,这个过程称为轮询,,这对CPU来说是很大的浪费
  3. 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
  4. IO多路转接:阻塞式的灯,但同时等待多个文件描述符的就绪状态
  5. 异步IO:由内核在数据拷贝完成时,通知应用程序拷贝完成

任何IO过程中,都包含两步:第一步是等待,第二步是数据拷贝,实际应用中,等待消耗的时间往往都大于数据拷贝的时间,如果我们要提高IO的效率,就需要将等待的比重降低,让等待的时间尽量少。

IO多路转接之select

概念

  select系统调用是用来让程序监视多个文件描述符的状态变化;程序停在select处等待,直到被监视的文件描述符有一个或者多个的状态发生了变化。

函数原型:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, 
                            fd_set *exceptfds, struct timeval *timeout);

参数:

  • nfsd是需要监听的最大文件描述符+1;
  • fd_set是一个结构体,rdset, wrset, exset分别对应于需要检测的可读文件描述符集合,可写文件描述符集合及异常文件描述符集合;
  • timeval是一个结构体,timeout设置select的等待时间:

    1. NULL表示select没有timeout,select将一直被阻塞,直到有某个文件描述上发生了事件;
    2. 0表示仅检测描述符集合的状态,然后立即返回,并不等待事件发生;
    3. 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

    fd_set这个结构是一个整数数组,或者说是位图,位图中对应的位来表示要监视的文件描述符,既然是位图,那么我们就一定有一组专门的接口来操作位图

void FD_CLR(int fd, fd_set *set);  //用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // ⽤用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // ⽤用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // ⽤用来清除描述词组set的全部位

函数返回值:

  1. 执行成功则返回文件描述词状态已改变的个数
  2. 返回0则代表在描述此状态改变前已超过timeout时间,没有返回
  3. 当有错误发生时则返回-1,错误原意存于errno,此时参数readfds, writefds, exceptfds和timeout的值不可预测
//常用代码段:
fd_set readset;
FD_SET(fd, &readset);
select(fd+1, &readset, NULL, NULL, NULL);
if(FD_ISSET(fd, readset)) {.....} 
select的执行过程

举一个例子,假如fd_set的长度为1个字节,其中每一个比特位表示一个文件描述符

  1. 执行fd_set set; FD_ZERO(&set); set8位表示为0000, 0000
  2. 若fd=5,执行FD_SET,set变为0001, 0000
  3. 若再加入fd=2,fd=1,则set变为0001, 0011
  4. 执行select(6, &set, 0, 0, 0, 0)阻塞等待
  5. 若fd=1,fd=2上都发生了可读事件,则select返回,set变为0000, 0011(fd=5没有发生事件被清空)

至此,我们已经大致明白了select的执行过程和select函数的使用方法,那么什么样的事件是读就绪,什么事件是写就绪呢?
读就绪:

  1. socket内核中,接受缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
  2. socket TCP通信中,对端关闭连接,此时对该socket读,则返回0;
  3. 监听的socket上有新的连接请求
  4. socket上有未处理的错误

写就绪

  1. socket内核中,发送缓冲区中的可用字节数(空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写该文件描述符,并且返回值大于0
  2. socket的写操作被关闭,对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号;
  3. socket使用非阻塞connect连接成功或失败后
  4. socket上有未读取的错误

异常就绪
  socket上收到带外数据

总结select的特点
  1. 可监控的文件描述符个数有限,取决于sizeof(fd_set),我的电脑上sizeof(fd_set)=128,也就是说最多可以支持128*8个文件描述符
  2. 需要一个数据结构array保存select监控集中的fd
    (1)select返回后,array作为源数据,遍历数组中的每一个数据,进行FDISSET判断是不是在fd_set中
    (2)select返回后会把以前假如的但没哟事件发生的fd清空,所以每次开始select前都要重新从array照片那个取得fd逐一加入,同时还要取得fd最大值maxfd,用于select的第一个参数。
  3. 虽然有很多缺点,但是相比于多进程、多线程版本,其效果还是非常高的。

select的缺点

  1. 每次调用select,需要手动设置fd集合,接口的使用角度来看不方便
  2. 每次调用select,需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时很大
  3. 每次调用select,都要在内核遍历传递进来的所有fd,这个开销在fd很多时很大
  4. select支持的文件描述符量太少

select编写的服务器

// 仅将客户端发送的消息回显,并没有做其他处理
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/select.h>
#include<string.h>

#define MAX_FDS sizeof(fd_set)*8
#define INIT_DATA -1
static void InitArray(int arr[], int num)
{
    int i=0;
    for(; i<num; i++)
        arr[i] = INIT_DATA;
}

static int addSockToArray(int arr[], int num, int fd)
{
    int i=0;
    for(; i<num; i++)
    {
        if(arr[i] < 0)
        {   
            arr[i] = fd;
            return i;
        }
    }
    return -1;
}


int setArrayToFdSet(int arr[], int num, fd_set* rfds)
{
    int i=0;
    int max_fd = INIT_DATA;
    for(; i<num; i++)
    {
        if(arr[i] >= 0)
        {
            FD_SET(arr[i], rfds);
            if(max_fd < arr[i])
                max_fd = arr[i];
        }
    }
    return max_fd;
}

int startup(int port)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("sock");
        exit(2);
    }
    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons(port);

    if(bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        perror("bind");
        exit(3);
    }
    if(listen(sock, 5) < 0)
    {
        perror("listen");
        exit(4);
    }
    return sock;
}

static void serviceIO(int arr[], int num, fd_set* rfds)
{
    int i = 0;
    for(; i<num; i++)
    {
        if(arr[i] > INIT_DATA)
        {
            int fd = arr[i];
            if(i==0 && FD_ISSET(arr[i], rfds))//listen_sock ready
            {
                struct sockaddr_in client;
                socklen_t len = sizeof(client);
                int sock = accept(fd, (struct sockaddr*)&client, &len);
                if(sock < 0)
                {
                    perror("accept");
                    continue;
                }

                printf("get a new client [%s:%d]\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
                if(addSockToArray(arr, num, sock) == -1)
                {
                    close(sock);
                }
            }
            else if(i != 0 && FD_ISSET(arr[i], rfds))
            {   // normal fd ready
                char buf[10240];
                ssize_t s = read(fd, buf, sizeof(buf)-1);
                if(s > 0)
                {
                    buf[s] = 0;
                    printf("client:> %s\n", buf);
                }
                else if(s == 0)
                {
                    close(fd);
                    arr[i] = INIT_DATA;
                    printf("client quit\n");
                }
                else
                {
                    perror("read");
                    close(fd);
                    arr[i] = INIT_DATA;
                }
            }
            else
            {
                //do nothing    not ready
            }
        }
    }
}


int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s [port]\n", argv[0]);
        return 1;
    }
    int listen_sock = startup(atoi(argv[1]));

    int fd_array[MAX_FDS];
    InitArray(fd_array, MAX_FDS);
    addSockToArray(fd_array, MAX_FDS, listen_sock);
    fd_set rfds;

    for(;;)
    {
        FD_ZERO(&rfds);
        int max_fd = setArrayToFdSet(fd_array, MAX_FDS, &rfds);

        struct timeval timeout = {3, 0};
        switch(select(max_fd+1, &rfds, NULL, NULL, NULL))
        {
            case -1:
                perror("select");
                break;
            case 0:
                printf("time out...\n");
                break;
            default:
                serviceIO(fd_array, MAX_FDS, &rfds);
                break;
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值