看板 C_and_CPP 關於我們 聯絡資訊
看了一系列的舊文章「Re: 什麼是 multi-thread」 https://www.ptt.cc/man/Programming/D156/DB3A/D13D/M.1147208004.A.A92.html 才知道原來 thread 是不需要 os kernel 支援就可以辦到的, 之前一直以為需要 os kernel 支援 kernel thread, thread library 才有辦法實作出來, 因為我實在想不到如果 kernel 不支援 kernel thread, library 到底要怎麼支援 thread, 在 user mode 要靠「什麼」才有辦法在 2 個 function 之間切換。 噢! 當然戰文本身也是很精彩的, 這系列文章有些你來我往的回文, 只要能說出個道理, 都是能從中學到東西的。 coroutine 之前研究過, 它是主動讓出 cpu 執行權, 如果不主動讓出, 該怎麼在執行的 function 中讓出 cpu 呢? os 靠的是 timer 中斷, user mode 程式要怎麼作到類似的效 果呢? 在查詢資料的過程, 找到 pthreads-1_60_beta6.tar.gz 這個 user mode pthread, 是由 Chris Provenzano 開發的 pthread 實作品。 這是搭配 linux kernel 1.X 用的 pthread library, 只支援 x86 32bit, 那時候的 linux kernel 還沒有 kernel thread 支援, 我像是挖到寶似的, 想看 Chris Provenzano 是怎麼辦到的, 本想編譯起來用 gdb 追蹤, 不過在目前的環境似乎編不起來, 我放棄了, 而我追 code 能力太差, 沒有從 souce code 看出什麼端倪。 另外找到一個課程的作業 - CS170: Project 2 - User Mode Thread Library (20% of project score), 我的媽呀! 還真的有這樣的課程, 不禁為他們學生默哀, 這應該會是讓 他們困擾很久的作業的吧! 這是 ucsb 的 os 課程。 ucsb 中文是 - 加州大學聖塔芭芭拉分校, 不太熟悉這間學校。 看到這些學校的作業是這麼的扎實, 記得自己的 os 課程就是教課書 - Operating System Concepts 的內容, 考試則是課本上的知識, 程度真的差太多, 實作太少了。 但是該課程也沒那麼狠心, 在 Implementation 一節中, 說明的一些實作細節, 用著我的 破英文很勉強的看了看, 得知了幾個關鍵。 需要使用 setjmp/longjmp, signal。 知道這個概念之後, 相當高興, 以為可以順利寫出來, 但在我開始下手時, 卻發現困難重 重, 我不知道應該在哪裡執行 setjmp, 在哪裡執行 longjmp。 ex.c 1 void func1() 2 { 3 printf("1\n"); 4 printf("2\n"); 5 printf("3\n"); 6 printf("4\n"); 7 printf("5\n"); 8 } 9 10 void func2() 11 { 12 printf("21\n"); 13 printf("22\n"); 14 printf("23\n"); 15 printf("24\n"); 16 printf("25\n"); 17 } ex.c 我該在哪裡插入 setjmp 呢? ex.c L3 ~ L7 之間嗎? 都不對阿! longjmp 應該安插 在哪裡呢? Implementation 原來還有背面, 我漏看了, 重新看過之後得到以下心得: 1. setjmp/longjmp - 這個用來保存 2 個 function 切換的狀態, 還需要特別保存 stack。 2. signal/SIGALRM - 這個就是我百思不得其解的關鍵, 使用 signal 來中斷正在執行 的 function, 在 signal handler 中, 保存正在執行的 function 狀態 (使用 setjmp), 再選出一個 function, 跳去執行它 (透過 longjmp)。 而在看過一些 source code 之後, 我又得到一些心得, 需要修改 jmp_buf 的 esp, eip 欄位。 CS170: Project 2 - User Mode Thread Library (20% of project score) Project Goals ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ The goals of this project are: ‧ to understand the idea of threads ‧ to implement independent, parallel execution within a process Administrative Information Implementation ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 略 ... 其實在看提示之前, 我有想到應該在 signal handler 使用 setjmp/longjmp, 只是我被 自 己迷惑了, 因為在 signal handler 的 stack, 已經不是原本程式的 stack, 為了跳到 signal handle, kernel 對原本的 stack 做了修改, 我自以為在這裡保存這個 stack 是 沒有用的, 是我自己想太多了。 基本概念是這樣, 假設我們有 func1, func2 這 2 個 function, func1 先執行, 使用 alarm signal, 讓 5ms 發動一次 alarm signal, 5ms 就會呼叫一次 signa handler, 這 時候就可以在這裡將目前執行的 function - func1 setjmp 起來, 然後使用 longjmp 跳 到 func2 去執行, 這樣就完成了 5ms 切換 func1, func2, 就達到了 user mode thread 的效果。 這個概念和 os 的 process 切換是類似的。 而對 func1, func2 來說, 需要有各自的 stack, 這樣才不會有相互蓋到 stack 的問題, 使用 setjmp 來保存 register 資料外, 還需要提供一個 stack 空間, 所以要把 jmp_buf 的 esp 欄位改到預先準備好的 stack 空間, simple_thread.c L112, L117。 另外要修改 jmp_buf 的另外一個欄位是 eip, 需要把它指到 func1, func2 的開頭, 這 樣一來, longjmp 就會從 func1, func2 的開頭執行。 而不幸的是, jmp_buf 和執行的 cpu 有關係, 所以得要搞懂這個平台的 jmp_buf 是怎麼 安排這些暫存器的資料結構。 Implementation 還提供了另外一個重要的訊息, 由於為了安全, jmp_buf 都會被用一個 演算法保護起來, 避免被亂改, 所以 Implementation 提供了一段程式碼 幫助同學處理這部份。 我沒有用這些方式, 我懶得搞懂這些, 我只想搞懂 user mode thread 怎麼做而已, 所以 準備了自己的 setjmp/longjmp, 叫做 my_setjmp, my_long_jmp, 當然對應的 jmp_buf 就是 my_jmp_buf。 再來還剩下一個難題: 在 signal handler 發動自己準備的 my_longjmp 之後, 會發現之 後的 signal handler 不會再次被呼叫了, 這裡存在一個很難發現的魔法, 需要對 singal 是怎麼實作有點了解才會知曉或是閱讀相關介紹 signal 書籍, tlpi (The Linux Programming Interface) 那本就不錯, 經典的 apue (Advanced Programming in the UNIX Environment) 當然也是。 如果想知道 signal 是怎麼實作的話, 可以參考「Linux 内核源代码情景分析」6.4 一節 。 總之在 signal handler 被呼叫之後, 預設情形這個 signal 會被 block 起來, 直到 signal handler 返回之後, 才會被 unblock, 這時候, 同個 signal 來了之後, 這個 signal handler 才會再次發動。 但是我們的 signal handler 並不會正常返回, 因為我們用 longjmp 跳到 func1 或是 func2, 所謂的 signal handler 正常返回是指在 signal handler return 之後, 還會呼 叫 sigreturn (man sigreturn), 這時候會從 user mode 再次切回 kernel mode, 然後 才有機會把原來被中斷的地方再次安插回原本的 stack, 如此一來, 下次這個 process 執行的時候, 才會從被中斷的地方繼續執行。 所以被 block 的 SIGUSR1 會被一直 block 住, 導致之後的收到 SIGUSR1 後, 都不會再執行 signal handler。 所以要在 simple_thread.c 加入 L62 ~ L64, unblock SIGUSR1。 但是如果你是使用 libc 的 setjmp/longjmp, sigsetjmp/siglongjmp 可能不需要自己 unblock SIGUSR1, 系統的 setjmp/longjmp 可能會處理被 block 的 signal, 如果用 _setjmp/_longjmp 就不會處理 signal, 類似我用自己的 my_setjmp, 這時候就要自己 unblock SIGUSR1。 這邊會遇到進階的 signal 議題, 例如: signal handler 可以被中斷嗎? 在執行 signal hadnler 時, 如果有 2 個 signal 送過來, signale handler 會再次執行 2 次嗎? 如果 對這些議題不熟也沒關係, 以這個範例來說, SIGUSR1 signal handler 在執行的時候, 如果再次收到 SIGUSR1, 會等到原本的 SIGUSR1 signal handler 做完, 然後才會再次執行。 這是可以設定的, 那一種作法好呢? 我還沒有答案。 而如果在執行 SIGUSR1 signal handler 期間收到 2 次以上的 SIGUSR1, 之後只會再執 行 SIGUSR1 signal handler 一次, 這樣的行為讓你有點擔心吧, 這表示很有可能 func1, func2 的切換行為有可能會漏掉幾次, 是的, 沒辦法, 傳統 signal 就是這麼「不可靠」。 signal 相關問題可參考 - linux/unix signal 議題 疑! 剛剛不是說要用 SIGALRM, 怎麼變成 SIGUSR1, 因為後來發現用 SIGUSR1 比較好測 試, 就改用這個了。 程式在 setjmp func1, func2 之後, 會使用 longjmp 執行 func2, 再來就是透過 signal handler 來切換到 func1, 再來又透過 signal handler 再次切換到 func2, 依序下去。 simple_thread.c 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <unistd.h> 4 #include <sys/time.h> 5 #include <signal.h> 7 #include "my_setjmp.h" 8 11 12 #define BUF_SIZE 32768 13 char func1_stack[BUF_SIZE+64]; 14 char func2_stack[BUF_SIZE+64]; 16 17 my_jmp_buf th1; 18 my_jmp_buf th2; 21 22 my_jmp_buf *cur_th; 23 my_jmp_buf *next_th; 24 25 26 void func1() 27 { 28 while(1) 29 { 30 printf("1"); 31 printf("2"); 32 printf("3"); 33 printf("4"); 34 printf("5"); 35 printf("6"); 36 printf("7"); 37 printf("8"); 38 printf("9"); 39 printf("a"); 40 printf("\n"); 41 } 42 } 43 void func2() 44 { 45 while(1) 46 { 47 printf("21 "); 48 printf("22 "); 49 printf("23 "); 50 printf("24 "); 51 printf("25 "); 52 printf("\n"); 53 } 54 } 55 57 void sigalrm_fn(int sig) 58 { 59 sigset_t sigs; 60 /* Unblock the SIGUSR1 signal that got us here */ 61 #if 1 62 sigemptyset (&sigs); 63 sigaddset (&sigs, SIGUSR1); 64 sigprocmask (SIG_UNBLOCK, &sigs, NULL); 65 #endif 66 printf("got USR1!\n"); 71 #if 1 72 if (cur_th == &th1) 73 { 74 printf("2\n"); 75 next_th = &th2; 76 } 77 else 78 { 79 printf("1\n"); 80 next_th = &th1; 81 } 82 #endif 83 86 if (my_setjmp(*cur_th) == 0) 87 { 88 cur_th = next_th; 91 my_longjmp(*next_th, 1); 92 } 93 else 94 { 95 return; 96 } 104 return; 105 } 106 107 int main(int argc, char *argv[]) 108 { 109 signal(SIGUSR1, sigalrm_fn); 110 my_setjmp(th1); 111 th1[0].eip = (unsigned long)func1; 112 th1[0].esp = (unsigned long)(func1_stack + BUF_SIZE); 113 114 if (my_setjmp(th2) == 0) 115 { 116 th2[0].eip = (unsigned long)func2; 117 th2[0].esp = (unsigned long)(func2_stack + BUF_SIZE); 118 cur_th = &th2; 119 my_longjmp(th2, 1); 120 } 131 132 while (1) 133 pause(); 181 return 0; 182 } func1 印出 123456789a, function 印出 21 22 23 24 25, 可以從以下影片看出, 當送 出 SIGUSR1, func1 和 func2 會相互切換, 基本上算是成功了。 當然離完成 pthread 這樣的 library 還很遠, 但至少邁出一小步了。 而我的「目的」當然也只是想知道 user mode thread library 是怎麼做的, 也不是想寫出一個 pthread library, 有興趣的朋友可以繼續下去, 完成 ucsb 的作業。 可以用以下指令送出 SIGUSR1 killall -s SIGUSR1 simple_thread https://www.youtube.com/watch?time_continue=1&v=uFv_39Ys2vg&feature=emb_logo
整個程式從開始到完成期間: 20200220 ~ 20200226, 20200312 補上 x86_64 setjmp/ longjmp 的版本。 CS170 作業可不是只要求這樣, 再來還需要有 atomic 的操作, 要寫支援 mutex 這個作 業, 是不是又感覺害怕了。 這個作業都是基本中的基本, 但是基本問題可不等於簡單問題, 這些觀念過了在久, 都不 會改變的, 把心思花在上頭並不會隨著時間而白費。 soure code: https://github.com/descent/simple_thread user mode thread implementation: ‧ ftp://ftp.gnu.org/gnu/pth/pth-1.0.0.tar.gz ‧ ftp.ai.mit.edu/pub/rst/rsthreads.tgz ‧ https://stuff.mit.edu/afs/sipb/project/pthreads/stable/src/release/ pthreads-1_60_beta6.tar.gz blog 版本 descent-incoming.blogspot.com/2020/03/user-mode-pthread-simplethread.html -- 紙上得來終覺淺,絕知此事要躬行。 -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 180.217.67.245 (臺灣) ※ 文章網址: https://www.ptt.cc/bbs/C_and_CPP/M.1584751926.A.0D6.html
LiloHuang: 感謝分享,順便提一下signal safety 03/21 10:05
LiloHuang: https://bit.ly/2wtVGDx 用 signal 時要留意一下就是 03/21 10:07
descent: 感謝提醒, 要把 signal handler 寫好, 真的難 03/21 10:35
descent: stdio.h 的 function 幾乎都不能用 03/21 10:35
ggBird: 推 03/21 10:47
ko27tye: 讚 03/21 11:52
a58524andy: 推 03/21 12:20
nevak: 感謝分享 03/21 13:58
CoNsTaR: M$ 的系統也可以嗎? 03/22 01:26
ms 若有類似 unix signal 的機制, 應該也辦的到, 但 message 機制應該和 signal 不同。
chuegou: 直覺先想到coroutine in c 03/22 03:42
KILLE: 為何要自己實作setjmp/longjmp呢? https://bit.ly/3dmbkBm 03/22 10:42
KILLE: 以上那代碼目的與原po相同 但並無實作setjmp/longjmp 03/22 10:43
感謝分享, 你看他在指定 jmp_buf 的 ip, sp, 需要用 i64_ptr_mangle() 做個運算, 而不是直接指定, 這就是我不想用標準程式庫 setjmp/longjmp 的原因。 我也和這位開發者類似, 用 c++ 寫了 pthread 介面的版本。 https://github.com/descent/simple_thread/blob/master/cpp/simple_thread.cpp
Caesar08: 推 03/22 14:50
sarafciel: 好文章 推 03/23 10:53
sunneo: 正想說之前才看過你的文章 :D 03/23 12:13
KILLE: https://bit.ly/33CsN46 這講setjmp/longjmp 也是講自幹協 03/23 12:32
KILLE: 程 03/23 12:32
感謝分享, 你真會找資料。 ※ 編輯: descent (175.98.141.254 臺灣), 03/23/2020 13:57:18
KILLE: 做鎖也未如難 只要有讓路函數就可https://bit.ly/2WJkB0n 03/25 15:37
KILLE: 還有 你講聖塔巴巴拉很變態 個人非常不以為然 03/25 15:40
KILLE: github上不少用戶態之線程實作 查查都還算完整 03/25 15:41
KILLE: CS170是2019春季作業 github上隨手抓都四年前作品 03/25 15:42
KILLE: https://bit.ly/39go3m6 https://bit.ly/2vShgRW兩個就是 03/25 15:49
KILLE: 交CS170-2作業之學生 並無實作鎖功能 03/25 15:50
KILLE: 這作業難度在於 在網上尋得並整理自己需要部份 03/25 15:51
KILLE: 這年代要人白白寫 無意義 也毋可能 03/25 15:52
KILLE: github上查uthread/uthreads看是要協程還是跳轉實作 03/25 15:58
KILLE: 任君挑選 03/25 15:58
ibmibmibm: 不要在signal handler呼叫非reentrant的function 04/12 23:38