高級IO部分

如圖:
在這裏插入圖片描述

五種IO模型

我們先講一個例子:我們去食堂喫飯,點餐後進行等待,現在有5種情況:

(1)A同學點餐之後一動不動就在窗口等着叫號,即阻塞IO;
(2)B同學點餐之後就開始玩手機,時不時看一下餐好了沒有即非阻塞IO(輪詢);
(3)C同學點餐之後告訴自己旁邊的同學餐好了叫一下他,然後開始玩手機,即信號驅動IO;
(4)D同學發現有好幾個窗口都可以排到這份餐於是他在這些窗口都排了號,等待任意一個窗口即可,即IO多路轉接;
(5)E同學則是讓自己的同學去幫他點餐,在拿到餐之後通知他來喫即可,即異步IO;

這5種情況相當於5種IO模型,同樣的還有釣魚的例子;
IO操作的流程=等待IP條件具備+數據拷貝
“等”的意思是等條件就緒,例如input等輸入條件就緒,output是等輸出條件就緒;
那麼高效IO=減少等的比重

  • 阻塞IO
    在內核將數據準備好之前,系統調用會一直等待,所有的套接字默認都是阻塞方式,直到條件具備,完成IO操作後調用返回(A同學的情況);如圖:
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 非阻塞IO
    爲了完成IO操作發起調用,若當前不具備IO操作條件,則立即報錯返回,可以乾點其他事情,循環過來進行判斷(B同學的例子);
    非阻塞IO往往需要程序員循環的方式反覆嘗試讀寫文件描述符, 這個過程稱爲輪詢. 這對CPU來說是較大的浪費, 一般只有特定場景下才使用.
    如圖:
    在這裏插入圖片描述
    在這裏插入圖片描述
  • 信號驅動IO
    提前對IO信號自定義處理方式,當IO條件具備時,操作系統通過信號通知進程,這時候IO條件已經具備,直接發起調用進行數據拷貝(C同學的例子);如圖:
    在這裏插入圖片描述
    在這裏插入圖片描述
  • IO多路複用/多路轉接
    雖然從流程圖上看起來和阻塞IO類似. 實際上最核心在於IO多路轉接能夠同時等待多個文件描述符select負責等,並且一次等多個文件描述符)的就緒狀態(D同學的例子);如圖:
    在這裏插入圖片描述
  • 異步IO
    IO操作條件的等待與拷貝都由操作系統來進行等待與操作,等到IO操作完成之後,通過信號通知進程,進程直接對數據進行操作(E同學的例子);如圖:
    在這裏插入圖片描述
    IO的幾種操作中,IO操作效率越來越高,但是流程和控制卻越來越複雜;

I/O多路轉接/多路複用

多路轉接/多路複用IO是對大量的文件描述符進行就緒事件監控(需要對描述符進行監控的場景都可以使用多路轉接模型),就緒事件就是可讀/可寫/異常事件;
監控的好處:讓進程可以只針對就緒了指定事件的描述符進行操作,提高效率性能,避免了因爲對沒有就緒的描述符操作導致的阻塞;

  • select
    select系統調用是用來讓程序監視多個文件描述符的狀態變化的,程序會停在select這裏等待,直到被監視的文件描述符有一個或多個發生了狀態改變;
    select一般用於socket網絡編程中,在網絡編程中,經常會遇到很多阻塞的函數,例如recv、recvfrom、connect,當函數不能成功執行時,程序會一直阻塞在這裏,無法執行下面的代碼,select就可以實現非阻塞編程;
    他是一個輪詢函數,即當循環詢問文件節點,可設置超時時間,超時時間到了就跳過代碼繼續執行;
    select的實現:
    1、進程定義指定事件的描述符集合,初始化清空集合,將需要監控的描述符根據需要監控的事件添加到指定的集合中;
    2、發起調用,將事件描述集合從用戶態拷貝到內核中進行輪詢遍歷判斷,若超時等待/有描述符就緒事件則調用返回,在調用返回的時候,會將事件描述符集合中沒有就緒的事件描述符從集合中移除;
    3、進程在監控調用返回的時候,得到就緒的描述符集合,判斷哪個描述符仍然還在哪個集合中,決定哪個描述符就緒了哪個事件,進而進行操作;
    select函數原型:
#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這三個是文件描述符集,用位圖表示,例如readfds,位圖的bit位的位置代表文件描述符,bit位的內容代表在調用時用戶命令內核要幫我關心哪些文件描述符的讀事件,返回時代表內核告訴用戶所關心的衆多文件描述符當中哪些已經就緒,此時我們代表只有1號文件描述符就緒(讀文件描述符),其他兩個一樣;timeout代表調用select的等待時間;
select的後4個參數都是輸入輸出參數,在每次調用時需要對參數進行重新設定;
select的返回值代表已經就緒的文件描述符個數,返回值爲0代表timeout超時,返回值小於0代表出錯;

struct timeval結構體用來設置超時時間,可以精確到秒和微秒;

struct timeval
{
	time_t tv_sec;
	suseconds_t tv-usec;
};

select相關函數

void FD_CLR(int fd, fd_set *set);//在指定位圖中刪掉指定bit位
int  FD_ISSET(int fd, fd_set *set);//判定一個文件描述符是否在該集合中
void FD_SET(int fd, fd_set *set);//用來設置描述詞組set中相關fd的位
void FD_ZERO(fd_set *set);//清除描述詞組set的全部位

select機制的優勢
例如recv操作,在默認的阻塞模式下的套接字中,recv會阻塞在那裏,直到套接字連接是哪個有數據可讀,把數據讀到buf中後recv函數才能返回,不然會一直阻塞,在單線程的程序中這種情況會導致主線程被阻塞,整個程序鎖死,若是永遠沒有數據過來,程序就會永遠鎖死,那麼我們就要使用select模型,它是使用一種有序的方式,對多個套接字進行統一管理和調度;
select執行過程
用戶首先將需要進行IO操作的socket添加到select中,然後阻塞等待select系統調用返回,當數據到達時,socket被激活,select函數返回,用戶線程正式發起read請求,讀取數據並繼續執行;(連接建立事件可以被當成讀事件處理)
從流程上來看,使用select函數進行IO請求和同步阻塞模型並沒有多大的區別,甚至效率不高,但是使用select的優勢是用戶可以在一個線程內同時處理多個socket的IO請求,在同步阻塞模型中必須使用多線程;
select特點
我們知道,select模型的優點是效率相對較高,可以在一個線程內同時處理多個socket的IO請求,而且它遵循POSIX標準,跨平臺移植性比較好;而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的;
但是也有缺點,即:
(1)select對描述符進行監控有最大數量限制,上限取決於宏 _FD_SESIZE,默認大小爲1024;
(2)在內核中進行監控,通過輪詢遍歷判斷實現的,性能會隨着描述符的增多而下降;
(3)只能返回就緒的集合,需要進行進行輪詢遍歷判斷才能得知哪個描述符就緒了哪個事件;
(4)每次監控都需要重新添加描述符到集合中,每次監控都需要將集合重新拷貝到內核中;
select實現實例
實現服務端功能:SelectServer.hpp

#pragma once
#include <iostream>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <stdlib.h>
#include <unistd.h>
#include <cstring>
#define SIZE sizeof(fd_set)*8
class SelectServer
{
    private:
        int port;
        int lsock;
    public:
        SelectServer(int _port=8888)
            :port(_port)
            ,lsock(-1)
    {}
        void InitServer()
        {
            lsock=socket(AF_INET,SOCK_STREAM,0);
            if(lsock<0)
            {
                cerr<<"socket error"<<endl;
                exit(2);
            }
            struct sockaddr_in local;
            local.sin_family=AF_INET;
            local.sin_port=htons(port);
            local.sin_addr.s_addr=htonl(INADDR_ANY);
            if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)
            {
                cerr<<"binf error"<<endl;
                exit(3);
            }
            if(listen(lsock,5)<0)//5表示底層全連接隊列長度
            {
                cerr<<"listen error"<<endl;
                exit(4);
            }
            int opt=1;
            setsockopt(lsock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//解決地址複用的問題
        }
        void Run()
        {
            int fd_array[SIZE];//保存歷史上獲取的文件描述符
            int i=0;
            for(;i<SIZE;++i)//對數組初始化
            {
                fd_array[i]=-1;
            }
            fd_set rfds;
            fd_array[0]=lsock;//第一個參數被設置成lsock,其他是-1
            int max=lsock;//文件描述符最大值
            for(;;)
            {
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                struct timeval timeout={0,0};//每隔0秒鐘timeout,對時間重新設置
                //對讀文件描述符集進行初始化
                FD_ZERO(&rfds);
                for(i=0;i<SIZE;++i)
                {
                    if(fd_array[i]==-1)
                    {
                        continue;//不用設置
                    }
                //將合法文件描述符數組添加進讀文件描述符集
                    FD_SET(fd_array[i],&rfds);
                    if(fd_array[i]>max)//進行最大的文件描述符值更新
                    {
                        max=fd_array[i];
                    }
                }
//                switch(select(max+1,&rfds,nullptr,nullptr,&timeout))//timeout輪詢
                switch(select(max+1,&rfds,nullptr,nullptr,nullptr))//阻塞方式
                {
                    case 0:
                        cout<<"timeout..."<<endl;
                        break;
                    case -1:
                        cerr<<"select error"<<endl;
                        break;
                    default:
                        for(i=0;i<SIZE;++i)
                        {
                            if(fd_array[i]==-1)
                            {
                                continue;
                            }
                            if(FD_ISSET(fd_array[i],&rfds))//判斷讀文件描述符是否設置成功
                            {
                                if(fd_array[i]==lsock)//如果是lsock,才能進行accept,才一定不會被阻塞
                                {
                                    int fd=accept(lsock,(struct sockaddr*)&peer,&len);
                                    if(fd<0)
                                    {
                                        cerr<<"accept error"<<endl;
                                        continue;
                                    }
                                    cout<<"get a new link..."<<endl;//獲得新鏈接
                                    //拿到的文件描述符不能直接讀寫,一但直接讀寫有可能會因爲數據未就位而阻塞
                                    //將新獲得的文件描述符添加進數組中,
                                    //下一次就可以將新文件描述符設置進讀文件描述符集
                                    int j=1;
                                    for(;j<SIZE;++j)
                                    {
                                        if(fd_array[j]==-1)
                                        {
                                            break;
                                        }
                                    }
                                    if(j==SIZE)//數組被放滿
                                    {
                                        cout<<"fd array is full!"<<endl;
                                        close(fd);
                                    }
                                    else
                                    {
                                        fd_array[j]=fd;
                                    }
                                }
                                else//普通文件描述符
                                {
                                    char buf[1024];
                                    ssize_t s=recv(fd_array[i],buf,sizeof(buf),0);
                                    if(s>0)
                                    {
                                        buf[s]=0;
                                        cout<<"client# "<<buf<<endl;
                                        send(fd_array[i],buf,strlen(buf),0);
                                    }
                                    else if(s==0)
                                    {
                                        cout<<"client quit!"<<endl;
                                        close(fd_array[i]);//將連接關掉
                                        fd_array[i]=-1;//回收這個文件描述符
                                    }
                                    else
                                    {
                                        cerr<<"recv error"<<endl;
                                        close(fd_array[i]);
                                        fd_array[i]=-1;
                                    }
                                }
                            }
                        }
                        break;
                }
            }
        }
        ~SelectServer()
        {
            if(lsock>=0)
            {
                close(lsock);
            }
        }
};

服務端server.cc

#include "SelectServer.hpp"
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        cout<<"Usage: "<<argv[0]<<"port"<<endl;
        exit(1);
    }
    SelectServer *sp=new SelectServer(atoi(argv[1]));
    sp->InitServer();
    sp->Run();
    delete sp;
    return 0;
}

最終可以進行多個客戶端通信;
select總結
1、是什麼?
是用來進行通過等待多個文件描述符來監測哪些文件描述符上時事件就緒的通知進制;
2、一次可以監測多個文件描述符,在進行調用時,用戶命令內核要幫我關心哪些文件描述符的事件,返回時代表內核告訴用戶所關心的衆多文件描述符當中哪些已經就緒;
源代碼(github):
https://github.com/wangbiy/Linux3/commit/5faa08d750d040a11139823f3cb7ad4bec894d3f

  • poll
    1、poll的作用
    與select一樣,唯一的區別是修正了select的2個缺點:
    (1)輸入輸出參數分離,不用每次調用時對輸入輸出參數重新設定;
    (2)poll解決了select等待文件描述符上限的問題;
    2、poll函數的接口
    例如:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

poll函數的參數fds表示監聽的結構列表,每一個元素包含三部分內容,文件描述符,請求事件(用戶想告訴內核的信息),返回事件(內核想告訴用戶的信息),這也就是將輸入輸出參數分離了;nfds表示fds數組的長度(文件描述符上限只與硬件配置有關,與poll無關,因此解決了select中文件描述符上限的問題);timeout表示poll函數的超時時間,單位是ms;
其中fds監聽的結構列表中events和revents的取值有:POLLIN(可讀)、POLLOUT(可寫)、POLLPRI(高優先級數據可讀)、POLLERR(錯誤)等,這些宏的規律是bit位依次置1,例如:
在這裏插入圖片描述
可以看到這4個宏的值是1 2 4 8,的確遵循bit位依次一個一個置爲1的規律;
返回值:返回值<0表示出錯,返回值=0表示poll函數等待超時,返回值大於0表示poll由於監聽的文件描述符就緒而返回;
poll的優點
(1)由於結構體pollfd包含了event和revent,將輸入輸出參數分離,不用每次調用時對輸入輸出參數重新設定;
(2)他解決了等待文件描述符上限的問題,沒有最大數量的限制(數量過大也會影響性能);
poll的缺點
(1)跨平臺移植性差
(2)每次監控依然需要向內核中拷貝監控數據
(3)在內核中監控依然採用輪詢遍歷,性能會隨着描述符的增多而下降
例如:使用poll來模擬標準輸入輸出:

#include <iostream>
#include <poll.h>
#include <string>
#include <stdlib.h>
#include <unistd.h>
using namespace std;
int main()

{
    struct pollfd rfds;
    rfds.fd=0;
    rfds.events=POLLIN;//讀事件
    rfds.events|=POLLOUT;//使用按位或加上寫事件
    rfds.revents=0;
    string buf;
    while(1)
    {
        switch(poll(&rfds,1,0))
        {
            case 0:
                cout<<"time out"<<endl;
                break;
            case -1:
                cout<<"poll error"<<endl;
                break;
            default://事件就緒
                {
                    if(rfds.revents & POLLIN)//返回時檢測
                    {
                        cin>>buf;
                    }
                    if(rfds.revents & POLLOUT)//判斷寫事件就緒
                    {
                        cout<<"echo# "<<endl;
                    }
                    sleep(1);
                }
                break;
        }
    }
    system("pause");
    return 0;
}

定義結構體變量rfds,將請求事件設爲讀和寫,返回事件設爲0,進行poll操作,如果返回值是0表示超時,返回值是-1表示錯誤,其他表示事件就緒,然後在讀事件就緒後輸入,寫事件就緒後輸出;

  • epoll(重點)
    首先我們來對epoll做一個簡單的介紹,它的作用是爲了處理大量句柄而做了改進的poll,其中句柄指唯一標識某個特定資源的(例如文件描述符),他幾乎具備了所有的一切優點,性能極佳;
    epoll的操作流程
    1、發起調用在內核中創建epoll句柄、epollevent結構體(這個結構體中包含很多信息,紅黑樹+雙向鏈表);
    2、發起調用對內核中的epollevent結構添加/刪除/修改所監控的描述符監控信息;
    3、發起調用開始監控,在內核中採用異步阻塞操作實現監控,等待超時/有描述符就緒了事件調用返回,返回給用戶就緒描述符的時間結構信息;
    4、進程直接對就緒的事件結構體中的描述符成員進行操作即可;
    epoll的系統調用

1、epoll_create(int size);

它的作用是創建一個新的句柄(也就是文件描述符),在用完之後,必須調用close()關閉;它的返回值是一個文件描述符(句柄),雖然size參數可以忽略,但是我們一般還是將它帶上;

2、epoll_ctl(int epfd,int op,int fd,struct epoll_event* event)

它的作用是註冊要監聽的事件類型,參數epfd表示epoll_create返回的句柄;ep表示動作,主要有3個宏(EPOLL_CTL_ADD,註冊新的fd到epfd中;EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;EPOLL_CTL_DEL:從epfd中刪除一個事件);fd表示需要監聽的fd;event表示告訴內核要監聽什麼事件;
注意:struct epoll_event結構體如下:

struct epoll_event
{
	uint32_t events;
	epoll_data_t data;
}_EPOLL_PACKED;

其中events可以是以下幾個宏的集合:(EPOLLIN:表示對應的文件描述符可以讀,EPOLLOUT:表示對應的文件描述符可以寫,EPOLLPRI:表示對應的文件描述符有緊急的數據可讀,EPOLLERR:表示對應的文件描述符發生錯誤,EPOLLUP:表示對應的文件描述符被掛斷,EPOLLET:表示將EPOLL設爲邊緣觸發模式,相對於水平觸發來說的,EPOLLONESHOT:表示只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列中);

3.int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);

這個函數的作用是收集在epoll監控的事件中已經發送的事件,其中參數events表示分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到events數組中(events不可以是空指針,內核只負責把數據複製到這個events數組中,不會幫助我們在用戶態中分配內存);maxevents表示告訴內核這個events有多大,這個maxevents的值不能大於創建epoll_create()時的size;參數timeout表示超時時間(0表示立即返回,-1表示永久阻塞);
如果函數調用成功,返回對應的I/O上已準備好的文件描述符數目,如果返回0表示已經超時,返回小於0表示函數失敗;
epoll監控原理(異步阻塞操作)
監控由系統完成,用戶添加到監控的描述符以及對應事件結構體會被添加到內核的eventpoll結構體中的紅黑樹中,一旦發起調用開始監控,則操作系統爲每個描述符的事件做了一個回調函數,功能是當描述符就緒了關心的事件,則將描述符對應的事件結構體添加到雙向鏈表中;
進程自身,只是每隔一段時間,判斷雙向鏈表是否是NULL,決定是否有就緒;
因此它的流程是:
1、創建句柄;
2、添加監控的描述符,以及對應事件結構體信息到內核;
3、開始異步阻塞監控,系統將就緒描述符的對應事件結構體信息添加到雙向鏈表中;
4、進程只需要根據就緒事件結構體中的事件信息決定對事件結構中的fd描述符進行相應操作即可;
那麼epoll的高效體現在
第一,回調機制讓操作系統從輪詢檢測中解放了出來;第二,雙向鏈表機制讓用戶以O(1)的時間複雜度直接將結點從雙向鏈表中拿出,當底層文件描述符數量越多,就越高效,值得注意的是,有一些說法說epoll中使用了內存映射機制,這種說法是錯誤的,我們定義的struct epoll_event是在用戶空間中分配好的內存,勢必需要將內核的數據拷貝到這個用戶空間的內存中的;
那麼這個雙向鏈表是怎麼維護的呢?
當我們執行epoll_ctl時,除了將需要註冊的事件掛載到對應的紅黑樹以外,還會給內核中斷處理程序維護一個回調機制,當有事件就緒時,就會觸發網卡驅動程序發出中斷,將對應的事件從紅黑樹中找到並添加到雙向鏈表中,讓操作系統從輪詢檢測中解放了出來,所以,當一個socket上有數據到了,內核把網卡上的數據拷貝到內核中後,把socket插入到雙向鏈表中;__
也就是執行epoll_create時,創建紅黑樹和雙向鏈表,調用epoll_ctl時,如果增加socket句柄,則檢查在紅黑樹中是否存在,如果存在立即返回,不存在則增加到樹上,然後向內核維護回調機制,用於當中斷時間來臨時向雙向鏈表中插入數據,執行epoll_wait時立即返回雙向鏈表中的數據即可;

注意:每一個epoll對象都有一個獨立的eventfd結構體,用於存放通過epoll_ctl方法向epoll對象中添加進來的事件;這些事件會被掛載到紅黑樹上,而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關係

  • epoll的優點
    (1)文件描述符的個數無上限;
    (2)監控信息只需要向內核添加一次;
    (3)監控使用異步阻塞操作完成,性能不會隨着描述符的增多而下降;
    (4)直接向用戶返回就緒的事件信息(包含描述符),進程直接可以針對描述符以及事件進行操作,不需要判斷是否就緒;
    epoll的缺點
    跨平臺移植性差;
    使用epoll設計一個網絡通信服務器:
    EpollServer.hpp:
#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <cstring>
using namespace std;
class Server
{
    private:
        int port;
        int lsock;
        int epfd;
    public:
        Server(int _port=8080)
            :port(_port)
             ,lsock(-1)
             ,epfd(-1)
        {}
        void InitServer()
        {
            lsock=socket(AF_INET,SOCK_STREAM,0);
            if(lsock<0)
            {
                cerr<<"socket error"<<endl;
                exit(2);
            }
            struct sockaddr_in local;
            local.sin_family=AF_INET;
            local.sin_addr.s_addr=htonl(INADDR_ANY);
            local.sin_port=htons(port);
            if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)
            {
                cerr<<"bind error"<<endl;
                exit(3);
            }
            if(listen(lsock,5)<0)
            {
                cerr<<"listen error"<<endl;
                exit(4);
            }
            int opt=1;
            setsockopt(lsock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
            if((epfd=epoll_create(256))<0)
            {
                cerr<<"epoll_create error"<<endl;
                exit(5);
            }
        }
        void HanderEvents(int epfd,struct epoll_event* revs,int num)
        {
            struct epoll_event ev;
            for(int i=0;i<num;++i)
            {
                int sock=revs[i].data.fd;//拿到套接字
                if(revs[i].events & EPOLLIN)//檢測是否有讀事件就緒
                {
                    if(sock==lsock)
                    {
                        //link event ready,獲取新鏈接
                        struct sockaddr_in peer;
                        socklen_t len=sizeof(peer);
                        int new_sock=accept(lsock,(struct sockaddr*)&peer,&len);
                        if(new_sock<0)
                        {
                            cerr<<"accept error"<<endl;
                            continue;
                        }
                        cout<<"get a new link,fd: "<<new_sock<<endl;
                        //連接獲取成功
                        //將當前文件描述符添加進epoll模型中
                        ev.events=EPOLLIN;
                        ev.data.fd=new_sock;
                        epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev);
                    }
                    else
                    {
                        //read event ready,普通socket
                        char buf[1024];
                        ssize_t s=recv(sock,buf,sizeof(buf)-1,0);
                        if(s>0)
                        {
                            buf[s]=0;
                            cout<<buf<<endl;
                            ev.events=EPOLLOUT;//改爲寫
                            ev.data.fd=sock;
                            epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&ev);//讀完後想要寫,就要將文件描述符改爲寫
                        }
                        else if(s==0)
                        {
                            cout<<"link .....close"<<endl;
                            close(sock);
                            epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
                        }
                        else
                        {
                            cout<<"recv error"<<endl;
                            close(sock);
                            epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
                        }
                    }
                }//if
                else if(revs[i].events & EPOLLOUT)//關心寫事件是否就緒
                {
                    string http_echo="HTTP/1.1 200 OK\r\n\r\n<html><body><h1>Hello EPOLL Server</h1><img src=\"https://c-ssl.duitang.com/uploads/item/201507/13/20150713153609_YKU8V.thumb.700_0.jpeg\" alt=\"test\"/></body></html>";
                    send(sock,http_echo.c_str(),http_echo.size(),0);
                    close(sock);//短鏈接
                    epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
                }
                else
                {
                    cout<<"bug?"<<endl;
                }
            }//for
        }
        void Run()
        {
            //將lsock註冊進epoll模型
            struct epoll_event ev;
            ev.events=EPOLLIN;
            ev.data.fd=lsock;
            epoll_ctl(epfd,EPOLL_CTL_ADD,lsock,&ev);//紅黑樹中新增了一個節點,叫做lsock
            struct epoll_event revs[128];
            for(;;)
            {
                int timeout=-1;
                int num=0;
                switch((num=epoll_wait(epfd,revs,128,timeout)))
                {
                    case 0:
                        cout<<"time out"<<endl;
                        break;
                    case -1:
                        cout<<"epoll wait error"<<endl;
                        break;
                    default:
                        //目前事件在revs中,只有listen事件
                        HanderEvents(epfd,revs,num);//處理事件
                        break;
                }

            }
        }
        ~Server()
        {
            if(lsock>=0)
            {
                close(lsock);
            }
            if(epfd>=0)
            {
                close(epfd);
            }
        }
};

分析:首先先進行套接字的創建、綁定、監聽、設置地址轉換機制、然後先創建epoll的句柄,然後我們將創建的套接字lsock註冊進epoll模型中,之後我們等待文件描述符就緒,將發生的事件賦值到revs數組中,如果epoll_wait的返回值不是0或者-1,表示文件描述符就緒,開始處理事件:
如果有讀事件就緒,拿到revs數組中所對應的套接字sock,看是否與註冊進epoll模型中的lsock相等,如果相等,建立新鏈接,創建了一個新的套接字,將它註冊進epoll模型中,如果不相等,說明是普通socket,開始進行讀寫;
如果是寫事件就緒,將要發送的消息發送出去(我這裏的消息是一個圖片),然後將套接字sock註冊進epoll模型即可;
server.cc

#include "EpollServer.hpp"
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        cout<<"Usage: "<<argv[0]<<"port"<<endl;
        exit(1);
    }
    Server* sp=new Server(atoi(argv[1]));
    sp->InitServer();
    sp->Run();
    delete sp;
    return 0;
}

epoll工作方式
epoll有兩種工作方式:(1)水平觸發(LT);(2)邊緣觸發(ET);
LT模式:
可讀事件:接收緩衝區中數據大小大於低水位標記,就會觸發可讀事件;
可寫事件:發送緩衝區中剩餘空間大小大於低水位標記,就會觸發可寫事件;
低水位標記:基準衡量值,默認爲一個字節;
ET模式:
可讀事件:只有新數據到來的時候,纔會出發一次事件;
可寫事件:發送緩衝區中剩餘空間從無到有的時候纔會出發一次事件;
注意:因爲觸發方式的不同,因此要求進程中事件觸發進行數據接收的時候,要求最好能夠一次將所有的數據全部讀取(因爲剩餘的數據不會觸發第二次事件,只有新數據到來時纔會觸發),然而循環讀取能夠保證讀完緩衝區中的所有數據,但是在沒有數據的時候就會造成阻塞,因此邊緣觸發方式中,描述符的操作都採用非阻塞操作;
非阻塞描述符操作在沒有數據或者超時的情況下會報錯返回:EAGIN或者EWOULDBLOCK;
如何設置非阻塞(描述符的所有操作都爲非阻塞)
int fcntl(int fd,int cmd,…/*arg/);
epoll的使用場景
對於多連接,且多連接中只有一部分連接比較活躍時,比較適合使用epoll,但是如果只有少數的幾個連接,這種情況不適合使用epoll;
epoll的驚羣問題
首先什麼是驚羣問題?
在多線程或者多進程下,使用epoll拉來監控處理事件,每個線程或者進程都同時監聽着socket,那麼當socket中某個事件就緒或者添加新的事件,操作系統就不知道應該將哪個線程或者進程喚醒來處理此次事件,所以操作系統的處理方法就是同時喚醒多個線程或者進程,可是此時必定只有一個線程或者進程能夠獲得處理此次事件的權力,那麼其他沒有競爭到處理權力的線程或者進程就會失敗,錯誤碼就是EAGIN;
那麼驚羣問題會造成系統資源的浪費,並且對系統的性能也有影響;
因此怎麼處理驚羣問題?
多線程可以採用master/worker線程模式,也就是讓其中一個線程(master線程)在epoll_wait監聽socket,當有新的連接請求時,由epoll_wait的線程調用accept,建立新的連接,然後交給其他線程(worker線程)處理後續的數據讀寫,這樣避免了多線程下的epoll_wait驚羣問題;
多進程使用nginx的解決思路,在同一時刻,永遠只有一個子進程在監聽的socket上epoll_wait,創建一個全局的pthread_mutex_t,在子進程進行epoll_wait前,先獲取鎖;
epoll源代碼(github):
https://github.com/wangbiy/Linux3/commit/109847df93ef31f80239989f89655ab154be5cff

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章