select的限制
用select實現的併發服務器,能達到的併發數一般受兩方面限制:
1)一個進程能打開的最大文件描述符限制。這可以通過調整內核參數。可以通過ulimit -n(number)來調整或者使用setrlimit函數設置,但一個系統所能打開的最大數也是有限的,跟內存大小有關,可以通過cat /proc/sys/fs/file-max 查看
- /**示例: 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;
- }
2)select中的fd_set集合容量的限制(FD_SETSIZE,1024),這需要重新編譯內核才能改變。
- /**測試: 測試服務器端最多能夠建立多少個連接
- server端完整源代碼如下(注意使用的是<Socket編程實踐(7)中的TCPServer類實現>):
- **/
- int main()
- {
- signal(SIGPIPE, sigHandlerForSigPipe);
- try
- {
- TCPServer server(8001);
- int listenfd = server.getfd();
- struct sockaddr_in clientAddr;
- socklen_t addrLen;
- int maxfd = listenfd;
- fd_set rset;
- fd_set allset;
- FD_ZERO(&rset);
- FD_ZERO(&allset);
- FD_SET(listenfd, &allset);
- //用於保存已連接的客戶端套接字
- int client[FD_SETSIZE];
- for (int i = 0; i < FD_SETSIZE; ++i)
- client[i] = -1;
- int maxi = 0; //用於保存最大的不空閒的位置, 用於select返回之後遍歷數組
- int count = 0;
- while (true)
- {
- rset = allset;
- int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
- if (nReady == -1)
- {
- if (errno == EINTR)
- continue;
- err_exit("select error");
- }
- if (FD_ISSET(listenfd, &rset))
- {
- addrLen = sizeof(clientAddr);
- int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
- if (connfd == -1)
- err_exit("accept error");
- int i;
- for (i = 0; i < FD_SETSIZE; ++i)
- {
- if (client[i] < 0)
- {
- client[i] = connfd;
- if (i > maxi)
- maxi = i;
- break;
- }
- }
- if (i == FD_SETSIZE)
- {
- cerr << "too many clients" << endl;
- exit(EXIT_FAILURE);
- }
- //打印客戶IP地址與端口號
- cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
- << ", " << ntohs(clientAddr.sin_port) << endl;
- cout << "count = " << ++count << endl;
- //將連接套接口放入allset, 並更新maxfd
- FD_SET(connfd, &allset);
- if (connfd > maxfd)
- maxfd = connfd;
- if (--nReady <= 0)
- continue;
- }
- /**如果是已連接套接口發生了可讀事件**/
- for (int i = 0; i <= maxi; ++i)
- if ((client[i] != -1) && FD_ISSET(client[i], &rset))
- {
- char buf[512] = {0};
- int readBytes = readline(client[i], buf, sizeof(buf));
- if (readBytes == -1)
- err_exit("readline error");
- else if (readBytes == 0)
- {
- cerr << "client connect closed..." << endl;
- FD_CLR(client[i], &allset);
- close(client[i]);
- client[i] = -1;
- }
- cout << buf;
- if (writen(client[i], buf, readBytes) == -1)
- err_exit("writen error");
- if (--nReady <= 0)
- break;
- }
- }
- }
- catch (const SocketException &e)
- {
- cerr << e.what() << endl;
- err_exit("TCPServer error");
- }
- }
- /**高併發測試端代碼: contest完整源代碼如下**/
- int main()
- {
- //最好不要修改: 不然會產生段溢出(stack-overflow)
- // struct rlimit rlim;
- // rlim.rlim_cur = 2048;
- // rlim.rlim_max = 2048;
- // if (setrlimit(RLIMIT_NOFILE, &rlim) == -1)
- // err_exit("setrlimit error");
- int count = 0;
- while (true)
- {
- int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- if (sockfd == -1)
- {
- sleep(5);
- err_exit("socket error");
- }
- struct sockaddr_in serverAddr;
- serverAddr.sin_family = AF_INET;
- serverAddr.sin_port = htons(8001);
- serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
- int ret = connect_timeout(sockfd, &serverAddr, 5);
- if (ret == -1 && errno == ETIMEDOUT)
- {
- cerr << "timeout..." << endl;
- err_exit("connect_timeout error");
- }
- else if (ret == -1)
- err_exit("connect_timeout error");
- //獲取並打印對端信息
- struct sockaddr_in peerAddr;
- socklen_t peerLen = sizeof(peerAddr);
- if (getpeername(sockfd, (struct sockaddr *)&peerAddr, &peerLen) == -1)
- err_exit("getpeername");
- cout << "Server information: " << inet_ntoa(peerAddr.sin_addr)
- << ", " << ntohs(peerAddr.sin_port) << endl;
- cout << "count = " << ++count << endl;
- }
- }
Server端運行截圖如圖所示:
解析:對於客戶端,最多隻能開啓1021個連接套接字,因爲總共是在Linux中最多可以打開1024個文件描述如,其中還得除去0,1,2。而服務器端只能accept 返回1020個已連接套接字,因爲除了0,1,2之外還有一個監聽套接字listenfd,客戶端某一個套接字(不一定是最後一個)雖然已經建立了連接,在已完成連接隊列中,但accept返回時達到最大描述符限制,返回錯誤,打印提示信息。
client在socket()返回-1是調用sleep(5)解析
當客戶端調用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,表示永不超時。
- //pollfd結構體
- struct pollfd
- {
- int fd; /* file descriptor */
- short events; /* requested events: 請求的事件 */
- short revents; /* returned events : 返回的事件*/
- };
events與revents取值(前3個最常用):
返回值:
成功: 返回一個正整數(this is the number of structures which have nonzero revents
fields (in other words, those descriptors with events or errors reported).
超時: 返回0(A value of 0 indicates that the call timed out and no file descriptors
were ready)
失敗: 返回-1(On error, -1 is returned, and errno is set appropriately.)
- /**poll-Server示例(將前面的select-server改造如下, 其沒有了FD_SETSIZE的限制, 關於第一個限制可以使用前文中的方法更改)(client端與測試端代碼如前)**/
- const int SETSIZE = 2048;
- int main()
- {
- signal(SIGPIPE, sigHandlerForSigPipe);
- try
- {
- TCPServer server(8001);
- //用於保存已連接的客戶端套接字
- struct pollfd client[SETSIZE];
- //將client置空
- for (int i = 0; i < SETSIZE; ++i)
- client[i].fd = -1;
- int maxi = 0; //用於保存最大的已佔用位置
- int count = 0;
- client[0].fd = server.getfd();
- client[0].events = POLLIN;
- while (true)
- {
- int nReady = poll(client, maxi+1, -1);
- if (nReady == -1)
- {
- if (errno == EINTR)
- continue;
- err_exit("poll error");
- }
- //如果是監聽套接口發生了可讀事件
- if (client[0].revents & POLLIN)
- {
- int connfd = accept(server.getfd(), NULL, NULL);
- if (connfd == -1)
- err_exit("accept error");
- bool flags = false;
- //略過client[0].fd(listenfd), 從1開始檢測
- for (int i = 1; i < SETSIZE; ++i)
- {
- if (client[i].fd == -1)
- {
- client[i].fd = connfd;
- client[i].events = POLLIN;
- flags = true;
- if (i > maxi)
- maxi = i;
- break;
- }
- }
- //未找到一個合適的位置
- if (!flags)
- {
- cerr << "too many clients" << endl;
- exit(EXIT_FAILURE);
- }
- cout << "count = " << ++count << endl;
- if (--nReady <= 0)
- continue;
- }
- /**如果是已連接套接口發生了可讀事件**/
- for (int i = 1; i <= maxi; ++i)
- if (client[i].revents & POLLIN)
- {
- char buf[512] = {0};
- int readBytes = readline(client[i].fd, buf, sizeof(buf));
- if (readBytes == -1)
- err_exit("readline error");
- else if (readBytes == 0)
- {
- cerr << "client connect closed..." << endl;
- close(client[i].fd);
- client[i].fd = -1;
- }
- cout << buf;
- if (writen(client[i].fd, buf, readBytes) == -1)
- err_exit("writen error");
- if (--nReady <= 0)
- break;
- }
- }
- }
- catch (const SocketException &e)
- {
- cerr << e.what() << endl;
- err_exit("TCPServer error");
- }
- }
附-getrlimit和setrlimit函數
每個進程都有一組資源限制,其中某一些可以用getrlimit和setrlimit函數查詢和更改。
- #include <sys/time.h>
- #include <sys/resource.h>
- int getrlimit(int resource, struct rlimit *rlim);
- int setrlimit(int resource, const struct rlimit *rlim);
- //rlimit結構體
- struct rlimit
- {
- rlim_t rlim_cur; /* Soft limit */
- rlim_t rlim_max; /* Hard limit (ceiling for rlim_cur) */
- };
軟限制是一個建議性的, 最好不要超越的限制, 如果超越的話, 系統可能向進程發送信號以終止其運行.
而硬限制一般是軟限制的上限;
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.只有超級用戶可以提高硬限制。