一、進程基本概念
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#defineTASK_NEW0x0800
其中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的接口定義:
#includepid_tfork(void);
fork系統調用不接受任何參數,返回值是個pid。第一次接觸fork的人難免會有疑惑,fork是怎么創建進程的呢?答案是fork會返回兩次,在父進程中返回一次,在子進程中返回一次,在父進程中返回的是子進程的pid,在子進程中返回的是0,如果創建進程失敗則返回-1。估計很多人還是難以理解這是什么意思。下面我們再舉個例子用代碼來演示一下。
#include#include #include #include #include int main(int argc, char *argv[]){ pid_t pid = fork(); if(pid == -1) { printf("fork error, exit "); exit(-1); } else if(pid == 0) { printf("I am child process, pid:%d ", getpid()); pause(); } else { printf("I am parent process, pid:%d, my child is pid:%d ", 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的接口:
#includeintexecve(constchar*pathname,char*constargv[],char*constenvp[]);
第一個參數是要執行的程序的路徑,可以是相對路徑也可以是絕對路徑。第二個參數是程序的參數列表,我們在命令行執行命令時后面跟的參數會被放到這里。第三個參數是環境變量列表,在命令行執行程序時bash會被自己的環境變量放到這里傳給子進程。
除此之外,libc還提供了幾個API可以用來執行新的進程,它們的功能是一樣的,只是參數有所差異,這些API的實現還是使用的系統調用execve。
#includeextern 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[]);intexecvpe(constchar*file,char*constargv[],char*constenvp[]);
下面我們來看一下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] != '?') 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函數返回。
2.進程調用了exit、_exit、_Exit 等函數。
3.進程的所有線程都調用了pthread_exit。
4.主線程調用了pthread_exit,其他線程都從線程函數返回。
非正常死亡的情況有:
1.進程訪問非法內存而收到信號SIGSEGV。
2.庫程序發現異常情況給進程發送信號SIGABRT。
3.在終端上輸入Ctrl+C給進程發送信號SIGINT。
4.通過kill命令給進程發送信號SIGTERM。
5.通過kill -9命令給進程發送信號SIGKILL。
6.進程收到其它一些會導致死亡的信號。
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;} voiddo_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 ", 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! "); 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 ", 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
+關注
關注
87文章
11294瀏覽量
209341 -
操作系統
+關注
關注
37文章
6803瀏覽量
123285 -
進程
+關注
關注
0文章
203瀏覽量
13960
原文標題:深入理解Linux進程管理
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論