進程創建: clone, fork, vfork系統調用
- clone系統調用
- 參數:
- 執行函數(fn), 參數(arg)
- flags|死亡時給父進程發的信號 (clone_flags): 以下介紹clone_flags
- 資源共享
- 段,頁,打開文件共享:
- 頁表(不是頁, CLONE_VM),
- 打開文件(clone_files),
- 建一個新tls段(clone_settls)
- 路徑和權限設置:
- clone_fs: 共享根目錄, 當前目錄, 創建文件初始權限.
- clone_newns: 新的根路徑, 自己的視野看文件系統
- 線程通信
- clone_sighand: 信號處理action, 阻塞和懸掛的信號
- clone_sysvsem: 共享undoable信號量操作
- 進程關係
- 同父: clone_parent 創建進程與新進程是兄弟 (同父), 新進程不是創建進程的子進程
- 爲了方便期間, 以下討論暫時不考慮這一因素(它很容易實現), 認爲創建進程就是父進程
- 同一個線程組: clone_thread. 屬於同一個進程(線程組)
- 都被trace: clone_ptrace
- 子進程不被trace: clone_untrace (內核設置, 覆蓋clone_ptrace)
- 返回tid
- 向父進程返回tid: clone_parent_settid
- 向子進程返回tid: clone_child_settid
- 子進程的狀態:
- 子進程開始就stop: clone_stopped
- 進程死亡或exec通知:
- 啓動內核機制: 如果子進程死亡或exec, 它自己空間內的tid(*ctid)清零, 並喚醒等待子進程死亡的進程.
- 賦給子進程的資源
- 子進程的棧(父進程alloc的內存地址)
- 線程局部倉庫段(tls)
- 返回子進程tid的地址
- clone, fork, vfork實現方式
- 大致相同:
- 系統調用服務例程sys_clone, sys_fork, sys_vfork三者最終都是調用do_fork函數完成.
- do_fork的參數與clone系統調用的參數類似, 不過多了一個regs(內核棧保存的用戶模式寄存器). 實際上其他的參數也都是用regs取的
- 區別在於:
- clone:
- clone的API外衣, 把fn, arg壓入用戶棧中, 然後引發系統調用. 返回用戶模式後下一條指令就是fn.
- sysclone: parent_tidptr, child_tidptr都傳到了 do_fork的參數中
- sysclone: 檢查是否有新的棧, 如果沒有就用父進程的棧 (開始地址就是regs.esp)
- fork, vfork:
- 服務例程就是直接調用do_fork, 不過參數稍加修改
- clone_flags:
- sys_fork: SIGCHLD|0;
- sys_vfork: SIGCHLD| (clone_vfork | clone_vm)
- 用戶棧: 都是父進程的棧.
- parent_tidptr, child_ctidptr都是NULL.
- 具體實現函數do_fork() (內核函數)的工作流程:
- 分配PID, 確定子進程到底是否traced.
- 分配空閒的PID
- 確定clone_ptrace位. (確定子進程到底要不要被trace, 而不是參數所說的希望被trace)
- 設置該位: 參數已設該位, 且創建線程被trace中
- 清除該位: 父進程沒有被trace, 或 clone_untrace已經設置.
- 複製進程描述符(copy_process)
- 檢查clone_flags是否兼容, 是否安全
- clone_newns 與 clone_fs 互斥
- clone_sighand 是 clone_thread 的必要條件: 線程必須共享信號處理
- clone_vm 是 clone_sighand 的必要條件 : 共享信號處理, 首先要共享信號處理的代碼(在進程頁面裏)
- 附加的安全檢查: security_task_create(clone_flags)
- 複製進程描述符
- 在父進程的thread_info裏保存浮點寄存器: __unlazy_fpu()
- 分配新的進程pd(alloc_task_struct), 並拷貝父進程pd
- 分配新的thread_info(alloc_thread_info), 並拷貝父進程的thread_info.
- 新的thread_info和新分配的pd 相互引, 新pd的引用計數設爲2 (表示:新pd有用, 且不是殭屍進程)
- 相關計數加1: (此處先相關計數檢查, 都通過後再都加1)
- 檢查並增加: 用戶擁有進程數, 系統總共進程數.
- 一般來說, 所有進程的thread_info總和, 不超過物理內存的1/8
- 新進程的可執行格式的引用計數(FIXME: pd裏標有可執行個數嗎)
- 系統執行fork總數.
- 進程pd的關鍵域的設置(順序與源碼可能不一致):
- 進程關係
- 設置父子關係 (parent, real_parent, 考慮被trace的情況)
- 設置新pd的PID
- 設置tgid, 線程組長的pd(pd->group_leader). (根據是不是線程組長, 即clone_thread位是否爲0)
- 加入PID哈希表(pid, tgid, 如果是進程組長加入pgid和sid表),(調attach_pid())
- 拷貝tid到父進程的用戶空間(parent_tidptr)
- 拷貝資源(如果clone_flags沒標明共享):
- 文件,目錄,內存:copy_files, copy_mm, copy_namespace,
- 進程通信: copy_signal, copy_sighand, copy_semundo
- 設置子進程的內核棧(thread_info), 內核態相關寄存器(thread_struct, 不知道這個結構的具體用處): copy_thread()
- 子進程的thread_struct:
- esp, esp0 - 內核棧頂, 內核棧底
- eip - ret_from_fork()的地址 (用戶態切到內核態的第一條指令)
- I/O許可位圖 - 如果父進程有, 就拷貝一份過來
- TLS - 如果用戶空間提供了TLS段, 拷貝過來
- 設置子進程的內核棧:
- child_regs.esp = 傳入的棧地址參數;
- child_regs.eax = 0, 給用戶態的返回值是0
- 清除thread_info中的, TIF_SYSCALL_TRACE位, 防止運行ret_from_fork時, 系統通知調試進程
- 設置子進程的thread_info的cpuid
- 設置調度信息(sched_fork())
- 設置task_running狀態,
- 初始化調度參數(時間片),
- 子進程禁止內核搶佔(thread_info.preempt_cout = 1)
- 其他:
- 如果沒有被trace,pd->ptrace = 0;
- 設置pd->exit_signal:
- 有clone_thread位: 設爲參數clone_flags中的退出信號
- 沒有clone_thread位: 設爲-1 (表示進程終止時, 該LWP不給父進程發信號)
- pd->flags: 清除PF_SUPERPRIV , 設置PF_FORKNOEXEC
- 大內核鎖 pd->lock_depth = -1
- exec次數: pd->did_exec = 0
- 拷貝child_tidptr到pd->set_child_tid. 以備子進程開始執行時, 把tid放到自己內存空間的child_tidptr
- 返回pd
- 設置父子進程的運行狀態, 調度信息
- 設置子進程的狀態.
- 掛信號: 如果創建出來的是停止(clone_stopped)或被trace(pd->ptrace裏有PT_PTRACE位)的進程, 懸掛一個SIGSTOP信號.
- 只有debugger發出SIGCONT信號後, 才能進入運行狀態
- 設狀態,入列隊:如果有clone_stopped位, 子進程設爲stopped狀態; 否則調用wake_up_new_task(), 把子進程加入就緒列隊:
- 調整父進程和子進程的調度參數 (主要是時間片)
- 如果父子在同一CPU上運行, 且頁表不同享, 子進程在插在父進程前
- 子進程很可能exec, 不與父進程共享頁. 這樣防止父進程無用的copy on write.
- 如果不同CPU上運行, 或者共享頁表, 子進程放在列隊最後
- 如果父進程處於被調試狀態, 程通知調試器
- 當前進程給debugger進程發信號, 告知自己創建了子進程; 並停止自己(進入traced狀態), 使debugger運行.
- 子進程的pid保存在current->ptrace_message中, 供debugger用
- 調試器發信號, 使父進程繼續後, 再進行下一步; 否則父進程一直處於traced狀態
- 設置父進程狀態
- 如果有clone_vfork, 把自己放到一個等待列隊.
- 內核處理完系統調用後, 會執行調度, 這樣就阻塞父進程了.
- 直到子進程釋放了它的內存地址空間, 即子進程終止或exec新程序, 用信號喚醒父進程.
- 返回子進程的pid.
- 子進程被調度後,執行pd.thread.eip(ret_from_fork). 調用關係(=>): ret_from_fork=>schedule_tail=>finish_task_switch.
- schedule_tail的另一件事就是: 把pid保存到地址pd->set_child_tid (創建進程使的parent_tidptr)
- finish_task_switch的動作是: 裝載內核棧保存的寄存器(regs->eax爲0),返回到用戶態。系統調用返回值就是eax(0)
- 內核線程:
- 只運行於kernel模式,只能訪問大於3G的空間。而普通進程在內核模式時,能訪問整個4G空間
- 創建方法, 類似於clone
- 準備返回地址fn: 構造一個regs. 裏面有fn, args, __KERNEL_CS等. regs->eip是彙編函數kernel_thread_helper
- do_fork (flags|CLONE_VM|clone_untraced, 0, ®s, 0, NULL, NULL)
- 創建線程, 與父進程共享頁. 用上步構造的regs初始化新程的內核棧
- 新線程被調度後. 由ret_from_fork, 用regs恢復寄存器, 開始執行kernel_thread_helper
- kernel_thread_helper: 把args壓入棧, call fn(args, fn都寄存器中)
- 典型的內核線程:
- 進程0: 所有進程的祖先
- 編譯時存在.
- pd, 內核棧: init_task, init_thread_union
- 資源: init_mm, init_files, init_fs. 信號: init_signals, init_sighand
- 頁表: swapper_gd_dir
- 功能
- 初始化系統數據,
- 多CPU系統中, 開始時BIOS禁用其他CPU.
- 初始化系統數據後, 進程0拷貝自己到其他CPU的調度列隊上, 啓動其他CPU, 所有的PID都是0.
- 使能中斷
- 創建內核線程1, (函數是init)
- 進入idle
- 進程1:
- init函數 exec可執行文件init, 使內核線程變成了普通進程.
- 管理其他進程, 稱爲託孤進程
- 其他內核線程:
- 執行工作列隊:
- ksoftirqd: 執行 softlets
- kblockd: 執行工作列隊 kblockd_workqueue, 定期激活塊設備驅動
- keventd (又叫events): 處理工作列隊 keventd_wq
- 管理資源:
- kapmd: 電源管理
- kswapd: 交換進程, 用於回收內存資源
- pdflush: flush髒的磁盤緩存
進程銷燬
- 進程終止
- 系統調用
- 整個進程終止: exit_group(), 由do_group_exit處理系統調用. c函數 exit()也是用的這系統調用
- 某個線程終止: _exit(), 由do_exit處理. C函數中用到此係統調用的API: pthread_exit
- do_group_exit流程: (整個組內至少有一個線程調用它, 用於整組協調)
- 檢查線程組的退出過程是否啓動: 檢查signal_group_exit(線程組內的公共數據)是否非零. 如果沒有啓動, 執行一下操作來啓動退出過程:
- 設置啓動標誌signal_group_exit.
- 存儲終止碼(exit_group的參數), 在current->signal->group_exit_cold
- 向其他線程發SIG_KILL信號, (它們收到信號後, 調do_exit())
- 調用do_exit, 使本線程退出
- do_exit流程:
- 設置線程的終止標誌, 退出碼
- 設置PF_EXITING, 標明要被終止
- 設置pd->exit_code
- 系統調用參數
- 或是內核提供的錯誤碼, 表示異常終止
- 釋放資源:
- 刪除該進程的定時器
- 去除對資源的引用:
- exit_mm, __exit_files;
- __exit_fs(root路徑,工作路徑, 創建文件權限), exit_namespace(掛載的文件系統的視野);
- exit_thread(thread_struct), exit_sem,
- 如果這個線程的函數實現了一種可執行格式, 可執行格式數的引用計數--; FIXME: 還沒看到這塊兒, 湊合翻譯的不一定對
- 改變父子關係, 並向父進程發信號, 改變自己的狀態(exit_notify)
- 託付終止線程創建的子進程:
- 如果終止線程還有同組線程: 終止線程創建的子進程, 作爲與同組線程的子進程.
- 否則: 終止線程創建的子進程, 作爲孤兒進程, 由init進程託管
- 向父進程發信號
- exit_signal有意義 && 最後線程 : 發exit_signal
- 否則:
- 被trace : 發SIGCHLD
- 沒被trace : 不發信號
- 殭屍自己或直接死亡, 並設置PF_DEAD位
- exit_signal沒意義 && 沒被trace : 直接死亡 (這種情況沒有發信號)
- 變成EXIT_DEAD狀態,
- release_task() (後面介紹). pd引用計數變爲1, 不會馬上釋放
- 否則: 殭屍
- exit_signal有意義 || 被trace : 殭屍
- 整理"殭屍"與"發臨殭屍信號"的關係:
- 將發信號的條件中"最後線程"去掉, 可簡化爲(exit_signal有意義)||(被trace) == (發信號)
- 可得出後: (發信號) == (殭屍)
- 又可推出: (沒有trace && exit_signal有意義 && 不是最後進程) == (殭屍了,但沒法信號) , 這種情況在移除死進程時, 會給其父進程發信號 (FIXME: 待驗證)
- 調度. 調度函數會忽略殭屍進程, 但會減少殭屍進程的pd的使用計數; 會檢查PF_DEAD位, 把它變成exit_dead狀態
|
|