調試linux內核(2): poll系統調用的實現

来源:https://www.cnblogs.com/kfggww/archive/2023/08/25/17653270.html
-Advertisement-
Play Games

linux內核為用戶態進程提供了一組IO相關的系統調用: select/poll/epoll, 這三個系統調用功能類似, 在使用方法和性能等方面存在一些差異. 使用它們, 用戶態的進程可以"監控"自己感興趣的文件描述符, 當這些文件描述符的狀態發生改變時, 比如可讀或者可寫了, 內核會通知進程去處理... ...


linux內核為用戶態進程提供了一組IO相關的系統調用: select/poll/epoll, 這三個系統調用功能類似, 在使用方法和性能等方面存在一些差異. 使用它們, 用戶態的進程可以"監控"自己感興趣的文件描述符, 當這些文件描述符的狀態發生改變時, 比如可讀或者可寫了, 內核會通知進程去處理, 這裡的文件描述符可以是socket, 設備文件, 管道等. 使用這組系統調用, 用戶態可以實現事件迴圈機制, 比如redis源碼中就基於此實現了自己內部使用的事件迴圈, 同樣還有很多其他專門提供事件迴圈機制的開源庫. 這裡通過一個驅動模塊實現的poll介面, 去分析內核中poll系統調用的實現原理. 主要討論了以下3個問題:

  1. 用戶態進程如何使用poll系統調用?
  2. 內核如何處理poll系統調用?
  3. 怎樣調試從進程發起poll調用到返回的過程?

問題1

用戶態進程如何使用poll系統調用?

簡單來說, 使用poll的時候, 進程需要告訴內核自己關心哪些文件描述符, 關心它們的什麼事件, 這些都是通過參數傳遞給poll系統調用的. 下麵是手冊中對poll的詳細說明:

POLL(2)                                                                                                                       Linux Programmer's Manual                                                                                                                       POLL(2)

NAME
       poll, ppoll - wait for some event on a file descriptor

SYNOPSIS
       #include <poll.h>

       int poll(struct pollfd *fds, nfds_t nfds, int timeout);

       #define _GNU_SOURCE         /* See feature_test_macros(7) */
       #include <signal.h>
       #include <poll.h>

       int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *tmo_p, const sigset_t *sigmask);

DESCRIPTION
       poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O.  The Linux-specific epoll(7) API performs a similar task, but offers features beyond those found in poll().

       The set of file descriptors to be monitored is specified in the fds argument, which is an array of structures of the following form:

           struct pollfd {
               int   fd;         /* file descriptor */
               short events;     /* requested events */
               short revents;    /* returned events */
           };

       The caller should specify the number of items in the fds array in nfds.

poll接受三個參數, 其中pollfd的數組用來告訴內核, 進程關心哪些文件描述符, 結構體的fd欄位是文件描述符的值, events是關心的事件, 比如希望fd可讀時收到內核通知, 就可以設為POLL_IN, 這個events欄位支持位或, 也就是關心的多種事件可以用或運算一起計算出events的最終數值, revents欄位表示poll系統調用返回之後, 在該fd上發生的事件. poll的第二和第三個參數分別表示數組的大小和超時時間, 其中timeout以毫秒為單位, 如果timeout==0, poll會立即返回, 如果timeout < 0, poll會一直等待, 直到fds中期待的事件發生, 或者進程收到信號, 或者其他原因進程退出了. 當fds中的事件沒有發生或者超時時間沒到時, 進程就會處於睡眠狀態. poll的返回值反映了三種可能的結果, 1) 出錯, 2) 超時, 3) 發生期待事件的fd的數量. 其他的信息可以自行閱讀manual.

以下代碼會用來發起poll調用, 然後調試poll的實現:

/*ignore include headers*/
int main(int argc, char *argv[])
{
	int dev_fd = open("/dev/cdev03", O_RDWR);
	if (dev_fd < 0) {
		perror("Can not open device file");
		return -1;
	}

	struct pollfd pollfd = {
		.fd = dev_fd,
		.events = POLL_IN,
		.revents = 0,
	};

	char buf[1024];
	int max_poll_calls = 3;
	while (max_poll_calls) {
		int ret = poll(&pollfd, 1, -1);
		if (ret == 1) {
			memset(buf, 0, 1024);
			read(dev_fd, buf, 1024);
			printf("poll_reader recv data: %s\n", buf);
		}
		max_poll_calls--;
	}

	close(dev_fd);
	return 0;
}

代碼中poll設備文件"/dev/cdev03"的狀態變化, 在poll三次之後退出.

問題2

內核如何處理poll系統調用?

因為進程傳遞給內核的可能是多個文件描述符, 所以在poll的實現中也需要遍歷這些fd並檢查它們的狀態, 實際poll的實現涉及到比較多的數據結構, 這裡先簡單概括一下進入到poll系統調用之後內核的處理邏輯:

# ATTENION: we are in poll syscall now

0) 計算超時狀態初始值;

while True:
	for fd in fds:
		1) 獲取當前fd的狀態;
		2) 記錄fd的狀態;
		3) 對符合條件的fd計數;

	if 存在符合條件的fd 或者 超時時間到:
		break

	4) 調用schedule相關API, 讓出CPU, 當前進程開始帶有超時的睡眠;

	5) 更新超時狀態;
	# 如果進程被喚醒, schedule調用就會返回, 進程將在內核態, 繼續這個迴圈

以上就是poll實現中的核心邏輯, 當然, 實際情況還是會稍微複雜億點的, 以上描述中省略了進程被信號喚醒等處理邏輯. 後面會用一個字元設備驅動, 跟蹤這個實現過程, 下麵是設備驅動和poll系統調用在交互過程中各自的職責劃分:

  • 內核怎麼獲取fd的狀態?
    對於字元設備驅動, 它要實現file_operations中的poll介面, 內核在步驟1)會調用, 得到設備的狀態
  • 設備怎麼通知進程設備的狀態發生了變化?
    在設備驅動實現的poll介面被調用時, 會使用poll_wait傳遞給調用者一個等待隊列, 調用驅動介面的上層代碼在這個隊列中插入元素, 通過這個元素, 可以間接找到睡眠的進程, 當有數據寫入設備時, 驅動模塊的write介面被調用, 驅動代碼可以在write介面中對這個等待隊列進行喚醒操作, 從而實現喚醒進程. 具體的數據結構細節在後面的調試過程中展開.

問題3

怎樣調試從進程發起poll調用到返回的過程?

這裡構造的場景如下:

  1. 實現一個字元設備驅動, 驅動中實現了poll, write, read介面;
  2. 在內核中插入該模塊, 在/dev下生成設備文件節點;
  3. 啟動一個用戶態進程, 並讓它後臺運行, 在進程中打開設備文件, 對該文件進行poll操作, 開始時設備數據為空, 進程將睡眠
  4. 使用echo命令向設備文件寫入數據, 驅動的write介面被調用, 睡眠的進程被喚醒, 並讀取設備數據;

在對poll的實現有了一個基本瞭解之後, 調試面臨的第一個問題就是找到這個系統調用的入口, 這裡提供兩個調試技巧:

  1. 你知道在linux內核中系統調用使用SYSCALL_DEFINEx巨集定義, 可以直接在代碼中用正則表達式SYSCALL_DEFINE.*poll去搜索poll系統調用的位置, 然後在入口打斷點, 開始調試即可.
  2. 你不知道poll的入口在那, 但是在你的字元設備驅動中實現了poll介面, 這個介面一定會出現在poll系統調用的調用鏈上, 可以在你的驅動模塊上打斷點, 斷點命中之後, 看調用棧找到syscall的入口, 再進一步調試. 這種方法需要藉助內核提供的gdb腳本載入驅動模塊的調試信息, 否則gdb無法獲得指令和源文件中各行的對應信息以及其他的符號信息.

關於調試的環境問題, 可以參考之前的文章, 以下是調試過程的視頻記錄:

poll系統調用涉及到的重要數據結構, 以及它們之間的關係總結如下:
image

在邏輯上可以分成如圖所示的兩個部分, 分別和poll系統調用的上層實現以及驅動模塊的poll介面實現相關. 各數據結構的作用如下:

  1. 進程進入poll系統調用時, 內核對poll_wqueues的各個成員進行初始化, 包括:

    • 用一個預設的函數初始化pt的_qproc函數指針;
    • 用current初始化polling_task, 記錄發起poll系統調用的進程;
    • 虛線框中的成員嵌套著wait_queue_entry, 這個被嵌套的數據類型, 是將來真正插入到驅動模塊提供的等待隊列wait_queue_head的節點;
  2. 當驅動模塊的poll介面被上層調用時:

    • 驅動代碼需要調用poll_wait函數, 以自己維護的等待隊列wait_queue_head作為參數, 並透傳poll_table指針和file指針;
      • 在poll_wait的實現中, 會檢查poll_table的_qproc是否為空, 不為空則繼續透傳參數, 調用_qproc;
        • 在_qproc中, 會從poll_wqueues中獲取一個空閑的poll_table_entry, 初始化圖中的三個成員, 其中的wait_queue_entry:
          • private指針被設為poll_wqueues的地址, 這樣將來被喚醒時就可以找到之前睡眠的進程, 也就是polling_task;
          • func被設為一個預設的函數,將來這個節點所屬的等待隊列被喚醒時, func被調用, 根據private指針找到要喚醒的進程;
          • 通過鏈表操作, 將節點插入到等待隊列中;
  3. 當有數據寫入設備時:
    驅動模塊檢測到設備有數據可讀了, 需要喚醒傳遞給poll_wait的等待隊列, 這時隊列上每個節點的func都會被調用, 最終之前睡眠的進程被喚醒;

  4. 當設備可寫時, 喚醒過程類似, 只是使用的隊列不同.

概括下來:

  • 驅動模塊只要維護自己的等待隊列, 在poll介面的實現中, 調用上層提供的poll_wait向隊列中插入元素, 並返回當前的設備狀態;
  • 驅動的其他部分在合適的時機對等待隊列執行喚醒操作;
  • poll系統調用的上層實現代碼, 負責維護一套數據結構, 記錄插入到等待隊列中的節點, 給節點進行必要的設置, 使得通過節點能夠喚醒正確的進程;

總結

設備驅動的開發是在內核提供的框架下進行的, 為了降低驅動的開發難度, 快速支持各種新設備, 這套框架的設計必然要經得住考驗, 這也導致驅動的開發存在很多模板一樣的套路, 有人戲稱為"完形填空". 但是以驅動開發為出發點, 深入瞭解內核的各個模塊, 個人感覺是學習linux的一個很好的方式. 歡迎加入技術討論qq群: 838923389 一起研究linux相關的底層技術.

本文來自博客園,作者:kfggww,轉載請註明原文鏈接:https://www.cnblogs.com/kfggww/p/17653270.html


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

-Advertisement-
Play Games
更多相關文章
  • 知乎網友問 鏈式重載是我自己創造的一個詞,意思是方法A里處理一下參數,return另一個方法A,第二個方法A里處理一下參數調第三個方法A,就這樣無限迴圈下去直到調到真正能出結果的方法A。 本人學藝不精,偶然進行C#開發,感覺看代碼要吐。比如看到某處調用方法A,除非有某個特征顯眼的參數,否則根本不知道 ...
  • # [The database operation was expected to affect 1 row(s), but actually affected 0 row(s); 解決樂觀併發](https://www.raokun.top/archives/thedatabaseoperatio ...
  • Trigger:當某些條件滿足時會觸發一個行為。 一、觸發器的類型 數據變化觸髮型:Trigger / DataTrigger 多條件觸髮型:MultiTrigger / MultiDataTrigger 事件觸髮型:EventTrigger 二、Trigger Trigger:Property用來 ...
  • # Unity UGUI的Toggle(覆選框)組件的介紹及使用 ## 1. 什麼是Toggle組件? Toggle(覆選框)是Unity UGUI中的一個常用組件,用於實現覆選框的功能。它可以被選中或取消選中,並且可以代碼通過其制控狀態。 ## 2. Toggle組件的工作原理 組Toggle件由 ...
  • 圖形系部分主要有`Shape`和`Goemetry`兩大類,可以直接對`Shape`進行排版、設定風格和數據綁定,後者則需要通過視覺元素才能在屏幕上顯示出來。動畫則一般分為簡單動畫、關鍵幀動畫以及沿路徑運動的動畫,日常使用過程種應該是關鍵幀動畫用的多一點,當然除了文章中例舉的關鍵幀類型,還有其他很多... ...
  • 哈嘍大家好,我是鹹魚 在《[SELinux 入門 pt.1](https://mp.weixin.qq.com/s?__biz=MzkzNzI1MzE2Mw==&mid=2247486365&idx=1&sn=4b81b3cc70b085eec6f0a595fda719fb&chksm=c2930b ...
  • 平時使用windows電腦和手機的時候,配置時間、時區都非常的簡便。但在命令行的linux下,就不知如何下手。本文就Centos7舉例,依次說明下時間日期和NTP\CHRONY的配置。 由於在伺服器側時間同步常用於集群之間,所以本文後面會針對集群間的配置做舉例。文中涉及到的網路安裝軟體部分,預設為在 ...
  • 虛擬記憶體的主要作用是提供更大的地址空間,使得每個進程都可以擁有大量的虛擬記憶體,而不受物理記憶體大小的限制。此外,虛擬記憶體還可以提供記憶體保護和共用的機制,保護每個進程的記憶體空間不被其他進程非法訪問,並允許多個進程共用同一份物理記憶體數據,提高了系統的資源利用率。虛擬記憶體的實現方式有分段和分頁兩種,其中分頁... ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...