container_of() 巨集的源碼分析

来源:https://www.cnblogs.com/puhanzhou/archive/2022/05/01/16212798.html
-Advertisement-
Play Games

簡介 container_of(ptr, type, member)是內核中的經典函數之一。該函數的作用是:根據結構體中一個成員的地址,找到結構體的地址。這個函數是內核實現面向對象的基礎設施,且最近在學習中經常見到這個函數,於是筆者在內核中查看了該函數的實現,故在此記錄。本文原本是為了展示conta ...


簡介

container_of(ptr, type, member)是內核中的經典函數之一。該函數的作用是:根據結構體中一個成員的地址,找到結構體的地址。這個函數是內核實現面向對象的基礎設施,且最近在學習中經常見到這個函數,於是筆者在內核中查看了該函數的實現,故在此記錄。本文原本是為了展示container_of的實現,但寫著寫著,發現有些內建函數與GNU C拓展的使用,所以就順便查了資料,也一併記錄於此,寫得比較亂,請大家諒解。

基礎知識

結構體在記憶體中的分佈,是按照成員的順序分配記憶體,同時保持記憶體對齊的要求

實現分析

源碼

該函數在5.17.5中的實現在include/linux/container_of.h

5.16之前,這個巨集都被放在include/linux/kernel.h

源碼如下:

/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 */
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	static_assert(__same_type(*(ptr), ((type *)0)->member) ||	\
		      __same_type(*(ptr), void),			\
		      "pointer type mismatch in container_of()");	\
	((type *)(__mptr - offsetof(type, member))); })

參數

  • ptr:成員指針
  • type:結構體類型
  • mem:成員在結構體里的名稱

第一行:賦值

將傳入的成員變數的地址,轉換為void *類型,並賦給另一個值。這個操作筆者沒有理解,所以找了以前版本的源碼來進行分析,在2.6.23里,他的實現是這樣的:

#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

這個版本中,第一行的作用其實相當於賦值+檢查,考慮如果傳進來的指針類型和member不一致,編譯器會報warning。
在使用查看了相關的log之後,發現這個巨集是在提交c7acec713d14c被改變的,改變的原因是:如果結構體內引入了一個非const數組成員,那麼這個指針就會產生變數賦值給常量的問題,這會在gcc-4.9中產生一個warning: initialization from incompatible pointer type。這一筆改動抽離出了類型檢查,但__mptr仍留在原處,筆者實在不清楚這個操作的深意,又或許只是歷史遺留問題?

第二行:檢查

static_assert(__same_type(*(ptr), ((type *)0)->member) ||	\
            __same_type(*(ptr), void),			\
            "pointer type mismatch in container_of()");	\

這個地方在5.16後修改成static_assert,之前使用的是BUILD_BUG_ON()這個巨集,他和static_assert被定義在同一個文件里,感興趣的朋友們可以去看一看相關實現,根據commit message顯示,使用static_assert可以給出更加直接的錯誤提示,並且在理論上可以提升一點點的build速度(commit message里寫了a tiny bit faster

一個斷言,用於檢查ptrmember的類型一致性。這個斷言函數static_assert()我們先放在一邊,來分析一下這個斷言的第一個參數:內部使用了__same_type()這個巨集,來看看這個巨集的實現:

/* Are two types/vars the same type (ignoring qualifiers)? */
#define __same_type(a, b) __builtin_types_compatible_p(typeof(a), typeof(b))

這個巨集使用了兩個函數:__builtin_types_compatible_p()typeof()

typeof()想必大家都比較熟悉了,它是一個GNU C的拓展,作用是獲取變數的類型。文檔地址:https://gcc.gnu.org/onlinedocs/gcc/Typeof.html
__builtin_types_compatible_p(type1, type2)是一個GNU C的內建函數,用於比較兩個類型是否相等,若相等則返回1,不等則返回0。需要註意的是,這個函數的參數並不是表達式,而是變數類型,所以需要使用typeof()先取得變數類型後再傳入。文檔地址:https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

最後我們再來看一看static_assert(),這個函數的實現位於include/linux/build_log.h,源碼如下:

#define static_assert(expr, ...) __static_assert(expr, ##__VA_ARGS__, #expr)
#define __static_assert(expr, msg, ...) _Static_assert(expr, msg)

關於C巨集定義中#符號的用法,可以總結為以下兩點:

  1. 前加##,轉換為合法標識符
#define to_symbol(x)  T_##x

// 下麵這句等效於 int T_1 = 10;
int to_symbol(1) = 10;
  1. 前加#,轉換為字元串
#define to_string(x) #x

// 下麵這句等效於 "a+b+c"
to_string(a+b+c);

##__VA_ARGS__又是什麼呢?它的功能有兩個:

  1. 如果可變參數列表為空,使編譯器忽略它以及它前面的逗號
  2. 如果可變參數列表不為空,編譯器將其替換為可變參數列表

接著再來看一看_Static_assert(expr, msg, ...),這是一個C11特性,用來在編譯時測試expr的正確性,如果正確則什麼都不會發生,如果錯誤,則列印指定信息msg。文檔地址:https://www.gnu.org/software/gnulib/manual/html_node/assert_002eh.html

綜上所述,第二行的作用就是:判斷傳入的ptrmember(或者void)是否為同一類型,若否,則列印"pointer type mismatch in container_of()"

第三行:定址

這一行真正用於獲取結構體的地址。

((type *)(__mptr - offsetof(type, member)));

看上去很簡單!就是用傳進來的成員變數地址值減去它在結構體里的偏移值嘛!
邏輯上來講確實很簡單,但是如何實現呢?如何在不同的對齊下讓這個函數均能成功運行呢?讓我們帶著這個疑問走進offsetof()這個巨集:

// At include/linux/stddef.h
#define offsetof(TYPE, MEMBER)	((size_t)&((TYPE *)0)->MEMBER)

也很簡單對吧?把0地址轉換成結構體類型指針,然後利用這個特殊的結構體指針獲取member,然後再對member取地址,得到的這個值就是member相對於0地址的偏移值,這個偏移值不就是member相對於結構體首地址的偏移值嘛!
看到這裡,如果你和筆者一樣是內核初學者,你可能會和筆者一樣驚訝:0地址還能這麼用?!!筆者也是在發出了這樣的感嘆之後,才決定記錄下這篇隨筆。
offsetof這個巨集還有另一個實現,即調用GNU C的內建函數__builtin_offsetof,本質上和上面的定義是一致的。

總結

這個巨集包括了三步:賦值、檢查、定址。筆者分析了2.6.23中的賦值操作目的與最新的5.17.5中的檢查和定址操作。
在最後希望詢問看到這篇文章的朋友們一個問題:為什麼最新的版本還需要賦值給__mptr,能否在第三行中直接使用(void *)ptr代替__mptr

原創文章,如有錯漏,敬請補充指正,如對於文章風格有建議,請在評論區直接提出,感謝。


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

-Advertisement-
Play Games
更多相關文章
  • 參考資料 The WebSocket Protocol(RFC 6455) Spring Boot 2.6.6 官方文檔 SockJS 什麼是 WebSocket ? WebSocket協議提供了一種標準化的方法,通過單個TCP連接在客戶機和伺服器之間建立全雙工、雙向的通信通道。它是一種不同於HTT ...
  • 前言 數美滑塊的加密及軌跡等應該是入門級別的吧,用他們的教程和話來說 就一個des 然後識別缺口位置可以用cv2或者ddddoc 軌跡 也可以隨便模擬一個,這些簡單的教程 在csdn已經有一大把可以搜到的,但是卻很少人告訴你,它的js好像是一周更新一次,更 新之後post的參數key和des的key ...
  • 今天給大家帶來的這篇文章是關於機器學習的,機器學習有其獨特的數學基礎,我們用微積分來處理變化無限小的函數,並計算 它們的變化;我們使用線性代數來處理計算過程;我們還用概率論與統計學建模不確定性。 在這其中,概率論有其獨特的地位,模型的預測結果、學習過程、學習目標都可以通過概率的角度來理解。 與此同時 ...
  • 除了從文件載入數據,另一個數據源是互聯網,互聯網每天產生各種不同的數據,可以用各種各樣的方式從互聯網載入數據。 一、瞭解 Web API Web 應用編程介面(API)自動請求網站的特定信息,再對這些信息進行可視化。每次運行,都會獲取最新的數據來生成可視化,因此即便網路上的數據瞬息萬變,它呈現的信息 ...
  • ​ 我們現在還是在學習階段因此我們不用配置那麼多的jdk,配置一個jdk8就夠應付日常的學習了。前面的文章我儘量寫詳細一些照顧剛入坑的朋友。後文還有教大家怎麼使用企業版的idea。 一、開發環境的搭建 1)官網下載:官網鏈接 Java Downloads | Oracle ​ 不過官網要註冊ORAC ...
  • 相信大家對二維碼都不陌生,生活中到處充斥著掃碼登錄的場景,如登錄網頁版微信、支付寶等。最近學習了一下掃碼登錄的原理,感覺蠻有趣的,於是自己實現了一個簡易版掃碼登錄的 Demo,以此記錄一下學習過程。 ...
  • 一個工作了6年的Java程式員,在阿裡二面,被問到“volatile”關鍵字。 然後,就沒有然後了… 同樣,另外一個去美團面試的工作4年的小伙伴,也被“volatile關鍵字“。 然後,也沒有然後了… 這個問題說實話,是有點偏底層,但也的確是併發編程裡面比較重要的一個關鍵字。 下麵,我們來看看普通人 ...
  • 在幾年前windows10系統就註意到,藍牙耳機連接windows電腦後會出現兩個模式,一個是Hands-free AG Audio(即免提模式,以下簡稱Hands-free),一個是stereo(立體聲模式),並且發現只有Hands-free模式才能使用耳機的麥克風,但是音質會差好多,stereo ...
一周排行
    -Advertisement-
    Play Games
  • 1、預覽地址:http://139.155.137.144:9012 2、qq群:801913255 一、前言 隨著網路的發展,企業對於信息系統數據的保密工作愈發重視,不同身份、角色對於數據的訪問許可權都應該大相徑庭。 列如 1、不同登錄人員對一個數據列表的可見度是不一樣的,如數據列、數據行、數據按鈕 ...
  • 前言 上一篇文章寫瞭如何使用RabbitMQ做個簡單的發送郵件項目,然後評論也是比較多,也是準備去學習一下如何確保RabbitMQ的消息可靠性,但是由於時間原因,先來說說設計模式中的簡單工廠模式吧! 在瞭解簡單工廠模式之前,我們要知道C#是一款面向對象的高級程式語言。它有3大特性,封裝、繼承、多態。 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 介紹 Nodify是一個WPF基於節點的編輯器控制項,其中包含一系列節點、連接和連接器組件,旨在簡化構建基於節點的工具的過程 ...
  • 創建一個webapi項目做測試使用。 創建新控制器,搭建一個基礎框架,包括獲取當天日期、wiki的請求地址等 創建一個Http請求幫助類以及方法,用於獲取指定URL的信息 使用http請求訪問指定url,先運行一下,看看返回的內容。內容如圖右邊所示,實際上是一個Json數據。我們主要解析 大事記 部 ...
  • 最近在不少自媒體上看到有關.NET與C#的資訊與評價,感覺大家對.NET與C#還是不太瞭解,尤其是對2016年6月發佈的跨平臺.NET Core 1.0,更是知之甚少。在考慮一番之後,還是決定寫點東西總結一下,也回顧一下.NET的發展歷史。 首先,你沒看錯,.NET是跨平臺的,可以在Windows、 ...
  • Nodify學習 一:介紹與使用 - 可樂_加冰 - 博客園 (cnblogs.com) Nodify學習 二:添加節點 - 可樂_加冰 - 博客園 (cnblogs.com) 添加節點(nodes) 通過上一篇我們已經創建好了編輯器實例現在我們為編輯器添加一個節點 添加model和viewmode ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...
  • 類型檢查和轉換:當你需要檢查對象是否為特定類型,並且希望在同一時間內將其轉換為那個類型時,模式匹配提供了一種更簡潔的方式來完成這一任務,避免了使用傳統的as和is操作符後還需要進行額外的null檢查。 複雜條件邏輯:在處理複雜的條件邏輯時,特別是涉及到多個條件和類型的情況下,使用模式匹配可以使代碼更 ...
  • 在日常開發中,我們經常需要和文件打交道,特別是桌面開發,有時候就會需要載入大批量的文件,而且可能還會存在部分文件缺失的情況,那麼如何才能快速的判斷文件是否存在呢?如果處理不當的,且文件數量比較多的時候,可能會造成卡頓等情況,進而影響程式的使用體驗。今天就以一個簡單的小例子,簡述兩種不同的判斷文件是否... ...
  • 前言 資料庫併發,數據審計和軟刪除一直是數據持久化方面的經典問題。早些時候,這些工作需要手寫複雜的SQL或者通過存儲過程和觸發器實現。手寫複雜SQL對軟體可維護性構成了相當大的挑戰,隨著SQL字數的變多,用到的嵌套和複雜語法增加,可讀性和可維護性的難度是幾何級暴漲。因此如何在實現功能的同時控制這些S ...