無處不在的container_of

無處不在的container_of

linux 內核中定義了一個非常精煉的雙向循環鏈表及它的相關操作。如下所示:

struct list_head {struct list_head* next, * prev;
};

ubuntu 12.04 中這個結構定義在 /usr/src/linux-headers-3.2.0-24-generic/include/linux/types.h 中,各種操作定義在 list.h 中。可以通過 grep “struct list_head {” 來查找到,也可以使用 ctags 在 include 目錄下生成 tags 文件,然后在 tags 里面查找到。

這個鏈表只有指針域,沒有數據域,所以我們不能直接拿來使用。而是需要把這個結構體嵌在我們自己定義的結構體里,可以放在任意位置,開頭,中間或結尾。比如:

struct book {int sn;char name[NAMESIZE];int price;struct list_head node;   //內核鏈表結構體放在最后
};

我們使用內核提供的鏈表操作函數或宏來快速地建立一個雙向鏈表,如下所示:

int main()
{struct book* bp;int i;LIST_HEAD(head);  //宏,建立頭結點for(i = 0 ; i < 3; i++){bp = (struct book*)malloc(sizeof(struct book));   /*if error*/bp->sn = i;snprintf(bp->name, NAMESIZE, "book%d", i);bp->price = rand()%60 + 20;/*insert*/list_add(&bp->node, &head);  //將該結點插入鏈表}/*TODO: travel*/return 0;
}

注意, LIST_HEAD(head) 不是函數,而是宏,所以不要對它傳了一個沒有定義的變量感到疑惑。這個宏的定義如下:

#define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) \struct list_head name = LIST_HEAD_INIT(name)

它的作用是生成了一個變量名為 name 的頭結點,并把指針域的值都初始化為指向自身。

list_add 函數實現把節點插入到以 head 為頭結點的鏈表中。

上述一段簡短的代碼,快速地生成了一個可供自己使用的雙向循環鏈表,其結構如下圖所示:


從圖中可以看到,每個節點中的 next 和 pre 指針指向的都是另一個結點中的 node 成員的地址,而不是整個節點的首地址,所以,問題就來了,我們要訪問節點中的其它成員怎么辦?那我們就必須通過 node 成員的地址,獲取它所在的節點的首地址。方法很簡單,用 node 成員的地址減去它相對首地址的偏移量即可,假設某個節點的 node 成員的地址是 ptr,偏移量暫時先用 offset(node, struct book) 來表示,則公式如下:

(struct book*) ( (char*)ptr - offset(node, struct book) )
~~~~~~~~~~~~~~~   ~~~~~~~~~~~   ~~~~~~~~~~~~~~~~~~~~~~~~~類型轉換        轉成指向 char 的指針  node 成員在結構體中的偏移量

那偏移量又該如何來計算呢,假想 struct book 這個結構體的首地址為 0,那么 node 節點的地址不就是偏移量嗎?因此我們可以把地址 0 先轉換成指向 struct book 的指針,再從這個指針取它的成員 node 的地址,就可以計算得偏移量,公式如下:

((size_t)    &((struct book*)0)->node)~~~~~~~~    ~ ~~~~~~~~~~~~~~~~~
轉成無符號整數 取址

也許不少人和我一樣,有一個疑惑,地址 0 轉換成指針,不是 NULL 嗎,用它去訪問成員,不是非法的嗎?我一開始也百思不得其解,后來明白了,取成員的地址,并不等于訪問該成員。我們可以用下面一段程序來驗證一下:

#include <stdio.h>struct st
{int a;      //0char b;     //4int c;      //8
};int main()
{printf("c addr  : %d\n", &((struct st*)0)->c);printf("c value : %d\n",  ((struct st*)0)->c);return 0;
}

在 ubuntu 12.04 32bit 上用 GCC 4.6.3 編譯后運行的結果:

believe@ubuntu:~$ ./a.out
c addr  : 8
段錯誤 (核心已轉儲)

我是這么來理解的,地址 0 開始的這一段內存空間,就像是透明的充滿機關的盒子,當里面放著結構體時,即使我們不打開這個盒子,從外面也可以知道里面的某個成員的位置(即地址),但你想打開盒子取出某個成員看看它的具體的值是多少時,卻是萬萬不可的,會中箭而亡!

通過上面的步驟,我們就取到了節點的首地址,內核它也是這么做的,把上面表達式里的 struct book 換成 TYPE/type,把 node 換成 MEMBER/member,就是內核定義的樣子:

// 定義在 include/linux/list.h 中
#define list_entry(ptr, type, member) \container_of(ptr, type, member)// 定義在 include/linux/kernel.h 中
#define container_of(ptr, type, member) ({          \(type*)( (char*)ptr - offsetof(type,member) );})// 定義在 include/linux/stddef.h 中
// 巧妙之處在于將地址0強制轉換為type類型的指針,從而定位到member在結構體中偏移位置。編譯器認為0是一個有效的地址,從而認為0是type指針的起始地址。
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE*)0)->MEMBER)

上面 container_of 為了便于理解,進行了簡化,實際完整的定義是這樣的:

/*** 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) ({          \const typeof( ((type *)0)->member ) * __mptr = (ptr); \(type *)( (char *)__mptr - offsetof(type,member) );})

container_of 宏分為兩部分,

第一部分:const typeof( ((type *)0)->member ) *__mptr = (ptr);

通過 typeof 定義一個member指針類型的指針變量 __mptr,(即 __mptr 是指向 member 類型的指針),并將 __mptr 賦值為 ptr。

第二部分: (type * )( (char * )__mptr - offsetof(type,member) ) ,通過 offsetof 宏計算出 member 在 type 中的偏移,然后用 member 的實際地址 __mptr 減去偏移,得到 type 的起始地址,即指向 type 類型的指針。

第一部分的目的是為了將統一轉換為 member 類型指針。

它增加了一句,作用是通過 GCC 特有的類型運算符 typeof,取得 member 成員的類型,定義了一個此類型的指針 __mptr,并使它的值等于 ptr,后面就用 __mptr 替代 ptr 操作。這句話我想其實是多余的,可能是為了做最大的保護吧,可以通過下面這個例子來理解為什么要重新定義一個變量。

用 define 定義一個最簡陋 MAX(a,b) 宏,像下面這樣:

#define MAX(a,b) a > b ? a : b

這樣的定義不堪一擊,MAX(a+1, b) 的調用就會讓它出錯。
在 windows 下,最嚴謹的定義也不過如此了:

#define MAX(a,b) ((a)>(b)?(a):(b))

可是這樣的定義,在面對這樣的調用時,依然無能為力:MAX(++a, ++b),大家可以實驗一下,其中的較大值會被加 2。

但在 linux 下,使用 typeof 運算符,可以解決這個問題:

#define MAX(a,b) ({typeof(a) A=a,B=b; A > B ? A : B;})

不過這種在 () 里包含 {} 的定義方法并不被標準 C 支持。

對于上面的這種定義,或許還是有人會有這樣的疑問,假如調用 MAX(++a, ++b),a 還是會被加兩次啊,typeof(++a) 時會加一次,A=++a 時又加一次。其實不會,因為 typeof 并不是一個運行時的函數或運算符,它和 sizeof 一樣,是在編譯的時候就確定了。下面這個例子可以驗證:

int main()
{int a = 5;typeof(++a) b = 3;  //等同于 int b = 3;printf("a = %d, b = %d\n", a, b);return 0;
}---------------
運行結果:
a = 5, b = 3

如果我們把內核鏈表結構體放在我們自己的結構體的開頭,其實就不需要這兩個宏了,只需要進行類型轉換即可。

另外,在 windows 內核中也有類似的宏,由于 windows 沒有 typeof 運算符,所以就顯得簡單了一些,定義如下:

#define CONTAININT_RECORD(address, type, field) \((type*)((PCHAR)(address) - (PCHAR)(&((type*)0)->field)))

下面的程序將完整地展示如何使用 container_of 實現遍歷。

int main()
{struct list_head *cur;struct book* bp;int i;LIST_HEAD(head);  //宏,建立頭結點for(i = 0 ; i < 3; i++){bp = (struct book*)malloc(sizeof(struct book));   /*if error*/bp->sn = i;snprintf(bp->name, NAMESIZE, "book%d", i);bp->price = rand()%60 + 20;/*insert*/list_add(&bp->node, &head);  //將該結點插入鏈表}/*travel*/__list_for_each(cur, &head)//這也是一個宏,展開后是這樣:for (cur = (&head)->next; cur != &head; cur = (&head)->next){bp = list_entry(cur, struct book, node);//list_entry 與 container_of 等價printf("sn = %d, name = %s, price = %d\n", bp->sn, bp->name, bp->price);}return 0;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/448055.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/448055.shtml
英文地址,請注明出處:http://en.pswp.cn/news/448055.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

程序員學習能力提升三要素

摘要&#xff1a;IT技術的發展日新月異&#xff0c;新技術層出不窮&#xff0c;具有良好的學習能力&#xff0c;能及時獲取新知識、隨時補充和豐富自己&#xff0c;已成為程序員職業發展的核心競爭力。本文中&#xff0c;作者結合多年的學習經驗總結出了提高程序員學習能力的三…

時間,數字 ,字符串之間的轉換

package com.JUtils.base;import java.sql.Timestamp; import java.text.SimpleDateFormat;/*** 轉換工具類<br>* 若待轉換值為null或者出現異常&#xff0c;則使用默認值**/ public class ConvertUtils {/*** 字符串轉換為int*** param str * 待轉換的字符串* param …

宏定義及相關用法

宏定義及相關用法 歡迎各位補充 目錄 一些成熟軟件中常用的宏定義&#xff1a;使用一些內置宏跟蹤調試&#xff1a;宏定義防止使用時錯誤&#xff1a;宏與函數 帶副作用的宏參數 特殊符號&#xff1a;’#’、’##’ 1、一般用法2、當宏參數是另一個宏的時候 __VA_ARGS__與##…

解決:Cannot read property ‘component‘ of undefined ( 即 vue-router 0.x 轉化為 2.x)

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 vue項目原本是用0.x版本的vue-router&#xff0c;但是去報出&#xff1a;Cannot read property component of undefined 這是因為版本問…

AMD Mantle再添新作,引發下代GPU架構猜想

摘要&#xff1a;今年秋天即將發布的《希德梅爾文明&#xff1a;太空》將全面支持AMD Mantle API&#xff0c;如此強大的功能背后離不開強大的CPU、GPU支持。上周AMD爆出了下一代海盜島R9 300系列&#xff0c;據網友猜測海盜島家族可能用上速度更快的HBM堆棧式內存。 小伙伴們…

不作35歲的程序員?

程序員三部曲--不作35歲的程序員?摩西2000 在中國&#xff0c;程序員不能超過35歲&#xff0c;似乎已經是不爭的事實&#xff0c;軟件開發工作就是青春飯&#xff0c;頂多靠畢業這十年的時間&#xff0c;超過這個年齡&#xff0c;要不成功躍身成為管理者&#xff0c;要不轉…

linux下使用TC模擬弱網絡環境

linux下使用TC模擬弱網絡環境 模擬延遲傳輸簡介 netem 與 tc: netem 是 Linux 2.6 及以上內核版本提供的一個網絡模擬功能模塊。該功能模塊可以用來在性能良好的局域網中,模擬出復雜的互聯網傳輸性能,諸如低帶寬、傳輸延遲、丟包等等情 況。使用 Linux 2.6 (或以上) 版本內核…

CDN 是什么 、CDN 引入

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 CDN 的全稱是 Content Delivery Network&#xff0c;即內容分發網絡。 CDN的基本原理是廣泛采用各種緩存服務器&#xff0c;將這些緩存…

長壽的人會有的8個健康理念

長壽的人會有的8個健康理念。年輕的時候總是在揮霍身體健康&#xff0c;吸煙、喝酒沒有節制&#xff0c;到老了之后身體會出現各種問題。老年人如果想要身體健康、長壽的話&#xff0c;就要從日常生活習慣做起。下面小編就來介紹長壽的人會有的8個健康理念&#xff1a; 1、少…

Ubuntu下selenium+Chrome的安裝使用

Ubuntu下seleniumChrome的安裝使用 安裝 chrome 官網下載安裝包 sudo dpkg -i google-chrome-stable_current_amd64.deb whereis google-chrome 安裝selenium pip3 install selenium 下載chromedriver(火狐使用geckodriver)驅動 http://npm.taobao.org/mirrors/chromed…

shoot for用法

Look, there are people like Ross who need to shoot for the stars, with his museum, and his papers getting published.---《老友記》 而像羅斯這種人則追求卓越&#xff0c;博物館&#xff0c;發表論文。 爭取;為...而努力Were shooting this year for a 50% increase in…

VUE : 雙重 for 循環寫法、table 解析任意 list 、萬能表格組件、解析一維數組、動態生成 table 所有數據

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1.需求&#xff1a; 我想要一個 table 組件能在實際調用時動態生成所有的 tr 、td 。 后端返回的只是一個 list &#xff0c; 前端頁…

安全離職妙招

高招的離職&#xff0c;不但有可能讓前老板幫你說好話&#xff0c;讓前同事成為你的啦啦隊&#xff0c;未來若有好機會&#xff0c;還會想到你&#xff0c;只要你學會克服離職流程中的五個尷尬情境。 情境一、離職怎么提&#xff1f; 口頭請辭&#xff0c;最先告知上司。 有…

字節內推~

大佬們有興趣來字節約飯么&#xff0c;下面是內推鏈接~ 社招內推鏈接&#xff1a;https://job.toutiao.com/s/LwpKWU8 校招內推鏈接&#xff1a;https://job.toutiao.com/s/LwsFw6g

使用編輯工具快速創建實體對象的方法

快速創建java類 (\w)\s(.) /** $2 */\nprivate String $1; search Mode 為 Reqular expression 轉載于:https://www.cnblogs.com/otways/p/11283303.html

超詳細 圖解 : IntelliJ IDEA 逆向生成 JAVA 實體類

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 1.配置數據庫,&#xff0c;這里連接的是mysql。 2.填寫 連接數據庫的信息&#xff0c;填寫完成后可以點擊Test Connection,測試一下是否…

用面粉和醋洗頭 讓你的頭發黑亮又濃密

用面粉和醋洗頭發&#xff0c;別看這些最廉價、最普通的東西&#xff0c;卻能帶來意想不到的效果。調配這種洗頭液很簡單&#xff0c;取50&#xff5e;100克面粉&#xff0c;加入少許涼水調成稀面糊&#xff0c;倒入沸水中煮開&#xff0c;然后加入25&#xff5e;50克醋&#x…

leetcode練習——數組篇(1)(std::ios::sync_with_stdio(false);std::cin.tie(nullptr);)

題號1. 兩數之和&#xff1a; 給定一個整數數組 nums 和一個目標值 target&#xff0c;請你在該數組中找出和為目標值的那 兩個 整數&#xff0c;并返回他們的數組下標。 你可以假設每種輸入只會對應一個答案。但是&#xff0c;你不能重復利用這個數組中同樣的元素。 示例: …

intellij idea 中去除 @Autowired 注入對象帶來的紅色下劃線報錯提示

前些天發現了一個巨牛的人工智能學習網站&#xff0c;通俗易懂&#xff0c;風趣幽默&#xff0c;忍不住分享一下給大家。點擊跳轉到教程。 PS&#xff1a; 有 2 種方法&#xff0c;第 2 種方法更簡單&#xff0c;在此謝謝好心友人的評論。 方法1&#xff1a; idea中通過Autow…

根據目標選擇減肥方法 少做無用功

不同的美體目標適合的減肥方法也是不同的&#xff0c;有些人想減去大部分體重&#xff0c;而有些人只是想讓身體曲線更柔美&#xff0c;這就要求有相應的減肥方法&#xff0c;對癥下藥&#xff0c;才會讓減肥少做無用功。 目標&#xff1a;我想穿上小一碼的衣服 建議&#x…