歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Linux下的socket編程實踐(八)Select的限制和poll(並發的初步知識)

Linux下的socket編程實踐(八)Select的限制和poll(並發的初步知識)

日期:2017/3/1 12:23:04   编辑:關於Linux

select的限制

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

1)一個進程能打開的最大文件描述符限制。這可以通過調整內核參數來改變。可以通過ulimit -n(number)來調整或者使用setrlimit函數設置(需要root權限),但一個系統所能打開的最大數也是有限的,跟內存大小有關,可以通過cat /proc/sys/fs/file-max 查看。

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

對於第一個限制:

nclude 
#include 

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  
#include  
#include  
#include  
#include  

#include  
#include  
#include  
#include  

#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   
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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#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查看一下本機的容量。

Copyright © Linux教程網 All Rights Reserved