1. 程式人生 > 其它 >linux epoll 開發指南-【ffrpc原始碼解析】

linux epoll 開發指南-【ffrpc原始碼解析】

摘要

關於epoll的問題很早就像寫文章講講自己的看法,但是由於ffrpc一直沒有完工,所以也就拖下來了。Epoll主要在伺服器程式設計中使用,本文主要探討伺服器程式中epoll的使用技巧。Epoll一般和非同步io結合使用,故本文討論基於以下應用場合:

  •   主要討論伺服器程式中epoll的使用,主要涉及tcp socket的相關api。
  •   Tcp socket 為非同步模式,包括socket的非同步讀寫,以及監聽的非同步操作。
  •   本文不會過多討論API的細節,而是專注流程與設計。

Epoll 的io模型

Epoll是為非同步io操作而設計的,epoll中IO事件被分為read事件和write事件,如果大家對於linux的驅動模組或者linux io 模型有接觸的話,就會理解起來更容易。Linux中IO操作被抽象為read、write、close、ctrl幾個操作,所以epoll只提供read、write、error事件,是和linux的io模型是統一的。

  •   當epoll通知read事件時,可以呼叫io系統呼叫read讀取資料
  •   當epoll通知write事件時,可以呼叫io系統呼叫write傳送資料
  •   當error事件時,可以close回收資源
  •   Ctrl相關的介面則用來設定socket的非阻塞選項等。

為什麼要了解epoll的io模型呢,本文認為,某些情況下epoll操作的程式碼的複雜性是由於程式碼中的模型(或者類設計)與epoll io模型不匹配造成的。換句話說,如果我們的編碼模型和epoll io模型匹配,那麼非阻塞socket的編碼就會很簡單、清晰。

按照epoll模型構建的類關係為:

//! 檔案描述符相關介面
typedef int socket_fd_t;
class fd_i
{
public:
    virtual ~fd_i(){}

    virtual socket_fd_t socket()          = 0;
    virtual int handle_epoll_read()  = 0;
    virtual int handle_epoll_write() = 0;
    virtual int handle_epoll_del()          = 0;

    virtual void close()                          = 0;
};
int epoll_impl_t::event_loop()
{
    int i = 0, nfds = 0;
    struct epoll_event ev_set[EPOLL_EVENTS_SIZE];

    do
    {
        nfds  = ::epoll_wait(m_efd, ev_set, EPOLL_EVENTS_SIZE, EPOLL_WAIT_TIME);

        if (nfds < 0 && EINTR == errno)
        {
            nfds = 0;
            continue;
        }
        for (i = 0; i < nfds; ++i)
        {
            epoll_event& cur_ev = ev_set[i];
            fd_i* fd_ptr            = (fd_i*)cur_ev.data.ptr;
            if (cur_ev.data.ptr == this)//! iterupte event
            {
                if (false == m_running)
                {
                    return 0;
                }

                //! 刪除那些已經出現error的socket 物件
                fd_del_callback();
                continue;
            }
    
            if (cur_ev.events & (EPOLLIN | EPOLLPRI))
            {
                fd_ptr->handle_epoll_read();
            }

            if(cur_ev.events & EPOLLOUT)
            {
                fd_ptr->handle_epoll_write();
            }

            if (cur_ev.events & (EPOLLERR | EPOLLHUP))
            {
                fd_ptr->close();
            }
        }
        
    }while(nfds >= 0);

    return 0;
}

Epoll的LT模式和ET模式的比較

         先簡單比較一下level trigger 和 edge trigger 模式的不同。

LT模式的特點是:

  •   若資料可讀,epoll返回可讀事件
  •   若開發者沒有把資料完全讀完,epoll會不斷通知資料可讀,直到資料全部被讀取。
  •   若socket可寫,epoll返回可寫事件,而且是隻要socket傳送緩衝區未滿,就一直通知可寫事件。
  •   優點是對於read操作比較簡單,只要有read事件就讀,讀多讀少都可以。
  •   缺點是write相關操作較複雜,由於socket在空閒狀態傳送緩衝區一定是不滿的,故若socket一直在epoll wait列表中,則epoll會一直通知write事件,所以必須保證沒有資料要傳送的時候,要把socket的write事件從epoll wait列表中刪除。而在需要的時候在加入回去,這就是LT模式的最複雜部分。

ET模式的特點是:

  •   若socket可讀,返回可讀事件
  •   若開發者沒有把所有資料讀取完畢,epoll不會再次通知epoll read事件,也就是說存在一種隱患,如果開發者在讀到可讀事件時,如果沒有全部讀取所有資料,那麼可能導致epoll在也不會通知該socket的read事件。(其實這個問題並沒有聽上去難,參見下文)。
  •   若傳送緩衝區未滿,epoll通知write事件,直到開發者填滿傳送緩衝區,epoll才會在下次傳送緩衝區由滿變成未滿時通知write事件。
  •   ET模式下,只有socket的狀態發生變化時才會通知,也就是讀取緩衝區由無資料到有資料時通知read事件,傳送緩衝區由滿變成未滿通知write事件。
  •   缺點是epoll read事件觸發時,必須保證socket的讀取緩衝區資料全部讀完(事實上這個要求很容易達到)
  •   優點:對於write事件,傳送緩衝區由滿到未滿時才會通知,若無資料可寫,忽略該事件,若有資料可寫,直接寫。Socket的write事件可以一直髮在epoll的wait列表。Man epoll中我們知道,當向socket寫資料,返回的值小於傳入的buffer大小或者write系統呼叫返回EWouldBlock時,表示傳送緩衝區已滿。

讓我們換一個角度來理解ET模式,事實上,epoll的ET模式其實就是socket io完全狀態機。

先來看epoll中read 的狀態圖:

當socket由不可讀變成可讀時,epoll的ET模式返回read 事件。對於read 事件,開發者需要保證把讀取緩衝區資料全部讀出,man epoll可知:

  •   Read系統呼叫返回EwouldBlock,表示讀取緩衝區資料全部讀出
  •   Read系統呼叫返回的數值小於傳入的buffer引數,表示讀取緩衝區全部讀出。

示例程式碼

int socket_impl_t:: handle_epoll_read ()
{
    if (is_open())
    {
        int nread = 0;
        char recv_buffer[RECV_BUFFER_SIZE];
        do
        {
            nread = ::read(m_fd, recv_buffer, sizeof(recv_buffer) - 1);
            if (nread > 0)
            {
                recv_buffer[nread] = '