深刻Linux內核架構——進程管理和調度(上)

 

若是系統只有一個處理器,那麼給定時刻只有一個程序能夠運行。在多處理器系統中,真正並行運行的進程數目取決於物理CPU的數目。內核和處理器創建了多任務的錯覺,是經過以很短的間隔在系統運行的應用程序之間不停切換作到的。由此,如下兩個問題必須由內核解決:除非明確要求,不然應用程序不能彼此干擾;CPU時間必須在各類應用程序之間儘量公平共享(一些程序可能比其餘程序更重要)。本篇博文主要涉及內核共享CPU時間的方法以及如何在進程之間切換(內核爲各進程分配時間,保證切換以後從上次撤銷其資源時執行環境徹底相同)。html

1、進程優先級

並不是全部進程的重要程度都相同,對於進程,首先比較粗糙的劃分,進程能夠分爲實時進程和非實時進程。node

  • 硬實時進程:有嚴格的時間限制,某些任務必須在指定時限內完成。好比汽車的安全氣囊,一旦發生碰撞,必須保證在必定時間內觸發,超過期限則產生災難性的後果。主流的Linux內核不支持實時處理,但有一些修改版本如RTLinux、Xenomai、RATI提供了這些特性,這些方案中,將Linux內核做爲獨立的「進程」運行處理次要軟件,實時工做在Linux內核外部完成。
  • 軟實時進程:軟實時進程是硬實時進程的一種弱化形式,優先於其餘普通進程,稍微晚一點不會形成巨大影響。好比對CD的寫入操做。
  • 普通進程:沒有特定時間約束的進程,能夠根據重要性對其進行分配優先級。

圖1 經過時間片分配CPU時間調度示意圖linux

圖1是CPU分配時間的一個簡圖。進程運行按時間片調度,分配進程的時間片額與其相對重要性至關。系統中時間的流動對應於圓盤的轉動,重要的進程會比次要的進程獲得更多CPU時間,進程被切換時,全部的CPU寄存器內容和頁表都會被保存,下次該進程恢復執行時,其執行環境能夠徹底恢復。這種簡化模型忽略了一些進程狀態相關的信息,不能使CPU時間利益回報儘量最大化。可是爲調度器的質量確立一種定量標準很是困難。自Linux內核誕生以來,調度器的代碼已經重寫了好幾回。按時間前後順序,主要有O(n)調度器,O(1)調度器和CFS(completely fair scheduler)調度器。詳細區別戳這裏算法

2、進程生命週期

進程並不老是能夠當即運行,有時候它須要等待來自外部信號源、不受其控制的事件(如文本編輯等待輸入)。在調度器進行進程切換時,必須知道每一個進程的狀態,由於將CPU事件分配給無事可作的進程沒有意義,進程在各個狀態之間的轉換也一樣重要。數組

圖2 進程狀態之間的切換示意圖安全

進程可能存在的狀態有:運行、等待和睡眠。圖2描述了進程的幾種狀態及其轉換。除了圖中所示的幾種狀態之外,還有一種狀態被稱爲殭屍態。網絡

  • 運行:該進程正在運行。
  • 等待(就緒):進程可以運行,但沒有獲得許可,由於CPU分配給了另外一個進程。調度器可能在下一次任務切換時選擇該進程。
  • 睡眠:進程正在睡眠沒法運行(「睡眠」狀態有兩種)。由於它在等待一個外部事件,調度器沒法在任務切換時選擇該進程。
  • 殭屍:進程已經死亡,可是它的數據尚未從進程表中刪除。(在UNIX操做系統下銷燬進程須要兩步,第一步由另外一個進程或一個用戶殺死(經過信號完成);第二步是進程的父進程在子進程終止時必須調用或已經調用wait4系統調用,使內核釋放爲子進程保留的資源。當條件一發生,第二個條件不成立的狀況,便會出現「殭屍」狀態)

爲了維持系統中現存的各個進程,防止它們與系統其餘部分相互干擾,Linux進程管理結構中還須要兩種進程狀態選項:用戶狀態核心態。進程一般處於用戶狀態,只能訪問自身的數據,沒法干擾系統中其餘進程。若是進程想要訪問系統數據,則必須切換到核心態,這種訪問必須經由明肯定義的路徑(系統調用)。從用戶狀態進入核心態的第二種方法是經過中斷,此時切換是自動觸發的,處理中斷操做,一般與中斷髮生時執行的程序無關。(系統調用是由用戶應用程序有意調用的,中斷則是不可預測的)session

內核的搶佔調度模型是優先讓優先級高的進程佔用CPU,它創建了一個層次結構,用於判斷哪些進程狀態可由其餘狀態搶佔。數據結構

  • 普通進程老是可能被搶佔,甚至由其餘進程搶佔。
  • 若是系統處於核心態並正在處理系統調用,那麼其餘進程是沒法奪取CPU時間的。
  • 中斷能夠暫停處於用戶態和和心態的進程,具備最高優先級。

在內核2.5開發期間,內核搶佔(kernel preemption)選項被添加到內核,它支持緊急狀況下切換到另外一個進程,甚至當前進程處於系統調用也行。內核搶佔能夠減小等待時間,但代價是增長了內核的複雜度,由於搶佔時有許多數據結構須要針對併發訪問進行保護。併發

3、進程表示

Linux內核涉及進程和程序的全部算法都圍繞數據結構task_struct創建,該結構定義在include/sched.h中。task_struct包含不少成員,將進程與各內核子系統聯繫起來。task_struct定義的簡化版本以下:

  1 struct task_struct {
  2     volatile long state; /* -1表示不可運行,0表示可運行,>0表示中止 */
  3     void *stack;
  4     atomic_t usage;
  5     unsigned long flags; /* 每進程標誌,下文定義 */
  6     unsigned long ptrace;
  7     int lock_depth; /* 大內核鎖深度 */
  8     int prio, static_prio, normal_prio;
  9     struct list_head run_list;
 10     const struct sched_class *sched_class;
 11     struct sched_entity se;
 12     unsigned short ioprio;
 13     unsigned long policy;
 14     cpumask_t cpus_allowed;
 15     unsigned int time_slice;
 16 #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
 17     struct sched_info sched_info;
 18 #endif
 19     struct list_head tasks;
 20 /*
 21 * ptrace_list/ptrace_children鏈表是ptrace可以看到的當前進程的子進程列表。
 22 */
 23     struct list_head ptrace_children;
 24     struct list_head ptrace_list;
 25     struct mm_struct *mm, *active_mm;
 26 /* 進程狀態 */
 27     struct linux_binfmt *binfmt;
 28     long exit_state;
 29     int exit_code, exit_signal;
 30     int pdeath_signal; /* 在父進程終止時發送的信號 */
 31     unsigned int personality;
 32     unsigned did_exec:1;
 33     pid_t pid;
 34     pid_t tgid;
 35 /*
 36 * 分別是指向(原)父進程、最年輕的子進程、年幼的兄弟進程、年長的兄弟進程的指針。
 37 *(p->father能夠替換爲p->parent->pid)
 38 */
 39     struct task_struct *real_parent; /* 真正的父進程(在被調試的狀況下) */
 40     struct task_struct *parent; /* 父進程 */
 41 /*
 42 * children/sibling鏈表外加當前調試的進程,構成了當前進程的全部子進程
 43 */
 44     struct list_head children; /* 子進程鏈表 */
 45     struct list_head sibling; /* 鏈接到父進程的子進程鏈表 */
 46     struct task_struct *group_leader; /* 線程組組長 */
 47 /* PID與PID散列表的聯繫。 */
 48     struct pid_link pids[PIDTYPE_MAX];
 49     struct list_head thread_group;
 50     struct completion *vfork_done; /* 用於vfork() */
 51     int __user *set_child_tid; /* CLONE_CHILD_SETTID */
 52     int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */
 53     unsigned long rt_priority;
 54     cputime_t utime, stime, utimescaled, stimescaled;
 55     unsigned long nvcsw, nivcsw; /* 上下文切換計數 */
 56     struct timespec start_time; /* 單調時間 */
 57     struct timespec real_start_time; /* 啓動以來的時間 */
 58 /* 內存管理器失效和頁交換信息,這個有一點爭論。它既能夠看做是特定於內存管理器的,
 59 也能夠看做是特定於線程的 */
 60     unsigned long min_flt, maj_flt;
 61     cputime_t it_prof_expires, it_virt_expires;
 62     unsigned long long it_sched_expires;
 63     struct list_head cpu_timers[3];
 64 /* 進程身份憑據 */
 65     uid_t uid,euid,suid,fsuid;
 66     gid_t gid,egid,sgid,fsgid;
 67     struct group_info *group_info;
 68     kernel_cap_t cap_effective, cap_inheritable, cap_permitted;
 69     unsigned keep_capabilities:1;
 70     struct user_struct *user;
 71     char comm[TASK_COMM_LEN]; /* 除去路徑後的可執行文件名稱-用[gs]et_task_comm訪問(其中用task_lock()鎖定它)-一般由flush_old_exec初始化 */
 72 /* 文件系統信息 */
 73     int link_count, total_link_count;
 74 /* ipc相關 */
 75     struct sysv_sem sysvsem;
 76 /* 當前進程特定於CPU的狀態信息 */
 77     struct thread_struct thread;
 78 /* 文件系統信息 */
 79     struct fs_struct *fs;
 80 /* 打開文件信息 */
 81     struct files_struct *files;
 82 /* 命名空間 */
 83     struct nsproxy *nsproxy;
 84 /* 信號處理程序 */
 85     struct signal_struct *signal;
 86     struct sighand_struct *sighand;
 87     sigset_t blocked, real_blocked;
 88     sigset_t saved_sigmask; /* 用TIF_RESTORE_SIGMASK恢復 */
 89     struct sigpending pending;
 90     unsigned long sas_ss_sp;
 91     size_t sas_ss_size;
 92     int (*notifier)(void *priv);
 93     void *notifier_data;
 94     sigset_t *notifier_mask;
 95 #ifdef CONFIG_SECURITY
 96     void *security;
 97 #endif
 98 /* 線程組跟蹤 */
 99     u32 parent_exec_id;
100     u32 self_exec_id;
101 /* 日誌文件系統信息 */
102     void *journal_info;
103 /* 虛擬內存狀態 */
104     struct reclaim_state *reclaim_state;
105     struct backing_dev_info *backing_dev_info;
106     struct io_context *io_context;
107     unsigned long ptrace_message;
108     siginfo_t *last_siginfo; /* 由ptrace使用。*/
109 ...
110 };
task_struct

task_struct結構體的內容能夠分解爲各個部分,每一個部分表示進程的一個方面。

  • 狀態和執行信息;
  • 有關已經分配的虛擬內存信息;
  • 進程身份憑據;
  • 使用的文件信息;
  • 線程信息記錄該進程特定於CPU的運行時間數據(與硬件無關);
  • 與其餘應用程序協做時所需的進程間通訊相關信息;
  • 該進程所用的信號處理程序,用於響應信號的到來。

對於進程管理,task_struct中state指定了當前狀態(TASK_RUNNING運行;TASK_INTERRUPTIBLE等待某事件/資源的睡眠狀態;TASK_UNINTERRUPTIBLE年內和指示停用的睡眠狀態;TASK_STOPPED特地中止運行(多用於調試);TASK_TRACED(用於調試區分常規進程);EXIT_ZOMBIE殭屍狀態;EXIT_DEAD指wait系統調用已發出)。

此外,Linux提供資源限制(resource limit)機制,對進程使用系統資源施加某些限制。在task_struct中反應在rlim數組上,系統調用setrlimit來增減當前限制。rlim數組中的位置標識了受限制資源的類型,這也是內核須要定義預處理器常數,將資源與位置關聯起來的緣由。具體代碼以及不一樣硬件上的值的設置手冊上有詳細描述。init進程的限制在系統啓動時生效, 定義在include/asm-generic-resource.h中的INIT_RLIMITS。

一、進程類型

典型的UNIX進程包括:二進制代碼組成的應用程序、單線程、分配給應用程序的一組資源。新進程使用fork和exec系統調用產生。

  • fork生成當前進程一個相同副本,該副本稱爲子進程,子進程複製全部父進程的資源,父子進程相互獨立。
  • exec從一個可執行文件加載另外一個應用程序,來替代當前運行的程序。(不建立新進程,必須首先使用fork複製一箇舊進程,而後調用exec在系統上建立另外一個應用程序)

除此之外,Linux還提供了clone系統調用,用於實現線程,但僅僅系統調用還不足以作到,還須要用戶空間庫配合實現。

  • clone工做原理基本與fork相同,但新進程不獨立於父進程,而能夠指定與父進程共享某些資源。

二、命名空間

命名空間提供了虛擬化的一種輕量級形式,使得咱們能夠從不一樣方面查看運行系統的全局屬性。

傳統上,在Linux以及其它衍生的UNIX變體中,許多資源是全局管理的。系統中全部進程都經過PID標識,內核必須管理一個全局的PID列表,用戶ID的管理方式相似,經過全局惟一的UID標識。全局ID使內核能夠有選擇容許或拒絕某些特權,但不能阻止若干個用戶能看到彼此。若是Web主機打算向用戶提供計算機的所有訪問權限,傳統意義上須要爲每一個用戶提供一臺物理機,使用KVM或VMWare時資源分配作得不是很是好。

對於計算機的各個用戶都須要創建獨立內核,和一份徹底安裝好的配套用戶層應用這個問題,命名空間提供了一種不一樣的解決方案。虛擬化系統中,一臺物理計算機能夠運行多個內核,多是並行的多個不一樣的操做系統,命名空間只使用一個內核在一臺物理機上運做,將前述的全部資源經過命名空間抽象,使得能夠將一組進程放置到容器中,各個容器彼此隔離。

 圖3 命名空間按層次關聯圖

3描述了命名空間能夠組織爲層次關係。一個命名空間是父命名空間,衍生了兩個子命名空間,子命名空間中各進程擁有本身的PID號。雖然子容器不瞭解系統中其餘容器,但父容器知道子命名空間的存在,也能夠看到其中執行的全部進程,所以自子容器中的進程能夠映射到父容器中,獲取全局中惟一的PID號。若命名空間比較簡單,也能夠設計成非層次的(UTS命名空間,父子命名空間沒有聯繫)。

新的命名空間建立方式有兩種:fork或clone系統調用建立進程時,有特定選項能夠控制是與父進程共享命名空間,仍是新建命名空間;unshare系統調用將進程的某些部分從父進程分離,其中也包括命名空間。

命名空間的實現分爲兩個部分:每一個子系統的命名空間結構;將給定進程關聯到所屬各命名空間的機制。圖4是進程與命名空間之間的聯繫示意圖。

4 進程和命名空間之間的聯繫

子系統的全局屬性封裝到命名空間中,每一個進程關聯到選定的命名該空間。每一個能夠感知命名空間的內核子系統都提供了一個數據結構struct_nsproxy(聚集了指向特定於子系統的命名空間包裝器的指針),將全部經過命名空間形式提供的對象集中起來。

1 struct nsproxy {
2     atomic_t count;
3     struct uts_namespace *uts_ns;        //包含了運行內核的名稱、版本、底層體系結構類型信息
4     struct ipc_namespace *ipc_ns;        //全部進程間通訊(IPC)相關信息
5     struct mnt_namespace *mnt_ns;        //已裝載文件系統視圖
6     struct pid_namespace *pid_ns;        //有關進程的ID信息
7     struct user_namespace *user_ns;        //用於限制每一個用戶資源的使用信息
8     struct net *net_ns;                    //包含全部網絡相關的命名空間參數
9 };
struct nsproxy

每一個命名空間都提供了相應的標誌用於fork創建一個新的命名空間。由於在每一個進程關聯到自身命名空間時,使用了指針,因此多個進程能夠共享一組子命名空間,修改給定的命名空間,對全部屬於該命名空間的進程都是可見的。

  • UTS(UNIX Timesharing System)命名空間

它存儲了系統的名稱(Linux...)、內核發佈版本、機器名等。使用uname工具能夠取得這些屬性的當前值。它幾乎不須要特別處理,由於它只須要簡單量,沒有層次組織。全部相關信息都聚集到結構uts_namespace中。

1 struct uts_namespace {
2     struct kref kref;        //嵌入的引用計數器,用於跟蹤內核有多少的地方使用了該命名空間實例
3     struct new_utsname name;        //命名空間所提供的屬性信息
4 };
struct uts_namespace

內核經過copy_utsname函數建立UTS命名空間,在讀取或設置UTS屬性值時,內核會保證老是操做特定於當前進程的uts_namespace實例,在當前進程修改UTS屬性不會反映到父進程,而父進程的修改也不會傳播到子進程。

  • 用戶命名空間

用戶命名空間維護了一些統計數據(如進程和打開文件數目),它在數據結構管理方面相似於UTS,在要求建立新的用戶命名空間時,生成當前用戶命名空間的一份副本,並關聯到當前進程nsproxy實例。

1 struct user_namespace {
2     struct kref kref;        //嵌入的計數器
3     struct hlist_head uidhash_table[UIDHASH_SZ];        //訪問各個實例列表
4     struct user_struct *root_user;        //負責記錄其資源消耗
5 };
struct user_namespace

 每一個用戶命名空間對其用戶資源使用的統計,與其餘命名空間徹底無關,對root用戶的統計也是如此。這是由於在克隆一個用戶命名空間時,爲當前用戶和root都建立了新的user_struct實例。

三、進程ID號

 UNIX進程會分配一個ID號(簡稱PID)做爲其命名空間中惟一的標識。ID有的多類型:

  • 進程處於某個線程組時,擁有線程組ID(TGID)(若進程沒有使用線程,則PID與TGID相同);
  • 獨立進程能夠合併成進程組,進程組成員的task_struct的pgrp屬性值相同(爲進程組組長PID)(用管道鏈接的進程便包含在同一個進程組中);
  • 幾個進程組能夠合併成一個會話,會話中全部進程都有會話ID(SID),保存在task_struct的session中。

命名空間增長了PID管理的複雜性。PID命名空間按層次組織,所以必須區分局部ID和全局ID。

  • 全局ID是在內核自己和初始命名空間中惟一的ID號,對每一個ID類型,都有一個全局ID,保證在整個系統中惟一;
  • 局部ID屬於某特定命名空間,不具有全局有效性。在所屬的命名空間內部有效,所以類型相同、值也相同的ID可能出如今不一樣的命名空間中。

PID分配器(pid allocator)用於加速新ID的分配(此處ID是廣義的包括TGID,SID等)。內核提供輔助函數,實現經過ID及其類型查找進程的task_struct的功能,以及將ID的內核表示形式和用戶空間可見的數值進行轉換的功能。

PID命名空間的表示方式以及含義:

1 struct pid_namespace {
2 ...
3     struct task_struct *child_reaper;    //每一個PID命名空間都具備一個進程,其發揮的做用至關於全局的init進程。child_reaper保存了指向該進程的task_struct的指針。
4 ...
5     int level;        //表示當前命名空間在命名空間層次結構中的深度,初始命名空間的level爲0。
6     struct pid_namespace *parent;        //指向父命名空間的指針
7 };
pid_namespace

PID的管理圍繞struct_pid和struct_upid展開,struct pid是內核對PID的內部表示,而struct upid則表示特定的命名空間中可見的信息。

1 struct upid {
2     int nr;        //ID的數值
3     struct pid_namespace *ns;        //指向該ID所屬的命名空間的指針
4     struct hlist_node pid_chain;        //將全部的upid連接在一塊兒的散鏈表
5 };
struct upid
1 truct pid
2 {
3     atomic_t count;            //引用計數器
4 /* 使用該pid的進程的列表 */
5     struct hlist_head tasks[PIDTYPE_MAX];     //每一項對應一個id類型,做爲散列表頭。對於其中的每項,由於一個ID可能用於幾個進程,全部共享同一給定ID的task_struct實例都經過該列表鏈接
6     int level;     //表示能夠看到該進程的命名空間的數目
7     struct upid numbers[1];        //upid實例數組,每一個數組項都對應於一個命名空間
8 };
struct pid
1 enum pid_type
2 {
3     PIDTYPE_PID,
4     PIDTYPE_PGID,
5     PIDTYPE_SID,
6     PIDTYPE_MAX        //ID類型的數目
7 };
enum pid_type

枚舉類型中定義的ID類型不包括線程組ID,由於線程組ID無非是線程組組長的PID。5對pid和upid兩個結構的關係進行了概述。對於members數組,形式上只有一個數組項,若是一個進程只包含在全局命名空間中,那麼確實如此。因爲該數組位於結構的末尾,所以只要分配更多的內存空間,便可向數組添加附加的項。

圖5 實現可感知命名空間的ID表示所用的數據結構

 

內核提供了若干輔助函數,用於操做和掃描上述複雜結構,完成如下兩個任務:

  • 給出局部數字id和對應的命名空間,查找此二元組的task_struct;
  • 給出task_struct、id類型、命名空間,取得命名空間局部的數字ID。

此外,內核還負責提供機制來生成惟一PID,具體方法:爲跟蹤已經分配和仍然可用的PID,內核使用一個大的位圖,其中每一個PID由一個比特標識。PID的值可經過對應比特在位圖中的位置計算而來。全部其餘的ID均可以派生自PID。

四、進程關係

完成了ID鏈接關係以後,內核還負責管理創建在UNIX進程建立模型之上的「家族關係」(若是由進程A造成了進程B,則A是父進程,B是子進程;若進程A造成了若干個子進程,則這些子進程之間成爲兄弟關係)。圖6說明了進程家族中的父子關係和兄弟關係,以及task_struct中children和sibling兩個鏈表表頭實現這些關係的方式。

 圖6 進程間家族關係

4、進程管理相關的系統調用

一、進程複製

Linux的進程複製有三種方式:

  • fork,重量級調用,它創建了父進程的完整副本,而後做爲子進程執行。(爲了減小工做量,提升效率,使用了寫時複製技術)
  • vfork,相似於fork,不建立父進程副本。相反,父子進程之間共享數據。(節省了大量CPU時間)vfork設計用於子進程造成後當即執行execve系統調用加載新程序的情形。在子進程退出或開始
    新程序以前,內核保證父進程處於堵塞狀態。
  • clone,產生線程,能夠對父子進程之間的共享、複製進行精確控制。

(1)寫時複製(COW)

寫時複製技術(copy-on-write),用來防止在fork執行時將父進程的全部數據複製到子進程。爲了解決不少狀況下不須要複製父進程信息時,複製父進程副本使用大量內存,耗費很長時間的問題。

fork以後,父子進程的地址空間指向一樣的物理內存頁,此時,物理內存頁處於只讀狀態。若是確實要對內存進行寫入操做,會產生缺頁異常,而後由內核分配內存空間。

2)執行系統調用

fork、vfork和clone系統調用的入口點分別是sys_fork、sys_vfork和sys_clone函數。這些入口函數都調用體系結構無關的do_fork函數,經過clone_flags這個標誌集合區分不一樣入口。區別也能夠戳這裏

1 long do_fork(
2     unsigned long clone_flags,    //標誌集合,指定控制複製過程的屬性
3     unsigned long stack_start,    //用戶狀態下棧的起始地址
4     struct pt_regs *regs,        //指向寄存器集合的指針,以原始形式保存了調用參數
5     unsigned long stack_size,    //用戶狀態下棧的大小,一般設爲0
6     int __user *parent_tidptr,    //指向用戶空間中父進程的PID
7     int __user *child_tidptr         //指向用戶空間中子進程的PID
8
long do_fork

3)do_fork的實現

do_fork的代碼流程圖如圖7所示。

7 do_fork代碼流程圖

子進程生產成功後,內核必須執行收尾操做:

  • 若是設置了CLONE_PID標誌,fork操做可能會建立新的pid命名空間。若是建立了新的命名空間,則須要在新命名空間獲取pid,不然直接獲取局部pid。
  • 若是用了ptrace監控,建立進程後,就向它發送SIGSTOP信號,讓調試器檢查數據。
  • 子進程使用wake_up_new_task喚醒(將它的task_struct添加到調度器隊列,讓它有機會執行)。
  • 若是使用vfork機制,必須啓用子進程的完成機制,子進程的task_struct的vfork_done成員即用於該目的,父進程用wait_for_completion函數在該變量中進入睡眠,直至子進程退出。子進程終止時,內核調用complete(vfork_done)喚醒因該變量睡眠的進程。

4)複製進程

do_fork中大多數工做是由copy_process函數完成的,該函數根據標誌的控制,處理了3個系統調用(fork、vfork和clone)的主要工做。copy_process流程圖如圖8所示。(詳見手冊)

8 copy_process的代碼流程圖

5)建立線程特別問題

用戶空間線程庫使用clone系統調用來生成新線程。該調用支持(上文討論以外的)標誌,對copy_process(及其調用的函數)具備某些特殊影響。

  • CLONE_PARENT_SETTID將生成線程的PID複製到clone調用指定的用戶空間中的某個地址
  • CLONE_CHILD_SETTID首先會將另外一個傳遞到clone的用戶空間指針(child_tidptr)保存在新進程的task_struct中。
  • CLONE_CHILD_CLEARTID首先會在copy_process中將用戶空間指針child_tidptr保存在task_struct中,此次是另外一個不一樣的成員。

上述標誌可用於從用戶空間檢測內核中線程的產生和銷燬。CLONE_CHILD_SETTID和CLONE_PARENT_SETTID用於檢測線程的生成。CLONE_CHILD_CLEARTID用於在線程結束時從內核向用戶空間傳遞信息,在多處理器系統上這些檢測能夠真正地並行執行。

二、內核線程

內核線程是直接由內核自己啓動的進程。內核線程其實是將內核函數委託給獨立的進程,與系統中其餘進程「並行」執行(實際上,也並行於內核自身的執行)。內核線程常常稱之爲(內核)守護進程,它們用於執行下列任務:

  • 週期性地將修改的內存頁與頁來源塊設備同步(例如,使用mmap的文件映射)。
  • 若是內存頁不多使用,則寫入交換區。
  • 管理延時動做(deferred action)。
  • 實現文件系統的事務日誌。

基本上,內核線程有兩種:

  • 線程啓動後一直等待,直至內核請求線程執行某一特定操做。
  • 線程啓動後按週期性間隔運行,檢測特定資源的使用,在用量超出或低於預置的限制值時採起行動。內核使用這類線程用於連續監測任務。

由於內核線程是由內核自身生成的,它有兩個特別之處:

  1. 它們在CPU的管態(supervisor mode)執行,而不是用戶狀態。
  2. 它們只能夠訪問虛擬地址空間的內核部分(高於TASK_SIZE的全部地址),但不能訪問用戶空間。

內核線程能夠用兩種方法實現:

古老的方法:

  • (1)將一個函數傳遞給kernel_thread,內核調用daemonize以轉換爲守護進程,從內核釋放父進程的全部資源。
  • (2)daemonize阻塞信號的接收。
  • (3)將init做爲守護進程的父進程。

備選方案:使用宏kthread_run(參數與kthread_create相同),它會調用kthread_create建立新線程,當即喚醒它。還可使用kthread_create_cpu代替kthread_create建立內核線程,使之綁定到特定的CPU。

三、啓動新程序

1)execve的實現

該系統調用的入口點是體系結構相關的sys_execve函數。該函數很快將其工做委託給系統無關的do_execve例程。do_execve的代碼流程圖如圖9所示。

9 do_execve代碼流程圖

 

do_execve的主要工做:

  • 打開要執行的文件;
  • bprm_init,申請進程空間並初始化,處理若干管理性任務;
  • prepare_binprm,提供父進程相關的值(特別是有效UID和GID),也是用於初始化;
  • search_binary_handler,查找一種適當的二進制格式,用於所要執行的特定文件。

一般,二進制格式處理程序執行下列操做:

  • 釋放原進程的全部資源;
  • 將應用程序映射到虛擬地址空間,參數和環境也映射到虛擬地址空間;
  • 設置進程的指令指針和其餘特定於體系結構的寄存器。

2)解釋二進制格式

Linux內核中,每種二進制格式都表示爲結構體linux_binfmt,都須要用register_binfmt向內核註冊。linux_binfmt結構爲:

1 struct linux_binfmt {
2     struct linux_binfmt * next;
3     struct module *module;
4     int (*load_binary)(struct linux_binprm *, struct pt_regs * regs);
5     int (*load_shlib)(struct file *);
6     int (*core_dump)(long signr, struct pt_regs * regs, struct file * file);
7     unsigned long min_coredump; /* minimal dump size */
8 };
struct linux_binfmt

二進制格式主要接口函數

  • load_binary用於加載普通程序。
  • load_shlib用於加載共享庫,即動態庫。
  • core_dump用於在程序錯誤的狀況下輸出內存轉儲,用於調試分析,以解決問題。

四、退出進程

進程必須用exit系統調用終止,使得內核有機會將該進程使用的資源釋放回系統。該調用的入口點是sys_exit函數,該函數的實現就是將各個引用計數器減1,若是引用計數器歸0而沒有進程再使用對應的結構,那麼將相應的內存區域返還給內存管理模塊。

相關文章
相關標籤/搜索