理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换
學號:384
原創作品轉載請注明出處 + https://github.com/mengning/linuxkernel/
實驗目標
1.分析fork函數對應的內核處理過程do_fork,理解創建一個新進程如何創建和修改task_struct數據結構
2.使用gdb跟蹤分析一個fork系統調用內核處理函數do_fork
3.理解編譯鏈接的過程和ELF可執行文件格式
實驗環境
ubuntu系統(ubuntu-16.04.2-desktop-amd64)+ VMware Workstation Pro
一、閱讀理解task_struct數據結構
代碼來源:http://codelab.shiyanlou.com/xref/linux-3.18.6/include/linux/sched.h#1235
該結構部分代碼:
在閱讀這個結構體之前,我們必須了解進程與程序的區別,進程是程序的一個執行的實例,為了管理進程,操作系統必須對每個進程所做的事情進行清楚的描述,為此,操作系統使用數據結構來代表處理不同的實體,這個數據結構就是通常所說的進程描述符或進程控制塊(PCB),在linux操作系統下這就是task_struct結構 ,它包含了這個進程的所有信息,在任何時候操作系統都能夠跟蹤這個結構的信息。該結構定義位于/include/linux/sched.h
對于進程控制塊PCB—task_struct:
狀態信息:如就緒、執行等狀態
鏈接信息:用來描述進程之間的家庭關系,例如指向父進程、子進程、兄弟進程等PCB的指針
各種標識符:如進程標識符、用戶及組標識符等
時間和定時器信息:進程使用CPU時間的統計等
調度信息:調度策略、進程優先級、剩余時間片大小等
處理機環境信息:處理器的各種寄存器以及堆棧情況等
虛擬內存信息:描述每個進程所擁有的地址空間
文件系統信息:記錄進程使用文件的情況
PCB幾個重要參數
volatile long state;//表示進程的當前狀態 unsigned long flags; //進程標志 long priority; //進程優先級。 Priority的值給出進程每次獲取CPU后可使用的時間(按jiffies計)。優先級可通過系統調用sys_setpriorty改變(在kernel/sys.c中)。 long counter; //在輪轉法調度時表示進程當前還可運行多久。unsigned long policy; //該進程的進程調度策略,可以通過系統調用sys_sched_setscheduler()更改(見kernel/sched.c)。二、分析fork函數對應的內核處理過程do_fork
fork、vfork和clone三個系統調用都可以創建一個新進程,而且都是通過調用do_fork來實現進程的創建;
具體過程如下:fork() -> sys_clone() -> do_fork() -> dup_task_struct() -> copy_process() -> copy_thread() -> ret_from_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;// ...// 復制進程描述符,返回創建的task_struct的指針p = copy_process(clone_flags, stack_start, stack_size,child_tidptr, NULL, trace);if (!IS_ERR(p)) {struct completion vfork;struct pid *pid;trace_sched_process_fork(current, p);// 取出task結構體內的pidpid = get_task_pid(p, PIDTYPE_PID);nr = pid_vnr(pid);if (clone_flags & CLONE_PARENT_SETTID)put_user(nr, parent_tidptr);// 如果使用的是vfork,那么必須采用某種完成機制,確保父進程后運行if (clone_flags & CLONE_VFORK) {p->vfork_done = &vfork;init_completion(&vfork);get_task_struct(p);}// 將子進程添加到調度器的隊列,使得子進程有機會獲得CPUwake_up_new_task(p);// ...// 如果設置了 CLONE_VFORK 則將父進程插入等待隊列,并掛起父進程直到子進程釋放自己的內存空間// 保證子進程優先于父進程運行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處理了以下內容:
1.調用copy_process,將當期進程復制一份出來為子進程,并且為子進程設置相應地上下文信息。
2.初始化vfork的完成處理信息(如果是vfork調用)
3.調用wake_up_new_task,將子進程放入調度器的隊列中,此時的子進程就可以被調度進程選中,得以運行。
4.如果是vfork調用,需要阻塞父進程,知道子進程執行exec。
如何創建一個新進程:
1.通過調用do_fork來實現進程的創建;
2.復制父進程PCB–task_struct來創建一個新進程,要給新進程分配一個新的內核堆棧;
3.修改復制過來的進程數據,比如pid、進程鏈表等等執行copy_process和copy_thread
4.成功創建新進程
三、使用gdb跟蹤分析一個fork系統調用內核處理函數do_fork
本次實驗是基于實驗樓中現有的實驗環境進行的。
進入menu文件夾,編輯test.c文件:
給qemu增加一個使用fork系統調用的菜單命令,如下所示:
在menu目錄下執行如下命令:make rootfs啟動MenuOS,結果如下所示:
使用GDB進行跟蹤調試,設置如下斷點:
在MenuOS中輸入fork菜單命令以后,后面的斷點依次如圖所示:
首先停在sys_clone位置處:
然后進入do_fork中:
接著進入copy_process中:
接著進入copy_thread中:
最后進入ret_from_fork中:
整個fork系統調用的執行流程如下:
fork->sys_clone->do_fork->copy_process->dup_task_struct->copy_thread->ret_from_fork
Linux內核通過復制父進程來創建一個新進程,調用do_fork為每個新創建的進程動態地分配一個task_struct結構。copy_thread()函數中的代碼p->thread.ip = (unsigned long) ret_from_fork;將子進程的 ip 設置為 ret_form_fork 的首地址,所以fork系統調用產生的子進程在系統調用處理過程中從ret_from_fork處開始執行。
copy_thread()函數中的代碼*childregs = *current_pt_regs();將父進程的regs參數賦值到子進程的內核堆棧,里面存放了SAVE ALL中壓入棧的參數,之后的RESTORE_ALL宏定義會恢復保存到堆棧中的寄存器的值。
fork系統調用發生一次,但是返回兩次。父進程中返回值是子進程的進程號,子進程中返回值為0,可以通過返回值來判斷當前進程是父進程還是子進程。
四、理解編譯鏈接的過程和ELF可執行文件格式
從源文件Hello.c編譯鏈接成Hello.out,需要經歷如下步驟:
ELF可執行文件格式具體分析代碼:https://blog.csdn.net/wu5795175/article/details/7657580
ELF文件格式包括三種主要的類型:可執行文件、可重定向文件、共享庫:
1.一個可執行(executable)文件保存著一個用來執行的程序;該文件指出了exec(BA_OS)如何來創建程序進程映象。
2.一個可重定位(relocatable)文件保存著代碼和適當的數據,用來和其他的object文件一起來創建一個可執行文件或者是一個共享文件。
3.一個共享庫文件保存著代碼和合適的數據,用來被不同的兩個鏈接器鏈接。
五、編程使用exec*庫函數加載一個可執行文件,動態鏈接分為可執行程序裝載時動態鏈接和運行時動態鏈接
第一步:先編輯一個hello.c
#include <stdio.h> #include <stdlib.h> int main() {printf("Hello World!\n");return 0; }第二步:生成預處理文件hello.cpp(預處理負責把include的文件包含進來及宏替換等工作)
第三步:編譯成匯編代碼hello.s
第四步:編譯成目標代碼,得到二進制文件hello.o
第五步:鏈接成可執行文件hello,(它是二進制文件)
第六步:運行一下./hello
動態鏈接分為可執行程序裝載時動態鏈接和運行時動態鏈接。
六、使用gdb跟蹤分析一個execve系統調用內核處理函數do_execve
在實驗樓提供的環境中,給qemu增加一個使用execve系統調用的菜單命令,如下所示:
在menu目錄下執行如下命令:make rootfs啟動MenuOS,結果如下所示:
使用GDB進行跟蹤調試,設置如下斷點:
在MenuOS中輸入execve菜單命令以后,截圖如下所示:
do_execve函數源代碼如下所示:
裝載和啟動一個可執行程序的大致流程如下所示:
sys_execve -> do_execve-> do_execve_common-> exec_binprm-> search_binary_handler -> load_elf_binary-> start_thread
- 對于靜態鏈接的可執行文件,eip指向該文件的文件頭e_entry所指的入口地址;對于動態鏈接的可執行文件,eip指向動態鏈接器。執行靜態鏈接程序時,execve系統調用修改內核堆棧中保存的eip的值作為新的進程的起點。
- 新的可執行程序修改內核堆棧eip為新程序的起點,從new_ip開始執行,start_thread把返回到用戶態的位置從int 0x80的下一條指令變成新的可執行文件的入口地址。
- 執行execve系統調用時,調用execve的可執行程序陷入內核態,使用execve加載的可執行文件覆蓋當前進程的可執行程序,當execve系統調用返回時,返回新的可執行程序的起點(main函數),故新的可執行程序能夠順利執行。
八、理解Linux系統中進程調度的時機
可以在內核代碼中搜索schedule()函數,看都是哪里調用了schedule(),判斷我們課程內容中的總結是否準確:
- 中斷處理過程(時鐘中斷、I/O中斷、系統調用和異常)中,直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule();
- 內核線程可以直接調用schedule()進行進程切換,也可以在中斷處理過程中進行調度,內核線程作為一類的特殊的進程既可以進行主動調度,也可以進行被動調度;
- 用戶態進程無法實現主動調度,只能夠通過陷入內核態后的某個時機點進行調度,即在中斷處理過程中進行調度。
九、使用gdb跟蹤分析一個schedule()函數
在實驗樓提供的環境中,設置斷點如下所示:
schedule()函數用于實現進程調度,它的任務是從運行隊列的鏈表中找到一個進程,并且隨后將CPU分配給這個進程。
從本質上來說,每個進程切換分為兩步:
1.切換頁全局目錄以安裝一個新的地址空間;
2.切換內核態堆棧和硬件上下文,因為硬件上下文提供了內核執行新進程所需要的所有信息,包括CPU寄存器。
十、分析switch_to中的匯編代碼
#define switch_to(prev, next, last) // prev指向當前進程,next指向被調度的進程 do { unsigned long ebx, ecx, edx, esi, edi; asm volatile("pushfl\n\t" /* 將標志位壓棧 */ "pushl %%ebp\n\t" /* 將當前ebp壓棧 */ "movl %%esp,%[prev_sp]\n\t" /* 保存當前進程的堆棧棧頂*/ "movl %[next_sp],%%esp\n\t" /* 將下一個進程的堆棧棧頂保存到esp寄存器,完成內核堆棧的切換*/ "movl $1f,%[prev_ip]\n\t" /* 保存當前進程的eip*/ "pushl %[next_ip]\n\t" /*將下一個進程的eip壓棧 */ "jmp __switch_to\n" /*jmp通過后面的寄存器eax、edx來傳遞參數,__switch_to()函數通過return把next_ip彈出來 */ "1:\t" "popl %%ebp\n\t" /*恢復當前堆棧的ebp*/ "popfl\n" /* 恢復當前堆棧的寄存器標志位*/ /* output parameters */ : [prev_sp] "=m" (prev->thread.sp), // 當前內核堆棧的棧頂[prev_ip] "=m" (prev->thread.ip), // 當前進程的eip "=a" (last), /* clobbered output registers: */ "=b" (ebx), "=c" (ecx), "=d" (edx), "=S" (esi), "=D" (edi) /* input parameters: */ : [next_sp] "m" (next->thread.sp), // 下一個進程的內核堆棧的棧頂[next_ip] "m" (next->thread.ip), // 下一個進程的eip/* regparm parameters for __switch_to(): */ [prev] "a" (prev), // 寄存器的傳遞[next] "d" (next)); __switch_canary_iparam : /* reloaded segment registers */ "memory"); } while (0)switch_to實現了進程之間的真正切換:
1.首先在當前進程prev的內核棧中保存esi,edi及ebp寄存器的內容。
2.然后將prev的內核堆棧指針ebp存入prev->thread.esp中。
3.把將要運行進程next的內核棧指針next->thread.esp置入esp寄存器中
4.將popl指令所在的地址保存在prev->thread.eip中,這個地址就是prev下一次被調度
5.通過jmp指令(而不是call指令)轉入一個函數__switch_to()
6.恢復next上次被調離時推進堆棧的內容。從現在開始,next進程就成為當前進程而真正開始執行
總結
1.Linux通過復制父進程來創建一個新進程,通過調用do_fork來實現并為每個新創建的進程動態地分配一個task_struct結構。fork()函數被調用一次,但返回兩次。可以通過fork,復制一個已有的進程,進而產生一個子進程。
2.Linux的進程調度基于分時技術和進程的優先級,內核通過調用schedule()函數來實現進程調度,其中context_switch宏用于完成進程上下文切換,它通過調用switch_to宏來實現關鍵上下文切換。
3.進程上下文切換需要保存切換進程的相關信息(thread.sp和thread.ip);中斷上下文的切換是在一個進程的用戶態到一個進程的內核態,或從進程的內核態到用戶態,切換進程需要在不同的進程間切換,但一般進程上下文切換是套在中斷上下文切換中的。
4.Linux系統的一般執行過程可以抽象成正在運行的用戶態進程X切換到運行用戶態進程Y的過程。
總結
以上是生活随笔為你收集整理的理解进程创建、可执行文件的加载和进程执行进程切换,重点理解分析fork、execve和进程切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 跟踪分析Linux内核5.0系统调用处理
- 下一篇: 刷题之旅2020.12.05