[心得] 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