contributed by < AmyLin0210
>
2021q1 Linux
在 container_of 中包含以下部份:
__extension__
的功用是在使用 GCC extension __typeof__
時,不會跳出警告。((type *)0)->member
將 0 這個位置強制轉型為 type *
__typeof__
得到 member 的型態,產生一個 pointer 給該型別的 __pmember
ptr
assign 給 __pmember
offsetof(type, member)
可以算出 member
在 type
這個 struct 中的位移量。(char*)__pmember
減去 offsetof(type, member)
,可以得到 struct 的起始位置。(type*)
再將起始位置轉行為 pointer to type
在這當中讓我最不解的是 ((type *)0)->member
這個操作,
參考 toastcheng 同學的作業與實驗,自己也試了一下程式碼:
定義出了一個 struct ,如下:
使用 gcc 產生組合語言
使用 diff 來比較產生的組合語言
發現除了 file name 以外其餘都相同,這個讓我感到意外。
typeof-2048
和 typeof-null
相同,我想可以解釋為都是對兩個位置強制轉型,所以會產生兩個同樣的組合語言。
但原先我預期 typeof-char
會有些不同的行為,因為已經宣告出一個 char 的變數 i
,所以組合語言中會多出對 i
做處理的部份,然而結果與我想像中的不同。
如同 toastcheng 同學的猜測,我想在 typeof 時並不會對當中的指標或參數做 dereference。
但除此之外,強制轉型如果在沒有取當中的數值做處理的情況下,是不是也不會做 dereference?
我測試了一下以下程式碼,當中的指標 i
皆指向 NULL,只有型態不同。
char.c
int.c
上述的程式碼在轉換成組合語言後使用 diff 做比較,
發現不管原參數的型態是否不同,結果仍然相同。
在這裡使用了 Designated Initializers,使得 head
內的 prev
與 next
都被初始化為自己本身的位置。
初始化 head
由於這是一個 circuler doubly linked list ,所以使用 head->prev
找到 linked list 的尾部,再將 node
接上。
將 node
移出這個 linked list
檢查 head
是否指回 head
自己,如果沒有指向其他 node ,便表示這個 linked list 為空。
檢查是否 prev
與 next
都指向同一個位置,若是相同便表示只有一個 node
將 list
整串 linked list 連接至 head
linked list 的最後面,形成一個新的環狀結構
將 head_from
後面到 node
前面的 linked list 裁下,連接至 head_to
後
未切割
切割後
利用快慢兩個指標來走訪節點,快指標一次走兩步,慢指標一次走一步。
當快指標走訪回最開頭時,慢指標會剛好走到中間。
假設這裡有 N 個節點,慢指標會在第 個位置
這段程式碼因為所有功能都有用函式包起,
乍看之下很像 merge sort 的 pseudocode。
運作邏輯如下:
確認是否只剩一個節點,如果只剩一個節點直接 return
尋找整個 list 的中心位置並把他切為兩半
對兩個 list 分別再進行 merge sort
完成後將兩邊 merge 起,這個步驟可以用下面圖解:
left
, q ( right )
, 與 sorted
三段將兩條 linked list 排序好並連接起
在資料型態的部分,
由於 list_head
就擁有了 tail
、 next
相關的資訊,所以我把上述的資料型態給縮減為以下:
也有針對這個改動修改其他對應的程式碼。
__attribute__
是一種檢查工具,主要是 gcc 在編譯時期會去查看。
在這裡選定的 target 是 nonnull
,表示在 cmp_func
這個 function pointer 中的第二及第三個參數指標不得為 NULL。
Declaring Attributes of Functions
考慮函式 func 接受一個 16 位元無號整數 N,並回傳小於或等於 N 的 power-of-2
由於以上函式的目標為回傳小於或等於 N 的 power-of-2,看到 return (N + 1) >> 1
,可以推測最後 N 的值會是
假設現在 func 的 input 為 1 << 15
,以下為執行順序:
在回傳值為 時,會發現有 overflow 的問題,應改為
return (N >> 1) + 1
來保證結果正確性。
在 linux/tools/include/linux/log2.h 內可以找到 is_power_of_2
的原始程式碼。
__attribute__((const))
是個 GNU compiler extension,他的目標為確保這個函式沒有讀取或修改任何的 global memory。
如果 n
為 2 的冪次 ( ),從 bit 的角度來看,就只有第 N 個 bit 為 1 ,其餘皆為零。
而 n-1
則會變成從第 N-1 個及前方的 bit 為 1 ,其餘皆為零
n
與 n-1
做 &
操作後會變成 0 。
好處是 Bitwise Operation 在硬體實做上比較有效率。
同樣在 linux/tools/include/linux/log2.h 內,可以找到 __roundup_pow_of_two
以及 __rounddown_pow_of_two
。
UL
代表 unsigned long int ( C99 標準, 6.4.4.1 )。
fls_long
代表的是找到第一個為一的 bit 在哪邊,
n
數值範圍應該要為 ~ fls_long(n - 1)
n
數值範圍應該要為 ~ fls_long(n) - 1
在 linux/tools/include/linux/bitops.h 中可以找到 fls_long
。
在這邊將 unsigned long l
分成 32 位元與 64 位元的兩種狀況去處理
在 linux/include/asm-generic/bitops/fls.h 內可以找到 fls
的程式原始碼。
fls
的函式目標為找到 x
內第一個唯一的 bit。
以上函式透過 bit mask 的方式一次次過濾是否前方有 1,進而算出第一個為 1 的 bit 在哪裡。
以下用 8 個 bit 來示範函式流程
在這裡我看到了 Linux kernel 的優雅操作,以上的函式都是用比較貼近硬體語言的方式操作,例如位移或者是 &
|
運算子,比起減法更有效率。
現在假設
_read = 2
_write = 3
count = 9
首先會先將 _read
、 _write
分作 lhs 與 rhs
找到由哪個 byte 開始讀取 bit 數值,示意圖中粉色方格為 source 。
將 source
中要讀取的片段複製到 data
內。
再將 data 的內容移動至 dest
目前的程式碼我認為有改進空間的部份有
在進入 while 迴圈之前,我把 src 對齊,確認在讀資料時,一定會從某個 byte 的第一個 bit 開始讀,節省 mask 的次數。
由圖片中可以看到,有將 src 對齊的執行時間比起原版還要更短。
在這裡由測試程式 test.c
流程進行介紹
test.c
cstr.h
第 1 行的 #pragma once
在 C 語言內並不是一個標準 (non-standard) ,但是有廣泛的被編譯器支援。目標與常見的 #ifndef...
類似,希望只被 include 一次。
第 29 行的 ## operator 被拿來連接兩個 token ,用以生成變數名稱
到目前為止可以看到我們生成了一個 a
的 str_buffer
與其相關的變數。
test.c
cstr.c
現在有一個叫做 a
的 cstr_buffer
與 "Hello "
這個字串被傳入 cstr_cat
中。
在這裡 s
內的資料經由上一步初始化為
如果字串除存的資料還在 stack 上面的話會有以下步驟:
i
表示現在 s
的 cstr_buffer
內的字串長度cstr
尾端,與前字串連接起。
cstr
內的字串長度小於 CSTR_STACK_SIZE - 1
str
還沒走到字串尾端 ( '\0' )cstr
的大小足以放下 str
,回傳 s
\0
放入 cstr
末端,進入與 type != CSTR_ONSTACK
相同的處理步驟當 type != CSTR_ONSTACK
或者字串長度已經超過 CSTR_STACK_SIZE
時,呼叫 cstr_cat2
,傳入 cstr
與 str
cstr_cat2
若字串的長度大於 CSTR_STACK_SIZE
但小於 CSTR_INTERNING_SIZE
時,會進入 cstr__interning
的函式。
(但在測驗程式碼中 CSTR_STACK_SIZE
為 128, CSTR_INTERNING_SIZE
為 32,這樣好像就永遠執行不到這段程式碼 @@)
傳入 cstr__interning
的參數為:
hash_blob
cstr_interning
在 cstr_interning
中第 4 行 CSTR_LOCK
與第 11 行的 CSTR_UNLOCK
使得在 interning 的過程中可以保證執行正確。
在這裡使用的是 gcc 提供的 Built-in functions for atomic memory access 來做 LOCK。
interning
如果 si
內的 hash
為空,代表第一次使用這個空間,回傳 NULL
,使用 expand
創造空間後再次進入。
第 9 行的 index
為 hash
與 si->size
取餘數的結果,接著進入 while 迴圈,目標為在那層 hash 內尋找是否有相同的字串,若有找的則回傳已經儲存的字串,若沒找到就在 pool 內創立新的 node
在第 19 行的地方,確認裡頭物件是否超過雜湊表的 4/5 ,若超過就回傳 NULL
,再使用 expand
擴增空間重新進入。
第 35 行,將新的 node 連接至 si->hash[index]
的開頭
expand
在 expand
宣告新的 struct __cstr_node
的指標的指標,並給予 struct __cstr_node *
原兩倍大小的空間 ( 若是內部還不曾有過東西,大小則為 HASH_START_SIZE
)