contributed by < Hlunlun >
yy214123
q_free
在 queue.h
中有實作:
可以如下改善:
前置作業
取得程式碼與測試
每次開發完一個函數就 commit ,紀錄開發過程
q_new
C99 [7.20.3.3] The malloc function
The malloc function returns either a null pointer or a pointer to the allocated space
malloc
如果沒有成功配置到記憶體是有可能回傳 NULL 的,所以要檢查 malloc
要求的記憶體是否有成功
問題:但後來在 make test
後發現第 13 個 trace 無法通過
猜測這個函式可能是一定要配置到記憶體位置才是正確的,因此使用迴圈檢查記憶體配值直到成功為止,經過 Commit 4417ea0 竟然就通過測資了,但是這樣可能會造成記憶體一直不夠一直再回圈中檢查,造成整個系統異常,要在想原因是什麼
2025/03/09 不要平感覺寫程式
q_free
先將 list
移除,在釋放整個節點
q_insert_head
、q_insert_tail
element->value
指向 s
,如果有在其他地方有人對 s
做任何變更或是釋放記憶體,element->value
就會也跟著改變,所以應該是要用 strdup
配置新的空間給 element->value
strdup
那為何要判斷 element->value
是否為 NULL
呢?
可以運用 linux 的說明文件 man page,在 terminal 打上 man strdup
,這時候就會看到因為在使用 strdup
時會用到 malloc
,而在規格書 C99 [7.20.3.3] 中有提到 malloc
有可能因為記憶體不夠用而造成記憶體配置失敗從而回傳 NULL
,所以才要檢查 element->value
The strdup() function returns a pointer to a new
string which is a duplicate of the string s. Memory
for the new string is obtained with malloc(3), and
can be freed with free(3).
q_remove_head
、q_remove_tail
重點:只要 unlinlk 就好,不用釋放記憶體,找到第一個 list_head
節點,然後 unlink
問題 :copying of string in remove_head overflowed destination buffer.
原本我是直接用 bufsize
有多大就複製多大的 entry->value
到 sp
,但如果 entry->value
的長度比 bufsize
小,會有 buffer overflow detected 的錯誤
這裡就會好奇所以 strncpy 是否會自動補上 \0 ,從 man page 看到 strncpy
的實做原理,可以看到這個函式會用到 strpncpy
並且在其中會檢查 dsize
和 src
的長度,但是並沒有和 dst
比較,而這裡的 q_remove_*
會有 bufsize
的限制,所以要而外檢查 element->value
的大小,經過 commit 27965a2 即可解決這個問題
關於可以複製多大的字串到 sp
,在 man strncpy
中,看到了 strncpy
的實做使用到 strnlen(src, dsize)
,這個函式會回傳 min(strlen(src), dsize)
,可以讓程式碼更精簡,因此做了 commit d03a3d8 的修改
q_delete_mid
運用 指標的指標 技巧,找到指向 middle node
的 指標 ,然後將它移除,只是這裡的迴圈判斷條件是 fast
和 fast->next
還沒到迭代到 head
之前,如 commit a351491
q_delete_dup
使用 list_for_each_entry_safe
才可以在迭代過程中移除節點,因為只要出現多於 1 次就要全部移除,所以需要一個標誌來確認目前此次刪除是否進行完,如 commit 49224d8
q_swap
先看 list_for_each_safe
,所以使用這個巨集時,要 swap 的就是 node
和 safe
因為是每兩個節點交換位置,所以每次迭代 safe
和 node
需要移動兩個位置,且要檢查 safe
和 node
都不是 head
,但 node
會在 list_for_each_safe
中被檢查是否是 head
,所以只要檢查 safe
就行,如 commit 6a80e4b
q_reverse
prev
和 next
,如 commit 7203bbdq_reverseK
為了可以直接使用到 q_reverse
,可以透過 list_cut_position
將雙向鍊結串列中的片段切割出來使用 q_reverse
去倒轉,將到轉完的雙向鍊結串列先存放在暫時的 new_head
中,繼續迭代 head
找出 k-group 的片段,最後在將 new_head
用 list_splice
全部拼接到 head
,如 commit b4dc453
q_sort
透過 研讀 Linux 核心原始程式碼的 lib/list_sort.c 了解排序運作原理後,實做在此函數,如 commit 4ebd88e
C99 [7.21.4.2] The strcmp function
應該使用 strcmp
函數來比較兩個字串,因為此函數是比較指標指向的記憶體位置中存放的值
The strcmp function compares the string pointed to by s1 to the string pointed to by s2.
C99 [6.5.8] Relational operators
如果直接使用 關係運算子 (Relational operator) 對兩個指標( list_entry(a, element_t, list)
的屬性 value
變數類型是指標) 進行比較,則是比較指標指向的記憶體位置,而非記憶體中存放的數值
When two pointers are compared, the result depends on the relative locations in the address space of the objects pointed to. If two pointers to object or incomplete types both point to the same object, or both point one past the last element of the same array object, they compare equal. If the objects pointed to are members of the same aggregate object, pointers to structure members declared later compare greater than pointers to members declared earlier in the structure, and pointers to array elements with larger subscript
q_ascend
、q_descend
兩個函數都只要將最右邊當作基準點去由右而左迭代,只要遇到 left
比 right
指向的節點還要 小( q_descend
) 或大( q_ascend
)就刪除,並且一直往左走直到 left
走到 head
,如 commit ed4547b
q_merge
此函數主要功能就是將在不同 q_context_t
中的雙向鍊結串列 q
全部合併到第一個 q_context_t
中的 q
,不同的 q_context_t
則是藉由 chain
來連接(下圖藍色線條),且在合併之前的每個 q
都保證已經排序過,因為是字串,所以 "10" 小於 "23"
合併後應該要是長這樣,除了第一個 q_context_t
中的 q
,其餘 q_context_t
中的 q
都會變空的雙向鍊結串列
這裡我直接把其他 queue_context_t
中的雙向鍊結串列拼接到第一個 queue_context_t
中,在用 q_sort
進行排列即完成,commit
至此,即佇列各種函數的實做,成功就會出現星之卡比的圖片
參考:坤諦江 、The Linux Kernel、Risheng
__attribute__
其中這段有提到 nonnull
function attribute 可能會用在至少有一個指標參數的函數,告訴編譯器哪幾個指標參數必為非空指標
The nonnull attribute may be applied to a function that takes at least one argument of a pointer type. It indicates that the referenced arguments must be non-null pointers. For instance, the declaration…informs the compiler that, in calls to my_memcpy, arguments dest and src must be non-null.
lib/list_sort.c 使用的 函數屬性 使其第 2、3、4 個參數 cmp
、a
、 b
必須為非空的指標,所以從程式碼可以間接知道 cmp
是指標,因為 __attribute__((nonnull))
只用於限制指標參數(可以透過閱讀 nonnull
function attribute 範例得出結論 )
仔細閱讀 函數屬性 在函數被觸發以及在編譯階段定義時會有不同的效果,在函數被觸發時如果在標記為 nonnull
的位置傳入 null
會觸發緊告,所以其實程式還是可以通過編譯,但是會有警告
If the compiler determines that a null pointer is passed in an argument slot marked as non-null, and the
-Wnonnull
option is enabled, a warning is issued.
在課堂上老師也不斷提及 linux kernel 是牽一髮而動全身的,所以開發者必須更謹慎小心,而 __attribute__
就是一種警告機制,把控傳參數這樣的細節
list_cmp_func_t cmp
解釋 函數屬性 時提及 cmp
是一個指標,其型態定義於 linux/list_sort.h ,將 整數 定義為一個 指標函數 (pointer to function) ,或者可以解釋為這是一個會回傳整數的指標函數
為何不直接使用它原本的整數型態,還特別定義為 指標函數 呢?
透過程式中的註解解釋 cmp
,此函數用於比較當前節點的數值,
merge
運用到 指標的指標 技巧,進行兩個雙向鍊結串列的合併,並沒有去修改 prev
指標指向的節點,舉例以下 a
和 b
可能的樣貌,且最後一個節點都會是 NULL
,原因可以看到 merge_final
的解釋內容
經過 merge
函數後,會呈現以下,也就是說只有將 next
指標指向對的節點,prev
指標依舊,也就是說按照 next
指標走,排列的順序是正確的
merge_final
首先看到函數定義可知,參數中 cmp
、head
、a
、b
必不為空指標
head
在 list_sort
中作為呼叫 merge_final
的引數,可以知道 head
是一個雙向鍊結串列的第一個節點,而在 merge_final
中 a
和 b
會有序的排列在 head
之後。
此處也可解釋為何在 merge
在過程中 a
和 b
明明是雙向鍊結串列的型態 list_head
結構中卻有 NULL
,因為以下程式碼第 11 行將 head->prev
這個節點的 next
指標指向 NULL
,也就是原本指向 head
的 head->prev->next
指標,現在指向 NULL
( 這跟直接將 head
設置為 NULL
還是差別的!所以 head
依然存在並未變成 NULL
),所以才可以用跟合併單向鍊結串列一樣的判斷條件是迭代直至 NULL
看完第 55 到 到 第 74 行 的程式碼可以知道 merge_final
和 merge
作法大致相同,也就是會比較 a
和 b
這兩個雙向鍊結串列並且做合併,但是 merge_final
有調整每個節點的 prev
指標
若是兩個鍊結串列不是平衡的,代表只有將其中一個鍊結串列走完並排列到 head
後面,需要繼續處理另一個鍊結串列,以下程式碼即是處理此情況
unlikely
、 likely
定義於 linux/compiler.h ,使用到 GCC 內建函式 __builtin_expect (long exp, long c)
如下
關於 __builtin_expect(long exp, long c)
的描述如下,此內建函數會提供編譯器分支預測的資訊,此處 分支 branch 指程式中的能夠改變執行指令順序的如 if-else
和 switch
的條件表達式,這種功能可以避免指令跳轉從而優化程式執行過程
You may use
__builtin_expect
to provide the compiler with branch prediction information…
其運作方式如字面上 expect
的含意,編譯器會預期 exp
的數值結果是 c
,回傳值會是 exp
的運算結果
The return value is the value of
exp
, which should be an integral expression. The semantics of the built-in are that it is expected thatexp == c
.
嘗試以下程式碼印出 __builtin_expect
的值,就是 exp
本身,此函數並不會影響 exp
本身,
所以,__builtin_expect(!!(x), 1)
代表預期布林值 !!(x)
是對的,即希望執行 likely
這個分支;反之 __builtin_expect(!!(x), 0)
則代表預期布林值 !!(x)
是錯的,即不希望執行 unlikely
這個分支。透過預期 exp
發生的機率高低讓編譯器可以提早預測是否按照順序執行程式碼或是需要執行會降低效能的跳轉指令。
list_sort
此處一樣有限制第 2 和 3 個參數須不為空的指標
其參數定義如下,head
即 list_sort
要進行排序的雙向鍊結串列, cmp
則為的用於比較的函數
@priv: private data, opaque to list_sort(), passed to @cmp
@head: the list to sort
@cmp: the elements comparison function
參考 RinHizakura 中解釋 [RESEND PATCH v2 5/5] lib/list_sort: Optimize number of calls to comparison function
開頭解釋由於 CONFIG_RETPOLINE
降低了 呼叫間接函數 的表現,所以應該要減少使用間接函數如用來比較的 cmp()
函數,這也就是為什麼 list_sort
的合併排列中兩個序列長度需要以 2:1 的比例進行,因為此方式可以減少比較的次數,也就減少了調用 cmp()
而降低了系統的性能情況發生。
CONFIG_RETPOLINE has severely degraded indirect function call performance, so it's worth putting some effort into reducing the number of times cmp() is called.
這段程式碼改變了以前是用 Bottom-up mergesort 的方式(以下為前版本描述),可以參考 patch 中提供的文獻:Bottom-up mergesort – A detailed analysis,比較特別的是這篇論文是一個經濟系和數學系的專家寫的,他們推導出可以表達 bottom-up mergesort 過程中做比較次數的表示式,並與 top-down 進行分析,並且得出結論雖然 top-down mergesort 的複雜度可能更低,但是它伴隨著需要先知道排列個數的限制,我想這也是為什麼以前是用 bottom-up 現在優化也不選擇 top-down 的原因
且合併排列的比例盡可能要是 2:1
cache thrashing 快取抖動:重複存取不在快取中的資料,導致快取不斷被替換,造成性能下降。
這邊有解釋到,因為這樣可以防止 cache thrashing,由於 CPU 的 cache 快取 存取速度由快到慢分別為 L1, L2, L3,所以如果 CPU 要存取資料會最先到 L1,這 個資料剛好可以都在 L1 就不會造成 cache thrashing
可以透過Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
lscpu
知道自己電腦的 L1 大小
我們需要計算兩個鍊結串列的長度並且檢查是否為 2 的次方,而用來計算的 count
就是控制是否合併的因素,當兩個鍊結串列長度為 就合併,也就是計算 pending
的長度,以下所說的:"在 k
個位置設為1後k
以前的位置就都變成0" 就是指 count
變成 的時候二進位的呈現
以下是計算方式的說明,說明有幾個 長度的鍊結串列並且何時合併
這邊用實際數字來模擬這個過程,為了方便表示這裡先用單向鍊結串列:
count
= 0: 000
count
= 1: 001
count
= 2: 010
count
= 3: 011
合併這兩個長度為 1 的鍊結串列為一個長度為 2 的鍊結串列、有一個長度為 1 的鍊結串列
count
= 4: 100
而這個過程合併用的函數就是定義在上面的 merge
,完成所有節點的排序後,最後在使用 merge_final
調整雙向鍊結串列的 prev
指標,至此,就是 list_sort
的註解解釋
head
的 prev
指標指向的節點的 next
指標指向 NULL
,所以 head
的 prev
並未斷開pending
,初始化這個指標指向 NULL
以下就是作者那一大串註解所說明的排序過程,
tail
在這裡的作用是在找 待排序的鍊結串列 pending
上的最後一個節點,pending
一開始是空的,所以 tail
指向 pending
count
初始化是 0 ,所以一開始不會執行這個迴圈
此處使用到的 likely
,即有很大可能 bits
是大於 0 的數字,但是如果有經過上面的迴圈 bits
還大於 0 的可能有以下兩種
bits
本身就是 bits
是 區間的整數count
= 0 count
= 1
將雙向鍊結串列上的節點移至 pending
上
第一次執行這段程式碼如以下,此時 pending
上就會有一個節點 12
,且其 prev
和 next
都會被斷開,如同從此雙向鍊結上移至另一個等待的鍊結串列上,此時也因應 pending
上增加一個節點,count
= 1
count
= 1 count
= 2:不 merge
count
= 2 count
= 3 : 合併
進入 if likely(bits)
的區塊準備進行合併,這裡使用的是只會調整每個節點的 next
指標的 merge
函數
此時節點 2
的 next
指標就會指向節點 12
(此處假設是升冪排序),且節點 2
的 prev
也會被清空指向 NULL
最後將下一個的節點增加至 pending
count
= 3 count
= 4 : 不合併
找目前在 pending
上的最後一個節點,並將 tail
指向該指標,因為 next
指標是有順序的指向下一個節點,如果要找到最後一個節點就是要用 prev
去找 tail
將下一個節點新增至 pending
count
= 4 count
= 5: 合併
將 pending
上的節點進行合併
經過合併後,節點 5
的 next
指標會指向節點 13
將下一個節點移至 pending
至此,由於 list
指向 NULL
,所以跳出迴圈
由上圖可知,雙向鍊結串列尚未排序完成,只有每格兩個節點用 next
去看順序是正確的,prev
目前也尚未完成,以下程式碼就是
將在 pending
的節點進行排序,
進入 for
迴圈進行排序
先將下一個節點存放至 next
經過 merge
,完成所有節點的 next
指標排序: 5
7
13
先將下一個節點存放至 next
經過 merge
,完成所有節點的 next
指標排序: 2
5
7
12
13
最後使用 merge_final
函數,將 prev
指標更新完成
此演算法是一個時間複雜度為 O(n) 和空間複雜度為 O(1) 的洗牌演算法,步驟如下:
假設有一個 n
長度的陣列 array
i
i
次迭代中
j
array[i]
和 array[j]
交換位置q_shuffle
實做 Fisher-Yates Shuffle 在雙向鍊結串列中
開發了一個可以檢測差不多 300 行的 C 語言程式的執行時間是否為常數時間的工具: dudect
動機
為了抵抗 Timing attack 的相關資安攻擊,雖然聽起來是很瑣碎的事情,但是如果發生 timing leaks 就會造成很嚴重的結果
Assessing whether an implementation runs in constant time is not a trivial task. Even implementations that were supposed to be constant-time turned out not to be so [GBY16 ], [ Cry16]— reinforcing the argument that timing leaks may not be easy to detect, but can have serious consequences.
貢獻
即使以前也有可以評估記憶體資源或時間的工具,但是這些工具都需要去模擬硬體,這其實是滿困難的,而這篇論文的作者開發的 dudect
並不需要,只要跑在他們的平台上即可評估時間複雜度,而且不是靜態的分析?是用統計分析程式執行時間?
Our tool does not rely on static analysis but on statistical analysis of execution timing measurements on the target platform. In this way, we circumvent the problem of modeling the underlying hardware.
第一步: 測執行時間
第二步: 後處理
第三步: 統計測試
deduct
開放在 github 上dudect
在 MacBook、Intel Xeon 和 ARM7TDMI 上都有效,具有其通用性make test
,先思考在執行
釐清 pointer
引用 C語言: 超好懂的指標,初學者請進~ 中的描述可以很好理解
指標 (Pointer) 就是某變數的位址。而這邊的指標變數 (Pointer Variable),則是用來存放指標的變數。
array subscripting 在編譯時期只確認以下兩件事:
前兩者以外的操作,都透過 pointer
array subscripting => syntax sugar
C has no real "array"!
關於為什麼 &b[0]
是 pointer to struct? &
不是 address of 用來取位置的嗎?那為什麼不是一個數值?
因為我們前面有了解過指標變數中存的值就是記憶體位置,在這裡我的理解是:把 b[0]
看成一個變數,&b[0]
這個數值就是記憶體位置,那什麼變數會存放記憶體位置呢?就是指標變數了,所以才會是 pointer to struct
遇到 gdb
無法存取記憶體
後來找到原因,因為我沒有把程式跑起來,但是跑起來後,因為 main()
中沒有程式碼,整個程式會直接結束,還來不急跟變數做互動它就結束的概念,所以要在 main()
這個地方做一個 break point,讓它停在那裡我們就可以對 b[0]
做修改記憶體了
關於 你所不知道的C語言:指標篇 的這個部份,我和影片中實做的結果不一樣
其中 &b+1
不是 a[0]
或是 a[3]
的位置,而是 calendar
的位置
在查看 calendar 位置的實做過程中,發現 memcpy
不能直接使用,要先強至轉型才能看到值
pointer to pointer 的實做理解
用 leetcode 82. Remove Duplicates from Sorted List II 來實做,並用 graphviz 畫每個步驟
每一個節點都是一個 ListNode
物件
最一開始 indirect
指向 head
,head
指向第一個節點的位置,cur
指向 indirect
指向的 head
指向的位置(指標的指標)
如果 cur
的值與下一個節點值不相同
改變 indirect
中儲存的記憶體位置,改到 指向位置的指標的 next
(也是一個指標)的位置
所以,indirect
不是直接指向 2
!! 而是指向指向 2
的 1->next
指標
如果 cur
的值與下一個節點值相同
迭代 cur
一直到不相同
然後把 indirect
指到的 next
指標,指向 cur
指向的節點 4
重複以上步驟,直到 indirect
指向的指標指向的物件為 NULL
*-safe 就是不管任何條件 *
,這個函式都可以執行