[心得] user mode pthread 實作

作者: descent (「雄辯是銀,沉默是金」)   2020-03-21 08:51:52
看了一系列的舊文章「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
作者: LiloHuang (十年一刻)   2020-03-21 10:05:00
感謝分享,順便提一下signal safetyhttps://bit.ly/2wtVGDx 用 signal 時要留意一下就是
作者: descent (「雄辯是銀,沉默是金」)   2020-03-21 10:35:00
感謝提醒, 要把 signal handler 寫好, 真的難stdio.h 的 function 幾乎都不能用
作者: ggBird (ggBird)   2020-03-21 10:47:00
作者: ko27tye (好滋好滋)   2020-03-21 11:52:00
作者: a58524andy (a58524andy)   2020-03-21 12:20:00
作者: nevak (^o^)   2020-03-21 13:58:00
感謝分享
作者: CoNsTaR ((const *))   2020-03-22 01:26:00
M$ 的系統也可以嗎?
作者: chuegou (chuegou)   2020-03-22 03:42:00
直覺先想到coroutine in c
作者: KILLE (啃)   2020-03-22 10:42:00
為何要自己實作setjmp/longjmp呢? https://bit.ly/3dmbkBm以上那代碼目的與原po相同 但並無實作setjmp/longjmp
作者: Caesar08 (Caesar)   2020-03-22 14:50:00
作者: sarafciel (Cattuz)   2020-03-23 10:53:00
好文章 推
作者: sunneo (艾斯寇德)   2020-03-23 12:13:00
正想說之前才看過你的文章 :D
作者: KILLE (啃)   2020-03-23 12:32:00
https://bit.ly/33CsN46 這講setjmp/longjmp 也是講自幹協程
作者: ibmibmibm (BestSteve)   2020-04-12 23:38:00
不要在signal handler呼叫非reentrant的functionhttps://stackoverflow.com/questions/16891019/

Links booklink

Contact Us: admin [ a t ] ucptt.com