2020年3月27日 星期五

[日本自助行 5/8] ] 20190911 大阪 - 名古屋 - 大須

日期: 20190911 (3) (5/8)
地點: 大阪 - 名古屋 - 大須觀音

本篇文章提到的時間均為日本時間, 沒寫單位的金額為日圓。

我不會日文, 英文也不好: 文章中有關問路或是其他問人的資訊, 都是用很克難的方式完成 (簡單英文、簡單日文、比手畫腳)。
[搭乘前往名古屋的新幹線]
今天要離開大阪準備前往名古屋, 在大阪只停留了短短的 2 天, 還無法認識大阪這個城市, 前往名古屋只是為了搭乘磁浮列車, 是的, 就只是因為想搭乘磁浮列車, 所以才安排了名古屋, 不過後來得知「高山」這個景點, 便在安排一天名古屋的行程, 打算前往高山。

從大版到名古屋有好多種交通方式, 我選擇了新幹線, 2020/9/9 前往京都在新大阪車站時, 我特地在售票機練習購買新幹線車票, 反覆幾次之後, 大概知道怎麼按了, 我會切換日文/中文/英文畫面對照, 如果還是不會買, 最差就是去櫃台買票, 但我還是希望可以用售票機購買, 付款時本來想用信用卡, 但機器要我輸入密碼, 我不知道要輸入什麼, 最後還是用現金付款。

fig 1 新幹線售票機 - 日文畫面

購買的是自由席票券, 我認為這個時間點應該不會沒有位子, 省點錢, 畢竟沒有 pass, 新幹線可是很貴的, 5830。

和我在旅遊書上查的資料不同, 我以為會有特急券+乘車券+新幹線券之類的組合票券, 不過最後只有一張, 不知道要怎麼樣才能購買到那些票券。



自由席還有一個特點, 就是時間可以很彈性, 對我這種不喜歡被綁死時間的遊客來說, 是個好選擇, 我不慌不忙的看著時刻表, 仔細打量可以搭乘的班次, 最後選了 11:06 的班次。

fig 2 搭乘 11:06 のぞめみ 14 新幹線
已經有不少遊客在排隊等車, 人不算多, 我應該有位置坐吧! 就算沒有, 站個一小時也還可以接受。車來了, 自由席在 1 ~ 3 車, 進去看看有沒位子吧, 如我所想, 果然是有位置的, 在新大阪的遊客進車之後, 幾乎已經沒有座位了, 下一個停靠站是「京都」, 前天剛來過, 只是不是搭新幹線, 到京都站時, 再次想起京都單車行。

  

[名古屋]

11:56 抵達名古屋, 大約花了一小時, 並不會太長, 大概是高鐵高雄 - 台中的時間。不急著離開, 我按照慣例, 在月台上花了點時間晃晃, 看看有沒有什麼有趣的東西, 之後才沿著出口離去。

fig 3 11:56 抵達名古屋

出了剪票口, 成功來到名古屋了, 名古屋車站還真不是普通的大, 我像著鄉巴佬進城, 傻眼的望著名古屋車站, 這也太大了吧, 走了好久都還沒有離開車站, 不僅如此, 人潮還非常多, 今天明明就不是假日, 怎麼人這麼多, 日本人都不用上班哦!

先去找膠囊旅館, 選擇的是 - first cabin, 我看著 google map, 往某個方向走過去, 經過了不短的時間, 我才穿越了名古屋車站, 再次看著 google map, 疑! 奇怪, 怎麼離 first cabin 越來越遠了, 我馬上意識到我走錯方向了, 我往名鐵線的方向去了, 那是錯的, 應該要往 JR 新幹線的出口過去才是。

本來 10 分鐘的路程我走了 40 分鐘才到, 冤枉。

進去辦理 checkin 手續, 櫃台人員說明要 17:00 才可辦理, 但是我可以先把行李放在櫃台, 太棒了, 這樣我就可以先去逛逛了。

  

安置好行李箱之後, 不急著出發, 我得先解決中餐, 肚子餓個不停, 先看看附近有什麼吃的, 被我找到一個 250 便當店, 真是太便宜了, 我馬上就過去購買, 這間店只能外帶, 沒有內用。日本餐飲店外帶、內用是 2 種執照, 所以提供外帶/內用的店家, 得需要有這 2 種執照。

fig 5 250 便當 (不含稅); 含稅: 270
含稅之後是 270, 之前我買過最便宜的便當是 550, 這次破紀錄了。我選了一個主食是魚排的便當, 想吃點不同的, 來日本都在吃燒肉。

再來得找個地方坐坐, 享受我的美食, 隨便的找了一間沒營業的建築前方, 就在騎樓吃了起來, 四周圍的日本人不知道會不會覺得奇怪?

接著便前往 volks 名古屋, 紀錄在「[娃店] volks 大阪、volks 名古屋、大阪 dolk

這時候名古屋下了一點小雨, 拿出我隨身的雨傘, 總算用到這隻雨傘了, 來日本大版這幾天, 氣候非常炎熱, 下點小雨總算清涼一些。

結束 volks 名古屋一行之後, 來到大須觀音商店街, 這裡有販售動漫週邊的地方, 我來到了 mandrake, 尋找 dd 娃物。

uber eat 最近很流行, 在日本也看到不少, 今天比較特別看到單車女騎士在送 uber eat。

從 volks 名古屋走到 mandrake 只有一小段, 並不會很遠, 大約 10 分鐘左右即可到達。我從地下鉄鶴舞線到「上前津駅」下車到 volks 名古屋, 再走路到 mandrake。

紀錄在: 20190911 (3) 名古屋 mandrake



離開 madrank 已經是 18:00, 這時我往大須商店街走去, 就去商店街晃晃吧! 一路就這樣走出商店街, 抵達大須觀音神社。不過時間有點晚了, 匆匆看了幾眼就去大須觀音站搭車回名古屋。



回名古屋車站時, 龐大的人潮依然讓我驚訝, 怎麼老是這麼多人, 真是誇張。我打算再去 250 便當店光顧, 在我好不容易找到它時, 便當店已經打烊了, 哇! 怎麼辦阿, 剛剛好像有看到吉野家, 就你了。




好像吃太多燒肉餐了, 因為不愛麵類, 喜歡吃日本米, 不過這次好像沒吃到好吃的日本米, 難道大阪和東京的米質有差別嗎?

[first cabin 膠囊旅館]

吃飽到 first cabin 辦理 check-in, 這時候大約是 20:00, 還真的有點晚了, 櫃台人員說明了一些規則, 勉強可以聽懂 (英文, 沒有中文, 但是有中文的住房規則), 反正那些住宿規則都差不多, 有鎖頭的櫃子在那, 說話不要太大聲影響到別人 ...



這邊的盥洗是在地下室, 一樣有個大浴池可以泡, 另外隔開的幾個空間是用來洗澡的, 我已經很習慣這樣的盥洗設備了。




first cabin 雖然是膠囊旅館, 但整體看起來非常氣派, 寬廣的大廳, 明亮的燈光, 穿著整齊的櫃台小姐, 還有滿滿櫃子的漫畫可以看, 當然是日文版的。

和東京的 Capsule Hotel Dandy (サウナ カプセル ダンディ) 膠囊旅館不同, 這邊可以自己帶外食進來, 也有熱水可以泡泡麵, 我有一餐就是回到這裡吃泡麵。

進入膠囊客房之後, 有一個空間是用來鎖行李箱的, 這時候把櫃台人員給的鎖和行李箱鎖在一起就可以了。

這邊沒有鞋櫃, 所以可以在走廊上看到各式各樣的鞋子以及紙拖鞋, 運氣不錯, 我的位置是在下層, 方便進出。

膠囊空間對一個人躺下來說足夠寬敞, 附有鎖頭的櫃子在床邊, 所以空間並不是很大, 但不用在跑去置物櫃的地方而是和床在一起其實比較方便。錢包這些東西去洗澡時就可以放在這並鎖上。

將所有東西整理好之後, 拿著換洗衣物, 到地下室洗了澡, 泡個大浴池, 舒服多了。

膠囊房間禁止打電話, 需要到一樓大廳, 和親朋好友報平安後, 就準備睡覺, 結束今天的旅程, 明天得要早起, 去高山可是很遠的。

2020年3月20日 星期五

執行遠端的 X 應用程式包含中文輸入法

本機: pc 端 linux 遠端 eeepc 901: 透過 ssh 連到「遠端 eeepc 901」

本機的 /etc/ssh/ssh_config 修改 ForwardX11 yes

這樣從本機連入 eeepc 901 時, eeepc 901 的終端機會設定 DISPLAY=localhost:10.0 就可以將遠端 eeepc 901 的 firefox 畫面執行到本機中。不用自己辛苦的設定 DISPLAY 變數。

遠端 xwindow app 使用中文輸入法

env: 連入 eeepc 901, 使用 eeepc 901 firefox, eeepc 901 裝的是 gcin, 本機裝的是 fcitx, 所以執行 eeepc 901 的 firefox 之後, 使用的是 gcin 來輸入中文, 本機的 x 應用程式則是用 fcitx, 有點複雜, 希望你沒昏頭。
ssh username_abc@eeepc901_ip # 請輸入對應的 username/ip
export XMODIFIERS=@im=gcin
export GTK_IM_MODULE=gcin
export QT_IM_MODULE=gcin # for telegram
gcin&
firefox
這樣就會看到 eeepc 901 的 firefox 在本機的 X 執行起來, 中文也可以正常輸入。

中文輸入的問題: 試了好多天, 終於搞定我的 X Client

如果遇到以下問題:
Error: cannot open display: localhost:10.0
X11 connection rejected because of wrong authentication.
參考:
xauth not creating .Xauthority file

使用 xauth 重新建立 ~/.Xauthority
# Rename the existing .Xauthority file by running the following command
mv .Xauthority old.Xauthority

# xauth with complain unless ~/.Xauthority exists
touch ~/.Xauthority

# only this one key is needed for X11 over SSH
xauth generate :0 . trusted

# generate our own key, xauth requires 128 bit hex encoding
xauth add ${HOST}:0 . $(xxd -l 16 -p /dev/urandom)

# To view a listing of the .Xauthority file, enter the following
xauth list

2020年3月13日 星期五

linux/unix signal 議題

以簡馭繁,以變為宗。
signal 是一個很複雜的東西, 如果和 fork, thread 搞在一起, 複雜到令人害怕, 關於 signal 和 thread 的其中一個複雜議題, 可以參考 - thread 和 signal

如果用 c++ 再搭配 c++ exception handling, 那更是複雜到爆炸, 隨便混在一起, 你清楚程式的執行路徑嗎?

在我剛開始接觸 linux programming 之時, 每本相關書籍都會提到 siganl, 但我總是懵懵懂懂, 好像知道了, 又好像完全不認識 signal, 在利用 signal + setjmp/longjmp 寫出 user mode thread 時, 我又重新學習了 signal。

signal 可以想成是中斷。一般 cpu 會有中斷控制器可以接受中斷, signal 則是 unix 用來模擬中斷的行為。

如果想練習中斷控制器的程式, 但沒有適當的硬體或是不想面對硬體的暫存器設定, 可以用 signal 程式來練習, 和寫中斷程式的觀念都一樣, 本篇就是要討論這些問題。

當然硬體的中斷不能佔用太多 cpu 時間來處理, signal 則沒有這樣的限制, 你要在 signal handler while(1) 也不會影響整個系統。但其餘要注意的可重入性都是和中斷一樣的。

而 signal 一開始的設計不完美, 衍生一些漏洞, 而之後改善的版本是以另外一組 signal 來提供, 之前不完美的 signal 就因為相容性保留下來。

「Linux 内核源代码情景分析」6.4 一節, 說明 linux 怎麼實作 signal, 我從這裡得知為什麼 pause() 可以被 signal 打斷, 而 system call 為什麼又會因為 signal 而有被打斷而沒有執行完整的問題。

傳統 signal (不完美版本)
Signal     Value     Action   Comment
───────────────────────────────────────────
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard
SIGQUIT       3       Core    Quit from keyboard
SIGILL        4       Core    Illegal Instruction
SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no
                              readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
SIGCHLD   20,17,18    Ign     Child stopped or terminated
SIGCONT   19,18,25    Cont    Continue if stopped
SIGSTOP   17,19,23    Stop    Stop process
SIGTSTP   18,20,24    Stop    Stop typed at terminal
SIGTTIN   21,21,26    Stop    Terminal input for background process
SIGTTOU   22,22,27    Stop    Terminal output for background process

改善的版本:
SIGRTMIN ~ SIGRTMAX

這篇文章要看幾個 signal 特性:
q11: 當處理 SIGUSR1 signal handler 發動時, 這時候再收到一個 SIGUSR1, 同一個 SIGUSR1 signal handler 會被中斷, 然後去執行這次收到的 SIGUSR1 引發的 SIGUSR1 signal handler 嗎?

注意: 這裡指再次收到同樣的 SIGUSR1 signal, 如果是處理 SIGUSR1 signal handler, 然後收到 SIGUSR2, 那是不同的情形。

答案是「會」, 也「不會」。

如果是「不會」的話, 等到第一次的 SIGUSR1 signal handler 執行完之後, 會再次執行一次 SIGUSR1 signal handler 嗎?

答案是「會」, 也「不會」。

如果是「會」的話, 再問一個問題:
q22: 當處理 SIGUSR1 signal handler 發動時, 這時候再收到「兩個」SIGUSR1, 同一個 SIGUSR1 signal handler 執行結束之後, 這個 SIGUSR1 signal handler 會再執行 2 次嗎?

在 q11 的前提上, 答案是不會, 一樣只執行一次。

但是如果 SIGUSR1 換成 SIGRTMIN, 同一個 signal handler 會執行 2 次, 很奇怪, 為什麼要設計成不一樣的行為? 這就是新舊 2 種 signal 的分別。

test_signal.c 用了 2 個 signal 測試, 一次是 SIGUSR1, 另外一次是 SIGRTMIN。我刻意讓 signal handler sigalrm_fn 做個 delay, 以便在還沒結束時, 可以再收到一個或是兩個 SIGUSR1/SIGRTMIN。

test_signal.c
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/time.h>
 5 #include <signal.h>
 7 
 8 void sigalrm_fn(int sig)
 9 {
10   static int cnt;
11   printf("got sig: %d, cnt: %d\n", sig, cnt);
12   for (int i=0 ; i < 10000 ; ++i)
13     for (int i=0 ; i < 1000 ; ++i)
14       for (int i=0 ; i < 500 ; ++i)
15         ;
16   printf("end USR1!\n", cnt);
17   ++cnt;
18 }
19 
20 int main(int argc, char *argv[])
21 {
22   printf("SIGRTMIN: %d\n", SIGRTMIN);
23   printf("SIGRTMAX: %d\n", SIGRTMAX);
24   //signal(SIGUSR1, sigalrm_fn);
25   signal(SIGRTMIN, sigalrm_fn);
26 
29   while (1) 
30     pause();
31 
32   return 0;
33 }

對於 SIGUSR1 來說, 如果在 sigalrm_fn 執行期間送出 2 個 SIGUSR1, sigalrm_fn 只會執行一次; 對於 SIGRTMIN 來說, 如果在 sigalrm_fn 執行期間送出 2 個 SIGRTMIN, sigalrm_fn 會執行 2 次, 一般來說都會覺得 SIGRTMIN 的行為比較正確, 這就是改善後的效果。

對於傳統的 signal, 2 次的 signal 會被合併成一個, 對於 rt 系列的 signal, 會被 queue 起來。

在我的系統上, SIGRTMIN 是 34, 所以用以下的指令測試。

killall -34 test_signal
killall -s SIGUSR1 simple_thread

在 end USR1! 訊息還沒有印出的時候, 再送出 2 次的 SIGRTMIN 或是 SIGUSR1, 就可以看到這個現象。

如果在你的平台不是這樣的結果可能是 signal 的預設行為不同, 那 ... 就算了。

如果這樣還沒有難倒你, 那再來看一個。

sysv_signal() 是古早不可靠時代的版本, 把 signal(SIGUSR1, sigalrm_fn) 換成 sysv_signal(SIGUSR1, sigalrm_fn) 之後, 會發現在 sigalrm_fn 執行期間, 送出 SIGUSR1 之後, 程式會突然結束。

這是因為古早的版本除了不會 queue 住訊號之外, 在執行的途中如果又來一個 SIGUSR1, 正在執行的 signal handler 會被中斷, 而在執行 SIGUSR1 signal handler 期間, 還會把設定好的 SIGUSR1 signal handler sigalrm_fn 換回預設行為, 而 SIGUSR1 的預設行為就是中斷程式, 所以程式就這麼結束了。

所以得在 SIGUSR1 signal handler 裡頭再次呼叫 signal, 讓 signal 不要把 SIGUSR1 signal handler 改回預設, 很奇怪的設計吧!

那我使用的 signal 行為到底是那一種呢? 很遺憾, 沒有固定的版本, glibc 不同版本的實作可能也不同, 所以還是使用 sigaction 會有比較好的可攜性, 不過 sigaction 用法就複雜多了。

現在你知道為什麼 sigaction 這麼複雜, 因為要知道這麼多東西, 才能把 sigaction 的那些參數設定好。

上述的 signal 相當於以下的 sigaction。
struct sigaction s1, old_s1;

sigemptyset(&s1.sa_mask);

s1.sa_flags = 0;

s1.sa_handler = sigalrm_fn;
sigaction(SIGUSR1, &s1, &old_s1);

如果想要在執行 signal handler 時, 可以中斷目前的 signal handler 執行, 使用以下的 flags。
s1.sa_flags = SA_NODEFER;

看起來只多了幾行, 但要理解背後的設計並不簡單, tlpi 花了 3 章在說明 signal, 但我想第一次看這 3 章的人應該不會一次就能理解。

本篇對於 signal 的敘述沒有很精確, 請參考相關專業書籍, tlpi (The Linux Programming Interface), 經典的 apue (Advanced Programming in the UNIX Environment), 最主要是想提提一些觀念, 讓大家知道 signal 不是好惹的。

我還閱讀了「Linux 内核源代码情景分析」6.4 一節, 理解 linux 怎麼實作 signal, 慢慢串起這些觀念。

windows message 機制我不熟, 但應該和 signal 中斷的方式是不一樣的。

signal 的設計太厲害了, 這麼複雜的東西到底是誰想到加入 unix 的。

2020年3月6日 星期五

pthread 實作練習 (0) - user mode pthread 實作 - simple_thread

熱極則風, 花盛必謝。
看了一系列的舊文章「Re: 什麼是 multi-thread」才知道原來 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 的內容, 考試則是課本上的知識, 程度真的差太多, 實作太少了, 這樣和非本科的差距並不會太大, 如果是會寫一個 user mode thread library, 和非本科的差距就顯現出來了。

以前老師給了另外的選擇, 看 bsd source code, 不過被全班投票的結果否決了, 真的可惜。

但是該課程也沒那麼狠心, 在 Implementation 一節中, 說明的一些實作細節, 用著我的破英文很勉強的看了看, 得知了幾個關鍵。

需要使用 setjmp/longjmp, signal。

如果對 setjmp/longjmp 很陌生, 可參考 - setjmp/longjmp 實作 (x86 32 bit)

知道這個概念之後, 相當高興, 以為可以順利寫出來, 但在我開始下手時, 卻發現困難重重, 我不知道應該在哪裡執行 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


The project is an individual project. It is due on uesday, April 30, 2019, 23:59:59 PST (no deadline extensions or late turn ins).

Implement a basic thread subsystem in Linux user mode


The goal of this project is to implement a basic thread system for Linux. As discussed in class, threads are independent units of execution that run (virtually) in parallel in the address space of a single process (and thus, share the same heap memory, open files, process identifier, ...). Each thread has its own context, which consists of (a) the set of CPU registers and (b) a stack. The goal of a thread subsystem is to provide applications that want to use threads a set of library functions (an interface) that the application can use to create and start new threads, terminate threads, or manipulate threads in different ways.
The most well-known and wide-spread standard that defines the interface for threads on Unix-style operating systems is called POSIX threads (or pthreads). The pthreads interface defines a set of functions, a few of which we want to implement for this project. Of course, there are different ways in which the pthreads interface can be realized, and systems have implemented a pthreads subsystem both in the OS kernel and in user mode. For this project, we aim to implement a few pthreads functions in user mode (as a library) on Linux.
More specifically, for this project, we want to implement the following POSIX thread functions (prototypes and explanations partially taken from the respective man pages):
int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
The pthread_create() function shall create a new thread within a process. Upon successful completion, pthread_create() shall store the ID of the created thread in the location referenced by thread. In our implementation, the second argument (attr) shall always be NULL. The thread is created executing start_routine with arg as its sole argument. If the start_routine returns, the effect shall be as if there was an implicit call to pthread_exit() using the return value of start_routine as the exit status. Note that the thread in which main() was originally invoked differs from this. When it returns from main(), the effect shall be as if there was an implicit call to exit() using the return value of main() as the exit status.
void pthread_exit(void *value_ptr);
The pthread_exit() function shall terminate the calling thread. In our current implementation, we ignore the value passed in as the first argument (value_ptr) and clean up all information related to the terminating thread. The process shall exit with an exit status of 0 after the last thread has been terminated. The behavior shall be as if the implementation called exit() with a zero argument at thread termination time.
pthread_t pthread_self(void);
The pthread_self() function shall return the thread ID of the calling thread.
The goal of this project is to implement the three functions introduced above. For more details about error handling, please refer to the respective man pages. The code for these three functions should be compiled into an object file called threads.o, which will act as our thread library. Note that your code will not contain a main routine, and hence, cannot run as a stand-alone executable. To create the thread library, simply call the compiler like this:
gcc/g++ -c -o threads.o [list of your source files]
You should include the pthreads header file (#include ) in your source(s) so that you have the definitions of types such as pthread_t or pthread_attr_t available. The object file that was created can then be combined with a (third party) application that requires threads (and hence, calls the three functions that you have implemented in your library). This is done by running:
gcc/g++ -o application [list of application object files] threads.o
To test your thread implementation, we will compile your thread library with our test application. This test application will call into your pthread functions and check whether threads are properly started and terminated.

Implementation


Implementing a user space thread library seems like a daunting task at first. This section contains a number of hints and directions on how you could approach the problem.
First, you will need a data structure that can store information about a single thread. This data structure will likely need to hold, at least, information about the state of the thread (its set of registers), information about its stack (e.g., a pointer to the thread's stack area), and information about the status of the thread (whether it is running, ready to run, or has exited). This data structure is often called a thread control block (TCB). Since there will be multiple threads active at the same time, the thread control blocks should be stored in a list or a table (array). To make it easier, you can assume that a program will not create more than 128 threads.
You will likely need a routine that initializes your thread subsystem. This init routine should be called when the application calls pthread_create for the first time. Before that, there is only one thread running (the main program), and there is not much you need to do.
Once multiple threads are running, you will need a way of switching between different threads. To do this, you could use the library functions setjmp and longjmp. In a nutshell, setjmp saves the current state of a thread into a jmp_buf structure. longjmp uses this structure to restore (jump back) to a previously saved state. You will find these functions very useful, since they do exactly what is needed when switching from one thread to another and later resuming this thread's execution. More precisely, you can have the currently executing thread call setjmp and save the result in the thread's TCB. Then, your thread subsystem can pick another thread, and use the saved jmp_buf of that thread together with longjmp to resume the execution of the new thread.
The process of picking another thread is called scheduling. In our system, we want to have a very simply scheduler. Just cycle through the available threads in a round robin fashion, giving an equal, fair share to each thread.
Following the discussion about setjmp and longjmp, there is one question that should immediately arise: How can we force the thread of a program to call setjmp every once in a while (and thus, giving up control of the CPU)? In particular, application developers do not need to know about your thread implementation, so it is unlikely that they will periodically call setjmp and allow your scheduler to switch to a new process. To solve this issue, we can make use of signals and alarms. More precisely, we can use the ualarm or the setitimer function to set up a periodic timer that sends a SIGALRM signal every X milliseconds (for this project, we choose X to be 50ms). Whenever the alarm goes of, the operating system will invoke the signal handler for SIGALRM. So, you can install your own, custom signal handler that performs the scheduling (switching between threads) for you. For installing this signal handler, you should use sigaction with the SA_NODEFER flag (read the man page for sigaction for details). Otherwise (e.g., when using the deprecated signal function, alarms are automatically blocked while you are running the signal handler. This is something that we clearly do not want for this project.
Note that we require that your thread system supports thread preemption and switches between multiple threads that are ready. It is not okay to run each individual thread to completion before giving the next one a chance to execute.
A final problem that needs to be addressed is how to create a new thread. We have previously discussed means to switch between two threads once they have been created. However, there must also be a mechanism to create a new thread "from scratch." To this end, the system has to properly initialize the TCB for the new thread. This means that a new thread ID needs to be created. In addition, the system has to allocate a new stack. This can be done easily using malloc. For this project, we want to allocate for each thread a stack of 32,767 bytes. The last step is to initialize the thread's state so that it "resumes" execution from the start function that is given as argument to the pthread_create function. For this, we could use setjmp to save the state of the current thread in a jmp_buf, and then, modify this jmp_buf in two important ways. First, we want to change the program counter (the EIP) to point to the start function. Second, we want the stack pointer (the ESP) to point to the top of our newly allocated stack.
To modify the jmp_buf directly, we have to first understand that it is a very operating system and processor family-specific data structure that is typically not modified directly. On the CSIL machines, we can see the definition of the jmp_buf here in this header file: /usr/include/bits/setjmp.h. Given that we work on a 64-bit machine on CSIL, the jmp_buf is defined as an array of 8 long (64-bit) integers. Moreover, libc (x86_64/jmpbuf-offsets.h) defines the following constants as the eight integer elements of this structure:
#define JB_RBX   0
  #define JB_RBP   1
  #define JB_R12   2
  #define JB_R13   3
  #define JB_R14   4
  #define JB_R15   5
  #define JB_RSP   6
  #define JB_PC    7
We can see that the stack pointer (RSP) has index 6 and the program counter (PC) has index 7 into the jmp_buf. This allows us to easily write the new values for ESP and EIP into a jmp_buf. Unfortunately, there is a small complication on the Linux systems in the lab. These machines are equipped with a libc that includes a security feature to protect the addresses stored in jump buffers. This security feature "mangles" (i.e., encrypts) a pointer before saving it in a jmp_buf. Thus, we also have to mangle our new stack pointer and program counter before we can write it into a jump buffer, otherwise decryption (and subsequent uses) will fail. To mangle a pointer before writing it into the jump buffer, make use of the following function:
static long int i64_ptr_mangle(long int p)
   {
        long int ret;
        asm(" mov %1, %%rax;\n"
            " xor %%fs:0x30, %%rax;"
            " rol $0x11, %%rax;"
            " mov %%rax, %0;"
        : "=r"(ret)
        : "r"(p)
        : "%rax"
        );
        return ret;
   }
    
We are almost there. Now, we just have to remember that the start routine of every new thread expects a single argument (a void pointer called arg). Also, when the start routine returns, it should perform an implicit jump to pthread_exit. We first have to understand how arguments are passed between functions. When you think back to your compiler class, you might remember that arguments were passed on the stack. Indeed, this was the way it was done on "old" 32-bit x86 machines. However, we now have 64-bit machines, and these machines have more registers. To improve the performance of function calls, compiler writers decided to leverage these additional registers. More specifically, the function calling convention was changed, and the first six arguments are passed in registers. Only the 7th argument and onwards are passed on the stack. This page provides some more details about this. Now, the question is how we can pass the required argument to the start function of the thread? One possible solution is to introduce a wrapper function. This wrapper function could grab the argument from the TCB and then invoke the actual start function, passing it the required argument. In addition, the wrapper function also provides a convenient way to handle the case when the start function returns and you have to call pthread_exit. For this, you can simply invoke pthread_exit after the thread returns from the previous call to the start function. When you use a wrapper function, you need to make the program counter (EIP) point to this function instead of the actual start function. And you need to find a way to make the address of the start function available to the wrapper.
Once your stack is initialized and the (mangled) stack pointer (ESP) and program counter (EIP) are written to the jmp_buf, your new thread is all ready to go, and when the next SIGALRM arrives, it is ready to be scheduled!
This is a complicated project. Take it one step at a time, and make sure that everything works before moving on. Also, the debugger (gdb) is your friend. Expect that the code will crash. At this point, use the debugger to understand what is going on. For this project, you might want to look at the gdb commands disassemble and stepi. For example, your code might crash when you invoke longjmp for the first time to switch to a new thread, and you don't understand why. To debug this problem, you could set a breakpoint in __longjmp (say yes when the debugger asks whether you want to "make breakpoint pending on future shared library load"). Since __longjmp is a libc library function, you don't have the source directly available to the debugger. However, you can issue the disassemble command to show the assembly code for this (short) function. And you can use stepi to step forward for a single machine (assembly) instruction. This allows you to check if you restore the correct register values and also see the address where you end up jumping to. You can use the commands info registers to list the content of all the CPU registers, and you can use x [address] to print out the value that is stored in the memory at [address]. This is nice when you want to see where your stack pointer points to.

Deliverables


Please follow the instructions below exactly!
  1. We use gradescope to manage your project submissions and to communicate the results back to you. You will submit all files that are part of your project via the gradescope web interface.
  2. All your files must be in a directory named threads. The name of the threads library that we will test must be threads.o, and the POSIX function implementation must be done in C/C++. Of course, you cannot leverage any of the existing pthread library code to implement your thread library.
  3. All files that you need to build your library must be included (sources, headers, makefile) in that folder. We will just call make and expect that the object file threads.o is built from your sources. Please do not include any object or executable files.
  4. Gradescope does support built-in autograding, but, currently, we do not intend to use it. Instead, we will test your projects in our own environment. So, do not worry if you don't get immediate feedback or if the system tells you that the autograder is not running.
  5. Your project must compile on a CSIL machine. If you worked on a Windows machine or your laptop at home, then make sure it still works on CSIL or modify it appropriately!
  6. Include a README with this project. Explain what you did in the README. If you had problems, tell us why and what.

其實在看提示之前, 我有想到應該在 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 的作業。

例如: main thread 並沒有繼續下去, 這個版本只會在 func1, func2 交錯執行, main 之後的程式再也不會執行。當然還有一堆不完整的東西沒有實作, 後來我開始實作某些 pthread function, 才發現我漏了很多東西, 而且這些東西難度也不低。只有這個主要觀念離簡單 pthread 實作都還很遠。

可以用以下指令送出 SIGUSR1
killall -s SIGUSR1 simple_thread

整個程式從開始到完成期間: 20200220 ~ 20200226, 20200312 補上 x86_64 setjmp/longjmp 的版本。

CS170 作業可不是只要求這樣, 再來還需要有 atomic 的操作, 要寫支援 mutex 這個作業, 是不是又感覺害怕了。我搞錯了, 不需要到 atomic 的操作, 只要類似關中斷的行為即可, 這個難度就下降很多。

這個作業都是基本中的基本, 但是基本問題可不等於簡單問題, 這些觀念過了在久, 都不會改變的, 把心思花在上頭並不會隨著時間而白費。

好了, 再來就是真的切換 thread, 只要把 SIGUSR1 換成 SIGALRM, 設定成 1 秒切換, 這樣 func1, func2 就會在 1 秒內切換, 你說 1 秒太慢了, 也可以換成 ms, 就留給各位朋友當作業了。



以下影片展示 gdb debug, 同時顯示 c/asm source code 和 asm 的 debug 視窗, 可以看到 setjmp/longjmp 是怎麼運作的。

ctrl+x 2 開啟 2 個視窗
layout asm 開啟 asm debug 
ctrl+x o 可以切換視窗
最上方是 source code
中間是反組譯的組合語言
最下方是 gdb 指令區



我的 simple 系列又多了一筆 - simple_thread, 不過 simple 系列一點都不 simple。
soure code:
https://github.com/descent/simple_thread

user mode thread implementation:

ref:

以下資訊感謝 KILLE 提供:
這是另外一個簡單的實作 (感覺像是修 CS170 的學生):
https://github.com/DennisZZH/User-Mode-Thread-Library

另外可參考的實作:
https://github.com/prabhendu/operating_system_1/blob/master/gtthread.c
https://github.com/citrix123/Uthreads/blob/master/uthread.c

徹底理解setjmp/longjmp並DIY一個簡單的協程
https://www.twblogs.net/a/5ccbd4fabd9eee1ac30bd85c

彻底理解setjmp/longjmp并DIY一个简单的协程
https://blog.csdn.net/dog250/article/details/89742140