1. 程式人生 > >epoll 水平觸發與邊緣觸發

epoll 水平觸發與邊緣觸發

epoll也是實現I/O多路複用的一種方法,為了深入瞭解epoll的原理,我們先來看下epoll水平觸發(level trigger,LT,LT為epoll的預設工作模式)與邊緣觸發(edge trigger,ET)兩種工作模式。

使用脈衝訊號來解釋LT和ET可能更加貼切。Level是指訊號只需要處於水平,就一直會觸發;而edge則是指訊號為上升沿或者下降沿時觸發。說得還有點玄乎,我們以生活中的一個例子來類比LT和ET是如何確定讀操作是否就緒的。

水平觸發 
兒子:媽媽,我收到了500元的壓歲錢。 
媽媽:嗯,省著點花。 
兒子:媽媽,我今天花了200元買了個變形金剛。 
媽媽:以後不要亂花錢。 
兒子:媽媽,我今天買了好多好吃的,還剩下100元。 
媽媽:用完了這些錢,我可不會再給你錢了。 
兒子:媽媽,那100元我沒花,我攢起來了 
媽媽:這才是明智的做法! 
兒子:媽媽,那100元我還沒花,我還有錢的。 
媽媽:嗯,繼續保持。 
兒子:媽媽,我還有100元錢。 
媽媽:…

接下來的情形就是沒完沒了了:只要兒子一直有錢,他就一直會向他的媽媽彙報。LT模式下,只要核心緩衝區中還有未讀資料,就會一直返回描述符的就緒狀態,即不斷地喚醒應用程序。在上面的例子中,兒子是緩衝區,錢是資料,媽媽則是應用程序瞭解兒子的壓歲錢狀況(讀操作)。

邊緣觸發 
兒子:媽媽,我收到了500元的壓歲錢。 
媽媽:嗯,省著點花。 
(兒子使用壓歲錢購買了變形金剛和零食。) 
兒子: 
媽媽:兒子你倒是說話啊?壓歲錢呢?

這個就是ET模式,兒子只在第一次收到壓歲錢時通知媽媽,接下來兒子怎麼把壓歲錢花掉並沒有通知媽媽。即兒子從沒錢變成有錢,需要通知媽媽,接下來錢變少了,則不會再通知媽媽了。在ET模式下, 緩衝區從不可讀變成可讀,會喚醒應用程序,緩衝區資料變少的情況,則不會再喚醒應用程序。

我們再詳細說明LT和ET兩種模式下對讀寫操作是否就緒的判斷。

水平觸發

1. 對於讀操作

只要緩衝內容不為空,LT模式返回讀就緒。

2. 對於寫操作

只要緩衝區還不滿,LT模式會返回寫就緒。

邊緣觸發

1. 對於讀操作

(1)當緩衝區由不可讀變為可讀的時候,即緩衝區由空變為不空的時候。

(2)當有新資料到達時,即緩衝區中的待讀資料變多的時候。

(3)當緩衝區有資料可讀,且應用程序對相應的描述符進行EPOLL_CTL_MOD 修改EPOLLIN事件時。

2. 對於寫操作

(1)當緩衝區由不可寫變為可寫時。

(2)當有舊資料被髮送走,即緩衝區中的內容變少的時候。

(3)當緩衝區有空間可寫,且應用程序對相應的描述符進行EPOLL_CTL_MOD

 修改EPOLLOUT事件時。

實驗

實驗1

實驗1對標準輸入檔案描述符使用ET模式進行監聽。當我們輸入一組字元並接下回車時,螢幕中會輸出”hello world”。

#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>

int main()
{
    int epfd, nfds;
    struct epoll_event event, events[5];
    epfd = epoll_create(1);
    event.data.fd = STDIN_FILENO;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
    while (1) {
        nfds = epoll_wait(epfd, events, 5, -1);
        int i;
        for (i = 0; i < nfds; ++i) {
            if (events[i].data.fd == STDIN_FILENO) {
                printf("hello world\n");
            }
        }
    }
}

輸出:

$ ./epoll1 

hello world 
abc 
hello world 
hello 
hello world 
ttt 
hello world

當用戶輸入一組字元,這組字元被送入緩衝區,因為緩衝區由空變成不空,所以ET返回讀就緒,輸出”hello world”。 
之後再次執行epoll_wait,但ET模式下只會通知應用程序一次,故導致epoll_wait阻塞。 
如果使用者再次輸入一組字元,導致緩衝區內容增多,ET會再返回就緒,應用程序再次輸出”hello world”。 
如果將上面的程式碼中的event.events = EPOLLIN | EPOLLET;改成event.events = EPOLLIN;,即使用LT模式,則執行程式後,會一直輸出hello world

實驗2

實驗2對標準輸入檔案描述符使用LT模式進行監聽。當我們輸入一組字元並接下回車時,螢幕中會輸出”hello world”。

#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>

int main()
{
    int epfd, nfds;
    char buf[256];
    struct epoll_event event, events[5];
    epfd = epoll_create(1);
    event.data.fd = STDIN_FILENO;
    event.events = EPOLLIN;  // LT是預設模式
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
    while (1) {
        nfds = epoll_wait(epfd, events, 5, -1);
        int i;
        for (i = 0; i < nfds; ++i) {
            if (events[i].data.fd == STDIN_FILENO) {
                read(STDIN_FILENO, buf, sizeof(buf));
                printf("hello world\n");
            }
        }
    }
}

輸出:

$ ./epoll2 
abc 
hello world 
eeeee 
hello world 
lihao 
hello world

實驗2中使用的是LT模式,則每次epoll_wait返回時我們都將緩衝區的資料讀完,下次再呼叫epoll_wait時就會阻塞,直到下次再輸入字元。 
如果將上面的程式改為每次只讀一個字元,那麼每次輸入多少個字元,則會在螢幕中輸出多少個“hello world”。有意思吧。

實驗3

實驗3對標準輸入檔案描述符使用ET模式進行監聽。當我們輸入任何輸入並接下回車時,螢幕中會死迴圈輸出”hello world”。

#include <unistd.h>
#include <stdio.h>
#include <sys/epoll.h>

int main()
{
    int epfd, nfds;
    struct epoll_event event, events[5];
    epfd = epoll_create(1);
    event.data.fd = STDIN_FILENO;
    event.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &event);
    while (1) {
        nfds = epoll_wait(epfd, events, 5, -1);
        int i;
        for (i = 0; i < nfds; ++i) {
            if (events[i].data.fd == STDIN_FILENO) {
                printf("hello world\n");
                event.data.fd = STDIN_FILENO;
                event.events = EPOLLIN | EPOLLET;
                epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &event);
            }
        }
    }
}

實驗3使用ET模式,但是每次讀就緒後都主動對描述符進行EPOLL_CTL_MOD 修改EPOLLIN事件,由上面的描述我們可以知道,會再次觸發讀就緒,這樣就導致程式出現死迴圈,不斷地在螢幕中輸出”hello world”。但是,如果我們將EPOLL_CTL_MOD 改為EPOLL_CTL_ADD,則程式的執行將不會出現死迴圈的情況。

參考資料