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),這需要重新編譯核心才能改變。
對於第一個限制:
其中,resource的一個取值 RLIMIT_NOFILE 代表指定比程序可開啟的最大檔案描述詞大一的值,超出此值,將會產生EMFILE錯誤。nclude <sys/time.h> #include <sys/resource.h> int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim);
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檢視一下本機的容量。