or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Syncing
xxxxxxxxxx
2020q1 Homework6 (fiber)
contributed by <
haogroot
>tags:
linux2020
題目連結
測試環境
Kernel version: 5.3.0-51-generic
OS: Ubuntu 19.10
CPU model: Intel® Core™ i7-8565U CPU @ 1.80GHz
作業要求 1
研讀 Paul Turner 的簡報檔案 User-level threads 及相對應的演講錄影,記錄你的認知,並回答以下問題:
作業要求 2
Trampoline
我的理解為他是一個埋在程式碼特定位置裡的 hook ,當執行到該特定位置,就會觸發去執行遠方的一段程式碼,執行完後再跳回來繼續執行原本程式碼。
Gerald 在 stackoverflow 討論串上分享一個有趣的例子: 他應用 trampoline 在遊戲上作弊,當遊戲開始時會去建立檔案,他找到建立檔案函式並修改前面幾個 bytes 來住入他自己的 assembly code ,這些 assembly code 會跳到他的 "trampoline" 函式中,他就可以做任何他想做的事情在他的 "trampoline" 中,然後就會跳回去,繼續執行建立檔案之後的動作。
quiz8 背景知識
Signal handling
sigset_t
用來儲存 POSIX signel 的集合,各個位置代表不同訊號,如果為 1,代表要阻塞該訊號。sigemptyset()
將所有訊號都清空為 0。sigfillset()
將所有訊號設為 1,全部都阻塞。檢查並改變 blocked signals。依據參數
how
內容做不同的動作。SIG_BLOCK
:set
和oldset
兩 signal set 裡的訊號若是接收到都阻塞。SIG_UNBLOCK
: 設定set
和oldset
兩 signal set 的交集為不想阻塞的訊號。SIG_SETMASK
: 此 process 對應的 signal set 將被set
所取代。sigsuspend
: 暫停目前的 task 來等待訊號,等到 signal handler 處理完後,會再繼續當前 task 。- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →可以避免訊號中斷了敏感的操作,例如
struct sigaction
裡的成員sa_mask
來阻塞特定訊號( 測驗中timer_handler()
內就使用這樣方法 )。- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →sigpending()
可以找出被 pending 的訊號。[reference]:
24 Signal Handling
24.7 Blocking Signals - GNU C Library Reference Manual
接下來探討測驗中的
lock_irq_save
和lock_irq_restore
。透過
local_irq_save
,阻塞除了 SIGINT 以外的所有訊號,可以理解為幫當前 task 攔截除了 SIGINT 以外的訊號。透過
local_irq_restore
則將 task 的 signal set 換成參數 sig_set。- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →以測驗中的
schedule()
為例:以我的理解,
local_irq_save()
應該是想先將所有的 signal 都先 block 住,避免被中斷,接著使用local_irq_store()
能將 signal set 恢復為原本的 (即呼叫local_irq_save()
之前的 signal set),回想前面討論 blocked signal 何時會被處理 中提到:如果這時候 pending 中的 signal 在這個恢復過程中由 block 轉為 unblock ,訊號就會被送達,當我們執行local_irq_store()
就能接收到這段時間被阻塞的訊號了。程式碼潛在危險
在 24.7.2 Signal Sets 中提到:
因此變數
set
應該先透過sigemptyset()
來初始化比較好。ucontext_t
在
<ucontext.h>
中定義4個函式getcontext()
,setcontext()
,makecontext()
和swapcontext()
。透過使用這系列函式可以在同個 process 下的 thread 之間做 user-level context switch ( user-level thread 與 kernel-level thread 是不同的,前者是無法被作業系統辨識出來,因此作業系統在 user level 要排程的話,是以 process 為單位。 )根據 wikipedia 對 context (computing) 的定義:
context
可以理解為該 task 當下的狀態,當今天發生 interrupt 時,透過context
還能夠回復到 interrupt 發生當下的狀態。有這個背景知識,再來看<ucontext.h>
中這4個函式就能夠比較了解他們的作用。ucontext_t
type 包含以下欄位:sigset_t
跟 signal 處理有關係,stack_t
包含現在context
所使用的 stack ,uc_link
則是現在context
執行結束後會執行的context
。getcontext(ucontext_t *ucp)
context
保留到參數ucp
中。setcontext(ucontext_t *ucp)
ucp
所指向的context
makecontext(ucontext_t *ucp, void (*func)(), int argc, ...)
context
,ucp->uc_stack
要由呼叫此函數者先行分配好一塊新的 stack ,ucp->uc_link
也由呼叫此函式者分配好其 Successor context 。makecontext()
產生的 context 接著可以透過setcontext()
或swapcontext()
來啟用,參數func
會被呼叫並且帶有 argc 所指定數量的其餘參數。func
回傳後, Successor context 再被啟用。 (若 Successor context 為 NULL, thread 存在著)swapcontext(ucontext_t *oucp, const ucontext_t *ucp)
swapcontext(oucp, ucp)
等同於getcontext(oucp)
再setcontext(ucp)
。swapcontext
有一點值得紀錄,這個函式第一次被使用的時候並不會回傳。但若之後oucp
又被 activated ,這次的swapcontext
就會回傳。[Reference]
我們來看 main function 會怎麼執行:
timer_init()
註冊訊號SIGALARM
,接收到該訊號會去執行timer_handler
,在timer_handler
預設是會 block 所有 SIGNAL 。task_init()
初始化存放 task 的 linked list 。task_add()
增加3個 task 到 linked list 中,帶有兩個參數,分別為task_trampoline()
和一整數。透過makecontext
將參數放到 task 中的 context ,這邊沒有為他們安排任何的 Successor context ,每個 task 都會 block 訊號SIGALARM
。preempt_disable()
不允許搶佔timer_create()
建立一個每 10 ms 會發出SIGALARM
的 timer 。main()
接著會一直執行timer_wait()
直到 linked list 沒有任何 node ,timer_wait()
暫停目前的 task 來等待SIGALARM
,訊號來之後並執行timer_handler()
,結束後會再繼續當前 task 。timer_handler()
中檢查是否允許搶佔,可以的話執行schedule()
。schedule()
會執行task_switch_to()
來切換 context (也就是在 user-level thread 之間切換),在這個測驗中有4個 context,分別是main()
以及3個透過task_add
建立的 task 。 這3個 task 要是執行完,schedule()
會將他們釋放掉。task_trampoline()
,再裏面又透過 callback function 方式去執行sort()
,執行完後會將task->reap_self
設為 true ,schedule()
就會自動將他釋放。 在執行sort()
過程中,會被 timer interrupt ,因而執行timer_handler
並切換到別的 context 。延伸問題:
preempt_disable
,preempt_enable
,local_irq_save
,local_irq_restore
等等preempt_enable
和preempt_disable
用來關閉搶佔。 spinlock 的設計中就是不允許搶佔,以下是 spinlock 的原始碼在嘗試持有 spinlock 前會先將搶佔關閉,為什麼 spinlock 不允許搶佔呢?
而 spinlock 有另一種形式,當你今天是要在 interrupt handler 內想要去獲取 spinlock 時,使用
spin_lock_irqsave()
先保存目前的中斷狀態並且關閉所有中斷,接著再去獲取 spinlock ,不需要 spinlock 後再透過spin_lock_irqrestore()
來恢復中斷前的狀態,從spin_lock_irqsave()
原始程式碼 中可以看到裏面有是呼叫local_irq_save()
,只是是保存中斷當下暫存器等,不同於測驗中是為了保留訊號。schedule()
應該先對 signal set 先行初始化。作業要求 3
RUSAGE_THREAD
的使用。可透過 eBPF 觀察及分析;在我的環境 中,編譯 threadpool 過程中,編譯 tasklet.c 會失敗,錯誤如下:
透過新增
#include <stdint.h>
後才能夠順利編譯。作業要求 4
Fiber
$ grep -r FIXME
找出程式碼標注的改進事項,特別是記憶體管理相關,若能引入高效能的 memory pool 實作,會有助益;Fiber 運作機制
clone
用來建立 kernel-level threads 。 除了第一個 main thread 以外的 thread 都要為他們分配好 stack 。fiber_create()
建立 user-level threads 。fiber_yield()
主動讓出 CPU 來達到 cooperative scheduling 。malloc()
失敗時的 error handling在
fiber_create()
中,若是 allocate memory 失敗應該直接回傳。由於 clang-format 關係,
fiber.c
的 header 順序會自動被更動,fiber.h
將會被移到 header 的第一個順序。這樣子會造成 compile 錯誤。錯誤訊息如下:我必須在
fiber.h
上增加stdlib.h
才能順利編譯並可以移除掉stdint.h
,。test-content()
兩個 user context 不斷做 swap 來輪流執行,他們的任務都可能機率性的結束,結束之後回到 main thread ,我們透過 vlagrind 來對他進行記憶體檢測,
-g
$ valgrind --leak-check=full ./tests/test-context
結果如下:
仍然有 16384 bytes 記憶體沒有被正確釋放,而程式中為兩個 user context 透過
malloc()
各分配了SIGSTKSZ
大小的記憶體,根據查詢 linux kernel 原始碼後, 他所代表大小為 8192 ,因此可以確認即為這兩塊為 user context 分配的記憶體未被正確釋放。因此我們在 main thread 準備結束前將他釋放即可以通過 valgrind 的檢查。
test-yield()
fiber_yield()
主動讓出 CPUSIGPROF
, 引發 timer handlerschedule()
執行,將目前執行的 user-level thread 放到 user-level thread queue 的尾端,並透過swapcontext()
執行 kernel-level thread 。在 main thread 中透過
fiber_join()
等待所有的 user-level thread 結束。最後執行fiber_destroy()
,此函數是用來收尾的,應該將該釋放的記憶體都釋放乾淨,在此專案中,會替 user-level thread 與 kernel-level thread 的 thread control block 分配記憶體,前者的釋放由k_thread_exec_func()
負責,後者的釋放卻沒有實作。test-mutex()
執行過程會發生 Core dumped. 一開始使用 Valgrind 想來找記憶體不當使用問題,但 Valgrind 並不支援
clone
,因此無法分析。透過 gdb 來追 core dumped 問題,以下紀錄過程:
-g
$ gdb -c core ./test-mutex
gdb test-mutex core
進到 gdb 內where
或bt full
可以知道執行到哪裡時發生記憶體問題而 fiber.c 第 350 行則是釋放當下執行執行緒所佔有的記憶體。
[Reference]
實作 fiber_destroy
在釋放 kernel-level thread 前,首先確認他是否已經沒有任務要執行,而他的主要任務就是執行 user-level thread ,所以若是 user-level thread 都結束了,kernel-level thread 自然可以被釋放,可以透過檢查變數
user_thread_num
是否為 0 來確認是否所有 user-level thread 都結束。在建立 kernel-level thread 時候,為其分配好記憶體,並透過
clone()
的參數來給定這塊 stack ,值得注意的是,在 clone man page 中提到 stack 是往下長的,所以這也是為什麼clone
所帶的參數會是分配的 stack 記憶體位置加上其大小。這邊我感到疑惑,我們分配好記憶體交由 child thread ,那這塊記憶體是否會隨著 child thread 回傳而直接被釋放呢或是該由 child thread 自己釋放呢? 但是記憶體又是由 parent thread 所分配的,會不會還是該由 parent thread 來釋放?
pthread_create()
一樣最終透過clone()
來建立 thread ,我參考pthread_create()
和pthread_join()
來尋找方向,在後者程式碼當中,可以看到他有針對 tcb 做釋放記憶體的動作。CLONE_VM
,意味著記憶體是由 parent 跟 child thread 共享的,最安全的作法應該是確認兩者都不會再 access 記憶體後再將其釋放。另外一點覺得奇怪的是建立 kernel-level thread 放到
fiber_create()
才做有點奇怪,最好方法應該是在fiber_init()
來建立並在fiber_destroy()
來釋放。