1. 程式人生 > >Linux下的socket程式設計實踐(八) Select的限制和poll(併發的初步知識)

Linux下的socket程式設計實踐(八) Select的限制和poll(併發的初步知識)

select的限制

用select實現的併發伺服器,能達到的併發數一般受兩方面限制:

1)一個程序能開啟的最大檔案描述符限制。這可以通過調整核心引數來改變。可以通過ulimit -n(number)來調整或者使用setrlimit函式設定(需要root許可權),但一個系統所能開啟的最大數也是有限的,跟記憶體大小有關,可以通過cat /proc/sys/fs/file-max 檢視。

2)select中的fd_set集合容量的限制(FD_SETSIZE,一般為1024),這需要重新編譯核心才能改變。

對於第一個限制:

nclude <sys/time.h>
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
其中,resource的一個取值  RLIMIT_NOFILE 代表指定比程序可開啟的最大檔案描述詞大一的值,超出此值,將會產生EMFILE錯誤。
rlim:描述資源軟硬限制的結構體,原型如下

struct rlimit {
    rlim_t rlim_cur; /* Soft limit */
    rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
返回說明:
成功執行時,返回0。失敗返回-1,errno被設為以下的某個值
EFAULT:rlim指標指向的空間不可訪問
EINVAL:引數無效
EPERM:增加資源限制值時,權能不允許

軟限制是一個建議性的, 最好不要超越的限制, 如果超越的話, 系統可能向程序傳送訊號以終止其執行.

而硬限制一般是軟限制的上限;

resource可用值

RLIMIT_AS

程序可用的最大虛擬記憶體空間長度,包括堆疊、全域性變數、動態記憶體

RLIMIT_CORE

核心生成的core檔案的最大大小

RLIMIT_CPU

所用的全部cpu時間,以秒計算

RLIMIT_DATA

程序資料段(初始化DATA段, 未初始化BSS段和堆)限制(以B為單位)

RLIMIT_FSIZE

檔案大小限制

RLIMIT_SIGPENDING

使用者能夠掛起的訊號數量限制

RLIMIT_NOFILE

開啟檔案的最大數目

RLIMIT_NPROC

使用者能夠建立的程序數限制

RLIMIT_STACK

程序棧記憶體限制, 超過會產生SIGSEGV訊號

程序的資源限制通常是在系統初啟時由0#程序建立的,在更改資源限制時,須遵循下列三條規則:

  1.任何一個程序都可將一個軟限制更改為小於或等於其硬限制。

  2.任何一個程序都可降低其硬限制值,但它必須大於或等於其軟限制值。這種降低,對普通使用者而言是不可逆反的。

  3.只有超級使用者可以提高硬限制。

/**示例: getrlimit/setrlimit獲取/設定程序開啟檔案數目**/  
int main()  
{  
    struct rlimit rl;  
    if (getrlimit(RLIMIT_NOFILE, &rl) == -1)  
        err_exit("getrlimit error");  
    cout << "Soft limit: " << rl.rlim_cur << endl;  
    cout << "Hard limit: " << rl.rlim_max << endl;  
    cout << "------------------------->"  << endl;  
  
    rl.rlim_cur = 2048;  
    rl.rlim_max = 2048;  
    if (setrlimit(RLIMIT_NOFILE, &rl) == -1)  
        err_exit("setrlimit error");  
  
    if (getrlimit(RLIMIT_NOFILE, &rl) == -1)  
        err_exit("getrlimit error");  
    cout << "Soft limit: " << rl.rlim_cur << endl;  
    cout << "Hard limit: " << rl.rlim_max << endl;  
}  
測試最多可以建立多少個連結,下面是客戶端的程式碼:
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <signal.h> 

#include <stdlib.h> 
#include <stdio.h> 
#include <errno.h> 
#include <string.h> 

#define ERR_EXIT(m) \ 
         do \ 
        { \ 
                perror(m); \ 
                exit(EXIT_FAILURE); \ 
        }  while( 0) 


int main( void) 
{ 
     int count =  0; 
     while( 1) 
    { 
         int sock; 
         if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) <  0) 
        { 
            sleep( 4); 
            ERR_EXIT( "socket"); 
        } 

         struct sockaddr_in servaddr; 
        memset(&servaddr,  0,  sizeof(servaddr)); 
        servaddr.sin_family = AF_INET; 
        servaddr.sin_port = htons( 5188); 
        servaddr.sin_addr.s_addr = inet_addr( "127.0.0.1"); 

         if (connect(sock, ( struct sockaddr *)&servaddr,  sizeof(servaddr)) <  0) 
            ERR_EXIT( "connect"); 

         struct sockaddr_in localaddr; 
        socklen_t addrlen =  sizeof(localaddr); 
         if (getsockname(sock, ( struct sockaddr *)&localaddr, &addrlen) <  0) 
            ERR_EXIT( "getsockname"); 

        printf( "ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); 
        printf( "count = %d\n", ++count); 

    } 

     return  0; 
}

我們來看一下server端輸出:

recv connect ip=127.0.0.1 port=57430
count = 2039
recv connect ip=127.0.0.1 port=57431
count = 2040
recv connect ip=127.0.0.1 port=57432
count = 2041
recv connect ip=127.0.0.1 port=57433
count = 2042
recv connect ip=127.0.0.1 port=57434
count = 2043
recv connect ip=127.0.0.1 port=57435
count = 2044
recv connect ip=127.0.0.1 port=57436
accept error: Too many open files

解析:對於客戶端,最多隻能開啟1021個連線套接字,因為總共是在Linux中最多可以開啟1024個檔案描述如,其中還得除去0,1,2。而伺服器端只能accept 返回1020個已連線套接字,因為除了0,1,2之外還有一個監聽套接字listenfd,客戶端某一個套接字(不一定是最後一個)雖然已經建立了連線,在已完成連線佇列中,但accept返回時達到最大描述符限制,返回錯誤,列印提示資訊。

client在socket()返回-1是呼叫sleep(4)解析

   當客戶端呼叫socket準備建立第1022個套接字時,如上所示也會提示錯誤,此時socket函式返回-1出錯,如果沒有睡眠4s後再退出程序會有什麼問題呢?如果直接退出程序,會將客戶端所開啟的所有套接字關閉掉,即向伺服器端傳送了很多FIN段,而此時也許伺服器端還一直在accept ,即還在從已連線佇列中返回已連線套接字,此時伺服器端除了關心監聽套接字的可讀事件,也開始關心前面已建立連線的套接字的可讀事件,read 返回0,所以會有很多 client close 欄位參雜在條目的輸出中,還有個問題就是,因為read 返回0,伺服器端會將自身的已連線套接字關閉掉,那麼也許剛才說的客戶端某一個連線會被accept 返回,即測試不出伺服器端真正的併發容量;

poll呼叫

poll沒有select第二個限制, 即FD_SETSIZE的限制, 不用修改核心,但是第一個限制暫時還是無法避免的;

#include <poll.h>  
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

引數nfds: 需要檢測事件的個數, 結構體陣列大小(也可表示為檔案描述符個數)(The caller should specify the number of items in the fds array in nfds.)

 引數timeout: 超時時間(單位milliseconds, 毫秒),若為-1,表示永不超時。


poll 跟 select 還是很相似的,比較重要的區別在於poll 所能併發的個數跟FD_SETSIZE無關,只跟一個程序所能開啟的檔案描述符個數有關,可以在select 程式的基礎上修改成poll 程式,在執行伺服器端程式之前,使用ulimit -n 2048 將限制改成2048個,注意在執行客戶端程序的終端也需更改,因為客戶端也會有所限制,這只是臨時性的更改,因為子程序會繼承這個環境引數,而我們是在bash命令列啟動程式的,故在程序執行期間,檔案描述符的限制為2048個。

使用poll 函式的伺服器端程式如下,和select大概用法差不多:

#include<stdio.h> 
#include<sys/types.h> 
#include<sys/socket.h> 
#include<unistd.h> 
#include<stdlib.h> 
#include<errno.h> 
#include<arpa/inet.h> 
#include<netinet/in.h> 
#include<string.h> 
#include<signal.h> 
#include<sys/wait.h> 
#include<poll.h> 
#include  "read_write.h" 

#define ERR_EXIT(m) \ 
     do { \ 
        perror(m); \ 
        exit(EXIT_FAILURE); \ 
    }  while ( 0) 


int main() 
{ 
     int count =  0; 
     signal(SIGPIPE, SIG_IGN); 
     int listenfd;  //被動套接字(檔案描述符),即只可以accept, 監聽套接字
     if ((listenfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) <  0) 
         //  listenfd = socket(AF_INET, SOCK_STREAM, 0)
        ERR_EXIT( "socket error"); 

     struct sockaddr_in servaddr; 
   	 memset(&servaddr,  0,  sizeof(servaddr)); 
   	 servaddr.sin_family = AF_INET; 
     servaddr.sin_port = htons( 5188); 
     servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 
     
     int on =  1; 
     if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on,  sizeof(on)) <  0) 
        ERR_EXIT( "setsockopt error"); 

     if (bind(listenfd, ( struct sockaddr *)&servaddr,  sizeof(servaddr)) <  0) 
        ERR_EXIT( "bind error"); 

     if (listen(listenfd, SOMAXCONN) <  0)  //listen應在socket和bind之後,而在accept之前
        ERR_EXIT( "listen error"); 

     struct sockaddr_in peeraddr;  //傳出引數
     socklen_t peerlen =  sizeof(peeraddr);  //傳入傳出引數,必須有初始值

     int conn;  // 已連線套接字(變為主動套接字,即可以主動connect)
     int i; 

     struct pollfd client[ 2048]; 
     int maxi =  0;  //client[i]最大不空閒位置的下標

     for (i =  0; i <  2048; i++) 
        client[i].fd = - 1; 

     int nready; 
    client[ 0].fd = listenfd; 
    client[ 0].events = POLLIN; 

     while (1) 
    { 
         /* poll檢測[0, maxi + 1) */ 
        nready = poll(client, maxi +  1, - 1); 
         if (nready == - 1) 
        { 
             if (errno == EINTR) 
                 continue; 
            ERR_EXIT( "poll error"); 
        } 

         if (nready == 0) 
             continue; 
         //如果是監聽套介面發生了可讀事件 
         if (client[0].revents & POLLIN) 
         { 
             conn = accept(listenfd, ( struct sockaddr *)&peeraddr, &peerlen);  //accept不再阻塞
             if (conn == - 1) 
                ERR_EXIT( "accept error"); 

             for (i =  1; i <  2048; i++) 
            { 
                 if (client[i].fd <  0) 
                { 
                    client[i].fd = conn; 
                     if (i > maxi) 
                        maxi = i; 
                     break; 
                } 
            } 

             if (i ==  2048) 
            { 
                fprintf(stderr,  "too many clients\n"); 
                exit(EXIT_FAILURE); 
            } 

            printf( "count = %d\n", ++count); 
            printf( "recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr), 
                   ntohs(peeraddr.sin_port)); 

            client[i].events = POLLIN; 

             if (--nready <=  0) 
                 continue; 
        } 

         for (i =  1; i <= maxi; i++) 
        { 
            conn = client[i].fd; 
             if (conn == - 1) 
                 continue; 
                 //已連線套介面發生了可讀事件 
             if (client[i].revents & POLLIN) 
            { 

                 char recvbuf[ 1024] = { 0}; 
                 int ret = readline(conn, recvbuf,  1024); 
                 if (ret == - 1) 
                    ERR_EXIT( "readline error"); 
                 else  if (ret  ==  0)    //客戶端關閉
                { 
                    printf( "client  close \n"); 
                    client[i].fd = - 1; 
                    close(conn); 
                } 

                fputs(recvbuf, stdout); 
                writen(conn, recvbuf, strlen(recvbuf)); 

                 if (--nready <=  0) 
                     break; 
            } 
        } 


    } 

     return  0; 
} 

/* poll 只受一個程序所能開啟的最大檔案描述符限制,這個可以使用ulimit -n調整 */

可以看到現在最大的連線數已經是2045個了,雖然伺服器端有某個連線沒有accept 返回。即poll 比 select 能夠承受更多的併發連線,只受一個程序所能開啟的最大檔案描述符個數限制。可以通過ulimit -n  修改,但一個系統所能開啟的檔案描述符個數也是有限的,這跟系統的記憶體大小有關係,所以說也不是可以無限地併發,我們在文章的開始也提到過,可以使用 cat /proc/sys/fs/file-max檢視一下本機的容量。