若是系統只有一個處理器,那麼給定時刻只有一個程序能夠運行。在多處理器系統中,真正並行運行的進程數目取決於物理CPU的數目。內核和處理器創建了多任務的錯覺,是經過以很短的間隔在系統運行的應用程序之間不停切換作到的。由此,如下兩個問題必須由內核解決:除非明確要求,不然應用程序不能彼此干擾;CPU時間必須在各類應用程序之間儘量公平共享(一些程序可能比其餘程序更重要)。本篇博文主要涉及內核共享CPU時間的方法以及如何在進程之間切換(內核爲各進程分配時間,保證切換以後從上次撤銷其資源時執行環境徹底相同)。html
並不是全部進程的重要程度都相同,對於進程,首先比較粗糙的劃分,進程能夠分爲實時進程和非實時進程。node
圖1 經過時間片分配CPU時間調度示意圖linux
圖1是CPU分配時間的一個簡圖。進程運行按時間片調度,分配進程的時間片額與其相對重要性至關。系統中時間的流動對應於圓盤的轉動,重要的進程會比次要的進程獲得更多CPU時間,進程被切換時,全部的CPU寄存器內容和頁表都會被保存,下次該進程恢復執行時,其執行環境能夠徹底恢復。這種簡化模型忽略了一些進程狀態相關的信息,不能使CPU時間利益回報儘量最大化。可是爲調度器的質量確立一種定量標準很是困難。自Linux內核誕生以來,調度器的代碼已經重寫了好幾回。按時間前後順序,主要有O(n)調度器,O(1)調度器和CFS(completely fair scheduler)調度器。詳細區別戳這裏。算法
進程並不老是能夠當即運行,有時候它須要等待來自外部信號源、不受其控制的事件(如文本編輯等待輸入)。在調度器進行進程切換時,必須知道每一個進程的狀態,由於將CPU事件分配給無事可作的進程沒有意義,進程在各個狀態之間的轉換也一樣重要。數組
圖2 進程狀態之間的切換示意圖安全
進程可能存在的狀態有:運行、等待和睡眠。圖2描述了進程的幾種狀態及其轉換。除了圖中所示的幾種狀態之外,還有一種狀態被稱爲殭屍態。網絡
爲了維持系統中現存的各個進程,防止它們與系統其餘部分相互干擾,Linux進程管理結構中還須要兩種進程狀態選項:用戶狀態和核心態。進程一般處於用戶狀態,只能訪問自身的數據,沒法干擾系統中其餘進程。若是進程想要訪問系統數據,則必須切換到核心態,這種訪問必須經由明肯定義的路徑(系統調用)。從用戶狀態進入核心態的第二種方法是經過中斷,此時切換是自動觸發的,處理中斷操做,一般與中斷髮生時執行的程序無關。(系統調用是由用戶應用程序有意調用的,中斷則是不可預測的)session
內核的搶佔調度模型是優先讓優先級高的進程佔用CPU,它創建了一個層次結構,用於判斷哪些進程狀態可由其餘狀態搶佔。數據結構
在內核2.5開發期間,內核搶佔(kernel preemption)選項被添加到內核,它支持緊急狀況下切換到另外一個進程,甚至當前進程處於系統調用也行。內核搶佔能夠減小等待時間,但代價是增長了內核的複雜度,由於搶佔時有許多數據結構須要針對併發訪問進行保護。併發
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中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系統調用產生。
除此之外,Linux還提供了clone系統調用,用於實現線程,但僅僅系統調用還不足以作到,還須要用戶空間庫配合實現。
命名空間提供了虛擬化的一種輕量級形式,使得咱們能夠從不一樣方面查看運行系統的全局屬性。
傳統上,在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 };
每一個命名空間都提供了相應的標誌用於fork創建一個新的命名空間。由於在每一個進程關聯到自身命名空間時,使用了指針,因此多個進程能夠共享一組子命名空間,修改給定的命名空間,對全部屬於該命名空間的進程都是可見的。
它存儲了系統的名稱(Linux...)、內核發佈版本、機器名等。使用uname工具能夠取得這些屬性的當前值。它幾乎不須要特別處理,由於它只須要簡單量,沒有層次組織。全部相關信息都聚集到結構uts_namespace中。
1 struct uts_namespace { 2 struct kref kref; //嵌入的引用計數器,用於跟蹤內核有多少的地方使用了該命名空間實例 3 struct new_utsname name; //命名空間所提供的屬性信息 4 };
內核經過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 };
每一個用戶命名空間對其用戶資源使用的統計,與其餘命名空間徹底無關,對root用戶的統計也是如此。這是由於在克隆一個用戶命名空間時,爲當前用戶和root都建立了新的user_struct實例。
UNIX進程會分配一個ID號(簡稱PID)做爲其命名空間中惟一的標識。ID有的多類型:
命名空間增長了PID管理的複雜性。PID命名空間按層次組織,所以必須區分局部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的管理圍繞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 };
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 };
1 enum pid_type 2 { 3 PIDTYPE_PID, 4 PIDTYPE_PGID, 5 PIDTYPE_SID, 6 PIDTYPE_MAX //ID類型的數目 7 };
枚舉類型中定義的ID類型不包括線程組ID,由於線程組ID無非是線程組組長的PID。圖5對pid和upid兩個結構的關係進行了概述。對於members數組,形式上只有一個數組項,若是一個進程只包含在全局命名空間中,那麼確實如此。因爲該數組位於結構的末尾,所以只要分配更多的內存空間,便可向數組添加附加的項。
圖5 實現可感知命名空間的ID表示所用的數據結構
內核提供了若干輔助函數,用於操做和掃描上述複雜結構,完成如下兩個任務:
此外,內核還負責提供機制來生成惟一PID,具體方法:爲跟蹤已經分配和仍然可用的PID,內核使用一個大的位圖,其中每一個PID由一個比特標識。PID的值可經過對應比特在位圖中的位置計算而來。全部其餘的ID均可以派生自PID。
完成了ID鏈接關係以後,內核還負責管理創建在UNIX進程建立模型之上的「家族關係」(若是由進程A造成了進程B,則A是父進程,B是子進程;若進程A造成了若干個子進程,則這些子進程之間成爲兄弟關係)。圖6說明了進程家族中的父子關係和兄弟關係,以及task_struct中children和sibling兩個鏈表表頭實現這些關係的方式。
圖6 進程間家族關係
Linux的進程複製有三種方式:
(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 )
(3)do_fork的實現
do_fork的代碼流程圖如圖7所示。
圖7 do_fork代碼流程圖
子進程生產成功後,內核必須執行收尾操做:
(4)複製進程
在do_fork中大多數工做是由copy_process函數完成的,該函數根據標誌的控制,處理了3個系統調用(fork、vfork和clone)的主要工做。copy_process流程圖如圖8所示。(詳見手冊)
圖8 copy_process的代碼流程圖
(5)建立線程特別問題
用戶空間線程庫使用clone系統調用來生成新線程。該調用支持(上文討論以外的)標誌,對copy_process(及其調用的函數)具備某些特殊影響。
上述標誌可用於從用戶空間檢測內核中線程的產生和銷燬。CLONE_CHILD_SETTID和CLONE_PARENT_SETTID用於檢測線程的生成。CLONE_CHILD_CLEARTID用於在線程結束時從內核向用戶空間傳遞信息,在多處理器系統上這些檢測能夠真正地並行執行。
內核線程是直接由內核自己啓動的進程。內核線程其實是將內核函數委託給獨立的進程,與系統中其餘進程「並行」執行(實際上,也並行於內核自身的執行)。內核線程常常稱之爲(內核)守護進程,它們用於執行下列任務:
基本上,內核線程有兩種:
由於內核線程是由內核自身生成的,它有兩個特別之處:
內核線程能夠用兩種方法實現:
古老的方法:
備選方案:使用宏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的主要工做:
一般,二進制格式處理程序執行下列操做:
(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 };
二進制格式主要接口函數
進程必須用exit系統調用終止,使得內核有機會將該進程使用的資源釋放回系統。該調用的入口點是sys_exit函數,該函數的實現就是將各個引用計數器減1,若是引用計數器歸0而沒有進程再使用對應的結構,那麼將相應的內存區域返還給內存管理模塊。