目錄
select函數原型
select服務器
select的缺點
前面介紹過多路轉接就是能同時等待多個文件描述符,這篇文章介紹一下多路轉接方案中的select的使用
select函數原型
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
先介紹一下各個參數以及返回值
多路轉接需要等待多個文件描述符的事件就緒,所以用戶勢必需要告訴操作系統,他關心的是哪些文件描述符,以及關心這些文件描述符上的讀事件還是寫事件。讀事件就緒就是這個文件描述符的緩沖區不為空有數據能讀,寫事件就緒就是緩沖區不為滿可以寫入。除了這兩種常見的事件外,還可以關心某個文件描述符的異常事件。
再來看select的參數,nfds是一個整數,可以告訴操作系統需要關心哪些文件描述符,具體來說,nfds是需要關心的文件描述符的最大值 + 1,可以預想到select函數會遍歷小于等于nfds - 1的文件描述符,查看是否有事件就緒
struct timeval {time_t tv_sec; /* Seconds */ //秒suseconds_t tv_usec; /* Microseconds */ //毫秒
};
timeout表示select的等待時間,timeout也可為空,表示阻塞等待直到某個文件描述符發生事件,timeout為0表示不等待事件發生,其他自定義值表示若在這段時間內沒有事件發生,則超時返回。
返回值為0表示超時返回;為-1表示有錯誤發生,并設置錯誤碼errno;為正數表示在timeout時間內事件就緒的文件描述符個數
為了介紹剩下的三個參數,先介紹一下fd_set
我們已經通過fds告訴操作系統要關心哪些文件描述符,timeout設置了等待時間,現在還需要告訴操作系統要關心哪些文件描述符的讀事件或寫事件
從抽象的層面上理解,fd_set是一個集合,是一個文件描述符的集合,readfds是關心讀事件的文件描述符集合,writefds是關心寫事件的文件描述符集合,exceptfds是關心異常事件的文件描述符集合。
還需要指出,這三個參數還是輸出型參數,操作系統會將等待后事件就緒的文件描述符加入集合,
比如關心4,5,6的讀事件,若就緒了4和5,集合就會變成4,5,這也為寫代碼帶來了麻煩
從具體實現上來看,fd_set是一個位圖,有若干個比特位表示文件描述符,值為1表示關心這個文件描述符,為0表示不關心,舉個例子
00011111001
下標從0開始的話,這個位圖表示關心3,4,5,6,7,10號文件描述符,其余的都不關心
/* fd_set for select and pselect. */
typedef struct{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */
#ifdef __USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif} fd_set;
fd_set封裝了一個大小固定的數組,數組的每個比特位都可以記錄是否關心這個文件描述符
作為用戶,想對fd_set操作,操作系統也提供了相關的接口
// 將文件描述符fd從集合set中刪除
void FD_CLR(int fd, fd_set *set);
// 判斷文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set);
// 將fd放入集合set中
void FD_SET(int fd, fd_set *set);
// 清空集合set
void FD_ZERO(fd_set *set);
select服務器
到這里,select已經可以等待多個文件描述符的一些事件了,可以來搭一個簡單的服務器,接收多個用戶的消息,回顯在屏幕上
這里只給出select_server的代碼,其他文件的代碼對理解select不重要,只需要了解套接字的使用便可輕松看懂,若要查看其他文件的代碼,詳見rokobo/wsl_code - Gitee.com
在這份代碼中,需要等待的事件有等待客戶端的連接和等待客戶端發消息,由于連接建立后會創建文件描述符,文件描述符會變多,需要一個數據結構把這些文件描述符管理起來,這里選擇了原生數組,因為可以直觀的感受到select的缺點之一,存在大量遍歷,性能不夠高。
#include "socket.hpp"
#include "Log.hpp"
#include <sys/select.h>
#include <memory>
#include <cstring>
#include <cerrno>
using namespace SocketModule;
using namespace LogModule;class select_server
{
// sizeof可以得到底層數組的字節數,乘8得到比特數
static const int NUM = sizeof(fd_set) * 8;
public:select_server():_listen_sock(std::make_shared<TcpSocket>()),_is_running(false){}void init(int port){_listen_sock->BuildTcpSocketMethod(port);for(int i=0;i<NUM;++i){fds[i] = -1;}//初始只需要關心_listen_sock這一個文件描述符fds[0] = _listen_sock->Fd();}void loop(){_is_running = true;int listenfd = _listen_sock->Fd();fd_set readset;while(_is_running){//readset作為輸出參數,select返回后可能被修改,需要清空后重新設置FD_ZERO(&readset);int max_fd = 0;for(int i=0;i<NUM;++i){if(fds[i] != -1){max_fd = fds[i] > max_fd ? fds[i] : max_fd;FD_SET(fds[i], &readset);}}struct timeval timeout = {2, 0};int ret = select(max_fd + 1, &readset, nullptr, nullptr, &timeout);if(ret == -1){LOG(LogLevel::ERROR) << "Error message: " << strerror(ret);continue;}else if(ret == 0){LOG(LogLevel::INFO) << "Time out\n";continue;}else{LOG(LogLevel::INFO) << "Dispatch begin\n";// 給不同種類的文件描述符分發不同的任務dispatcher(readset);}} }void accepter(int fd){InetAddr client;auto client_sock = _listen_sock->Accepter(&client);if(client_sock == nullptr){LOG(LogLevel::ERROR) << "Accept error";return;}int client_fd = client_sock->Fd();if(client_fd < 0){LOG(LogLevel::ERROR) << "Client fd error";return;}//將client_fd加入到fds中//如果fds滿了,關閉連接int i=0;for(i=0;i<NUM;++i){if(fds[i] == -1){fds[i] = client_fd;LOG(LogLevel::INFO) << "Accept success: " << client_sock->Fd() << " " << client.Addr();break;}}if(i == NUM){LOG(LogLevel::ERROR) << "Too many connections";client_sock->Close();return;}}void recver(int who){int fd = fds[who];std::string buffer;auto client_sock = std::make_shared<TcpSocket>(fd);ssize_t ret = client_sock->Recv(&buffer);if(ret == -1){LOG(LogLevel::ERROR) << "Recv error" << strerror(errno);client_sock->Close();//將fd從fds中刪除fds[who] = -1;return;}else if(ret == 0){LOG(LogLevel::INFO) << "Client closed: " << client_sock->Fd();client_sock->Close();//將fd從fds中刪除fds[who] = -1;return;}else{LOG(LogLevel::INFO) << "Recv success: " << buffer;return;}}void dispatcher(fd_set &readset){//找到所有合法的fd,分發for(int i=0;i<NUM;++i){if(fds[i] == -1)continue;if(FD_ISSET(fds[i], &readset)){//分發給處理連接的函數if(fds[i] == _listen_sock->Fd()){accepter(fds[i]);}//分發給處理IO的函數else{recver(i);}}}}void stop(){}
private:std::shared_ptr<TcpSocket> _listen_sock;int fds[NUM];bool _is_running;
};
主函數
#include "select_server.hpp"
#include <string>
int main()
{select_server s_svr;s_svr.init(8080);s_svr.loop();return 0;
}
?
select的缺點
從代碼中大量的遍歷,甚至select底層還要遍歷,可以感受到select有太多遍歷,效率不高,而且fd_set的底層數組是靜態的無法擴容,能同時關心的文件描述符有限,而且需要用戶自己去定義數據結構管理需要關心的文件描述符,更是增加了編碼的復雜性,每次調用select,都需要把fd_set從用戶態拷貝到內核態,這個拷貝的開銷在fd很多時開銷很大