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