Linux內核筆記:epoll實現原理(一)

来源:http://www.cnblogs.com/sduzh/archive/2017/04/16/6714281.html
-Advertisement-
Play Games

一、說明 針對的內核版本為4.4.10。 本文只是我自己看源碼的簡單筆記,如果想瞭解epoll的實現,強烈推薦下麵的文章: The Implementation of epoll(1) The Implementation of epoll(2) The Implementation of epol ...


一、說明

針對的內核版本為4.4.10。

本文只是我自己看源碼的簡單筆記,如果想瞭解epoll的實現,強烈推薦下麵的文章:

The Implementation of epoll(1)

The Implementation of epoll(2)

The Implementation of epoll(3)

The Implementation of epoll(4)

 

二、epoll_create()

系統調用epoll_create()會創建一個epoll實例並返回該實例對應的文件描述符fd。在內核中,每個epoll實例會和一個struct eventpoll類型的對象一一對應,該對象是epoll的核心,其聲明在fs/eventpoll.c文件中.

epoll_create的介面定義在這裡,主要源碼分析如下:

首先創建一個struct eventpoll對象:

struct eventpoll *ep = NULL;
...
error = ep_alloc(&ep);
if (error < 0)
    return error;  

然後分配一個未使用的文件描述符:

fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
if (fd < 0) {
    error = fd;
    goto out_free_ep;
}

然後創建一個struct file對象,將file中的struct file_operations *f_op設置為全局變數eventpoll_fops,將void *private指向剛創建的eventpoll對象ep:

struct file *file;
...
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
if (IS_ERR(file)) {
    error = PTR_ERR(file);
    goto out_free_fd;
}

然後設置eventpoll中的file指針:

ep->file = file;

最後將文件描述符添加到當前進程的文件描述符表中,並返回給用戶

fd_install(fd, file);
return fd;

操作結束後主要結構關係如下圖:

三、epoll_ctl()

系統調用epoll_ctl()在內核中的定義如下,各個參數的含義可參見epoll_ctl的man手冊

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event)

epoll_ctl()首先判斷op是不是刪除操作,如果不是則將event參數從用戶空間拷貝到內核中:

struct epoll_event epds;
...
if (ep_op_has_event(op) &&
     copy_from_user(&epds, event, sizeof(struct epoll_event)))
         goto error_return;

ep_op_has_event()實際就是判斷op是不是刪除操作:

static inline int ep_op_has_event(int op)
{
    return op != EPOLL_CTL_DEL;
}

接下來判斷用戶是否設置了EPOLLEXCLUSIVE標誌,這個標誌是4.5版本內核才有的,主要是為瞭解決同一個文件描述符同時被添加到多個epoll實例中造成的“驚群”問題,詳細描述可以看這裡。 這個標誌的設置有一些限制條件,比如只能是在EPOLL_CTL_ADD操作中設置,而且對應的文件描述符本身不能是一個epoll實例,下麵代碼就是對這些限制的檢查:

/*
 *epoll adds to the wakeup queue at EPOLL_CTL_ADD time only,
 * so EPOLLEXCLUSIVE is not allowed for a EPOLL_CTL_MOD operation.
 * Also, we do not currently supported nested exclusive wakeups.
 */
 if (epds.events & EPOLLEXCLUSIVE) {
     if (op == EPOLL_CTL_MOD)
         goto error_tgt_fput;
     if (op == EPOLL_CTL_ADD && (is_file_epoll(tf.file) ||
            (epds.events & ~EPOLLEXCLUSIVE_OK_BITS)))
         goto error_tgt_fput;
}

接下來從傳入的文件描述符開始,一步步獲得struct file對象,再從struct file中的private_data欄位獲得struct eventpoll對象:

struct fd f, tf;
struct eventpoll *ep;
... 
f = fdget(epfd); 
... 
tf = fdget(fd); 
...
ep = f.file->private_data;

如果要添加的文件描述符本身也代表一個epoll實例,那麼有可能會造成死迴圈,內核對此情況做了檢查,如果存在死迴圈則返回錯誤。這部分的代碼目前我還沒細看,這裡不再貼出。

接下來會從epoll實例的紅黑樹里尋找和被監控文件對應的epollitem對象,如果不存在,也就是之前沒有添加過該文件,返回的會是NULL。

struct epitem *epi;
...
epi = ep_find(ep, tf.file, fd);

ep_find()函數本質是一個紅黑樹查找過程,紅黑樹查找和插入使用的比較函數是ep_cmp_ffd(),先比較struct file對象的地址大小,相同的話再比較文件描述符大小。struct file對象地址相同的一種情況是通過dup()系統調用將不同的文件描述符指向同一個struct file對象。

static inline int ep_cmp_ffd(struct epoll_filefd *p1, 
struct epoll_filefd *p2) { return (p1->file > p2->file ? +1: (p1->file < p2->file ? -1 : p1->fd - p2->fd)); }

接下來會根據操作符op的不同做不同的處理,這裡我們只看op等於EPOLL_CTL_ADD時的添加操作。首先會判斷上一步操作中返回的epollitem對象地址是否為NULL,不是NULL說明該文件已經添加過了,返回錯誤,否則調用ep_insert()函數進行真正的添加操作。在添加文件之前內核會自動為該文件增加POLLERR和POLLHUP事件。

if (!epi) {
    epds.events |= POLLERR | POLLHUP;
    error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else
    error = -EEXIST;
if (full_check)
    clear_tfile_check_list();

 ep_insert()返回之後會判斷full_check標誌,該標誌和上文提到的死迴圈檢測相關,這裡也略去。

四、ep_insert()

ep_insert()函數中,首先判斷epoll實例中監視的文件數量是否已超過限制,沒問題則為待添加的文件創建一個epollitem對象:

int error, revents, pwake = 0;
unsigned long flags;
long user_watches;
struct epitem *epi;
struct ep_pqueue epq;
 
user_watches = atomic_long_read(&ep->user->epoll_watches);
if (unlikely(user_watches >= max_user_watches))
        return -ENOSPC;
if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
        return -ENOMEM; 

接下來是對epollitem的初始化:

INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;
ep_set_ffd(&epi->ffd, tfile, fd);
epi->event = *event;
epi->nwait = 0;
epi->next = EP_UNACTIVE_PTR;
if (epi->event.events & EPOLLWAKEUP) {
        error = ep_create_wakeup_source(epi);
        if (error)
                goto error_create_wakeup_source;
} else {
        RCU_INIT_POINTER(epi->ws, NULL);
}

接下來是比較重要的操作:將epollitem對象添加到被監視文件的等待隊列上去。等待隊列實際上就是一個回調函數鏈表,定義在/include/linux/wait.h文件中。因為不同文件系統的實現不同,無法直接通過struct file對象獲取等待隊列,因此這裡通過struct file的poll操作,以回調的方式返回對象的等待隊列,這裡設置的回調函數是ep_ptable_queue_proc:

struct ep_pqueue epq;
...
/* Initialize the poll table using the queue callback */
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

/*
 * Attach the item to the poll hooks and get current event bits.
 * We can safely use the file* here because its usage count has
 * been increased by the caller of this function. Note that after
 * this operation completes, the poll callback can start hitting
 * the new item.
 */
revents = ep_item_poll(epi, &epq.pt);

上面代碼中結構體ep_queue的作用是能夠在poll的回調函數中取得對應的epollitem對象,這種做法在Linux內核里非常常見。

在回調函數ep_ptable_queue_proc中,內核會創建一個struct eppoll_entry對象,然後將等待隊列中的回調函數設置為ep_poll_callback()。也就是說,當被監控文件有事件到來時,比如socker收到數據時,ep_poll_callback()會被回調。ep_ptable_queue_proc()代碼如下:

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                                 poll_table *pt)
{
        struct epitem *epi = ep_item_from_epqueue(pt);
        struct eppoll_entry *pwq;

        if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
                init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
                pwq->whead = whead;
                pwq->base = epi;
                if (epi->event.events & EPOLLEXCLUSIVE)
                        add_wait_queue_exclusive(whead, &pwq->wait);
                else
                        add_wait_queue(whead, &pwq->wait);
                list_add_tail(&pwq->llink, &epi->pwqlist);
                epi->nwait++;
        } else {
                /* We have to signal that an error occurred */
                epi->nwait = -1;
        }
}

eppoll_entry和epitem等結構關係如下圖:

在回到ep_insert()函數中。ep_item_poll()調用完成之後,會將epitem中的fllink欄位添加到struct file中的f_ep_links鏈表中,這樣就可以通過struct file找到所有對應的struct epollitem對象,進而通過struct epollitem找到所有的epoll實例對應的struct eventpoll。

spin_lock(&tfile->f_lock);
list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);
spin_unlock(&tfile->f_lock);

然後就是將epollitem插入到紅黑樹中:

ep_rbtree_insert(ep, epi)

最後再更新下狀態就返回了,插入操作也就完成了。

在返回之前還會判斷一次剛纔添加的文件是不是當前已經有事件就緒了,如果是就將其加入到epoll的就緒鏈表中,關於就緒鏈表放到下一部分中講,這裡略過。

最後是我畫的幾個結構體之間的結構圖。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • 以下麵這個表的數據作為示例。 什麼是聚合函數? 聚合函數:聚合函數就是對一組值進行計算後返回單個值(即分組)。聚合函數在計算時都會忽略空值(null)。 所有的聚合函數均為確定性函數。即任何時候使用一組相同的輸入值調用聚合函數執行後的返回值都是相同的,無二義性。 COUNT(統計函數):COUNT函 ...
  • 我相信大部分人都碰到過,處理數據的時候,欄位的值是以 ',' (逗號)分隔的形式,所以我也不能避免。 然後我才知道,sql 是沒有類似於 C# 和 Javascript 這種分割字元串的方法。( Split ) 所以我自己定義了一個 sql 函數(多聲明表值函數),代碼如下: 下麵來調用一下試試效果 ...
  • 本文出處:http://www.cnblogs.com/wy123/p/6709520.html 1,SQL語句或者存儲過程的最大長度(SQL字元串容量)是多少? 經常有人問,我的SQL語句是拼湊出來的,可能很長,如果太長了,是不是SQL Server支持不動了? SQL語句的或者存儲過程的最大長度 ...
  • 查詢每種業務類別下的業務產品數量: SELECT CASE BusProductType WHEN 1 THEN '機票類' WHEN 2 THEN '酒店類' ELSE '其他' END '業務類別', COUNT(1) 業務產品數量 FROM YSB_BusProductInfo GROUP B ...
  • 這章是基礎課程,幫大家進入大數據領域打好Linux基礎,以便更好地學習Hadoop,NoSQL,Oracle,MySQL,Spark,Storm等眾多課程。因為企業中無一例外的是使用Linux來搭建或部署項目。 1) Linux的介紹,Linux的安裝:VMware Workstation虛擬軟體安 ...
  • 這次把整個電腦都裝了Ubuntu,向Linux這條路越走越遠了,也感謝社會對Linux的支持越來越完善了,才讓我下定這個決心,再次表示感謝 之前都是裝雙系統或者在vm下安裝的Linux,現在再裝一次,竟然出問題了 第一個問題是裝完以後開機進不了系統,是我自己分的區,如果用它的預設安裝方式就可以。 試 ...
  • 內核模塊分析 這裡主要分析一下內核模塊中各語句的作用,下麵是一段簡單的模塊代碼,只做了模塊的初始化和退出操作 代碼簡析: 代碼詳析: 1.first_module_init()函數是模塊的入口點,在模塊裝載時被調用。 module_init()是一個巨集調用,它唯一的參數便是模塊的初始化函數。 模塊的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...