IO複用之——select

一. select

    前面提到Linux下的五種IO模型中有一個是IO複用模型,這種IO模型是可以調用一個特殊的函數同時監聽多個IO事件,當多個IO事件中有至少一個就緒的時候,被調用的函數就會返回通知用戶進程來處理已經ready事件的數據,這樣通過同時等待IO事件來代替單一等待一個IO窗口數據的方式,可以大大提高系統的等待數據的效率;而接下來,就要討論在Linux系統中提供的一個用來進行IO多路等待的函數——select;



二. select函數的用法

    首先在使用select之前,要分清在IO事件中,往往關心的不是數據的讀取就是數據的發送,也就是數據的,當然也有同時關心讀寫的,沒有任何一個IO事件既不關係讀也不關心寫的,因此,在對於使用select對多個IO事件進行監聽檢測的時候,就要對這些事件進行讀寫的分類,以便日後在select返回時通過檢測能夠得知當前事件是讀發生了還是寫發生了;


wKioL1dG6cKhl8kIAAAZ5JVGFv0428.png

函數參數中,

nfds表示當前最大文件描述符值+1;

readfds表示當前的事件中有多少是關心數據的讀取的;

writefds表示當前的事件中有多少是關心數據的寫入的;

excptfds表示當前事件中關心異常發生的事件集,也是數據的寫入;


其中,fd_set是一個文件符集的數據類型

對於fd_set文件描述符集的設置,系統提供了四個函數來進行操作:

FD_CLR是對文件描述符集中的所有文件描述符進行清除;

FD_ISSET是判斷某個文件描述符是否已經被設置進某個文件描述符集中;

FD_SET是將某個文件描述符設置進某個文件描述符集中;

FD_ZERO是對某個文件描述符集進行初始化;


timeout是時間的設定,表示當超過設定的時間仍然沒有事件就緒時就超時返回不再等待;

timeout的結構體類型如下:

wKiom1dG7M2A-dOWAAAn8rMmYzg457.png

tv_sec是秒的設置;

tv_usec是微秒的設置;


對於select函數的返回值:

當返回值爲-1的時候,表示函數出錯並會置相應的錯誤碼;

當返回值爲0的時候,表示超時返回;

當返回值大於0的時候,表示至少已經有一個事件已經就緒可以處理其數據了;



三. 栗子時間

    前面有一篇本人寫的博客是基於TCP協議的socket編程,其中一個服務器爲了能處理多個連接請求將listen監聽和accept處理連接請求分開,每當listen到一個連接請求的時候就fork出一個子進程讓子進程去處理,或者使用多線程,這樣就不耽誤對網絡中連接請求的監聽了;

    但是同樣是單進程,可以使用select的IO複用模型來解決對多個連接的數據處理,程序設計如下:


server服務器端:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define _BACKLOG_ 5//設置監聽隊列裏面允許等待的最大值

int fds[20];//用於集存需要進行處理的IO事件

void usage(const char *argv)//進行命令行參數的差錯判斷
{
    printf("%s   [ip]   [port]\n", argv);
    exit(0);
}

int creat_listen_sock(int ip, int port)//創建listen socket
{
    int sock = socket(AF_INET, SOCK_STREAM, 0); 
    if(sock < 0)
    {   
        perror("socket");
        exit(1);
    }   

    struct sockaddr_in server;//設置本地server端的網絡地址信息
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = ip; 

    if(bind(sock, (struct sockaddr*)&server, sizeof(server)) < 0)//綁定端口號和網絡地址信息
    {   
        perror("bind");
        exit(3);
    }
    
    if(listen(sock, _BACKLOG_) < 0)//進行監聽
    {
        perror("listen");
        exit(2);
    }

    return sock;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
        usage(argv[0]);

    int port = atoi(argv[2]);
    int ip = inet_addr(argv[1]);

    int listen_sock = creat_listen_sock(ip, port);//獲取監聽端口號

    struct sockaddr_in client;//創建對端網絡地址信息結構體用於保存對端信息
    socklen_t client_len = sizeof(client);

    size_t fds_num = sizeof(fds)/sizeof(fds[0]);
    size_t i = 0;
    for(; i < fds_num; ++i)//將存放文件描述符的數組進行初始化
        fds[i] = -1;

    fds[0] = listen_sock;//首先將listen socket添加進去
    fd_set read_fd;//創建讀事件文件描述符集
    fd_set write_fd;//創建寫事件文件描述符集
    int max_fd = fds[0];//首先將最大的文件描述符集設定爲listen socket
    
    while(1)
    {
        FD_ZERO(&read_fd);//將兩個文件描述符集進行初始化
        FD_ZERO(&write_fd);
        struct timeval timeout = {10, 0};//設定超時時間

        size_t i = 0;
        for(; i < fds_num; ++i)//每次循環都要將數組中的文件描述符進行重新添加設置
        {
            if(fds[i] > 0)
            {
                FD_SET(fds[i], &read_fd);
                if(fds[i] > max_fd)
                    max_fd = fds[i];
            }
        }

        switch(select(max_fd+1, &read_fd, &write_fd, NULL, &timeout))//進行select等待
        {
            case -1://出錯
                perror("select");
                break;
            case 0://超時
                printf("time out...\n");
                break;
            default://至少有一個IO事件已經就緒
                {
                    size_t i = 0;
                    for(; i < fds_num; ++i)
                    {
                    //當爲listen socket事件就緒的時候,就表明有新的連接請求
                        if(FD_ISSET(fds[i], &read_fd) && (fds[i] == listen_sock))
                        {
                            int accept_sock = accept(listen_sock, (struct sockaddr*)&client, &client_len);
                            if(accept_sock < 0)
                            {
                                perror("accept");
                                continue;
                            }

                            char *client_ip = inet_ntoa(client.sin_addr);
                            int client_port = ntohs(client.sin_port);
                            printf("connect with a client...  [ip]:%s  [port]:%d\n", client_ip, client_port);

                            size_t i = 0;
                            for(; i < fds_num; ++i)//將新的連接請求的文件描述符添加進數組保存
                            {
                                if(fds[i] == -1)
                                {
                                    fds[i] = accept_sock;
                                    break;
                                }
                            }
                            if(i == fds_num)
                                close(accept_sock);
                        }
                        //除了listen socket就是別的普通進行數據傳輸的文件描述符
                        else if(FD_ISSET(fds[i], &read_fd) && (fds[i] > 0))
                        {
                            char buf[1024];
                            ssize_t  size = read(fds[i], buf, sizeof(buf)-1);
                            if(size < 0)
                                perror("read");
                            else if(size == 0)
                            {//當client端關閉就關閉相應的文件描述符
                                printf("client closed...\n");
                                close(fds[i]);
                                fds[i] = -1;
                            }
                            else
                            {
                                buf[size] = '\0';
                                printf("client# %s\n", buf);
                            }
                        }
                        else
                        {}
                    }

                }
                break;
        }
    }
    return 0;
}


因爲客戶端的程序和前面的TCP的程序一樣,這裏就不再多寫;


上面的程序可以分爲如下步驟:

  1. 創建監聽套接字並綁定本地網絡地址信息進行監聽;

  2. 創建一個全局的數組用於存放已有事件的文件描述符,便於重新進行整理;

  3. 創建讀、寫事件集,這裏忽略異常事件集;

  4. 循環等待各個事件的就緒,每次都重新初始化事件集和重新添加設置,因爲select會將沒有就緒的事件清爲0;

  5. select完成進行返回值的一個判斷:如果是-1,則出錯返回;如果是0,則超時返回;如果是大於零的值,則表明至少有一個事件就緒,轉到第6步;

  6. 將數組中的事件拿出一一進行判斷:如果是listen socket就緒表明有新的連接請求,新創建一個文件描述符用於處理數據的傳輸,並將其添置進數組中;如果是別的文件描述符就緒表明有數據傳輸過來需要讀取,轉第7步;

  7. 讀取數據時,如果判斷client端關閉就將數組中相應位置還原回無效值並且關閉相應的socket文件描述符,讀取成功輸出數據,繼續循環;


運行程序:

wKiom1dHBFKRBrL0AAA9lENErsI024.png


    可以注意到上面的程序中sever端只將所有的連接請求都作爲讀事件添加進去了,而並沒有關心寫事件,事實上socket支持全雙工的通信,因此,將上面的程序改爲server端讀取數據的同時將數據再寫回給client端,以此來告知client端server端已經成功收到了數據,程序改進如下:


在循環每一次重新整理數組中的文件描述符集的時候將不是listen socket的文件描述符集同時添加進讀事件集和寫事件集:

        FD_ZERO(&read_fd);
        FD_ZERO(&write_fd);
        FD_SET(listen_sock, &read_fd);//先將listen socket添加進讀事件集
        struct timeval timeout = {10, 0}; 

        size_t i = 1;//循環跳過listen socket從1開始
        for(; i < fds_num; ++i)
        {
            if(fds[i] > 0)
            {
                FD_SET(fds[i], &read_fd);//同時添加進讀事件集和寫事件集
                FD_SET(fds[i], &write_fd);
                if(fds[i] > max_fd)
                    max_fd = fds[i];
            }   
        }


而當數據就緒進行讀取完畢之後,再將同一個緩衝區中的數據寫回client端,這裏因爲讀寫事件中使用的是同一個文件描述符,因此,當一個socket的讀事件準備就緒的時候,說明寫事件同樣也是就緒的,而且使用同一個緩衝區中相同的數據:

else if(FD_ISSET(fds[i], &read_fd) && (FD_ISSET(fds[i], &write_fd)) && (fds[i] > 0))
{
     char buf[1024];
     ssize_t  size = read(fds[i], buf, sizeof(buf)-1);
     if(size < 0)
         perror("read");
     else if(size == 0)
     {
         printf("client closed...\n");
         close(fds[i]);
         fds[i] = -1;
         break;
      }
      else
      {
         buf[size] = '\0';
         printf("client# %s\n", buf);
      }
      if(FD_ISSET(fds[i], &write_fd))
      {
          size = write(fds[i], buf, strlen(buf));
          if(size < 0)
               perror("write");
      }
      else
         printf("can not write back...\n");
}


因此,在client端也需要進行讀取;

運行程序:

wKiom1dG_0nirB9zAAAQw1CAM34776.png

    總結如上,雖然select實現IO複用在等待數據的效率看來要比單一的等待高,但是不難發現當需要等待多個事件的時候,是需要不斷地進行復制和循環判斷的,這也同樣增加了時間複雜度增加了系統的開銷,而且,作爲一個數據類型的fd_set是由上限的,我的當前機器sizeof(fd_set)值爲128,而一個字節能添加8個文件描述符,也就是總共只能添加128*8=1024個文件描述符,這個數目還是有些小的,無疑也是一個缺點。


《完》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章