實驗内容:
- 結合中斷上下文切換和程序上下文切換分析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。