Linux內核分析---進程的建立,執行與切換

學號:210linux

「原創做品轉載請註明出處 + https://github.com/mengning/linuxkernel/ 」git

一.實驗要求github

從整理上理解進程建立、可執行文件的加載和進程執行進程切換,重點理解分析fork、execve和進程切換。數據結構

二.實驗內容函數

1.理解task_struct數據結構優化

爲了管理進程,內核必須對每一個進程進行清晰的描述。每一個進程在內核中都有一個進程控制塊(PCB)來維護進程相關的信息,Linux內核的進程控制塊是task_struct結構體。進程描述符提供了內核所需瞭解的進程信息:基本信息,管理信息,控制信息等。spa

task_struct是Linux內核的一種數據結構,每一個進程都把它的信息放在 task_struct 這個數據結構體,task_struct 包含了這些內容:線程

(1)標示符 : 描述本進程的惟一標識符,用來區別其餘進程。 3d

(2)狀態 :任務狀態,退出代碼,退出信號等。 
(3)優先級 :相對於其餘進程的優先級。 
(4)程序計數器:程序中即將被執行的下一條指令的地址。 
(5)內存指針:包括程序代碼和進程相關數據的指針,還有和其餘進程共享的內存塊的指針。
(6)上下文數據:進程執行時處理器的寄存器中的數據。 
(7) I/O狀態信息:包括顯示的I/O請求,分配給進程的I/O設備和被進程使用的文件列表。 
(8) 記帳信息:可能包括處理器時間總和,使用的時鐘數總和,時間限制,記帳號等。
2.分析fork函數對應的內核處理過程do_fork,理解建立一個新進程如何建立和修改task_struct數據結構指針

Linux系統中,除第一個進程是被捏造出來的,其餘進程都是經過do_fork()複製出來的。

函數原型:int do_fork(unsigned long clone_flags, unsigned long stack_start,struct pt_regs *regs, unsigned long stack_size)

1.建立子進程task_struct結構體

首先須要申請進程最基本的單位task_struct結構,而後須要將父進程task_struct結構中的各類參數複製到子進程task_struct中。

2.獲取一個空閒的pid。

3.複製各類資源:copy_files(clone_flags, p)父進程中可能打開了一系列文件,所以要複製給子進程。copy_fs(clone_flags, p),複製父進程當前目錄環境,如當前文件系統。複製父進程的用戶空間。copy_thread()等。

3.使用gdb跟蹤分析一個fork系統調用內核處理函數do_fork

1.啓動Menu OS

cd LinuxKernel 
rm menu -rf
git clone https://github.com/mengning/menu.git
cd menu
mv test_fork.c test.c
make rootfs
  
2.進入gdb調試模式
gdb
file linux-3.18.6/vmlinux
在這幾個地方設置斷點
b sys_clone
b do_fork
b dup_task_struct
b copy_process
b copy_thread
b ret_from_for

首先在sys_clone處

do_fork處

copy_process處

進入copy_thread

查看p值

4.理解編譯連接的過程和ELF可執行文件格式

1.連接過程

預處理:主要是作一些代碼文本的替換工做。(該替換是一個遞歸逐層展開的過程。)

(1)將全部的#define刪除,並展開全部的宏定義
(2)處理全部的條件預編譯指令,如:#if  #ifdef #elif #else #endif
(3)處理#include預編譯指令,將被包含的文件插進到該指令的位置,這個過程是遞歸的
(4)刪除全部的註釋//與/* */
(5)添加行號與文件名標識,以便產生調試用的行號信息以及編譯錯誤或警告時可以顯示行號
(6)保留全部的#pragma編譯器指令,由於編譯器須要使用它們

編譯:把預處理完的文件進行一系列詞法分析語法分析語義分析優化後生成彙編代碼,這個過程是程序構建的核心部分。 

彙編:將彙編代碼轉成機器指令。

連接:此時的連接,嚴格說應該叫靜態連接。將多個目標文件、庫拼合成最終的可執行文件。

 

5.使用exec*庫函數加載一個可執行文件

exec()族函數功能是將當前的進程替換成一個新的進程,執行到exec()函數時當前進程就會結束新進程則開始執行。

process.c代碼

 

運行結果:

 

 

6.理解Linux系統中進程調度的時機

1.中斷處理過程(包括時鐘中斷、I/O中斷、系統調用和異常)中,直接調用schedule(),或者返回用戶態時根據need_resched標記調用schedule();

2.內核線程能夠直接調用schedule()進行進程切換,也能夠在中斷處理過程當中進行調度,也就是說內核線程做爲一類的特殊的進程能夠主動調度,也能夠被動調度;

3.用戶態進程沒法實現主動調度,僅能經過陷入內核態後的某個時機點進行調度,即在中斷處理過程當中進行調度。

7.特別關注並仔細分析switch_to中的彙編代碼,理解進程上下文的切換機制,以及與中斷上下文切換的關係

1.關鍵函數的調用關係:

schedule() --> context_switch() --> switch_to --> __switch_to()

2.代碼分析

asm volatile("pushfl\n\t" /* 保存當前進程的標誌位 */
"pushl %%ebp\n\t" /* 保存當前進程的堆棧基址EBP */
"movl %%esp,%[prev_sp]\n\t" /* 保存當前棧頂ESP */
"movl %[next_sp],%%esp\n\t" /* 把下一個進程的棧頂放到esp寄存器中,完成了內核堆棧的切換,今後往下壓棧都是在next進程的內核堆棧中。 */

"movl $1f,%[prev_ip]\n\t" /* 保存當前進程的EIP */
"pushl %[next_ip]\n\t" /* 把下一個進程的起點EIP壓入堆棧 */
__switch_canary
"jmp __switch_to\n" /* 由於是函數因此是jmp,經過寄存器傳遞參數,寄存器是prev-a,next-d,當函數執行結束ret時由於沒有壓棧當前eip,因此須要使用以前壓棧的eip,就是pop出next_ip。 */

"1:\t" /* 認爲next進程開始執行。 */
"popl %%ebp\n\t" /* restore EBP */
"popfl\n" /* restore flags */
/* output parameters 由於處於中斷上下文,在內核中
prev_sp是內核堆棧棧頂
prev_ip是當前進程的eip */
: [prev_sp] "=m" (prev->thread.sp),
[prev_ip] "=m" (prev->thread.ip), //[prev_ip]是標號
"=a" (last),
/* clobbered output registers: */
"=b" (ebx), "=c" (ecx), "=d" (edx),
"=S" (esi), "=D" (edi)

__switch_canary_oparam

/* input parameters:
next_sp下一個進程的內核堆棧的棧頂
next_ip下一個進程執行的起點,通常是$1f,對於新建立的子進程是ret_from_fork*/
: [next_sp] "m" (next->thread.sp),
[next_ip] "m" (next->thread.ip),

/* regparm parameters for __switch_to(): */
[prev] "a" (prev),
[next] "d" (next)

__switch_canary_iparam

: /* reloaded segment registers */
"memory");
} while (0)

內核在switch_to中執行以下操做:

1.進程切換, 即esp的切換, 因爲從esp能夠找到進程的描述符

2.硬件上下文切換, 設置ip寄存器的值, 並jmp到__switch_to函數

3.堆棧的切換, 即ebp的切換, ebp是棧底指針, 它肯定了當前用戶空間屬於哪一個進程

經過系統調用,用戶空間的應用程序就會進入內核空間,由內核表明該進程運行於內核空間,這就涉及到上下文的切換,用戶空間和內核空間具備不一樣的地址映射,通用或專用的寄存器組,而用戶空間的進程要傳遞不少變量、參數給內核,內核也要保存用戶進程的一些寄存器、變量等,以便系統調用結束後回到用戶空間繼續執行,所謂的進程上下文,就是一個進程在執行的時候,CPU的全部寄存器中的值、進程的狀態以及堆棧中的內容,當內核須要切換到另外一個進程時,它須要保存當前進程的全部狀態,即保存當前進程的進程上下文,以便再次執行該進程時,可以恢復切換時的狀態,繼續執行。

相關文章
相關標籤/搜索