Linux下的socket編程實踐(八) Select的限制和poll(并發的初步知識)
來源:程序員人生 發布時間:2016-03-01 08:11:29 閱讀次數:5330次
select的限制
用select實現的并發服務器,能到達的并發數1般受兩方面限制:
1)1個進程能打開的最大文件描寫符限制。這可以通過調劑內核參數來改變。可以通過ulimit -n(number)來調劑或使用setrlimit函數設置(需要root權限),但1個系統所能打開的最大數也是有限的,跟內存大小有關,可以通過cat /proc/sys/fs/file-max 查看。
2)select中的fd_set集合容量的限制(FD_SETSIZE,1般為1024),這需要重新編譯內核才能改變。
對第1個限制:
nclude
#include
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
其中,resource的1個取值 RLIMIT_NOFILE
代表指定比進程可打開的最大文件描寫詞大1的值,超越此值,將會產生EMFILE毛病。
rlim:描寫資源軟硬限制的結構體,原型以下
struct rlimit {
rlim_t rlim_cur; /* Soft limit */
rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
};
返回說明:
成功履行時,返回0。失敗返回⑴,errno被設為以下的某個值
EFAULT:rlim指針指向的空間不可訪問
EINVAL:參數無效
EPERM:增加資源限制值時,權能不允許
軟限制是1個建議性的, 最好不要超出的限制, 如果超出的話, 系統可能向進程發送信號以終止其運行.
而硬限制1般是軟限制的上限;
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#進程建立的,在更改資源限制時,須遵守以下3條規則:
1.任何1個進程都可將1個軟限制更改成小于或等于其硬限制。
2.任何1個進程都可下降其硬限制值,但它必須大于或等于其軟限制值。這類下降,對普通用戶而言是不可逆反的。
3.只有超級用戶可以提高硬限制。
/**示例: getrlimit/setrlimit獲得/設置進程打開文件數目**/
int main()
{
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) == ⑴)
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) == ⑴) err_exit("setrlimit error"); if (getrlimit(RLIMIT_NOFILE, &rl) == ⑴) 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 ", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port)); printf( "count = %d ", ++count); } return 0; }
我們來看1下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以外還有1個監聽套接字listenfd,客戶端某1個套接字(不1定是最后1個)雖然已建立了連接,在已完成連接隊列中,但accept返回時到達最大描寫符限制,返回毛病,打印提示信息。
client在socket()返回⑴是調用sleep(4)解析
當客戶端調用socket準備創建第1022個套接字時,如上所示也會提示毛病,此時socket函數返回⑴出錯,如果沒有睡眠4s后再退出進程會有甚么問題呢?如果直接退出進程,會將客戶端所打開的所有套接字關閉掉,即向服務器端發送了很多FIN段,而此時或許服務器端還1直在accept ,即還在從已連接隊列中返回已連接套接字,此時服務器端除關心監聽套接字的可讀事件,也開始關心前面已建立連接的套接字的可讀事件,read 返回0,所以會有很多 client close 字段參雜在條目的輸出中,還有個問題就是,由于read 返回0,服務器端會將本身的已連接套接字關閉掉,那末或許剛才說的客戶端某1個連接會被accept 返回,即測試不出服務器端真實的并發容量;
poll調用
poll沒有select第2個限制, 即FD_SETSIZE的限制, 不用修改內核,但是第1個限制暫時還是沒法避免的;
#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, 毫秒),若為⑴,表示永不超時。
poll 跟 select 還是很相似的,比較重要的區分在于poll 所能并發的個數跟FD_SETSIZE無關,只跟1個進程所能打開的文件描寫符個數有關,可以在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
");
exit(EXIT_FAILURE);
}
printf( "count = %d
", ++count);
printf( "recv connect ip=%s port=%d
", 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 "); client[i].fd = - 1; close(conn); } fputs(recvbuf, stdout); writen(conn, recvbuf, strlen(recvbuf)); if (--nready <= 0) break; } } } return 0; } /* poll 只受1個進程所能打開的最大文件描寫符限制,這個可使用ulimit -n調劑 */
可以看到現在最大的連接數已是2045個了,雖然服務器端有某個連接沒有accept 返回。即poll 比 select 能夠承受更多的并發連接,只受1個進程所能打開的最大文件描寫符個數限制。可以通過ulimit -n 修改,但1個系統所能打開的文件描寫符個數也是有限的,這跟系統的內存大小有關系,所以說也不是可以無窮地并發,我們在文章的開始也提到過,可使用 cat /proc/sys/fs/file-max查看1下本機的容量。
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈