IO多路轉接模型:select/poll/epoll
對大量描述符進行事件監控(可讀/可寫/異常)
select模型
- 用戶定義描述符的事件監控集合 fd_set(這是一個位圖,用于存儲要監控的描述符); 用戶將需要監控的描述符添加到集合中,這個描述符集合的大小取決于一個宏 _FD_SETSIZE = 1024
- 將集合拷貝到內核中進行監控;在內核中對所有描述符進行輪詢遍歷判斷是否有關心的事件就緒
- 若有描述符就緒,從監控集合中將未就緒的描述符移除;然后調用返回(返回給用戶就緒描述符饑集合)
- 用戶遍歷所有描述符,判斷描述符是否在集合中,若在集合中,則這個描述符是就緒描述符
- 用戶針對這個就緒的描述符事件進行相應的處理,用戶僅僅對大量描述符中就緒的描述符進行處理,sock程序就可以避免accept/recv處因為沒有數據到來而阻塞
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h> int select(int nfds, fd_set *readfds,fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds: 監控的文件描述符集里最大文件描述符加1,因為此參數會告訴內核檢測前多少個文件描述符的狀態
- readfds: 監控有讀數據到達文件描述符集合,傳入傳出參數
- writefds: 監控寫數據到達文件描述符集合,傳入傳出參數
- exceptfds: 監控異常發生達文件描述符集合,如帶外數據到達異常,傳入傳出參數
- timeout: 定時阻塞監控時間,3種情況
- NULL,永遠等下去
- 設置timeval,等待固定時間
- 設置timeval里時間均為0,檢查描述字后立即返回,輪詢
struct timeval
{
long tv_sec; /* seconds */ //秒
long tv_usec; /* microseconds */ //微秒
};
void FD_CLR(int fd, fd_set *set);
//把文件描述符集合里fd清0 將指定的描述符從集合中移除int FD_ISSET(int fd, fd_set *set);
//測試文件描述符集合里fd是否置1 判斷指定的描述符是否在集合中void FD_SET(int fd, fd_set *set);
//把文件描述符集合里fd位置1 將指定的描述符添加到集合中void FD_ZERO(fd_set *set);
//把文件描述符集合里所有位清0 清空描述符集合
select優缺點
- select 能監聽的文件描述符個數受限于 FD_SETSIZE,一般為 1024,單純改變進程打開的文件描述符個數并不 能改變 select 監聽文件個數
- 每次都需要重新將監控集合拷貝到內核(select會修改集合)
- 解決 1024 以下客戶端時使用 select 是很合適的,但如果鏈接客戶端過多,select 采用的是輪詢模型,會大大降低服務器響應效率
- select返回給用戶就緒的描述符集合(將未就緒的描述符從集合中移除),但是并沒有告訴用戶具體哪一個描述符就緒,需要用戶遍歷描述符是否在集合中來判斷哪個描述符就緒,這個判斷是一個遍歷的過程,性能隨著描述符增多而下降,并且復雜度更高
- select每次返回都會修改監控集合,因此每次都需要用戶重新向集合中添加所有描述符
- select遵循posix標準,支持跨平臺;
- 監控的超時等待時間可以精細到微秒
class Select
{
public:Add(TcpSocket &sock); //將用戶關心socket描述符添加到監控集合中Del(TcpSocket &sock); //從監控集合中移除不再關心的socket描述符Wait(std::vector<TcpSocket>&list,init timeout_sec,int timeout_sec); //從開始監控,并且向用戶返回就緒的socket
private:fd_set _rfds;int_max_fd;
};
實現
select服務端
/** 這個文件封裝一個select類,向外界提供更加簡單點的select監控接口* 將用戶關心socket描述符添加到監控集合中* 從監控集合中移除不再關心的socket描述符* 從開始監控,并且向用戶返回就緒的socket*/#include<vector>
#include<sys/select.h>
#include"tcpsocket.hpp"class Select
{public:Select(): _max_fd (-1){FD_ZERO(&_rfds);//清空集合} bool Add(TcpSocket &sock){int fd = sock.GetFd();//void FD_SET(int fd,fd_set *set)//向set描述符集合中添加fd描述符FD_SET(fd,&_rfds);_max_fd = _max_fd > fd ? _max_fd : fd; return true;} bool Del(TcpSocket &sock){int fd = sock.GetFd();//void FD_CLR(int fd, fd_set *set)//從set描述符集合中移除FD_CLR(fd,&_rfds);//從最大的往前遍歷for(int i = _max_fd ; i >= 0; i--){//int FD_ISSET(int fd, fd_set *set);//判斷fd描述符是否還在set集合中if(FD_ISSET(i,&_rfds)){_max_fd = i;break;}}} bool Wait(std::vector<TcpSocket>&list,int timeout_sec = 3){struct timeval tv; //超時時間tv.tv_sec = timeout_sec;tv.tv_usec = 0;fd_set set = _rfds;int ret =select(_max_fd + 1, &set, NULL ,NULL,&tv);if(ret < 0){perror("select error");return false;}else if(ret == 0){std::cout<< "select wait timeout\n";return false;}for(int i =0 ;i <= _max_fd; i++){if(FD_ISSET(i,&set)){TcpSocket sock;sock.SetFd(i);list.push_back(sock);}}return true;} private:fd_set _rfds;int _max_fd;
};int main()
{TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind("192.168.145.132",9000));CHECK_RET(sock.Listen());Select s;s.Add(sock); while(1){std::vector<TcpSocket>list;if(s.Wait(list)==false){continue;}for(int i =0 ; i < list.size(); i++){//判讀socket是監聽socket還是通信socketif(list[i].GetFd() == sock.GetFd()){TcpSocket clisock;std::string cli_ip;uint16_t cli_port;if(sock.Accept(clisock,cli_ip,cli_port) == false){continue;}s.Add(clisock);}else{std::string buf;if(list[i].Recv(buf) == false){s.Del(list[i]);list[i].Close();continue;}std::cout<<"client say:"<<buf <"\n";}}}sock.Close();return 0;
}
客戶端
#include <signal.h>
#include "tcpsocket.hpp"void sigcb(int signo){printf("connection closed\n");
}
int main(int argc, char *argv[])
{if (argc != 3) {std::cout<<"./tcp_cli 192.168.122.132 9000\n";return -1; } std::string ip = argv[1];uint16_t port = atoi(argv[2]);signal(SIGPIPE, sigcb);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Connect(ip, port));while(1) {std::string buf;std::cout <<"client say:";fflush(stdout);std::cin >>buf;sock.Send(buf);}sock.Close();return 0;
}
poll模型
poll函數接口
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd結構
struct pollfd { int fd; /* 用戶監控的描述符 */ short events; /* 描述符關心的事件 POLLIN/POLLOUT */ short revents; /* 描述符實際就緒的事件 */ }
參數說明
- fds是一個poll函數監聽的結構列表. 每一個元素中, 包含了三部分內容: 文件描述符, 監聽的事件集合, 返回的事件集合. 描述事件結構數組
- nfds表示fds數組的長度. 要監控事件個數
- timeout表示poll函數的超時時間, 單位是毫秒(ms).
events和revents的取值:
- POLLIN 普通或帶外優先數據可讀,即POLLRDNORM | POLLRDBAND
- POLLRDNORM 數據可讀
- POLLRDBAND 優先級帶數據可讀
- POLLPRI 高優先級可讀數據
- POLLOUT 普通或帶外數據可寫
- POLLWRNORM 數據可寫
- POLLWRBAND 優先級帶數據可寫
- POLLERR 發生錯誤
- POLLHUP 發生掛起
- POLLNVAL 描述字不是一個打開的文件
實現原理
- 用戶定義描述符事件數組,向數組中添加關心的描述符事件
- 將pollfd事件數組,拷貝到內核中進行遍歷輪詢監控,判斷是否就緒了關心的事件
- 將描述符實際就緒的事件信息,標記到revents中
- 當poll返回。用戶遍歷pollfd事件數組,通過revents判斷描述符就緒了什么事件,進而進行相應操作
使用poll監控標準輸入
#include <poll.h>
#include <unistd.h>
#include <stdio.h>int main() { struct pollfd poll_fd; //一個結構就是一個事件poll_fd.fd = 0; poll_fd.events = POLLIN; //可讀事件for (;;) { //開始監控int ret = poll(&poll_fd, 1, 1000); //遍歷輪詢 if (ret < 0) { //出錯perror("poll"); continue; }if (ret == 0) { //超時printf("poll timeout\n"); continue; } //ret>0if (poll_fd.revents == POLLIN) { //看事件是否為我們所關心的事件 //對事件進行操作 char buf[1024] = {0}; read(0, buf, sizeof(buf) - 1); printf("stdin:%s", buf); } }
}
poll有缺點分析
優點:
- poll采用事件結構形式對描述符關心的事件進行監控,簡化了select三種集合操作的流程
- poll沒有描述符上限的設置
缺點
- 不能跨平臺,只能用于Linux下
- 在內核中進行輪詢遍歷判斷就緒,性能隨著描述符事件增多而下降
- 也不會告訴用戶具體哪一個描述符就緒,需要用戶輪詢遍歷判斷事件中的revents;進而對描述符進行相應事件操作
revents & POLLIN/POLLOUT
- 需要每次都向內核中拷貝監控信息