2020年5月24日 星期日

pthread 實作練習 (1) - pthread_create, pthread_exit, pthread_self

相分食有賰, 相搶食無份
在上次的「user mode pthread 實作 - simple_thread」已經知道如何實作 user mode thread 的觀念, 這次進一步來完成「CS170: Project 2 - User Mode Thread Library (20% of project score)」要求的 3 個函式:
  1. int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg);
  2. void pthread_exit(void *value_ptr);
  3. pthread_t pthread_self(void);
自己嘗試了一下, 比我想像的還要難一些。

這篇文章說明 pthread_create(), pthread_exit() 我自己實作的過程, pthread_self() 感覺好像不難, 但也沒那麼單純。

[pthread_t pthread_self(void)]

pthread_self 把 pthread_t 當成 thread id 回傳, 這就造成我只能把 pthread_t 定義成一個數字, 雖然也可以把 pthread_t 定義成 struct, 但在這邊就不好處理了。

原本我把 pthread_t 定義為 struct, 只好改為

typedef unsigned long long pthread_t;

struct ThreadData
{
  my_x32_jmp_buf jmp_buf_;
};
typedef std::pair ThreadPair;

另外準備 ThreadPair 當做管理 thread 的資料結構。

pthread_t pthread_self(void)
{
  return thread_vec[current_index].first;
}

pthread_self() 就這麼簡單。當目前的 thread 執行時, 傳回目前的 ThreadPair 資料結構。所以 current_index 指向正確的資料結構是很重要的, 計算錯誤就指到別的 thread 了。

[void pthread_exit(void *value_ptr)]

pthread_create 相對簡單一些, pthread_exit 就有點難倒我。

在「user mode pthread 實作 - simple_thread」我是用 while(1) 測試一個 thread, 但如果這個 thread 正常結束了, 應該要怎麼辦呢? 應該要正常離開吧。

但什麼才是正常離開呢?

schedule 不會再去把這個 thread 選出來執行, 否則這個 thread 永遠都會被執行, 但是由於 function 已經結束, 再選出來執行的時候, 只會亂跑, cpu 跳到不正確的地方 (因為已經沒有正常的程式碼了), 最後會造成整個 process segmentation fault。

另外一個問題是, 如果這個 thread 沒有呼叫 pthread_exit(), 我們希望可以作到在 thread 結束時, 會去呼叫 pthread_exit(), 覺得是不可能的任務嗎?

simple_thread.cpp
206 int func3_ret = 33;
207 void *func3(void *arg)
208 {
209   {
210     printf("331 ");
211     printf("332 ");
212     printf("333 ");
213     printf("334 ");
214     printf("335 ");
215     printf("\n");
216   }
217   return &func3_ret;
218 }

在這個 func3 thread 結束之後, 要讓他呼叫 pthread_exit(), 而 pthread_exit 會做一些事情, 將 func3 這個 thread 移除, 之後就不會再選 func3 來執行。

這樣應該會覺得已經很難了, 不過作業還不只這樣, 作業希望可以把 func3_ret 的值傳給 pthread_exit(void *value_ptr), 這樣在 pthread_exit(void *value_ptr) 印出 value_ptr 時, 會得到 33, list 1 的結果。

list 1
331 332 333 334 335
thread exit: 0x81d5074, retval: 33, current_index: 2

有點不可思議是嗎?

結果又是我自己把他想得太難了, 在看完「CS170: Project 3 - Thread Synchronization (20% of project score)」Implementation 之後, 原來有個這麼簡單的方式, 我恍然大悟。

先說明我自己那麼胡亂的想法。

怎麼在 func3 之後呼叫 pthread_exit(), 操作 stack, 在 stack 的 return address 把 pthread_exit 位址存入之後即可, 不算太難, 但如果要把 &func3_ret 傳給 pthread_exit 的參數呢? 有點難, 我先 push 一個 function push_arg, 再次 push pthread_exit, 讓 pthread_exit 可以取得 push_arg 的參數也就是 &func3_ret, 看起來很厲害, 聽不懂沒關係, 因為完全是多此一舉。

漂亮的解法是, 只要用一個 wrapper function 包住這個 thread function 即可, 文字說明可能不清楚, show me the code。

simple_thread.cpp
215   void wrap_routine(void *(*start_routine) (void *), void *arg)
216   {
217     void *ptr;
218     if (start_routine)
219     {
220       printf("arg: %p\n", arg);
221       ptr = start_routine(arg);
222     }
223     pthread_exit(ptr);
224   }

236     thread_pair.second.jmp_buf_[0].eip = (intptr_t)wrap_routine;

251     *(intptr_t*)thread_pair.second.jmp_buf_[0].esp = (intptr_t)arg;
252     thread_pair.second.jmp_buf_[0].esp -= sizeof(intptr_t);
253
254     *(intptr_t*)thread_pair.second.jmp_buf_[0].esp = (intptr_t)start_routine;
255     thread_pair.second.jmp_buf_[0].esp -= sizeof(intptr_t);
256
257     *(intptr_t*)thread_pair.second.jmp_buf_[0].esp = (intptr_t)0; // simulate push return address
258     thread_pair.second.jmp_buf_[0].esp -= sizeof(intptr_t);

simple_thread.cpp L215 用了一個 wrap_routine 包住要執行的 thread function start_routine, 最後再呼叫 pthread_exit() 即可。

而 jmp_buf 的 eip 則是把 wrap_routine 填入即可。

而 wrap_routine 的 2 個參數: start_routine, arg, 就從 stack push 進去, simple_thread.cpp L251 ~ 257 的部份。

如此一來, 就算你的 thread funcion 沒呼叫 pthread_exit, wrap_routine 也會幫你呼叫, 也可以取得 start_routine 的 return value, 實在妙以。

這樣就順利解決 pthread_exit 取得 thread function return value 的問題, 再來由於這個 thread 已經結束, 那 pthread_exit 還要做什麼呢? 我是讓 pthread_exit 去挑下一個 thread function 來執行, 並在相關的資料結構標記目前的 thread 已經結束, 之後就不會挑這個 thread 來執行。

但是這邊有個討厭的難題, 由於存取到 global variable, 這個 global variable 在 signal handler 也會使用, 所以要注意到同步的問題, 我目前沒有處理。Todo

做這些是為了之後的 pthread_join 做準備。

[int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, void *(*start_routine)(void*), void *restrict arg)]

這裡有一個我之前忽略的問題, 沒有讓 main thread 繼續執行下去, 得補上這段。另外, 如果 main thread 結束, 所有的 thread 也要跟著結束。

list 2. pthread_create
235   int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
236   {
237     static int init_main_thread = 0;
238     static DS::ThreadPair main_thread_pair;
239
240 #if 1
241     sigset_t sigs;
242     sigemptyset(&sigs);
243     sigaddset(&sigs, SIGRTMIN);
244     sigprocmask(SIG_SETMASK, &sigs, 0);
245 #endif
246
247     if (0 == init_main_thread)
248     { 
249       main_thread_pair.first = 1; // fixed to 1
250       thread_vec.push_back(main_thread_pair);
251       init_main_thread = 1;
252     }
253
254     ThreadPair thread_pair;
255     *thread = gen_tid();
256     thread_pair.first = *thread;
257
260     thread_pair.second.jmp_buf_[0].eip = (intptr_t)wrap_routine;
262     
263     auto stack_addr = thread_malloc_stack(BUF_SIZE);
264     if (stack_addr == 0)
265       return -1;
266     
267     printf("xx sizeof(intptr_t): %u\n", sizeof(intptr_t));
268 
269     thread_pair.second.jmp_buf_[0].esp = ((intptr_t)stack_addr + BUF_SIZE - sizeof(intptr_t)); // current stack - 4 or 8
270   
271     printf("stack_addr + BUF_SIZE: %p\n", (char *)stack_addr+BUF_SIZE);
274 
275     *(intptr_t*)thread_pair.second.jmp_buf_[0].esp = (intptr_t)arg;
276     thread_pair.second.jmp_buf_[0].esp -= sizeof(intptr_t);
277     
278     *(intptr_t*)thread_pair.second.jmp_buf_[0].esp = (intptr_t)start_routine;
279     thread_pair.second.jmp_buf_[0].esp -= sizeof(intptr_t);
280 
281     *(intptr_t*)thread_pair.second.jmp_buf_[0].esp = (intptr_t)0; // simulate push return address
282     thread_pair.second.jmp_buf_[0].esp -= sizeof(intptr_t);
299 
300     thread_vec.push_back(thread_pair);
301     cur_thread = thread_vec.end() - 1;
302     current_index = thread_vec.size() - 1;
303     
304     auto &m_thread_pair = thread_vec[0];
305     if (my_setjmp(m_thread_pair.second.jmp_buf_) == 0)
306     {
307       my_longjmp(DS::thread_vec[DS::current_index].second.jmp_buf_, 1);
308     }
309     else
310     {
311       printf("m th\n");
312     }
313     return 0;
314   }

list 2. L304 ~ 312, 就是在保存 main thread jmp_buf, list 2. L247 ~ 252 把 main thread 的資料結構加入 thread_vec, tid 固定在 1, 所以呼叫一次 pthread_create, 會產生 2 個 thread 資料結構, 一個是該 function, 另外一個是 main thread。

所以 signal handler 要保證在之後才能發動, 要不然 main thread 的 jmp_buf 沒設定, 就無法回到 main thread 了。我沒有處理此狀況, 不難, 先 block 該 signal 即可, setjmp 之後在 unblock。

其他就是在設定 jmp_buf 的 eip, esp, 和之前一樣。

source code:
https://github.com/descent/simple_thread/tree/master/cpp



thread 的上限:
cat /proc/sys/kernel/threads-max 
我的系統是 39319

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。