I/O多路復用(IO multiplexing)
? I/O多路復用是通過一種機制,可以監視多個檔案描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒,還有例外就緒),能夠通知程式進行相應的讀寫操作,比較常用的有select/epoll,有些地方也稱這種IO方式為事件驅動 IO(event driven IO),
select
原理:客戶端操作服務器時就會產生這三種檔案描述符(簡稱fd):writefds(寫)、readfds(讀)、和exceptfds(例外),select會阻塞住監視3類檔案描述符,等有資料、可讀、可寫、出例外 或超時、就會回傳;回傳后通過遍歷fdset整個陣列來找到就緒的描述符fd,然后進行對應的IO操作,
優點:幾乎在所有的平臺上支持,跨平臺支持性好
缺點:
- 由于是采用輪詢方式全盤掃描,會隨著檔案描述符FD數量增多而性能下降,
- 每次呼叫 select(),需要把 fd 集合從用戶態拷貝到內核態,并進行遍歷(訊息傳遞都是從內核到用戶空間)
- 默認單個行程打開的FD有限制是1024個,可修改宏定義,但是效率仍然慢,
select 介面的原型:
FD_ZERO(int fd, fd_set* fds)//將指定的檔案描述符集清空,在對檔案描述符集合進行設定前,必須對其進行初始化,如果不清空,由于在系統分配記憶體空間后,通常并不作清空處理,所以結果是不可知的,即清空set中所有的位,全置為0
FD_SET(int fd, fd_set* fds)//用于在檔案描述符集合中增加一個新的檔案描述符,即設定set中相關fd的位,將其置1
FD_ISSET(int fd, fd_set* fds)//用于判斷指定的檔案描述符是否在該集合中,即判斷set中相關fd是否存在,該位是否被置1
FD_CLR(int fd, fd_set* fds)//用于在檔案描述符集合中洗掉一個檔案描述符,即清除set中相關fd的位,將其置為0
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set*exceptfds,struct timeval *timeout)
1. maxfd:是需要監視的最大的檔案描述符值+1
2. readfds:需要檢測的可讀檔案描述符的集合
3. writefds:需要檢測的可寫檔案描述符的集合
4. exceptfds:需要檢測的例外檔案描述符的集合
5. timeout:指向timeval結構體的指標,通過傳入的這個timeout引數來決定select()函式的三種執行方式,
- 傳入的timeout為NULL,則表示將select()函式置為阻塞狀態,直到我們所監視的檔案描述符集合中某個檔案描述符發生變化是,才會回傳結果,
- 傳入的timeout為0秒0毫秒,則表示將select()函式置為非阻塞狀態,不管檔案描述符是否發生變化均立刻回傳繼續執行,
- 傳入的timeout為一個大于0的值,則表示這個值為select()函式的超時時間,在timeout時間內一直阻塞,超過時間即回傳結果 ,
簡單使用
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLNE 4096
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
fd_set rfds, rset, wfds, wset;
FD_ZERO(&rfds);
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int max_fd = listenfd;
while (1) {
rset = rfds;
wset = wfds;
int nready = select(max_fd+1, &rset, &wset, NULL, NULL);
if (FD_ISSET(listenfd, &rset)) { //
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rfds);
if (connfd > max_fd) max_fd = connfd;
if (--nready == 0) continue;
}
int i = 0;
for (i = listenfd+1;i <= max_fd;i ++) {
if (FD_ISSET(i, &rset)) { //
n = recv(i, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
FD_SET(i, &wfds);
//reactor
//send(i, buff, n, 0);
} else if (n == 0) { //
FD_CLR(i, &rfds);
//printf("disconnect\n");
close(i);
}
if (--nready == 0) break;
} else if (FD_ISSET(i, &wset)) {
send(i, buff, n, 0);
FD_SET(i, &rfds);
}
}
}
close(listenfd);
return 0;
}
epoll
原理:epoll使用一個檔案描述符管理多個描述符,將用戶關系的檔案描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次,
epoll之所以高性能是得益于它的三個函式:
- epoll_create()系統啟動時,在Linux內核里面申請一個B+樹結構檔案系統,回傳epoll物件,也是一個fd
- epoll_ctl() 每新建一個連接,都通過該函式操作epoll物件,在這個物件里面修改添加洗掉對應的鏈接fd, 系結一個callback函式
- epoll_wait() 輪訓所有的callback集合,并完成對應的IO操作
優點:
- 沒fd這個限制,所支持的FD上限是作業系統的最大檔案句柄數,1G記憶體大概支持10萬個句柄
- 效率提高,使用回呼通知而不是輪詢的方式,不會隨著FD數目的增加效率下降
- 內核和用戶空間mmap同一塊記憶體實作(mmap是一種記憶體映射檔案的方法,即將一個檔案或者其它物件映射到行程的地址空間)
epoll介面
epoll操作程序需要三個介面,分別如下:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) int epoll_create(int size);
函式是一個系統函式,函式將在內核空間內開辟一塊新的空間,可以理解為epoll結構空間,回傳值為epoll的檔案描述符編號,方便后續操作使用
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
? poll的事件注冊函式,epoll與select不同,select函式是呼叫時指定需要監聽的描述符和事件,epoll先將用戶感興趣的描述符事件注冊到epoll空間內,此函式是非阻塞函式,作用僅僅是增刪改epoll空間內的描述符資訊,第一個引數是epoll_create()的回傳值,第二個引數表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中洗掉一個fd;
第三個引數是需要監聽的fd(一般指socket_fd),第四個引數是告訴內核需要監聽什么事,struct epoll_event結構如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events可以是以下幾個宏的集合:
EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這里應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被掛斷;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的,
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列里
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生,類似于select()呼叫,引數epfd是epoll的檔案描述符,events用來從內核得到事件的集合,maxevents告之內核這個events有多大,這個maxevents的值不能大于創建epoll_create()時的size,引數timeout是超時時間(毫秒,0非阻塞會立即回傳,-1將不確定,也有說法說是永久阻塞),該函式回傳需要處理的事件數目,如回傳0表示已超時,小于0表示出錯
作業模式
epoll對檔案描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger),LT模式是默認模式,LT模式與ET模式的區別如下:
LT(水平觸發)模式:當epoll_wait檢測到描述符事件發生并將此事件通知應用程式,應用程式可以不立即處理該事件,下次呼叫epoll_wait時,會再次回應應用程式并通知此事件,
ET(邊緣觸發)模式:當epoll_wait檢測到描述符事件發生并將此事件通知應用程式,應用程式必須立即處理該事件,如果不處理,下次呼叫epoll_wait時,不會再次回應應用程式并通知此事件,
ET模式在很大程度上減少了epoll事件被重復觸發的次數,因此效率要比LT模式高,epoll作業在ET模式的時候,必須使用非阻塞套介面,以避免由于一個檔案句柄的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死,
編碼流程
- 創建epoll描述符
- 注冊epoll事件
- 等待epoll事件
- 判斷觸發epoll事件的描述符和事件
- 關閉epoll描述符
簡單使用
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/epoll.h>
#define MAXLNE 4096
#define POLL_SIZE 1024
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
int epfd = epoll_create(1); //int size
struct epoll_event events[POLL_SIZE] = {0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nready = epoll_wait(epfd, events, POLL_SIZE, 5);
if (nready == -1) {
continue;
}
int i = 0;
for (i = 0;i < nready;i ++) {
int clientfd = events[i].data.fd;
if (clientfd == listenfd) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if (events[i].events & EPOLLIN) {
n = recv(clientfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(clientfd, buff, n, 0);
} else if (n == 0) { //
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
close(listenfd);
return 0;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/445857.html
標籤:C++
上一篇:vs不同版本支持的c++版本和PlatformToolset,及在vs中切換c++版本
下一篇:java高級-續1
