深入理解 Linux 進程管理
作者簡介:
程磊,一線碼農,在某手機公司擔任系統開發工程師,日常喜歡研究內核基本原理。
目錄
一、進程基本概念
1.1 進程與程序
1.2 進程與線程
1.3 進程與內核
1.4 進程與內存
1.5 進程運行狀態
1.6 進程親緣關係
二、進程的實現
2.1 基本原理
2.2 進程結構體
2.3 進程標識符
2.4 進程的狀態
三、進程的生命週期
3.1 進程的創建
3.2 進程的裝載
3.3 進程的加載
3.4 進程的初始化
3.5 進程的運行
3.6 進程的死亡
四、回顧總結
一、進程基本概念
================
進程是計算機裏面最重要的概念之一。操作系統的目的就是爲了運行進程。那麼到底什麼是進程,操作系統又是如何實現進程和管理進程的呢?
1.1 進程與程序
進程是程序的執行過程。程序是靜態的,是存在於外存之中的,電腦關機後依然存在。進程是動態的,是存在於內存之中的,是程序的執行過程,電腦關機後就不存在進程了。進程的內容來源於程序,進程的啓動過程就是把程序從外存加載到內存的過程。程序文件是有格式的,UNIX-Like 操作系統的通用程序文件格式是 ELF。程序文件是從源碼文件編譯過來的,源碼文件很多是用 C 或者 C++ 書寫的。
1.2 進程與線程
進程是操作系統分配和管理系統資源的基本單位。進程本來也是程序執行的基本單位,但是自從有了線程之後就不是了。現在線程是程序執行的基本單位,代表一個執行流,一個進程可以有多個執行流。最初的時候,一個進程就只有一個執行流,也就是主線程,此時進程就是線程,線程就是進程。當程序需要多個執行流的時候,採取的都是多進程的方式。但是創建一個新進程是一個很耗費資源的事情,而且多個進程之間還要進行進程間通信也很費事。於是人們便想到了開發進程內併發機制,也就是在一個進程內能同時存在多個執行流 (線程)。不同的人設計的進程內併發機制並不相同。按照線程的管理是否實現在內核裏,進程內併發機制可以分爲兩大類,分別是內核級線程(內核級線程也被叫做輕量級進程) 和用戶級線程,注意這兩個名詞都帶個級,它們是進程內併發機制的兩個子類,並不是具體的線程。內核級線程下的線程,按照運行主體是在內核空間還是在用戶空間可以分爲內核線程和用戶線程。用戶級線程下的線程,按照運行主體是在內核空間還是在用戶空間也可以分爲內核線程和用戶線程,但是由於用戶級線程實現在用戶空間,所以它的線程不可能存在於內核空間。內核級線程下的用戶線程一般被叫做用戶線程,簡稱線程。用戶級線程下的用戶線程如果再叫用戶線程或者線程就會產生混淆,於是就被叫做協程或者纖程。如下圖所示:
這兩種實現多線程的方法各有優缺點。在用戶空間實現的話,優點是簡單,不用改內核,只需要實現一個庫就行了,創建線程開銷小,缺點是線程之間做不到真併發,一個線程阻塞就會阻塞同一進程的所有其它線程。在內核空間實現的話,缺點是麻煩,需要改內核,創建線程開銷大,但是優點是能做到真併發,一個進程的多個線程可以同時在多個 CPU 上運行,能充分利用 CPU。當然這兩者並不是對立的,它們可以同時實現,一個進程可以有多個內核級線程,一個內核級線程又可以有多個用戶級線程,編程者可以靈活選擇使用哪種多線程方式。
1.3 進程與內核
進程與內核在同一個虛擬地址空間中,但是在不同的子空間,進程是在用戶空間,內核是在內核空間。整個系統只有一個內核空間,但是卻有很多用戶空間,不過當前用戶空間永遠只有一個 (對於一個 CPU 來說)。雖然內核空間和用戶空間在同一個空間中,但是它們的權限並不相同。內核空間處於特權模式,用戶空間處於非特權模式。內核可以隨意訪問和操作用戶空間,但是用戶空間對內核空間卻是看得見摸不着。內核空間可以做很多特權操作,用戶空間沒有權限做,但是有些時候又需要做,所以內核爲用戶空間開了一個口子,就是系統調用,用戶空間可以通過系統調用來請求內核的服務。
下面我們用一張圖來總結內核和進程之間的關係:
這個圖是在講進程調度的時候畫的,但是用在這裏表示進程和內核的關係也很合適。
1.4 進程與內存
對於內核來說,內存是有虛擬內存和物理內存之分的。但是對於進程來說,這些都是透明的,進程只需要知道自己獨佔一個用戶空間的內存就可以了,它不知道也不需要知道自己是否運行在虛擬內存上。如果非要說進程知道物理內存和虛擬內存,那麼進程也只能分配和管理虛擬內存,它沒法分配管理物理內存,因爲物理內存對它來說是透明的。內核在合適的時候會爲進程分配相應的物理內存,保證進程在訪問內存的時候一定會有對應的物理內存,但是進程對此毫不知情,也管不了。
進程需要內存的時候可以通過系統調用 brk、sbrk、mmap 來向內核申請分配虛擬內存。但是直接使用系統調用來分配管理內存顯然很麻煩效率也低,爲此 libc 向進程提供了 malloc 庫,malloc 提供了 malloc、free 等幾個接口供進程使用。這樣進程需要內存的時候就可以直接使用 malloc 去分配內存,使用完了就用 free 去釋放內存,不用考慮分配效率、內存碎片等問題了。目前比較流行的 malloc 庫有 ptmalloc、jemalloc、scudo 等。
1.5 進程運行狀態
很多操作系統的書籍上都會講進程的運行狀態,有的講的是三態,有的講的是五態。其實兩者並不矛盾,三態只有進程運行時的狀態,五態把進程的新建和死亡狀態也算上去了,如下圖所示:
進程剛創建之後處於新建態,但是新建態不是持久狀態,它會立馬轉變爲就緒狀態。然後進程就會一直處於就緒、執行、阻塞三態的循環之中。就緒態會由於進程調度而轉爲執行態;執行態會由於時間片耗盡而轉爲就緒態,也會由於等待某個事件而轉爲阻塞態;阻塞態會由於某個事件的發生而轉爲就緒態。最後進程可能會由於主動退出或者發生異常而死亡。死亡態也不是一個持久態,進程死亡之後就不存在了。
1.6 進程親緣關係
所有進程都通過父子關係連接而構成一顆親緣樹,這顆樹的樹根是 init 進程 (pid 1)。Init 進程是第一個用戶空間進程,所有的用戶空間進程都是 init 進程的子孫進程。Init 進程的父進程是零號進程,零號進程是在代碼中通過硬編碼創建的,其它所有的進程都是通過 fork 創建的。這裏爲什麼叫做零號進程呢?因爲零號進程的職責發生過變化,在系統剛啓動的時候,零號進程是 BSP(bootstrap process),start_kernel 函數就是在零號進程中運行的。當系統初始化完成的時候,零號進程退化爲了 idle 進程。當我們只強調零號進程的身份而不關心它的職責的時候,就叫它零號進程。當後面我們強調它的 idle 職責的時候,就叫它 idle 進程。
零號進程有兩個親兒子,除了 init 之外,還有一個是 kthreadd(pid 2)。Kthreadd 是一個內核線程,它是所有其它內核線程的父進程。內核線程比較特殊的點在於它只運行在內核空間,所以所有的內核線程都可以看做是同一個進程下的線程,因爲內核空間只有一個。但是每個內核線程在邏輯意義上又是一個獨立的進程,它們執行獨立的任務,有着獨立的進程人格。所以當我們說一個內核線程的時候,心裏也要明白它是一個單獨的進程,是一個只有主線程的單線程進程。
我們來畫一下進程的親緣關係:
進程除了父子這種血緣關係之外,還存在着家族關係。一個是大家族關係,會話組 (session),一個是小家族關係,進程組(process group)。會話組的產生來源於早期的大型計算機,當時一個公司或者一個科研單位只能買得起一臺大型機。然後每個人都通過一個終端連接到這個大型機,用自己的用戶名和密碼登錄上去。每個用戶都有自己的用戶 id,一個用戶運行的所有的程序構成了一個會話組。有了會話組的概念,就可以方便我們把一個用戶運行的所有進程作爲一個整體進行管理。進程組的產生來源於命令行操作的作業管理。什麼是作業管理呢?就是把一行命令的執行整體作爲一個作業。一行命令的執行不一定只有一個進程,比如命令 ps -ef | grep bash,就有兩個進程,我們需要有個概念把這兩個進程作爲一個整體來處理,這個概念就是進程組。有了進程組的概念,作業管理就比較方便了,比如 Ctrl+C 就是給當前正在執行的命令(進程組) 發信號,進程組中的每個進程都會收到信號。
一個進程誕生的時候默認繼承父進程的會話組和進程組,但是進程可以通過系統調用 (setsid,setpgrp) 創建新的會話組或者進程組。會話組的第一個進程叫做這個會話組的組長,進程組的第一個進程叫做這個進程組的組長,會話組的 id 等於會話組組長的 pid,進程組的 id 等於進程組組長的 pid。一個進程只有當它不是某個進程組組長的時候,它纔可以調用 setpgrp 創建新的進程組,同時它也成爲了這個新建的進程組的組長。這個也很好理解,只有臣子造反當皇帝,哪有皇帝自己造自己的反重新創建一個朝代的。同理,只有不是會話組組長的進程才能通過 setsid 創建新的會話組,併成爲這個會話組組長。而且在這個新的會話組裏也不能沒有進程組啊,於是還會創建一個進程組,這個會話組組長還會成爲這個新建的進程組的組長,這也要求了這個進程之前不能是進程組組長。所以只有既不是進程組組長又不是會話組組長的進程才能創建新的會話組。
任何一個進程,它必然屬於某個進程組,而且只能同時屬於一個進程組。任何一個進程,它必然屬於某個會話組,而且只能屬於一個會話組。任何一個進程組,它的所有進程必須都屬於同一個會話組。一個進程所屬的會話組只有兩種來源,要麼是繼承而來的,要麼是自己創建的,進程是不能轉會話組的。不過一個進程是可以轉進程組的,但是隻能在同一個會話組中的進程組之間轉。因此我們可以得出一個結論,一個會話組的所有進程肯定都是其會話組組長的子孫進程,一個進程組的所有進程一般情況下都是其進程組組長的子孫進程。
我們來畫一下進程的家族關係:
二、進程的實現
===============
明白了進程的基本概念之後,我們來看一看 Linux 是怎麼實現進程的。按照標準的操作系統理論,進程是資源分配的單位,線程是程序執行的單位,內核裏用進程控制塊 (PCB Process Control Block) 來管理進程,用線程控制塊 (TCB Thread Control Block) 來管理線程。那麼 Linux 是按照這個邏輯來實現進程的嗎?我們來看一下。
2.1 基本原理
Linux 內核並不是按照標準的操作系統理論來實現進程的,在內核裏找不到典型的進程控制塊和線程控制塊。內核裏只有一個 task_struct 結構體,初學內核的人會很疑惑這是代表進程還是代表線程呢。之所以會這樣,是由於歷史原因造成的。Linux 最開始的時候是不支持多線程的,也可以認爲此時一個進程只能有一個線程就是主線程,因此線程就是進程,進程就是線程。所以最初的時候,task_struct 既代表進程又代表線程,因爲進程和線程沒有區別。但是後來 Linux 也要支持多線程了,我們在 1.2 節中討論過,多線程的實現方法可以在內核實現,也可以在用戶空間實現,也可以同時實現,Linux 選擇的是在內核實現。爲了最大限度地利用已有的代碼,儘量不對代碼做大的改動,Linux 選擇的方法是:task_struct 既是線程又是進程的代理。注意這句話,task_struct 既是線程又是進程的代理 (不是進程本身)。Linux 並沒有設計單獨的進程結構體,而是用 task_struct 作爲進程的代理,這是因爲進程是資源分配的單位,線程是程序執行的單位,同一個進程的所有線程共享相同的資源,因此我們讓同一個進程下的所有線程(task_struct) 都指向相同的資源不就可以了嘛。線程在執行的時候會通過 task_struct 裏面的指針訪問資源,同一個進程下的線程自然就會訪問到相同的資源,而且這麼做還有很大的靈活性。
我們下面再來強調一下這句話,以加深對這句話的理解。
task_struct 既是線程又是進程的代理 (不是進程本身)。
2.2 進程結構體
當我們明白了 task_struct 既是線程又是進程的代理之後,再來理解 task_struct 就容易多了。task_struct 的字段由兩部分組成,一部分是線程相關的,一部分是進程相關的,線程相關的一般是直接內嵌其它數據,進程相關的一般是用指針指向其它數據。線程代表的是執行流,所以 task_struct 的線程相關部分是和執行有關的,進程代表的是資源分配,所以 task_struct 的進程相關部分是和資源有關的。我們可以想一下和執行有關的都有哪些,和資源有關的都哪些?可以很輕鬆地想到,和執行有關的肯定是進程調度相關的數據啊 (進程調度雖然叫進程調度,但實際上調度的是線程)。和資源相關的,最重要的首先肯定是虛擬內存啊,其次是文件系統。
下面我們來看一下 task_struct 的定義:
linux-src/include/linux/sched.h
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
struct thread_info thread_info;
#endif
unsigned int __state;
void *stack;
unsigned int flags;
int on_cpu;
unsigned int cpu;
int recent_used_cpu;
int wake_cpu;
int on_rq;
int prio;
int static_prio;
int normal_prio;
unsigned int rt_priority;
const struct sched_class *sched_class;
struct sched_entity se;
struct sched_rt_entity rt;
struct sched_dl_entity dl;
unsigned int policy;
int nr_cpus_allowed;
cpumask_t cpus_mask;
struct sched_info sched_info;
struct list_head tasks;
struct mm_struct *mm;
struct mm_struct *active_mm;
struct vmacache vmacache;
int exit_state;
int exit_code;
int exit_signal;
pid_t pid;
pid_t tgid;
struct task_struct __rcu *real_parent;
struct task_struct __rcu *parent;
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
unsigned long nvcsw;
unsigned long nivcsw;
u64 start_time;
u64 start_boottime;
unsigned long min_flt;
unsigned long maj_flt;
char comm[TASK_COMM_LEN];
struct fs_struct *fs;
struct files_struct *files;
struct signal_struct *signal;
struct sighand_struct __rcu *sighand;
sigset_t blocked;
sigset_t real_blocked;
sigset_t saved_sigmask;
struct sigpending pending;
struct thread_struct thread;
};
這個結構體定義有 700 多行,本文把一些暫時用不到的都刪除了,現在還有 70 多行,我們來看一下大概都有哪些內容。先看和進程相關的,首先最重要的是虛擬內存空間信息 mm、active_mm,這兩個都是指針,對於用戶線程來說兩個指針的值永遠都是相同的,同一個進程的所有線程都指向相同的 mm,這個值就表明了同一個進程的線程都在同一個用戶空間。其次比較重要的是文件管理相關的兩個字段 fs 和 files,也都是指針,fs 代表的是文件系統掛載相關的,這個不僅是同進程的所有線程都相同,而且整個系統默認的值都一樣,除非使用了 mount 命名空間,files 代表的是打開的文件資源,這個是同進程的所有線程都相同。然後我們再來看一下信號相關的,信號有的數據是進程全局的,有的是線程私有的,信號的處理是進程全局的,所以 signal、sighand 兩個字段都是指針,同進程的所有線程都指向同一個結構體,信號掩碼是線程私有的,所以 blocked 直接是內嵌數據。進程相關的數據基本就這些,下面我們來看一下線程相關的數據。首先是進程的運行退出狀態,有幾個字段,__state、on_cpu、cpu、exit_state、exit_code、exit_signal。然後是和線程調度相關的幾個字段,有和優先級相關的 rt_priority、static_prio、normal_prio、prio,有和調度信息統計相關的兩個結構體,se、sched_info。還有兩個非常重要的字段我們下一節講。
2.3 進程標識符
task_struct 裏面有兩個重要的字段 pid、tgid。我們在用戶空間的時候也有 pid、tid,那麼用戶空間的 pid 是不是就是內核的 pid 呢,那 tgid 又是啥呢。很多初學內核的人會認爲用戶空間的 pid 就是內核的 pid,剛開始我也是這麼認爲的,給我的內核學習帶來了很大的困擾。實際上用戶空間的 tid 是內核空間 pid,用戶空間的 pid 是內核空間的 tgid,內核空間的 tgid 是內核裏主線程的 pid。爲什麼會這樣呢?主要還是前面講的問題,task_struct 既是線程又是進程的代理,沒有單獨的進程結構體。當進程創建時,也就是進程的第一個線程創建時,會爲 task_struct 分配一個 pid,就是主線程的 tid,然後進程的 pid 也就是字段 tgid 會被賦值爲主線程的 tid。此後再創建的線程都會繼承父線程的 tgid,所以在每個線程中都能直接獲取進程的 pid。
我們在這裏畫個圖總結一下進程與線程的關係、pid 與 tgid 之間的關係:
Linux 裏面雖然沒有進程結構體,但是所有 tgid 相同、虛擬內存等資源相同的線程構成一個虛擬的進程結構體。創建進程的第一個線程 (task_struct) 就是同時在創建進程,其對應的 mm_struct、files_struct、signal_struct 等資源都會被創建出來。創建進程的第二個線程那就是純粹地創建線程了。
2.4 進程的狀態
進程的狀態在 Linux 中是如何表示的呢?task_struct 中有兩個字段用來表示進程的狀態,__state 和 exit_state,前者是總體狀態,後者是進程在死亡時的兩個子狀態。
我們來看一下代碼中的定義:
linux-src/include/linux/sched.h
/* Used in tsk->state: */
#define TASK_RUNNING 0x0000
#define TASK_INTERRUPTIBLE 0x0001
#define TASK_UNINTERRUPTIBLE 0x0002
#define __TASK_STOPPED 0x0004
#define __TASK_TRACED 0x0008
/* Used in tsk->exit_state: */
#define EXIT_DEAD 0x0010
#define EXIT_ZOMBIE 0x0020
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_PARKED 0x0040
#define TASK_DEAD 0x0080
#define TASK_WAKEKILL 0x0100
#define TASK_WAKING 0x0200
#define TASK_NOLOAD 0x0400
#define TASK_NEW 0x0800
其中 TASK_RUNNING 代表的是 Runnable 和 Running 狀態。在 Linux 中不是用 flag 直接區分 Runnable 和 Running 狀態的,它們都用 TASK_RUNNING 表示,區分它們的方法是進程是否在運行隊列的當前進程字段上。Blocked 狀態有兩種表示,TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE,它們的區別是前者在睡眠時能被信號喚醒,後者不能被信號喚醒。表示死亡的狀態是 TASK_DEAD,它有兩個子狀態 EXIT_ZOMBIE、EXIT_DEAD,這兩個狀態在 3.6 中講解。
三、進程的生命週期
=================
瞭解了進程的基本概念,明白了進程在 Linux 中的實現,下面我們再來看一看進程的生命週期。進程的生命週期和進程的五態轉化有關聯,但是又不完全相同。我們先來回顧一下進程的五態轉化圖。
進程從無到有要經歷新建的狀態,在 Linux 上創建進程和加載程序是兩個不同的步驟。剛創建出來的進程和父進程幾乎是一模一樣,要想執行新的程序還得經歷裝載的過程。程序裝載完成之後就會進入就緒、執行、阻塞的循環了,這個是進程調度裏面的內容。實際上程序在 main 函數之前還經歷了兩個過程,分別是 so 的加載和程序本身的初始化。進程執行到最後總會經歷死亡,無論是主動退出還是意外死亡。下面我們就詳細分析一下進程的這幾個生命週期。
3.1 進程的創建
Linux 上創建進程和我們直觀想象的不同,我們一般想象的是有個類似 create_process 的系統調用,可以直接創建進程並執行新的程序。但是在 UNIX-like 的系統上,創建進程和執行新的程序是分開的,fork 是用來創建進程的,創建的進程和父進程是同一個程序,然後可以在子進程中通過 exec 系統調用來執行你想要執行的程序。UNIX 爲什麼要這麼設計呢?有兩個原因,一是當時還沒有多線程,使用 fork 可以實現多進程;二是 fork 之後可以進行一些操作再用 exec 裝載新程序,可以提高靈活性。我們這節只講 fork,在下一節講 exec。
我們先來看一下 fork 的接口定義:
#include <unistd.h>
pid_t fork(void);
fork 系統調用不接受任何參數,返回值是個 pid。第一次接觸 fork 的人難免會有疑惑,fork 是怎麼創建進程的呢?答案是 fork 會返回兩次,在父進程中返回一次,在子進程中返回一次,在父進程中返回的是子進程的 pid,在子進程中返回的是 0,如果創建進程失敗則返回 - 1。估計很多人還是難以理解這是什麼意思。下面我們再舉個例子用代碼來演示一下。
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
pid_t pid = fork();
if(pid == -1) {
printf("fork error, exit\n");
exit(-1);
} else if(pid == 0) {
printf("I am child process, pid:%d\n", getpid());
pause();
} else {
printf("I am parent process, pid:%d, my child is pid:%d\n", getpid(), pid);
waitpid(pid, NULL, 0);
}
}
從這個例子中,我們可以看到 fork 的用法,當 fork 返回值爲 0 時代表是子進程,我們可以在這裏做一些要在子進程中做的事。
那麼 fork 系統調用是怎麼實現的呢?讓我們來看一下代碼:
linux-src/kernel/fork.c
SYSCALL_DEFINE0(fork)
{
struct kernel_clone_args args = {
.exit_signal = SIGCHLD,
};
return kernel_clone(&args);
}
pid_t kernel_clone(struct kernel_clone_args *args)
{
u64 clone_flags = args->flags;
struct completion vfork;
struct pid *pid;
struct task_struct *p;
int trace = 0;
pid_t nr;
/*
* For legacy clone() calls, CLONE_PIDFD uses the parent_tid argument
* to return the pidfd. Hence, CLONE_PIDFD and CLONE_PARENT_SETTID are
* mutually exclusive. With clone3() CLONE_PIDFD has grown a separate
* field in struct clone_args and it still doesn't make sense to have
* them both point at the same memory location. Performing this check
* here has the advantage that we don't need to have a separate helper
* to check for legacy clone().
*/
if ((args->flags & CLONE_PIDFD) &&
(args->flags & CLONE_PARENT_SETTID) &&
(args->pidfd == args->parent_tid))
return -EINVAL;
/*
* 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 (args->exit_signal != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;
if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}
p = copy_process(NULL, trace, NUMA_NO_NODE, args);
add_latent_entropy();
if (IS_ERR(p))
return PTR_ERR(p);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
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, args->parent_tid);
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);
return nr;
}
內核本身有 fork 的系統調用,但是 glibc 的 fork API 是用 clone 系統調用來實現的,我們知道這一點就行了,實際上它們最後調用的代碼還是一樣的,所以我們還用 fork 系統調用來講解,沒有影響。可以看到 fork 系統調用什麼也沒做,直接調用的 kernel_clone 函數,kernel_clone 以前叫做 do_fork,現在改名了。kernel_clone 的邏輯也很簡單,就是做了兩件事,一是 copy_process 複製 task_struct,二是 wake_up_new_task 喚醒新進程。copy_process 會根據 flag 來決定新的 task_struct 是自己創建新的 mm_struct、files_struct 等結構體,還是和父線程共享這些結構體,由於我們這裏是創建進程,所以這些結構體都會創建新的。系統調用執行完成後就會返回,返回值是子進程的 pid。而子進程被 wake_up 之後會被調度執行,它返回到用戶空間時返回值是 0。
3.2 進程的裝載
新的進程剛剛創建之後執行的還是舊的程序,想要執行新的程序的話還得使用系統調用 execve。execve 會把當前程序替換爲新的程序。下面我們先來看一下 execve 的接口:
#include <unistd.h>
int execve(const char *pathname, char *const argv[], char *const envp[]);
第一個參數是要執行的程序的路徑,可以是相對路徑也可以是絕對路徑。第二個參數是程序的參數列表,我們在命令行執行命令時後面跟的參數會被放到這裏。第三個參數是環境變量列表,在命令行執行程序時 bash 會被自己的環境變量放到這裏傳給子進程。
除此之外,libc 還提供了幾個 API 可以用來執行新的進程,它們的功能是一樣的,只是參數有所差異,這些 API 的實現還是使用的系統調用 execve。
#include <unistd.h>
extern char **environ;
int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
int execlp(const char *file, const char *arg, ... /*, (char *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
下面我們來看一下 execve 系統調用的實現:
linux-src/fs/exec.c
SYSCALL_DEFINE3(execve,
const char __user *, filename,
const char __user *const __user *, argv,
const char __user *const __user *, envp)
{
return do_execve(getname(filename), argv, envp);
}
static int do_execve(struct filename *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
struct user_arg_ptr envp = { .ptr.native = __envp };
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}
static int do_execveat_common(int fd, struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp,
int flags)
{
struct linux_binprm *bprm;
int retval;
if (IS_ERR(filename))
return PTR_ERR(filename);
/*
* We move the actual failure in case of RLIMIT_NPROC excess from
* set*uid() to execve() because too many poorly written programs
* don't check setuid() return code. Here we additionally recheck
* whether NPROC limit is still exceeded.
*/
if ((current->flags & PF_NPROC_EXCEEDED) &&
is_ucounts_overlimit(current_ucounts(), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {
retval = -EAGAIN;
goto out_ret;
}
/* We're below the limit (still or again), so we don't want to make
* further execve() calls fail. */
current->flags &= ~PF_NPROC_EXCEEDED;
bprm = alloc_bprm(fd, filename);
if (IS_ERR(bprm)) {
retval = PTR_ERR(bprm);
goto out_ret;
}
retval = count(argv, MAX_ARG_STRINGS);
if (retval < 0)
goto out_free;
bprm->argc = retval;
retval = count(envp, MAX_ARG_STRINGS);
if (retval < 0)
goto out_free;
bprm->envc = retval;
retval = bprm_stack_limits(bprm);
if (retval < 0)
goto out_free;
retval = copy_string_kernel(bprm->filename, bprm);
if (retval < 0)
goto out_free;
bprm->exec = bprm->p;
retval = copy_strings(bprm->envc, envp, bprm);
if (retval < 0)
goto out_free;
retval = copy_strings(bprm->argc, argv, bprm);
if (retval < 0)
goto out_free;
retval = bprm_execve(bprm, fd, filename, flags);
out_free:
free_bprm(bprm);
out_ret:
putname(filename);
return retval;
}
static int bprm_execve(struct linux_binprm *bprm,
int fd, struct filename *filename, int flags)
{
struct file *file;
int retval;
retval = prepare_bprm_creds(bprm);
if (retval)
return retval;
check_unsafe_exec(bprm);
current->in_execve = 1;
file = do_open_execat(fd, filename, flags);
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec();
bprm->file = file;
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. This allows the code in exec to
* choose to fail when the executable is not mmaped into the
* interpreter and an open file descriptor is not passed to
* the interpreter. This makes for a better user experience
* than having the interpreter start and then immediately fail
* when it finds the executable is inaccessible.
*/
if (bprm->fdpath && get_close_on_exec(fd))
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
/* Set the unchanging part of bprm->cred */
retval = security_bprm_creds_for_exec(bprm);
if (retval)
goto out;
retval = exec_binprm(bprm);
if (retval < 0)
goto out;
/* execve succeeded */
current->fs->in_exec = 0;
current->in_execve = 0;
rseq_execve(current);
acct_update_integrals(current);
task_numa_free(current, false);
return retval;
out:
/*
* If past the point of no return ensure the code never
* returns to the userspace process. Use an existing fatal
* signal if present otherwise terminate the process with
* SIGSEGV.
*/
if (bprm->point_of_no_return && !fatal_signal_pending(current))
force_fatal_sig(SIGSEGV);
out_unmark:
current->fs->in_exec = 0;
current->in_execve = 0;
return retval;
}
static int exec_binprm(struct linux_binprm *bprm)
{
pid_t old_pid, old_vpid;
int ret, depth;
/* 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();
/* This allows 4 levels of binfmt rewrites before failing hard. */
for (depth = 0;; depth++) {
struct file *exec;
if (depth > 5)
return -ELOOP;
ret = search_binary_handler(bprm);
if (ret < 0)
return ret;
if (!bprm->interpreter)
break;
exec = bprm->file;
bprm->file = bprm->interpreter;
bprm->interpreter = NULL;
allow_write_access(exec);
if (unlikely(bprm->have_execfd)) {
if (bprm->executable) {
fput(exec);
return -ENOEXEC;
}
bprm->executable = exec;
} else
fput(exec);
}
audit_bprm(bprm);
trace_sched_process_exec(current, old_pid, bprm);
ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
proc_exec_connector(current);
return 0;
}
static int search_binary_handler(struct linux_binprm *bprm)
{
bool need_retry = IS_ENABLED(CONFIG_MODULES);
struct linux_binfmt *fmt;
int retval;
retval = prepare_binprm(bprm);
if (retval < 0)
return retval;
retval = security_bprm_check(bprm);
if (retval)
return retval;
retval = -ENOENT;
retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
if (need_retry) {
if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
printable(bprm->buf[2]) && printable(bprm->buf[3]))
return retval;
if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
return retval;
need_retry = false;
goto retry;
}
return retval;
}
linux-src/fs/binfmt_elf.c
static int load_elf_binary(struct linux_binprm *bprm)
{
struct file *interpreter = NULL; /* to shut gcc up */
unsigned long load_addr = 0, load_bias = 0;
int load_addr_set = 0;
unsigned long error;
struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
struct elf_phdr *elf_property_phdata = NULL;
unsigned long elf_bss, elf_brk;
int bss_prot = 0;
int retval, i;
unsigned long elf_entry;
unsigned long e_entry;
unsigned long interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
unsigned long reloc_func_desc __maybe_unused = 0;
int executable_stack = EXSTACK_DEFAULT;
struct elfhdr *elf_ex = (struct elfhdr *)bprm->buf;
struct elfhdr *interp_elf_ex = NULL;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
struct mm_struct *mm;
struct pt_regs *regs;
retval = -ENOEXEC;
/* First of all, some simple consistency checks */
if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
goto out;
if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)
goto out;
if (!elf_check_arch(elf_ex))
goto out;
if (elf_check_fdpic(elf_ex))
goto out;
if (!bprm->file->f_op->mmap)
goto out;
elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
if (!elf_phdata)
goto out;
elf_ppnt = elf_phdata;
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
char *elf_interpreter;
if (elf_ppnt->p_type == PT_GNU_PROPERTY) {
elf_property_phdata = elf_ppnt;
continue;
}
if (elf_ppnt->p_type != PT_INTERP)
continue;
/*
* This is the program interpreter used for shared libraries -
* for now assume that this is an a.out format binary.
*/
retval = -ENOEXEC;
if (elf_ppnt->p_filesz > PATH_MAX || elf_ppnt->p_filesz < 2)
goto out_free_ph;
retval = -ENOMEM;
elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
if (!elf_interpreter)
goto out_free_ph;
retval = elf_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz,
elf_ppnt->p_offset);
if (retval < 0)
goto out_free_interp;
/* make sure path is NULL terminated */
retval = -ENOEXEC;
if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
goto out_free_interp;
interpreter = open_exec(elf_interpreter);
kfree(elf_interpreter);
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_ph;
/*
* If the binary is not readable then enforce mm->dumpable = 0
* regardless of the interpreter's permissions.
*/
would_dump(bprm, interpreter);
interp_elf_ex = kmalloc(sizeof(*interp_elf_ex), GFP_KERNEL);
if (!interp_elf_ex) {
retval = -ENOMEM;
goto out_free_ph;
}
/* Get the exec headers */
retval = elf_read(interpreter, interp_elf_ex,
sizeof(*interp_elf_ex), 0);
if (retval < 0)
goto out_free_dentry;
break;
out_free_interp:
kfree(elf_interpreter);
goto out_free_ph;
}
elf_ppnt = elf_phdata;
for (i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(elf_ex, elf_ppnt,
bprm->file, false,
&arch_state);
if (retval)
goto out_free_dentry;
break;
}
/* Some simple consistency checks for the interpreter */
if (interpreter) {
retval = -ELIBBAD;
/* Not an ELF interpreter */
if (memcmp(interp_elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
goto out_free_dentry;
/* Verify the interpreter has a valid arch */
if (!elf_check_arch(interp_elf_ex) ||
elf_check_fdpic(interp_elf_ex))
goto out_free_dentry;
/* Load the interpreter program headers */
interp_elf_phdata = load_elf_phdrs(interp_elf_ex,
interpreter);
if (!interp_elf_phdata)
goto out_free_dentry;
/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
elf_property_phdata = NULL;
elf_ppnt = interp_elf_phdata;
for (i = 0; i < interp_elf_ex->e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_PROPERTY:
elf_property_phdata = elf_ppnt;
break;
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(interp_elf_ex,
elf_ppnt, interpreter,
true, &arch_state);
if (retval)
goto out_free_dentry;
break;
}
}
retval = parse_elf_properties(interpreter ?: bprm->file,
elf_property_phdata, &arch_state);
if (retval)
goto out_free_dentry;
/*
* Allow arch code to reject the ELF at this point, whilst it's
* still possible to return an error to the code that invoked
* the exec syscall.
*/
retval = arch_check_elf(elf_ex,
!!interpreter, interp_elf_ex,
&arch_state);
if (retval)
goto out_free_dentry;
/* Flush all traces of the currently running executable */
retval = begin_new_exec(bprm);
if (retval)
goto out_free_dentry;
/* Do this immediately, since STACK_TOP as used in setup_arg_pages
may depend on the personality. */
SET_PERSONALITY2(*elf_ex, &arch_state);
if (elf_read_implies_exec(*elf_ex, executable_stack))
current->personality |= READ_IMPLIES_EXEC;
if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
current->flags |= PF_RANDOMIZE;
setup_new_exec(bprm);
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP),
executable_stack);
if (retval < 0)
goto out_free_dentry;
elf_bss = 0;
elf_brk = 0;
start_code = ~0UL;
end_code = 0;
start_data = 0;
end_data = 0;
/* Now we do a little grungy work by mmapping the ELF image into
the correct location in memory. */
for(i = 0, elf_ppnt = elf_phdata;
i < elf_ex->e_phnum; i++, elf_ppnt++) {
int elf_prot, elf_flags;
unsigned long k, vaddr;
unsigned long total_size = 0;
unsigned long alignment;
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (unlikely (elf_brk > elf_bss)) {
unsigned long nbyte;
/* There was a PT_LOAD segment with p_memsz > p_filesz
before this one. Map anonymous pages, if needed,
and clear the area. */
retval = set_brk(elf_bss + load_bias,
elf_brk + load_bias,
bss_prot);
if (retval)
goto out_free_dentry;
nbyte = ELF_PAGEOFFSET(elf_bss);
if (nbyte) {
nbyte = ELF_MIN_ALIGN - nbyte;
if (nbyte > elf_brk - elf_bss)
nbyte = elf_brk - elf_bss;
if (clear_user((void __user *)elf_bss +
load_bias, nbyte)) {
/*
* This bss-zeroing can fail if the ELF
* file specifies odd protections. So
* we don't check the return value
*/
}
}
}
elf_prot = make_prot(elf_ppnt->p_flags, &arch_state,
!!interpreter, false);
elf_flags = MAP_PRIVATE;
vaddr = elf_ppnt->p_vaddr;
/*
* If we are loading ET_EXEC or we have already performed
* the ET_DYN load_addr calculations, proceed normally.
*/
if (elf_ex->e_type == ET_EXEC || load_addr_set) {
elf_flags |= MAP_FIXED;
} else if (elf_ex->e_type == ET_DYN) {
/*
* This logic is run once for the first LOAD Program
* Header for ET_DYN binaries to calculate the
* randomization (load_bias) for all the LOAD
* Program Headers, and to calculate the entire
* size of the ELF mapping (total_size). (Note that
* load_addr_set is set to true later once the
* initial mapping is performed.)
*
* There are effectively two types of ET_DYN
* binaries: programs (i.e. PIE: ET_DYN with INTERP)
* and loaders (ET_DYN without INTERP, since they
* _are_ the ELF interpreter). The loaders must
* be loaded away from programs since the program
* may otherwise collide with the loader (especially
* for ET_EXEC which does not have a randomized
* position). For example to handle invocations of
* "./ld.so someprog" to test out a new version of
* the loader, the subsequent program that the
* loader loads must avoid the loader itself, so
* they cannot share the same load range. Sufficient
* room for the brk must be allocated with the
* loader as well, since brk must be available with
* the loader.
*
* Therefore, programs are loaded offset from
* ELF_ET_DYN_BASE and loaders are loaded into the
* independently randomized mmap region (0 load_bias
* without MAP_FIXED).
*/
if (interpreter) {
load_bias = ELF_ET_DYN_BASE;
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd();
alignment = maximum_alignment(elf_phdata, elf_ex->e_phnum);
if (alignment)
load_bias &= ~(alignment - 1);
elf_flags |= MAP_FIXED;
} else
load_bias = 0;
/*
* Since load_bias is used for all subsequent loading
* calculations, we must lower it by the first vaddr
* so that the remaining calculations based on the
* ELF vaddrs will be correctly offset. The result
* is then page aligned.
*/
load_bias = ELF_PAGESTART(load_bias - vaddr);
total_size = total_mapping_size(elf_phdata,
elf_ex->e_phnum);
if (!total_size) {
retval = -EINVAL;
goto out_free_dentry;
}
}
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
if (BAD_ADDR(error)) {
retval = IS_ERR((void *)error) ?
PTR_ERR((void*)error) : -EINVAL;
goto out_free_dentry;
}
if (!load_addr_set) {
load_addr_set = 1;
load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset);
if (elf_ex->e_type == ET_DYN) {
load_bias += error -
ELF_PAGESTART(load_bias + vaddr);
load_addr += load_bias;
reloc_func_desc = load_bias;
}
}
k = elf_ppnt->p_vaddr;
if ((elf_ppnt->p_flags & PF_X) && k < start_code)
start_code = k;
if (start_data < k)
start_data = k;
/*
* Check to see if the section's size will overflow the
* allowed task size. Note that p_filesz must always be
* <= p_memsz so it is only necessary to check p_memsz.
*/
if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||
elf_ppnt->p_memsz > TASK_SIZE ||
TASK_SIZE - elf_ppnt->p_memsz < k) {
/* set_brk can never work. Avoid overflows. */
retval = -EINVAL;
goto out_free_dentry;
}
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss)
elf_bss = k;
if ((elf_ppnt->p_flags & PF_X) && end_code < k)
end_code = k;
if (end_data < k)
end_data = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if (k > elf_brk) {
bss_prot = elf_prot;
elf_brk = k;
}
}
e_entry = elf_ex->e_entry + load_bias;
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;
/* Calling set_brk effectively mmaps the pages that we need
* for the bss and break sections. We must do this before
* mapping in the interpreter, to make sure it doesn't wind
* up getting placed where the bss needs to go.
*/
retval = set_brk(elf_bss, elf_brk, bss_prot);
if (retval)
goto out_free_dentry;
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
retval = -EFAULT; /* Nobody gets to see this, but.. */
goto out_free_dentry;
}
if (interpreter) {
elf_entry = load_elf_interp(interp_elf_ex,
interpreter,
load_bias, interp_elf_phdata,
&arch_state);
if (!IS_ERR((void *)elf_entry)) {
/*
* load_elf_interp() returns relocation
* adjustment
*/
interp_load_addr = elf_entry;
elf_entry += interp_elf_ex->e_entry;
}
if (BAD_ADDR(elf_entry)) {
retval = IS_ERR((void *)elf_entry) ?
(int)elf_entry : -EINVAL;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);
fput(interpreter);
kfree(interp_elf_ex);
kfree(interp_elf_phdata);
} else {
elf_entry = e_entry;
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
goto out_free_dentry;
}
}
kfree(elf_phdata);
set_binfmt(&elf_format);
#ifdef ARCH_HAS_SETUP_ADDITIONAL_PAGES
retval = ARCH_SETUP_ADDITIONAL_PAGES(bprm, elf_ex, !!interpreter);
if (retval < 0)
goto out;
#endif /* ARCH_HAS_SETUP_ADDITIONAL_PAGES */
retval = create_elf_tables(bprm, elf_ex,
load_addr, interp_load_addr, e_entry);
if (retval < 0)
goto out;
mm = current->mm;
mm->end_code = end_code;
mm->start_code = start_code;
mm->start_data = start_data;
mm->end_data = end_data;
mm->start_stack = bprm->p;
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {
/*
* For architectures with ELF randomization, when executing
* a loader directly (i.e. no interpreter listed in ELF
* headers), move the brk area out of the mmap region
* (since it grows up, and may collide early with the stack
* growing down), and into the unused ELF_ET_DYN_BASE region.
*/
if (IS_ENABLED(CONFIG_ARCH_HAS_ELF_RANDOMIZE) &&
elf_ex->e_type == ET_DYN && !interpreter) {
mm->brk = mm->start_brk = ELF_ET_DYN_BASE;
}
mm->brk = mm->start_brk = arch_randomize_brk(mm);
#ifdef compat_brk_randomized
current->brk_randomized = 1;
#endif
}
if (current->personality & MMAP_PAGE_ZERO) {
/* Why this, you ask??? Well SVr4 maps page 0 as read-only,
and some applications "depend" upon this behavior.
Since we do not have the power to recompile these, we
emulate the SVr4 behavior. Sigh. */
error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
}
regs = current_pt_regs();
#ifdef ELF_PLAT_INIT
/*
* The ABI may specify that certain registers be set up in special
* ways (on i386 %edx is the address of a DT_FINI function, for
* example. In addition, it may also specify (eg, PowerPC64 ELF)
* that the e_entry field is the address of the function descriptor
* for the startup routine, rather than the address of the startup
* routine itself. This macro performs whatever initialization to
* the regs structure is required as well as any relocations to the
* function descriptor entries when executing dynamically links apps.
*/
ELF_PLAT_INIT(regs, reloc_func_desc);
#endif
finalize_exec(bprm);
START_THREAD(elf_ex, regs, elf_entry, bprm->p);
retval = 0;
out:
return retval;
/* error cleanup */
out_free_dentry:
kfree(interp_elf_ex);
kfree(interp_elf_phdata);
allow_write_access(interpreter);
if (interpreter)
fput(interpreter);
out_free_ph:
kfree(elf_phdata);
goto out;
}
execve 系統調用的邏輯比較複雜,這裏就簡單解析一下。函數首先會調用 alloc_bprm 分配一個 linux_binprm 結構體,這個結構體記錄着可執行程序的一些信息。在 alloc_bprm 會創建一個新的 mm_struct,此後進程就會用這個新的虛擬內存空間了,還會創建一個 vma 作爲主線程的棧,初始大小爲 4k。然後調用 bprm_execve,bprm_execve 會調用 exec_binprm,exec_binprm 會調用 search_binary_handler,在 search_binary_handler 裏會通過函數指針 load_binary 調用最後的函數 load_elf_binary。
在 load_elf_binary 裏,會先對 ELF 文件頭部信息進行解析。然後會加載解釋器 (interpreter)。什麼是解釋器呢?一個程序往往並不是只有可執行程序,而是由一個可執行程序加上 n 個 so 組成。so 是在程序啓動時動態加載的,可能很多人會認爲這個工作是由內核完成的,實際上這個工作是由一個 so 完成的,這個 so 就叫做程序解釋器,在教科書上往往被叫做加載器,也有叫動態鏈接器的。X86_64 上的解釋器文件是 / lib64/ld-linux-x86-64.so.2,這一般是個軟連接文件,它會指向真正的解釋器。內核負責加載解釋器,解釋器負責加載所有其它的 so。很多人可能認爲進程返回用戶空間之後就要直接執行 main 函數了,實際上還早着呢。進程返回用戶空間首先執行的是解釋器的入口函數,解釋器執行完了之後會執行可執行程序的入口函數,入口函數執行完了之後纔會去執行 main 函數。這個正是我們下面兩節要講的內容。
3.3 進程的加載
這一節要講的是解釋器的加載過程,這個過程也被叫做動態鏈接。加載器的實現是在 Glibc 裏面。我們這裏就是大概介紹一下加載器的邏輯,具體的細節大家可以去看參考文獻中的書籍。ELF 格式的可執行程序和共享庫裏面有一個段叫做. dynamic,這個段裏面會記錄程序所依賴的所有 so。so 裏面的. dynamic 段也會記錄自己所依賴的所有 so。解釋器會通過深度優先或者廣度優先的方法找到一個程序所依賴的所有 so,然後加載它們。加載一個 so 會首先解析它的 ELF 頭部信息,然後通過 mmap 爲它的數據段代碼段分配內存,並設置不同的讀寫執行權限。最後會對 so 進行重定位,重定位包括全局數據重定位和函數重定位。
3.4 進程的初始化
進程完成加載之後不是直接就執行 main 函數的,而是會執行 ELF 文件的入口函數。這個入口函數叫做_start,_start 完成一些基本的設置之後會調用__libc_start_main。__libc_start_main 完成一些初始化之後纔會調用 main 函數。你會發現,我們上學的時候講的是程序執行的時候會首先執行 main 函數,實際上在 main 函數執行之前還發生了很多很複雜的事情,只不過這些事情繫統都幫我們悄悄地做了,如果我們想要研究透徹的話還是很麻煩的。
__libc_start_main 的具體細節請參看:
http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
3.5 進程的運行
程序在運行的時候會不停地經歷就緒、運行、阻塞的過程,具體情況請參看《深入理解 Linux 進程調度》。
3.6 進程的死亡
進程執行到最後總會死亡的,進程死亡的原因可以分爲兩大類,正常死亡和非正常死亡。
正常死亡的情況有:
1.main 函數返回。
-
進程調用了 exit、_exit、_Exit 等函數。
-
進程的所有線程都調用了 pthread_exit。
-
主線程調用了 pthread_exit,其他線程都從線程函數返回。
非正常死亡的情況有:
-
進程訪問非法內存而收到信號 SIGSEGV。
-
庫程序發現異常情況給進程發送信號 SIGABRT。
-
在終端上輸入 Ctrl+C 給進程發送信號 SIGINT。
-
通過 kill 命令給進程發送信號 SIGTERM。
-
通過 kill -9 命令給進程發送信號 SIGKILL。
-
進程收到其它一些會導致死亡的信號。
main 函數返回本質上也是調用的 exit,因爲 main 函數外還有一層函數__libc_start_main,它會在 main 函數返回後調用 exit。exit 的實現調用的是系統調用 exit_group,pthread_exit 的實現調用的是系統調用 exit。這裏就體現出了 API 和系統調用的不同。進程由於信號原因而死的,其死亡方法也是內核在信號處理中調用了系統調用 exit_group,只不過是直接調用的函數,沒有走系統調用的流程。系統調用 exit 的作用是殺死線程,系統調用 exit_group 的作用是殺死當前線程,並給同進程的所有其它線程發 SIGKILL 信號,這會導致所有的線程都死亡,從而整個進程也就死了。每個線程死亡的時候都會釋放對進程資源的引用,最後一個線程死亡的時候,資源的引用計數會變成 0,從而會去釋放這個資源。總結一下就是進程的第一個線程創建的時候會去創建進程的資源,進程的最後一個線程死亡的時候會去釋放進程的資源。
進程死亡的過程可以細分爲兩步,殭屍和火化,對應着進程死亡的兩個子狀態 EXIT_ZOMBIE 和 EXIT_DEAD。進程只有被火化之後纔算是徹底死亡了。就像人死了需要被家屬送去殯儀館火化一樣,進程死了也需要親屬送去火化,只不過對於進程來說只有父進程有權力去火化子進程。如果父進程一直不去火化子進程,那麼子進程就會一直處於殭屍狀態。父進程火化子進程的方法的是什麼呢?就是系統調用 wait、waitid、waitpid、wait3、wait4。如果父進程提前死了怎麼辦呢?子進程會被託孤給 init 進程,由 init 進程負責對其火化。任何進程死亡都會經歷殭屍狀態,只不過大部分情況下這個狀態持續時間都非常短,用戶空間感覺不到。當父進程沒有對子進程 wait 的時候,子進程就會一直處於殭屍狀態,不會被火化,這時候用戶空間通過 ps 命令就可以看到殭屍狀態的進程了。殭屍進程不是沒有死,而是死了沒人送去火化,所以殺死殭屍進程的說法是不對的。清理殭屍進程的方法是 kill 其父進程,父進程死了,殭屍進程會被託孤給 init 進程,init 進程會立馬對其進行火化。
當一個進程的 exit_group 執行完成之後,這個進程就變成了殭屍進程。殭屍進程是沒有用戶空間的,也不可能再執行了。殭屍進程的文件等所有資源都被釋放了,唯一剩下的就是還有一個 task_struct 結構體。如果父進程此時去 wait 子進程或者之前就已經在 wait 子進程,此時 wait 會返回,task_struct 會被銷燬,這個進程就徹底消失了。
下面然我們來看看 exit_group 系統調用的代碼:
linux-src/kernel/exit.c
SYSCALL_DEFINE1(exit_group, int, error_code)
{
do_group_exit((error_code & 0xff) << 8);
/* NOTREACHED */
return 0;
}
void
do_group_exit(int exit_code)
{
struct signal_struct *sig = current->signal;
BUG_ON(exit_code & 0x80); /* core dumps don't get here */
if (signal_group_exit(sig))
exit_code = sig->group_exit_code;
else if (!thread_group_empty(current)) {
struct sighand_struct *const sighand = current->sighand;
spin_lock_irq(&sighand->siglock);
if (signal_group_exit(sig))
/* Another thread got here before we took the lock. */
exit_code = sig->group_exit_code;
else {
sig->group_exit_code = exit_code;
sig->flags = SIGNAL_GROUP_EXIT;
zap_other_threads(current);
}
spin_unlock_irq(&sighand->siglock);
}
do_exit(exit_code);
/* NOTREACHED */
}
void __noreturn do_exit(long code)
{
struct task_struct *tsk = current;
int group_dead;
/*
* We can get here from a kernel oops, sometimes with preemption off.
* Start by checking for critical errors.
* Then fix up important state like USER_DS and preemption.
* Then do everything else.
*/
WARN_ON(blk_needs_flush_plug(tsk));
if (unlikely(in_interrupt()))
panic("Aiee, killing interrupt handler!");
if (unlikely(!tsk->pid))
panic("Attempted to kill the idle task!");
/*
* If do_exit is called because this processes oopsed, it's possible
* that get_fs() was left as KERNEL_DS, so reset it to USER_DS before
* continuing. Amongst other possible reasons, this is to prevent
* mm_release()->clear_child_tid() from writing to a user-controlled
* kernel address.
*/
force_uaccess_begin();
if (unlikely(in_atomic())) {
pr_info("note: %s[%d] exited with preempt_count %d\n",
current->comm, task_pid_nr(current),
preempt_count());
preempt_count_set(PREEMPT_ENABLED);
}
profile_task_exit(tsk);
kcov_task_exit(tsk);
ptrace_event(PTRACE_EVENT_EXIT, code);
validate_creds_for_do_exit(tsk);
/*
* We're taking recursive faults here in do_exit. Safest is to just
* leave this task alone and wait for reboot.
*/
if (unlikely(tsk->flags & PF_EXITING)) {
pr_alert("Fixing recursive fault but reboot is needed!\n");
futex_exit_recursive(tsk);
set_current_state(TASK_UNINTERRUPTIBLE);
schedule();
}
io_uring_files_cancel();
exit_signals(tsk); /* sets PF_EXITING */
/* sync mm's RSS info before statistics gathering */
if (tsk->mm)
sync_mm_rss(tsk->mm);
acct_update_integrals(tsk);
group_dead = atomic_dec_and_test(&tsk->signal->live);
if (group_dead) {
/*
* If the last thread of global init has exited, panic
* immediately to get a useable coredump.
*/
if (unlikely(is_global_init(tsk)))
panic("Attempted to kill init! exitcode=0x%08x\n",
tsk->signal->group_exit_code ?: (int)code);
#ifdef CONFIG_POSIX_TIMERS
hrtimer_cancel(&tsk->signal->real_timer);
exit_itimers(tsk->signal);
#endif
if (tsk->mm)
setmax_mm_hiwater_rss(&tsk->signal->maxrss, tsk->mm);
}
acct_collect(code, group_dead);
if (group_dead)
tty_audit_exit();
audit_free(tsk);
tsk->exit_code = code;
taskstats_exit(tsk, group_dead);
exit_mm();
if (group_dead)
acct_process();
trace_sched_process_exit(tsk);
exit_sem(tsk);
exit_shm(tsk);
exit_files(tsk);
exit_fs(tsk);
if (group_dead)
disassociate_ctty(1);
exit_task_namespaces(tsk);
exit_task_work(tsk);
exit_thread(tsk);
/*
* Flush inherited counters to the parent - before the parent
* gets woken up by child-exit notifications.
*
* because of cgroup mode, must be called before cgroup_exit()
*/
perf_event_exit_task(tsk);
sched_autogroup_exit_task(tsk);
cgroup_exit(tsk);
/*
* FIXME: do that only when needed, using sched_exit tracepoint
*/
flush_ptrace_hw_breakpoint(tsk);
exit_tasks_rcu_start();
exit_notify(tsk, group_dead);
proc_exit_connector(tsk);
mpol_put_task_policy(tsk);
#ifdef CONFIG_FUTEX
if (unlikely(current->pi_state_cache))
kfree(current->pi_state_cache);
#endif
/*
* Make sure we are holding no locks:
*/
debug_check_no_locks_held();
if (tsk->io_context)
exit_io_context(tsk);
if (tsk->splice_pipe)
free_pipe_info(tsk->splice_pipe);
if (tsk->task_frag.page)
put_page(tsk->task_frag.page);
validate_creds_for_do_exit(tsk);
check_stack_usage();
preempt_disable();
if (tsk->nr_dirtied)
__this_cpu_add(dirty_throttle_leaks, tsk->nr_dirtied);
exit_rcu();
exit_tasks_rcu_finish();
lockdep_free_task(tsk);
do_task_dead();
}
linux-src/kernel/signal.c
int zap_other_threads(struct task_struct *p)
{
struct task_struct *t = p;
int count = 0;
p->signal->group_stop_count = 0;
while_each_thread(p, t) {
task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
count++;
/* Don't bother with already dead threads */
if (t->exit_state)
continue;
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
}
return count;
}
四、回顧總結
==============
在本文中我們學習了進程的基本概念,知道了進程在 Linux 上是怎麼實現的,也明白了進程的各個生命週期的活動。下面我們再來看一下進程的實現圖,回顧一下:
在 Linux 中沒有嚴格的進程線程之分,內核沒有實現進程控制塊,只有一個 task_struct,它既是線程又是進程的代理。當進程的第一個線程創建的時候,此時進程被創建,進程相應的資源結構體會被創建。當進程的最後一個線程死亡的時候,進程相應的所有資源都會被釋放,進程就死亡了。
參考文獻:
《Linux Kernel Development》
《Understanding the Linux Kernel》
《Professional Linux Kernel Architecture》
《The Linux Programming Interface》
《Advanced Programming in the UNIX Environment》
《Linkers & Loaders》
《程序員的自我修養》
《深度探索 Linux 操作系統》
https://man7.org/linux/man-pages/man2/fork.2.html
https://man7.org/linux/man-pages/man2/execve.2.html
https://man7.org/linux/man-pages/man3/exec.3.html
https://man7.org/linux/man-pages/man2/exit.2.html
https://man7.org/linux/man-pages/man3/exit.3.html
https://man7.org/linux/man-pages/man2/wait.2.html
https://man7.org/linux/man-pages/man2/wait4.2.html
http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
如需進羣溝通請添加小馬微信邀請:Linuxer2022
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/cxQdUAK0RFnLVELEiT0CdQ