2012年12月6日 星期四

"Orange's 一個作業系統的實現"的 fork 實作

看到 spinning parentspinning child 的字串時, 心中的興奮難以言喻, 這個 fork 實作可把我搞慘了, 花了不少時間冥想 + gdb trace。

我學習寫 os 的其中一個理由是: 我想把 fork 搞懂, 這個 function 實在是太神奇了, 怎麼有辦法呼叫一次, 可以 return 0 又 return 非零呢?實在是令我太好奇了。一般的 function 是不能做到這樣的功能的。而搞懂的方式唯有實作一次才能稱上是真正的理解。

這篇不是要討論 fork 如何實作, 書上用了一章的篇幅來說明, 我怎麼可能一篇 blog 可以說完, 這是記錄我自己的學習過程,  如果你也剛好看到這章, 也卡在這裡, 我的學習經驗也許可以幫點忙。如果不是在讀這本書, 本篇的內容並不足以學會實作 fork。

其他版本的 fork 實作我不清楚, 本書的實作方式是使用 message 傳送的方式來實作, 有一個 mm process 接受執行 fork 的 message, 然後傳回 fork 的執行結果。

所以還要把第八章的 ipc 複習過一遍, message 的流動實在複雜, 要搞懂第十章的 fork, 一定要弄清楚第八章的 ipc。所以實際上是要看完兩個章節才能理解 fork 實作。而我使用了 romfs/ramdisk 來替代第九章的檔案系統實作, 所以我簡化檔案系統的部份,不用去看第九章的硬碟實作。我急著想搞清楚 fork實作嘛!

對付 fork 實作, 我幾乎出動了十八般武藝, 隨著開發 os 的經驗累積, 我的武器愈來愈多, bochs, qemu 的 gdb 除錯, 看著我不熟悉反組譯後的組合語言, 拿出計算機一一計算整個記憶體位址, dump stack 的值。說實在的, 著實辛苦; 不過辛苦過後的果實, 果然特別的香甜。

再來就剩下 exec, wait, exit 的實作了, 這應該簡單多了, 我已經完成 exec 的部份, 整個實作觀念也大底清楚, 剩下的就是實作細節了。

很高興我犯了好幾個錯誤, 當我一一認清這些錯誤時, 正確的 fork 版本終於搞定。

犯下的錯誤:
  • 被 fork process base, limit 和 原 process 重疊。
  • 原 process 程式碼沒有被複製到被 fork process memory 空間。
  • 每一個 process stack 的分配有錯, 原本分配給 process stack 空間太小, 而分配給每一個 process stack 的位址也錯了, 造成 process stack 爛掉。
執行 fork 時, 訊息的流動, 每個 process 進入接收 message 模式時, 就不會被 schedule 選來執行, 必須要等到某個 process 送訊息給這個 process 之後才能被選來執行。

init call fork() 訊息流動:

MM recv any msg (MM in recv msg state)

INIT
call fork(), fork will send msg to MM then recv msg form MM.
send msg to MM (解除 MM recv msg state, 這樣才能被 schedule 選來執行)
recv msg from MM (INIT in recv msg state)
這時候 EIP 指向 sendrec int $0x90 的下一行, 因為進入 system call 了。等到接收訊息的 system call return, 從 sendrec int $0x90 的下一行繼續執行。下圖程式碼的箭頭部份。

sendrec:
  mov $_NR_SENDREC, %eax
  mov 4(%esp), %ebx
  mov 8(%esp), %ecx
  mov 12(%esp), %edx
  int $INT_VECTOR_SYS_CALL # int $0x90
-> pop    %edx
  pop    %ecx                          
  pop    %ebx
  ret

MM
do_fork - send msg to FORK_INIT (因為 INIT in recv msg state, so FORK_INIT in recv msg state, too)

do_fork send msg to FORK_INIT, so 解除 FORK_INT recv msg state

send msg to INIT (解除 INIT recv msg state, 這樣才能被 schedule 選來執行)

可是 FORK_INIT eip 和 INIT 一樣, 但是包含程式碼的位址不同??
這是因為 base 不一樣, init base 是 0, forked init base 是 0xa00000, 雖然 eip 一樣, 但執行位址一個從 0 算, 一個從 0xa00000 算。這是 x86保護模式下的 segment address。

fork 執行流程:
fork ->  send_recv -> sendrec (system call) -> sys_sendrec -> msg_send/msg_receive


fork 返回時的函式呼叫
 1    │0x100ba4 <sendrec>              mov    $0x3,%eax                           │
 2    │0x100ba9 <sendrec+5>            mov    0x4(%esp),%ebx                      │
 3    │0x100bad <sendrec+9>            mov    0x8(%esp),%ecx                      │
 4    │0x100bb1 <sendrec+13>           mov    0xc(%esp),%edx                      │
 5    │0x100bb5 <sendrec+17>           int    $0x90                               │
 6   >│0x100bb7 <sendrec+19>           ret          
 7 
 8 
 9   >│0x107d46 <send_recv+125>        mov    %eax,-0xc(%ebp)                  │
10    │0x107d49 <send_recv+128>        jmp    0x107da2 <send_recv+217>      │
11    │0x107d4b <send_recv+130>        mov    0x10(%ebp),%eax                     │
12    │0x107d4e <send_recv+133>        mov    %eax,0x8(%esp)                      │
13    │0x107d52 <send_recv+137>        mov    0xc(%ebp),%eax                      │
14    │0x107d55 <send_recv+140>        mov    %eax,0x4(%esp)                      │
15    │0x107d59 <send_recv+144>        mov    0x8(%ebp),%eax  
16 
17 
18   >│0x107ec9 <fork+40>        mov -0x2c(%ebp),%eax  # 取回 pid, 0 或是 child pid
19    │0x107ecc <fork+43>        leave
20    │0x107ecd <fork+44>        ret       
21 
22 esp            0x4f86d0 0x4f86d0
23 ret addr: 0x4f86d0:       0x00106924
24 
25 
26 
27    │0x106903 <init+6>       call   0x107ea1 <fork>                             │
28    │0x106908 <init+11>      mov    %eax,-0x10(%ebp)                            │
29 B+ │0x10690b <init+14>      movl   $0x5,-0xc(%ebp)                             │
30    │0x106912 <init+21>      cmpl   $0x0,-0x10(%ebp)                            │
31    │0x106916 <init+25>      je     0x106926 <init+41>                          │
32    │0x106918 <init+27>      movl   $0x109085,(%esp)                            │
33    │0x10691f <init+34>      call   0x106884 <spin>                             │
34   >│0x106924 <init+39>      jmp    0x106932 <init+53>                          │
35    │0x106926 <init+41>      movl   $0x10908c,(%esp)                            │
36    │0x10692d <init+48>      call   0x106884 <spin>                             │
37    │0x106932 <init+53>      leave                                              │
38    │0x106933 <init+54>      ret    

debug 小秘訣:

我使用的 gdb script

file p_kernel.elf.gdb
target remote localhost:1234
b do_fork
display proc_table[7].p_flags
display proc_table[3].p_flags

display ready_process->name

停在 do_fork 後, 中斷點設在
mm/mm.c #189  int x=0; // for debug
觀察
proc_table[7].p_flags
proc_table[3].p_flags
ready_process->name
再把中斷點設在 restart (b restart), 再觀察:
proc_table[7].p_flags
proc_table[3].p_flags
ready_process->name
等到 ready_process 是 init_fork (process name: INIT_7) 那個後, 再使用 layout asm, 追蹤 init_fork 這個被 fork 的 process。

you often don't really understand the problem until after the first time you implement a 
solution. The second time, maybe you know enough to do it right. So if you want to get it 
right, be ready to start over at least once. 
                                                                    Eric S. Raymond

這段話經過這次的學習後, 印象更是深刻。

ref:
http://firstmonday.org/htbin/cgiwrap/bin/ojs/index.php/fm/article/view/578/499

沒有留言:

張貼留言

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

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