1、理解Epoll和對應接口
poll依然需要OS去遍歷所有fd。一個進程去多個特定的文件中等待,只要有一個就緒,就使用select/poll系統(tǒng)調(diào)用,讓操作系統(tǒng)把所有文件遍歷一遍,哪些就緒就加上哪些fd,再返回。一旦文件太多了,遍歷效率就顯而易見地低。epoll是為處理大批量句柄而作了改進的poll,句柄就是訪問某種資源時標識這個資源的東西,比如C語言中的FILE結構體,文件描述符等。不過select/poll并不是沒有用處,一些老型操作系統(tǒng)并不支持epoll,就得使用poll或者select。epoll是在Linux內(nèi)核2.5.44時引入的,到現(xiàn)在為止都是Linux中最高效的多路轉(zhuǎn)接IO方案。
epoll有3個接口。
size是一個被忽略的參數(shù),只要大于0就行。如果成功,返回一個epoll文件描述符,在系統(tǒng)內(nèi)部創(chuàng)建一些數(shù)據(jù)結構,幫助進行已就緒的fd的管理,暫且叫做epoll模型,失敗返回-1。不用這個epoll文件描述符后要close(epollfd)。
創(chuàng)建后,用戶要告訴內(nèi)核,應當關心哪個文件描述符上的哪個事件是否就緒,select通過一個位圖結構fd_set來實現(xiàn),poll通過poll_fd來實現(xiàn)的。另外,內(nèi)核要告訴用戶,關心的哪些fd上的哪些事件event已經(jīng)就緒了。epoll還有兩個接口去做這兩個事。
epfd就是創(chuàng)建函數(shù)的返回值;op表示想做什么,有3個值,EPOLL_ADD,EPOLL_MOD,EPOLL_DEL,分別是添加、修改、刪除;fd表示哪一個fd,event表示這個fd上的哪個事件要被關心。
進行等待的接口。返回值和select,poll接口一樣,就緒的fd數(shù)量;timeout的作用和poll一樣,輸入型參數(shù),單位是毫秒ms,為0表示非阻塞,小于0表示阻塞,大于0poll在這段時間內(nèi)阻塞等待,如果一直沒有事件就緒,那么超過時間就返回0;中間兩個參數(shù)是輸出型參數(shù),操作系統(tǒng)通過這兩個告知用戶就緒的fd上就緒的事件event。
events是一個32位整數(shù),用戶輸入的是關心的事件,返回時操作系統(tǒng)通過這個整數(shù)來告訴用戶哪些fd的events事件就緒了;data的類型是一個聯(lián)合體,通常會使用prt或者fd。events有幾種取值:
EPOLLIN:表示對應的文件描述符可以讀 (包括對端SOCKET正常關閉)
EPOLLOUT:表示對應的文件描述符可以寫
EPOLLPRI:表示對應的文件描述符有緊急的數(shù)據(jù)可讀 (這里應該表示有帶外數(shù)據(jù)到來)
EPOLLERR:表示對應的文件描述符發(fā)生錯誤
EPOLLHUP:表示對應的文件描述符被掛斷
EPOLLET:將EPOLL設為邊緣觸發(fā)(Edge Triggered)模式,這是相對于水平觸發(fā)(Level Triggered)來說的
EPOLLONESHOT:只監(jiān)聽一次事件,當監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要
再次把這個socket加入到EPOLL隊列里
上面的就是宏。這里只關心EPOLLIN和EPOLLOUT。
TCP報頭中6個標記位中有一個代表PSH,用來提示對方應用層立刻從接收緩沖區(qū)讀取數(shù)據(jù)。但PSH并不一定能讓應用層讀取數(shù)據(jù),它的催促是讓套接字觀察的fd對應的文件里的數(shù)據(jù)處于就緒狀態(tài)。
操作系統(tǒng)可以把數(shù)據(jù)從應用層拷貝到緩沖區(qū),然后將數(shù)據(jù)交給網(wǎng)卡。當網(wǎng)卡收到數(shù)據(jù)后,網(wǎng)卡會發(fā)送硬件中斷,操作系統(tǒng)通過查看中斷向量表,知道發(fā)來的中斷號是網(wǎng)卡的,所以就知道網(wǎng)卡有了數(shù)據(jù)。select/poll都是在軟件層面去檢測是否有數(shù)據(jù)的。
CPU有對應的寄存器,寄存器是二進制序列,是一種存儲單元,由硬件電路構成。數(shù)據(jù)拷貝到CPU的硬件本質(zhì)是利用高低電頻對CPU內(nèi)的寄存器進行充放電,讓CPU的寄存器變成和內(nèi)存一樣的值。CPU和所有外設之間都有針腳間接相連。發(fā)送中斷就像是某個外設產(chǎn)生電流,從和它間接相連的針腳向寄存器充電,把數(shù)據(jù)放到寄存器中。之后網(wǎng)卡就可以發(fā)送中斷號讓CPU拷貝數(shù)據(jù)到內(nèi)存了。所以數(shù)據(jù)是可以從外設拷貝到內(nèi)存的。
用戶層往下是系統(tǒng)調(diào)用層,再往下是操作系統(tǒng),再往下就是傳輸層及以下了。當用戶層創(chuàng)建epoll時,OS會維護一個紅黑樹,開始時只有一個根節(jié)點,并且epoll還會創(chuàng)建一個就緒隊列,為空。紅黑樹的節(jié)點是結構體,里面有fd,有事件event,整個紅黑樹就是用戶告訴OS,要關心哪些fd,以及fd上的哪些事件。所以可以看出epoll_ctl本質(zhì)是對這個紅黑樹進行增刪改,比如要刪,就傳對應的fd,事件設為nullptr/NULL,那就是對紅黑樹某個節(jié)點的刪除。fd決定節(jié)點是紅還是黑,左節(jié)點還是右節(jié)點,插入到哪里。內(nèi)核中,一個數(shù)據(jù)結構對象,既可以屬于紅黑樹,也可以屬于另一個結構。
紅黑樹上只有某個fd上有對應的事件發(fā)生了,那么就把這個fd的節(jié)點接入到就緒隊列中,隊列只保存已經(jīng)準備好的fd && 對應的event。隊列每一個元素也可以是一個結構體,只取紅黑樹中已就緒節(jié)點里面的值來填充。epoll_wait接口中間兩個參數(shù)就是從就緒隊列中拿取節(jié)點,這個接口只看就緒隊列,可以以時間復雜度為O(1)的方式來檢測事件就緒,也就是隊列是否為空。
節(jié)點放入隊列實際不是將一個節(jié)點內(nèi)容拷貝到隊列節(jié)點里,而是紅黑樹節(jié)點也是隊列節(jié)點,節(jié)點就是一個結構體,結構體里可以放入表示已經(jīng)就緒的事件,放入紅黑樹相關指針信息,放入隊列相關指針信息,建立起隊列就是用這個隊列相關的指針去指向下一個節(jié)點。
當數(shù)據(jù)就緒時,操作系統(tǒng)通過網(wǎng)卡,經(jīng)過網(wǎng)絡協(xié)議棧,拷貝到每個文件的文件緩沖區(qū)中。每個節(jié)點都有回調(diào)機制,假設每個文件結構體都有一個變量,如果沒設置回調(diào),就置為空,每次操作系統(tǒng)拷貝數(shù)據(jù)到緩沖區(qū)后就去判斷一下這個變量,為空就退出,不為空就調(diào)用回調(diào)函數(shù),回調(diào)函數(shù)做的工作就是把紅黑樹上已就緒的節(jié)點放到就緒隊列中。
紅黑樹,就緒隊列,回調(diào)機制這三個整體就是epoll模型,所以epoll_create使用時就是創(chuàng)建了這些,從操作系統(tǒng)內(nèi)部到系統(tǒng)調(diào)用形成了一個體系。紅黑樹就像select/poll中的數(shù)組,但epoll這里核心的維護交由系統(tǒng)來做,不讓用戶去做。
為什么epoll_create要返回就緒fd的個數(shù),以及另外兩個接口還需要用這個數(shù)字?整個機制是由系統(tǒng)做的,接口是由進程調(diào)用的,進程在運行時,會創(chuàng)建task_struct指向文件描述符表files_struct,表里有一個數(shù)組,類型是struct file,012默認被占用,當創(chuàng)建epoll模型,操作系統(tǒng)也創(chuàng)建了一個struct file,里面有個指針指向epoll模型,這個struct file就在調(diào)用epoll接口的進程的文件描述符表中。用戶,進程,task_struct,files_struct,struct file,這是一整個路線。通過epoll_create的返回值,也就是另外兩個接口的參數(shù)epfd,兩個接口就可以找到進程維護的文件描述符表,進而找到struct file,然后找到epoll模型,就可以對紅黑樹,就緒隊列進行操作了。
epoll的紅黑樹比數(shù)組更有效率;也不需要底層在線性遍歷所有節(jié)點;上層也不需要遍歷節(jié)點只需要查看就緒隊列;用戶只需要調(diào)用接口就可以操作整個體系。
2、簡單實現(xiàn)
Main.cc
#include "EpollServer.hpp"
#include <memory>
int main()
{
std::unique_ptr<EpollServer> svr(new EpollServer());
svr->InitServer();
svr->Start();
return 0;
}
Makefile
epollserver:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f epollserver
EpollServer.hpp中先寫基礎的
#pragma once
#include <iostream>
#include <string>
#include "Sock.hpp"
#include "log.hpp"
const static int gport = 8888;
class EpollServer
{
public:
EpollServer(uint16_t port = gport) : port_(port)
{}
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
}
void Start()
{
while(true)
{
sleep(3);
}
}
~EpollServer()
{}
private:
uint16_t port_;
Sock listensock_;
};
現(xiàn)在還不能Accept,因為還不知道底層是否有文件就緒,如果沒有,整個服務器就得阻塞了。epoll這里的思路就是把自己的權利交給epoll。要將listensock添加到epoll中,不過得先有epoll模型。
創(chuàng)建一個Epoll.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/epoll.h>
static const int defaultepfd = -1;
class Epoller
{
public:
Epoller():epfd_(defaultepfd)
{}
~Epoller()
{}
private:
int epfd_;
};
完善一下Epoll模型,并初始化和析構
Epoll.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/epoll.h>
#include "err.hpp"
#include "log.hpp"
static const int defaultepfd = -1;
static const int gsize = 128;
class Epoller
{
public:
Epoller():epfd_(defaultepfd)
{}
void Create()
{
epfd_ = epoll_create(gsize);
if(epfd_ < 0)
{
logMessage(Fatal, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));
exit(EPOLL_CREAT_ERR);//err.hpp里加上這個錯誤
}
}
int Fd()
{
return epfd_;
}
void Close()
{
if(epfd_ != defaultepfd) close(epfd_);
}
~Epoller()
{}
private:
int epfd_;
};
EpollServer.hpp
#pragma once
#include "Epoll.hpp"
#include "Sock.hpp"
#include "log.hpp"
const static int gport = 8888;
class EpollServer
{
public:
EpollServer(uint16_t port = gport) : port_(port)
{}
void InitServer()
{
listensock_.Socket();
listensock_.Bind(port_);
listensock_.Listen();
epoller_.Create();
logMessage(Debug, "init server success");
}
void Start()
{
//1、將listensock添加到epoll中,要先有epoll模型
while(true)
{
sleep(3);
}
}
~EpollServer()
{
listensock_.Close();
epoller_.Close();
}
private:
uint16_t port_;
Sock listensock_;
Epoller epoller_;
};
接下來關注事件。
Epoll.hpp
//用戶告訴內(nèi)核要關心哪些事件
bool AddEvent(int fd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd;//fd就是就緒的文件描述符
int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
if(n < 0)
{
logMessage(Fatal, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
return false;
}
return true;
}
EpollServer.hpp
void Start()
{
//1、將listensock添加到epoll中,要先有epoll模型
bool r = epoller_.AddEvent(listensock_.Fd(), EPOLLIN);//只關心讀事件
assert(r);//可以做別的判斷
(void)r;
while(true)
{
;
}
}
然后就可以在循環(huán)中獲取事件了,使用wait。從隊列里拿數(shù)據(jù)這個過程是線性拷貝的,因為系統(tǒng)不相信用戶,所以要定義一個struct epoll_event類型的數(shù)組來接收。以及wait接口中的events參數(shù)里,由于拷貝的緣故,數(shù)據(jù)是從左到右連續(xù)有效的,而返回值 - 1就是當前最后一個有效的下標。
EpollServer.hpp
void Start()
{
//1、將listensock添加到epoll中,要先有epoll模型
bool r = epoller_.AddEvent(listensock_.Fd(), EPOLLIN);//只關心讀事件
assert(r);//可以做別的判斷
(void)r;
struct epoll_event revs_[gnum];
int timeout = 1000;
while(true)
{
int n = epoller_.Wait(revs_, gnum, timeout);
switch (n)
{
case 0:
logMessage(Debug, "timeout...");
break;
case -1:
logMessage(Warning, "epoll_wait failed");
break;
default:
logMessage(Debug, "有%d個事件就緒了", n);
HandlerEvents(n);//一定有數(shù)據(jù)就緒
break;
}
}
}
void HandlerEvents(int num)
{
for(int i = 0; i < num; i++)
{
int fd = revs_[i].data.fd;
uint32_t events = revs_[i].events;
logMessage(Debug, "當前正在處理%d上的%s", fd, (events&EPOLLIN) ? "EPOLLIN" : "OTHER");
if(events & EPOLLIN)//判斷讀事件就緒
{
if (fd == listensock_.Fd())
{
// 1、新連接到來
std::string clientip;
uint16_t clientport;
int sock = listensock_.Accept(&clientip, &clientport);
if (sock < 0)
continue;
logMessage(Debug, "%s:%d 已經(jīng)連上服務器了", clientip.c_str(), clientport);
// 還不能recv,即使有了連接但也不知道有沒有數(shù)據(jù)
// 只有epoll知道具體情況,所以將sock添加到epoll中
bool r = epoller_.AddEvent(sock, EPOLLIN);
assert(r);
(void)r;
}
else // 2、讀事件
{
char buffer[1024];
ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s - 1] = 0;//對打印格式
buffer[s - 2] = 0;//做一下調(diào)整
std::string echo = buffer;
echo += " [epoll server echo]\r\n";
std::cout << "client# " << echo << std::endl;
send(fd, echo.c_str(), echo.size(), 0);
}
else
{
if (s == 0)
logMessage(Info, "client quit ...");
else
logMessage(Warning, "recv error, client quit...");
close(fd);
//將文件描述符移除
//在處理異常的時候,fd必須合法才能被處理
epoller_.DelEvent(fd);
}
}
}
}
}
Epoll.hpp
//用戶告訴內(nèi)核要關心哪些事件
bool AddEvent(int fd, uint32_t events)
{
struct epoll_event ev;
ev.events = events;
ev.data.fd = fd;//屬于用戶的數(shù)據(jù),epoll底層不對該數(shù)據(jù)做任何修改,為了給未來就緒返回
int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
if(n < 0)
{
logMessage(Fatal, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
return false;
}
return true;
}
bool DelEvent(int fd)
{
return epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
}
int Wait(struct epoll_event* revs, int num, int timeout)
{
return epoll_wait(epfd_, revs, num, timeout);
}
讀事件處理中,我們目前無法讀到一個完整的報文。因為完整報文由應用層協(xié)議規(guī)定,我們的代碼沒有應用層協(xié)議,所以得自定義一個。
先用回調(diào)函數(shù)來處理數(shù)據(jù)
#include <functional>
using func_t = std::function<std::string (std::string)>;
public:
EpollServer(func_t func, uint16_t port = gport) : func_(func), port_(port)
{}
private:
uint16_t port_;
Sock listensock_;
Epoller epoller_;
struct epoll_event revs_[gnum];
func_t func_;
讀事件處理時
else // 2、讀事件
{
char request[1024];
ssize_t s = recv(fd, request, sizeof(request) - 1, 0);
if (s > 0)
{
request[s - 1] = 0;//對打印格式
request[s - 2] = 0;//做一下調(diào)整
std::string response = func_(request);
send(fd, response.c_str(), response.size(), 0);
}
else
{
if (s == 0)
logMessage(Info, "client quit ...");
else
logMessage(Warning, "recv error, client quit...");
close(fd);
//將文件描述符移除
//在處理異常的時候,fd必須合法才能被處理
epoller_.DelEvent(fd);
}
}
在Main.cc中傳入函數(shù)
#include "EpollServer.hpp"
#include <memory>
std::string echoServer(std::string r)
{
std::string resp = r;
resp += "[echo]\r\n";
return resp;
}
int main()
{
std::unique_ptr<EpollServer> svr(new EpollServer(echoServer));
svr->InitServer();
svr->Start();
return 0;
}
下一篇仍然是Epoll代碼。
基本版Epoll文章來源:http://www.zghlxwxcb.cn/news/detail-802823.html
結束。文章來源地址http://www.zghlxwxcb.cn/news/detail-802823.html
到了這里,關于Linux學習記錄——??? 高級IO(4)--- Epoll型服務器(1)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!