程序描述
作業系統透過程序控制塊PCB來描述程序,對應Linux核心資料結構struct task_struct
在Linux3.18.6核心中,定義於include/linux/sched.h#1235
pid
和tgid
標識程序
state
程序狀態
stack
程序堆疊
CONFIG_SMP
在多處理器時使用
fs
檔案系統描述
tty
控制檯
files
程序開啟檔案的檔案描述符
mm
記憶體管理描述
signal
程序間通訊的訊號描述
struct list_head tasks
管理程序資料結構的雙向連結串列
資料結構list_head
,定義於include/linux/types.h#186
struct list_head{
struct list_head *next, *prev;
};
Linux程序狀態
就緒態,執行態,阻塞態
程序狀態轉換如下:
程序呼叫do_fork()
建立子程序,轉為就緒態TASK_RUNNING
- 就緒態
當排程器從就緒態中選擇一個程序時,轉為執行態TASK_RUNNING
- 執行態
當高優先順序程序搶佔或時間片用盡時,轉為就緒態TASK_RUNNING
當程序睡眠等待待定事件或特定資源時,轉為阻塞態TASK_RUNNING
呼叫do_exit()
,程序終止 - 阻塞態
當事件發生或資源可用,程序被喚醒,進入就緒佇列
作業系統原理中,就緒態和執行態是兩個狀態,在Linux核心中都是TASK_RUNNING
當程序為TASK_RUNNING
狀態時,是可執行的,即就緒態,是否執行取決於有沒有獲得CPU控制權
正在執行的程序,呼叫使用者態庫函式exit()
,會陷入核心執行do_exit()
終止程序,轉為TASK_ZOMBIE
TASK_ZOMBIE
狀態的程序一般稱為殭屍程序,核心會在適當的時候處理殭屍程序,釋放程序描述符
阻塞態有兩種
TASK_INTERRUPTIBLE
和TASK_UNINTERRUPTIBLE
前者是可以被訊號或wake_up()
喚醒,而後者只能被wake_up()
喚醒
Linux核心中定義的程序狀態,位於include/linux/sched.h#203
程序建立
0號程序初始化
雙向連結串列的第一個節點為init_task
0號程序描述符結構體變數init_task
的初始化是透過硬編碼方式固定下來
除此之外,其他程序都是透過do_fork()
複製父程序的方式進行初始化
init_task
變數初始化程式碼如下,位於init/init_task.c#18
// initial task structure
struct task_struct init_task= INIT_TASK(init_task);
EXPORT_SYMBOL(init_task);
其中INIT_TASK
宏定義位於include/linux/init_task.h#173
記憶體管理相關程式碼
程序描述符結構struct task_struct
中管理記憶體的結構struct mm_struct *mm, *active_mm
mm
描述程序地址空間
active_mm
描述記憶體管理
每個程序都有若干個資料段、程式碼段、堆疊段等,都是由這個資料結構管理
邏輯地址空間分段分頁記憶體管理單元MMU
用來管理實體地址和邏輯地址轉換
32位x86體系結構中,程序是4GB的邏輯地址空間
程序之間的父子、兄弟關係
程序描述符struct task_struct
中除了雙向連結串列struct list_head tasks
管理程序外
記錄當前程序的父程序
struct task_struct __rcu* real_parent
struct task_struct __rcu* parent
雙向連結串列記錄當前程序的子程序
struct list_head children
雙向連結串列記錄當前程序的兄弟程序
struct list_head sibling
保持程序上下文中CPU相關的一些狀態資訊的資料結構
核心在程序描述符中使用struct thread_struct thread
記錄程序上下文中CPU狀態資訊
定義於arch/x86/include/asm/processor.h#468
sp
和ip
用來儲存程序上下文中的ESP暫存器和EIP暫存器狀態
程序的建立過程分析
在start_kernel()
中透過kernel_thread
方式(fork
方式)建立了兩個核心執行緒:kernel_init和kthreadd核心執行緒
kernel_init
啟動使用者態程序init
kthreadd
是所有核心執行緒的祖先,負責管理所有核心執行緒
系統啟動時,處理0號程序的初始化是透過手動編碼建立外
1號init程序的建立實際上是複製0號程序,並修改pid等,然後再載入一個init可執行程式
2號程序kthreadd核心執行緒也是透過複製0號執行緒建立
1.使用者態建立程序的方法
int pid= fork()
if(pid<0){ // fork error
}else if(pid==0){ // 子程序
}else{}
fork()
系統呼叫複製當前程序,建立了一個子程序,兩個程序執行相同的程式碼
2.fork系統呼叫
在觸發系統呼叫時,使用者態有個int $0x80
指令觸發中斷機制
將當前程序使用者態的堆疊SS:ESP
、CS:EIP
和EFLAGS
壓棧到當前程序的核心堆疊,由CPU自動完成
從使用者態堆疊轉換到核心態堆疊
接下來執行到彙編程式碼system_call
,用於儲存現場、執行系統呼叫核心處理函式、處理完返回、恢復現場
最後iret
將CPU關鍵現場SS:ESP
、CS:EIP
和EFLAGS
恢復到對應暫存器中
並回到使用者態int $0x80
的下一條指令
fork()
也是一個系統呼叫
建立一個子程序,複製了父程序所有的程序資訊,如核心堆疊、程序描述符等
問題是:當子程序被排程時,是從哪裡開始執行的呢?
建立程序相關的幾個系統呼叫核心處理函式,位於kernel/fork.c#1693
可以看出fork()
、vfork()
和clone()
這三個系統呼叫和kernel_thread()
核心函式都可以建立程序
而且都是透過do_fork()
函式建立程序,只不過傳遞的引數不同
3.fork()
核心處理過程
透過fork()
建立的父子程序,大部分資訊都是一樣的,但是如pid值,核心堆疊等資訊需要修改
fork一個子程序時,在複製父程序資源過程中,採用了寫時複製copy on write技術,不需要修改程序資源,父子程序共享記憶體儲存空間
從do_fork()
跟蹤分析程式碼,位於kernel/fork.c#1617
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user* parent_tidptr,
int __user* child_tidptr);
引數說明
clone_flags
子程序建立標誌,控制對父程序資源有選擇的複製
標誌定義於include/uapi/linux/sched.h#4stack_start
子程序使用者態堆疊的地址regs
指向pt_regs
結構體的指標
當發生系統呼叫時,int
指令和SAVE_ALL
儲存現場等操作會將CPU暫存器值按順序壓棧stack_size
使用者態棧大小,通常不需要,總是被設定為0parent_tidptr
和child_tidptr
父程序、子程序使用者態下的pid地址
為了便於理解,下述為do_fork()函式體關鍵程式碼
struct task_struct* p; // 建立程序描述符指標
int trace= 0;
long nr; // 子程序pid
...
// 建立子程序的描述符,和執行時所需的其他資料結構
p= copy_process(clone_flags, stack_start, stack_size, child_tidptr, NULL, trace);
if(!IS_ERR(p)){
struct completion vfork; // 定義完成量,一個執行單元等待另一個執行單元完成任務
struct pid* pid;
...
pid= get_task_pid(p, PIDTYPE_PID); // 獲取 task 結構體中的 pid
nr= pid_vnr(pid); // 根據 pid 結構體獲取程序 pid
...
// 若 clone_flags 包含 CLONE_VFORK 標誌,就將完成量 vfork 賦值給程序描述符中的 vfork_done 欄位
// 此處只是對完成量進行初始化
if(clone_flags & CLONE_FLORK){
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
...
// 若 clone_flags 包含 CLONE_VFORK 標誌
// 就將父程序插入等待佇列直到子程序呼叫 exec()或退出,此處是具體的阻塞
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; // 返回子程序 pid (父程序的fork()返回值為子程序pid的原因)
do_fork()
呼叫copy_process()
完成複製父程序資訊、獲取pid,呼叫wake_up_new_task將子程序加入排程器佇列,透過clone_flags
標誌做一些輔助工作
copy_process()說明如下,位於kernel/fork.c#1174
static struct task_struct* copy_process(){
int retval;
struct task_struct* p;
...
retval= security_task_create(clone_flags); // 安全性檢查
...
p= dup_task_struct(current); // 複製PCB,為子程序建立核心棧,程序描述符
ftrace_graph_init_task(p);
...
retval= -EAGAIN;
// 檢查該使用者的程序數是否超過限制
if(atomic_read(&p->real_cred->user->processes)>=task_rlimit(p,RLIMIT_NPROC)){
// 檢查使用者是否具有相關許可權,不一定是root
if(p->real_cred->user!=INIT_USER && !capable(CAP_SYS_RESOURCE) && !capable(CAP_SYS_ADMIN)){
goto bad_fork_free;
}
...
// 檢查程序數量是否超過 max_threads,後者取決於記憶體大小
if(nr_threads>=max_threads){
goto bad_fork_cleanup_count;
}
if(!try_module_get(task_thread_info(p)->exec_domain->module)){
goto bad_fork_cleanup_count;
}
...
spin_lock_init(&p->alloc_lock); // 初始化自旋鎖
init_sigpending(&p->pending); // 初始化掛起程序
posix_cpu_timers_init(p); // 初始化cpu定時器
...
// 初始化新程序排程程式資料結構,將新程序的狀態設定為TASK_RUNNING,並禁止核心搶佔
retval= sched_fork(clone_flags, p);
...
// 複製所有程序資訊
shm_init_task(p);
retval= copy_semundo(clone_flags, p);
...
retval= copy_files(clone_flags, p);
...
retval= copy_fs(clone_flags, p);
...
retval= copy_sighand(clone_flags, p);
...
retval= copy_signal(clone_flags, p);
...
retval= copy_mm(clone_flags, p);
...
retval= copy_namespaces(clone_flags, p);
...
retval= copy_io(clone_flags, p);
...
retval= copy_thread(clone_flags, stack_start, stack_size, p); // 初始化子程序核心棧
...
// 若傳入的 pid 指標和全域性結構體變數 init_struct_pid 的地址不同
// 則為子程序分配新的 pid
if(pid!=&init_struct_pid){
retval= -ENOMEM;
pid= alloc_pid(p->nsproxy->pid_ns_for_children);
if(!pid){
goto bad_fork_cleanup_io;
}
}
...
p->pid= pid_nr(pid); // 根據 pid 結構體獲取程序 pid
// 若 clone_flags 包含 CLONE_THREAD 標誌,說明子程序和父程序在同一個執行緒組
if(clone_flags & CLONE_THREAD){
p->exit_signal= -1;
p->group_leader= current->group_leader; // 將執行緒組的 leader 設為子程序的組 leader
p->tgid= current->tgid; // 子程序繼承父程序的 tgid
}else{
if(clone_flags & CLONE_PARENT){
p->exit_signal= current->group_leader->exit_signal;
}else{
p->exit_signal= (clone_flags & CSIGNAL);
}
p->group_leader= p; // 子程序的組 leader 是其自己
p->tgid= p->pid; // 組號 tgid 是其自己的 pid
}
...
if(){
ptrace_init_task();
init_task_pid();
if(){
}else{
}
attach_pid(p, PIDTYPE_PID);
nr_threads++; // 增加系統的程序數
}
...
return p; // 返回被建立的子程序描述符指標
...
}
}
copy_process()
呼叫dup_task_struct()
複製當前程序描述符task_struct、資訊檢查、初始化、設定程序狀態為TASK_RUNNING(此時子程序為就緒態)、採用寫時複製技術複製程序資源,呼叫copy_thread()
初始化子程序核心棧、設定子程序pid等
dup_task_struct()說明如下,位於kernel/fork.c#305
static struct task_struct* dup_task_struct(struct task_struct* orig){
struct task_struct* tsk;
struct thread_info* ti;
int node= tsk_fork_get_node(orig);
int err;
tsk= alloc_task_struct_node(node); // 為子程序建立程序描述符分配儲存空間
...
ti= alloc_thread_info_node(tsk, node); // 實際上建立了兩個頁,一部分用來存放 thread_info,另一部分是核心棧
...
err= arch_dup_task_struct(tsk, orig); // 複製父程序的 task_struct 資訊
...
tsk->stack= ti; // 將棧底的值賦給新節點的 stack
// 對子程序的 thread_info 結構進行初始化,複製父程序的 thread_info 結構,然後將 task 指標指向子程序的程序描述符
setup_thread_stack(tsk, orig);
...
return tsk; // 返回新建立的程序描述符指標
...
}
thread_info
結構,被稱為小型的程序描述符,記憶體區域大小為8KB,佔據連續兩個頁框
透過task
指標指向程序描述符,該結構體位於arch/x86/include/asm/thread_info.h#26
核心棧由高地址向低地址增長,C語言中thread_info
由低地址向高地址增長
核心透過遮蔽ESP暫存器的低13有效位獲取thread_info
結構的基地址
在較新的核心程式碼中,task_struct
沒有直接指向thread_info
結構的指標,而是用一個void
指標表示,然後透過型別轉換來訪問該結構
核心棧和thread_info
結構被定義在一個聯合體中,alloc_thread_info_node
分配一個聯合體
thread_union
定義於include/linux/sched.h#2241
union thread_union{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
4.核心堆疊關鍵資訊的初始化
dup_task_struct()
為子程序分配了核心棧,copy_thread()
才能真正完成核心棧關鍵資訊的初始化
copy_thread()程式碼說明如下,位於arch/x86/kernel/process_32.c#132
int copy_thread(){
struct pt_regs* childregs= task_pt_regs(p);
struct task_struct* tsk;
int err;
p->thread.sp= (unsigned long)childregs;
p->thread.sp0= (unsigned long)(childregs+1);
memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));
if(unlikely(p->flags & PF_KTHREAD)){
// kernel thread
memset(childregs, 0, );
// 若建立
p->thread.ip= (unsigned long)ret_from_kernel_thread;
task_user_gs(p)= __KERNEL_STACK_CANARY;
childregs->ds= __USER_DS;
childregs->es= __USER_DS;
childregs->fs= __KERNEL_PERCPU;
childregs->bx= sp; // function
childregs->bp= arg;
childregs->orig_ax= -1;
childregs->cs= __KERNEL_CS | get_kernel_rpl();
childregs->flags= X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr= NULL;
return 0;
}
// 複製核心堆疊
*childregs= * current_pt_regs();
childregs->ax= 0; // 將子程序的 eax 置0,所以 fork 的子程序返回值為 0
...
// ip 指向 ret_from_fork,子程序從此處開始執行
p->thread.ip= (unsigned long)ret_from_fork;
task_user_gs(p)= get_user_gs(current_pt_regs());
...
return err;
}
對子程序開始執行的起點ret_from_kernel_thread
核心執行緒或ret_from_fork
使用者態程序
以及在子程序中fork()
系統呼叫的返回值,都進行了註釋