實驗內容: 結合中斷上下文切換和進程上下文切換分析Linux內核一般執行過程 以fork和execve系統調用為例分析中斷上下文的切換 分析execve系統調用中斷上下文的特殊之處 分析fork子進程啟動執行時進程上下文的特殊之處 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Li ...
實驗內容:
- 結合中斷上下文切換和進程上下文切換分析Linux內核一般執行過程
- 以fork和execve系統調用為例分析中斷上下文的切換
- 分析execve系統調用中斷上下文的特殊之處
- 分析fork子進程啟動執行時進程上下文的特殊之處
- 以系統調用作為特殊的中斷,結合中斷上下文切換和進程上下文切換分析Linux系統的一般執行過程
實驗環境:
VMWare虛擬機下的Ubuntu18.04.4,實驗採用的內核版本為linux-5.4.34。
1 基礎概念
CPU工作狀態
CPU的工作狀態分為系統態(管態)和用戶態(目態)。
引入這兩個工作狀態的原因是為了避免用戶程式錯誤地使用特權指令,保護操作系統不被用戶程式破壞。
當CPU處於用戶態時,不允許執行特權指令;當CPU處於系統態時,可執行包括特權指令在內的一切機器指令。
中斷與系統調用
-
系統調用
程式員或系統管理員通常並非直接和系統調用打交道。在實際應用中,程式員通過調用函數(或稱應用程式介面、API),管理員則使用更高層次的系統命令。
操作系統為每個系統調用在標準C函數庫中構造一個具有相同名字的封裝函數,由它來屏蔽下層的複雜性,負責把操作系統提供的服務介面(即系統調用)封裝成應用程式能夠直接調用的函數(庫函數)
-
中斷
所謂中斷是指CPU對系統發生的某個事件做出的一種反應,CPU暫停正在執行的程式,保留現場後自動地轉去執行相應的處理程式,處理完該事件後再返回斷點繼續執行被“打斷”的程式。
中斷概念主要分為三類
- 外部中斷,如I/O中斷,時鐘中斷,控制臺中斷等。
- 異常,如CPU本身故障(電源電壓或頻率),程式故障(非法操作碼、地址越界、浮點溢出等),即CPU的內部事件或程式執行中的事件引起的過程。
- 陷入(陷阱),在程式中使用了請求系統服務的系統調用而引發的過程。
-
中斷與系統調用
外部中斷與異常通常都稱作中斷,它們的產生往往是無意、被動的。
陷入是有意和主動的,系統調用本身是一種特殊的中斷。
進程上下文與中斷上下文
-
進程上下文
用戶空間的應用程式,通過系統調用進入內核空間。用戶空間的進程需要傳遞變數、參數的值給內核,在內核態運行時也要保存用戶進程的一些寄存器值、變數等。進程上下文,可以看作是用戶進程傳遞給內核的這些參數以及內核要保存的那一整套的變數、寄存器值和當時的環境等。
相對於進程而言,就是進程執行時的環境。具體來說就是各個變數和數據,包括所有的寄存器變數、進程打開的文件、記憶體信息等。一個進程的上下文可以分為三個部分:用戶級上下文、寄存器上下文以及系統級上下文。
-
中斷上下文
為了在 中斷執行時間儘可能短 和 中斷處理需完成大量工作 之間找到一個平衡點,Linux將中斷處理程式分解為兩個半部:頂半部和底半部。頂半部完成儘可能少的比較緊急的功能,它往往只是簡單地讀取寄存器中的中斷狀態並清除中斷標誌後就進行“登記中斷”的工作。“登記中斷”意味著將底半部處理程式掛到該設備的底半部執行隊列中去。這樣,頂半部執行的速度就會很快,可以服務更多的中斷請求。
對於中斷而言,內核調用中斷處理程式,進入內核空間。這個過程中,硬體的一些變數和參數也要傳遞給內核,內核通過這些參數進行中斷處理,中斷上下文就可以理解為硬體傳遞過來的這些參數和內核需要保存的一些環境,主要是被中斷的進程的環境。
2 fork系統調用
Linux中通過fork系統調用來處理進程創建的任務。
對於進程的創建,sys_clone, sys_vfork,以及sys_fork系統調用的內部都使用了do_fork函數。
在sys_clone,sys_vfork和sys_fork處打下斷點,運行系統,在sys_clone處停下:
sys_clone源碼:
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int, tls_val,
int __user *, child_tidptr)
#elif defined(CONFIG_CLONE_BACKWARDS2)
SYSCALL_DEFINE5(clone, unsigned long, newsp, unsigned long, clone_flags,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#elif defined(CONFIG_CLONE_BACKWARDS3)
SYSCALL_DEFINE6(clone, unsigned long, clone_flags, unsigned long, newsp,
int, stack_size,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#else
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
int, tls_val)
#endif
{
return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
#endif
代碼最終調用do_fork函數,轉到do_fork執行,其他創建進程函數調用過程與此類似,do_fork函數如下:
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;
/*
* Determine whether and which event to report to ptracer. When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;
trace_sched_process_fork(current, p);
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}
wake_up_new_task(p);
/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);
if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}
put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}
do_fork會進行一些pcb的拷貝工作。
在調用copy_process函數時,會進行一些實際內容的拷貝:複製當前進程產生子進程,並且傳入關鍵參數為子進程設置響應進程上下文。具體過程為:先通過調用 dup_task_struct 複製一份task_struct結構體,作為子進程的進程描述符。再初始化與調度有關的數據結構,調用sched_fork,將子進程的state設置為TASK_RUNNING。之後複製所有的進程信息,包括fs、信號處理函數、信號、記憶體空間(包括寫時複製)等。最終調用copy_thread,設置子進程的堆棧信息, 為子進程分配一個pid。
在調用wake_up_new_task函數時,主要任務是將子進程放入調度隊列中,從而使CPU有機會調度並得以運行。
3 execve系統調用
execve系統調用的作用是運行另外一個指定的程式。它會把新程式載入到當前進程的記憶體空間內,當前的進程會被丟棄,它的堆、棧和所有的段數據都會被新進程相應的部分代替,然後會從新程式的初始化代碼和 main 函數開始運行。同時,進程的 ID 將保持不變。
與fork系統調用不同,從一個進程中啟動另一個程式時,通常是先 fork 一個子進程,然後在子進程中使用 execve變為運行指定程式的進程。 例如,當用戶在 Shell 下輸入一條命令啟動指定程式時,Shell 就是先 fork了自身進程,然後在子進程中使用 execve來運行指定的程式。
execve系統調用的函數原型為:
int execve(const char *filename, char *const argv[], char *const envp[]);
filename 用於指定要運行的程式的文件名,argv 和 envp 分別指定程式的運行參數和環境變數。除此之外,該系列函數還有很多變體(execl、execlp、execle、execv、execvp、execvpe),它們執行大體相同的功能,區別在於需要的參數不同,但都是通過execve系統調用進入內核。
execve系統調用的過程:首先,執行__x64_sys_execve系統調用,進入內核態後調用do_execve載入可執行文件,之後再通過調用search_binary_handler覆蓋當前進程的可執行程式。
static int exec_binprm(struct linux_binprm *bprm)
{
pid_t old_pid, old_vpid;
int ret;
/* Need to fetch pid before load_binary changes it */
old_pid = current->pid;
rcu_read_lock();
old_vpid = task_pid_nr_ns(current, task_active_pid_ns(current->parent));
rcu_read_unlock();
ret = search_binary_handler(bprm);
if (ret >= 0) {
audit_bprm(bprm);
trace_sched_process_exec(current, old_pid, bprm);
ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
proc_exec_connector(current);
}
return ret;
}
最後將IP設置為新的進程的入口地址,然後返回用戶態,繼續執行新進程。最終舊進程的上下文被完全替換,但進程pid 不變,調用返回新進程。
4 Linux系統的一般執行過程
當前linux系統中正在運行用戶態進程X,需要切換到用戶態進程Y的時候,會執行以下過程:
-
用戶態進程X正在運行
-
運行的過程當中,發生了中斷
-
中斷上下文切換,swapgs指令保存現場後,再載入當前進程內核堆棧棧頂地址到RSP寄存器,由進程X的用戶態轉到進程X的內核態。
-
中斷處理過程中或中斷返回前調用schedule函數,完成進程調度演算法。
-
switch_to調用__switch_to_asm彙編代碼,完成關鍵的進程上下文切換。
-
中斷上下文恢復。
-
繼續運行用戶態進程Y
Linux一般切換流程中有CPU的上下文的切換和內核中的進程上下文的切換。中斷和中斷返回有中斷上下文的切換,CPU和內核代碼中斷處理程式入口的彙編代碼結合起來完成中斷上下文的切換。進程調度過程中有進程上下文的切換,而進程上下文的切換完全由內核完成。
幾種特殊情況
(1)通過中斷處理過程中的調度時機,用戶態進程與內核線程之間互相切換和內核線程之間互相切換,與最一般的情況非常類似,只是內核線程運行過程中發生中斷沒有進程用戶態和內核態的轉換。
(2)內核線程主動調用schedule(),只有進程上下文的切換,沒有發生中斷上下文的切換,與最一般的情況略簡略。
(3)創建子進程的系統調用在子進程中的執行起點及返回用戶態,如fork。
(4)載入一個新的可執行程式後返回到用戶態的情況,如execve。