1.進程css
1.1進程的概念node
計算機上全部可運行的軟件,一般也包括操做系統,被組織成若干順序進程(sequential process),簡稱進程(process)。linux
一個進程就是一個正在執行程序的實例,包括程序設計器、寄存器和變量的當前值。一個進程是某種類型的一種活動,它有程序、輸入、輸出以及狀態。單個處理器能夠被若干進程共享,它使用某種調度算法決定什麼時候中止一個進程的工做,並轉而爲了另外一個進程服務功能。算法
1.2進程的特徵數組
Linux系統中主要的活動實體就是進程。服務器
每一個進程執行一段獨立的程序而且在進程初始化的時候擁有一個獨立的控制線程。換句話說,每個進程都擁有一個獨立的程序計數器,用這個這個程序計數器能夠追蹤下一條將要被執行的指令。session
全部的進程都被放在一個叫作進程控制塊(PCB),的數據結構中,能夠理解爲進程屬性的集合,該控制塊由操做系統建立和管理。每一個進程在內核中都有一個進程控制塊來維護進程相關的信息,Linux內核的進程控制塊是(task_struct)結構體。數據結構
2.2進程的標識符(PID)多線程
pid_t pid; //內核中用以標識進程的id pid_t tgid; //用來實現線程機制
struct pid { atomic_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; };
每一個進程都有一個惟一的標識符(PID),內核經過這個標識符來識別不一樣的進程,同時,進程標識符(PID)也是內核提供給用戶程序的接口,用戶程序經過PID對進程發號施令。併發
PID是32位的無符號整數,它被順序編號:新建立進程的PID一般是前一個進程的PID加1。然而,爲了與16位硬件平臺的傳統Linux系統保持兼容,在Linux上容許的最大PID號是32767,當內核在系統中建立第32768個進程時,就必須從新開始使用已閒置的PID號。在64位系統中,PID可擴展到4194303。
2.3Linux系統組織進程
task_struct
結構體來描述一個進程的全部信息,結構體被定義在 include/linux/sched.h
中。struct task_struct { volatile long state; //進程狀態 void *stack; //內存指針 atomic_t usage; unsigned int flags; //進程標號(進程名字) unsigned int ptrace; int lock_depth; //BLK 鎖深度 #ifdef CONFIG_SMP #ifdef __ARCH_WANT_UNLOCKED_CTXSW //配置多核多線程 int oncpu; #endif #endif int prio, static_prio, normal_prio; //進程的優先級 unsigned int rt_priority; //實時進程的優先級 const struct sched_class *sched_class; //調度器的指針 struct sched_entity se; //調度器 實例化的對象 struct sched_rt_entity rt; //實時 調度器的一個對象 #ifdef CONFIG_PREEMPT_NOTIFIERS //配置搶佔通知器 /* struct preempt_notifier列表 */ struct hlist_head preempt_notifiers; #endif /*fpu_count 裏面內容是若是一個浮點運算器被使用,它記錄着連續的上下文切換的次數,若是fpu_Count超過一個 臨界值,不怎麼工做的FPU會火力全開以致於當fpu_count超過 256次後才變得閒置下來,爲了解決這個問題,FPU 僅僅使用一段時間 */ unsigned char fpu_counter; //定義 fpu_count #ifdef CONFIG_BLK_DEV_IO_TRACE //配置 BLK 鎖開發版的輸入輸出跟蹤器 unsigned int btrace_seq; #endif unsigned int policy; cpumask_t cpus_allowed; #ifdef CONFIG_TREE_PREEMPT_RCU //配置搶佔樹,搶佔的結構體的讀寫機制,即RCU機制。 int rcu_read_lock_nesting; char rcu_read_unlock_special; struct rcu_node *rcu_blocked_node; struct list_head rcu_node_entry; #endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */ #if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT) struct sched_info sched_info; //調度器的狀態 #endif struct list_head tasks; struct plist_node pushable_tasks; struct mm_struct *mm, *active_mm; //虛擬地址空間的結構體 //進程退出時getpid 就獲取status就是它。 int exit_state; //task 狀態 ,正常退出狀態 int exit_code, exit_signal; //退出信號 int pdeath_signal; //當成爲孤兒進程時發送信號 unsigned int personality; //代表進程的狀態 unsigned did_exec:1; unsigned in_execve:1; //第一個表已經調過了exec族函數,已經發生了進程的程序替換 第二個表明該進程正在調用execve函數 第三個 正在等待i/o設備 第四個 表示當fork生成子進程時,是否恢復了進程的默認優先級 unsigned in_iowait:1; /* 在分叉時恢復默認優先級/策略*/ unsigned sched_reset_on_fork:1; pid_t pid; pid_t tgid; #ifdef CONFIG_CC_STACKPROTECTOR //配置堆棧保護措施 unsigned long stack_canary; //canary值 保護編譯器 防止堆棧溢出 致使的返回地址被填充 #endif struct task_struct *real_parent; struct task_struct *parent; struct list_head children; //子節點和兄弟節點的定義 struct list_head sibling; struct task_struct *group_leader; //線程組的頭結點 struct list_head ptraced; //跟蹤器的頭結點,跟蹤器 跟蹤 進程的邏輯流,即PC指令流 struct list_head ptrace_entry; struct pid_link pids[PIDTYPE_MAX]; //定義 PID_LINK 結構體用它經過PID在哈希散列表中查找相應的task_struct struct list_head thread_group; //用來保存線程組的PID struct completion *vfork_done; int __user *set_child_tid; //指向用戶創造創立的線程的TID號 int __user *clear_child_tid; //指向被清除的線程的TID號 cputime_t utime, stime, utimescaled, stimescaled; cputime_t gtime; cputime_t prev_utime, prev_stime; unsigned long nvcsw, nivcsw; //上下文切換的次數 struct timespec start_time; struct timespec real_start_time; unsigned long min_flt, maj_flt; struct task_cputime cputime_expires; struct list_head cpu_timers[3]; const struct cred *real_cred; const struct cred *cred; struct mutex cred_guard_mutex; struct cred *replacement_session_keyring; char comm[TASK_COMM_LEN]; //文件系統信息 int link_count, total_link_count; #ifdef CONFIG_SYSVIPC //配置進程的通訊機制 struct sysv_sem sysvsem; #endif #ifdef CONFIG_DETECT_HUNG_TASK unsigned long last_switch_count; #endif struct thread_struct thread; //CPU特殊狀態的測試,線程結構體 struct fs_struct *fs; //fs 指向一個文件系統信息結構體,該結構體有文件系統的信息 //指向記錄打開文件信息的 結構體 struct files_struct *files; //命名空間的定義 struct nsproxy *nsproxy; //配置進程的信號處理 struct signal_struct *signal; //如下是普通訊號部分 struct sighand_struct *sighand; //這個指向 handler表 sigset_t blocked, real_blocked; //這個表示進程的屏蔽字 sigset_t saved_sigmask; struct sigpending pending; //pending表 unsigned long sas_ss_sp; // 如下是實時信號部分 size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; struct audit_context *audit_context; #ifdef CONFIG_AUDITSYSCALL // 配置系統調用 uid_t loginuid; unsigned int sessionid; #endif seccomp_t seccomp; #ifdef CONFIG_UTRACE struct utrace *utrace; unsigned long utrace_flags; #endif u32 parent_exec_id; u32 self_exec_id; /* 配置器保護措施配置 */ spinlock_t alloc_lock; #ifdef CONFIG_GENERIC_HARDIRQS struct irqaction *irqaction; #endif spinlock_t pi_lock; #ifdef CONFIG_RT_MUTEXES // 互斥的配置 struct plist_head pi_waiters; struct rt_mutex_waiter *pi_blocked_on; #endif #ifdef CONFIG_LOCKDEP // 死鎖模塊的配置 # define MAX_LOCK_DEPTH 48UL u64 curr_chain_key; int lockdep_depth; unsigned int lockdep_recursion; struct held_lock held_locks[MAX_LOCK_DEPTH]; gfp_t lockdep_reclaim_gfp; #endif // 文件系統的日誌信息 void *journal_info; struct bio *bio_list, **bio_tail; //VM 虛擬機的狀態 struct reclaim_state *reclaim_state; struct backing_dev_info *backing_dev_info; struct io_context *io_context; unsigned long ptrace_message; siginfo_t *last_siginfo; struct task_io_accounting ioac; #ifdef CONFIG_CPUSETS nodemask_t mems_allowed; //定義一個結構體 標誌 內存是否容許訪問 保護配置器的鎖的 #ifndef __GENKSYMS__ unsigned short cpuset_mem_spread_rotor; unsigned short cpuset_slab_spread_rotor; int mems_allowed_change_disable; #else int cpuset_mem_spread_rotor; int cpuset_slab_spread_rotor; #endif #endif #ifdef CONFIG_CGROUPS // 配置控制組信息 struct css_set *cgroups; struct list_head cg_list; #endif #endif };
3.進程的狀態
3.1進程的六種狀態
#define TASK_RUNNING
1.表示進程要麼正在執行,要麼正在準備執行。
#define TASK_INTERRUPTIBLE
2.表示進程被阻塞(睡眠),只有當某個條件是TRUE時,其狀態相應的設置爲 TASK_RUNNING。它能夠被信號和wake_up喚醒。
#define TASK_UNINTERRUPTIBLE
3.表示進程被阻塞(睡眠),只有當某個條件是TRUE時,其狀態相應的設置爲 TASK_RUNNING。它只能被wake_up喚醒。
#define TASK_STOPPED
4.表示進程被中止執行。
#define TASK_TRACED
5.表示進程被debugger等進程監視着。
#define EXIT_ZOMBIE
6.表示進程的執行被終止,可是其父進程尚未使用wait()等系統調用來獲知它的終止信息。
#define EXIT_DEAD
7.表示進程的最終狀態。
3.2狀態轉換圖
4.進程的調度
4.1Linux下的O(1)調度算法
4.1.1O(1)調度器
在O(1)調度中,要問最重要的數據結構是運行隊列。運行隊列描繪了進程隊列的結構,在內核源碼中用runqueue結構體表示。
struct runqueue { unsigned long nr_running; task_t *curr; prio_array_t *active,*expired,arrays[2]; };
4.1.2優先級數組
O(1)算法的另外一個核心數據結構即爲prio_array結構體。該結構體中有一個用來表示進程動態優先級的數組queue,它包含了每一種優先級進程所造成的鏈表。
#define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40) typedef struct prio_array prio_array_t; struct prio_array { unsigned int nr_active; unsigned long bitmap[BITMAP_SIZE]; struct list_head queue[MAX_PRIO]; };
4.1.3靜態優先級和動態優先級
進程有兩個優先級,一個是靜態優先級,一個是動態優先級.靜態優先級是用來計算進程運行的時間片長度的,動態優先級是在調度器進行調度時用到的,調度器每次都選取動態優先級最高的進程運行.
靜態優先級的計算: nice值和靜態優先級之間的關係是:靜態優先級=100+nice+20 而nice值的範圍是-20~19,因此普通進程的靜態優先級的範圍是100~139
動態優先級的計算: 動態優先級=max(100 , min(靜態優先級 – bonus + 5 , 139))
4.1.4時間片
O(1)算法採用過時進程數組和活躍進程數組解決以往調度算法所帶來的O(n)複雜度問題。過時數組中的進程都已經用完了時間片,而活躍數組的進程還擁有時間片。當一個進程用完本身的時間片後,它就被移動到過時進程數組中,同時這個過時進程在被移動以前就已經計算好了新的時間片。能夠看到O(1)調度算法是採用分散計算時間片的方法,並不像以往算法中集中爲全部可運行進程從新計算時間片。當活躍進程數組中沒有任何進程時,說明此時全部可運行的進程都用完了本身的時間片。那麼此時只須要交換一下兩個數組便可將過時進程切換爲活躍進程,進而繼續被調度程序所調度。兩個數組之間的切換其實就是指針之間的交換,所以花費的時間是恆定的。
struct prop_array *array = rq->active; if (array->nr_active != 0) { rq->active = rq->expired; rq->expired = array; }
上面的代碼說明了兩個數組之間的交換,經過分散計算時間片、交換過時和活躍兩個進程集合的方法可使得O(1)算法在恆定的時間內爲每一個進程從新計算好時間片。
進程運行的時間片長度的計算 靜態優先級<120,基本時間片=max((140-靜態優先級)*20, MIN_TIMESLICE) 靜態優先級>=120,基本時間片=max((140-靜態優先級)*5, MIN_TIMESLICE)
4.1.5調度算法
在每次進程切換時,內核依次掃描就緒隊列上的每個進程,計算每一個進程的優先級,再選擇出優先級最高的進程來運行;儘管這個算法理解簡單,可是它花費在選擇優先級最高進程上的時間卻不容忽視。系統中可運行的進程越多,花費的時間就越大,時間複雜度爲O(n)。
//僞代碼 for (系統中的每一個進程) { 從新計算時間片; 從新計算優先級; }
4.2CFS調度器
CFS 背後的主要想法是維護爲任務提供處理器時間方面的平衡(公平性)。這意味着應給進程分配至關數量的處理器。分給某個任務的時間失去平衡時(意味着一個或多個任務相對於其餘任務而言未被給予至關數量的時間),應給失去平衡的任務分配時間,讓其執行。
要實現平衡,CFS 在叫作虛擬運行時的地方維持提供給某個任務的時間量。任務的虛擬運行時越小, 意味着任務被容許訪問服務器的時間越短 — 其對處理器的需求越高。CFS 還包含睡眠公平概念以便確保那些目前沒有運行的 任務(例如,等待 I/O)在其最終須要時得到至關份額的處理器。
可是與以前的 Linux 調度器不一樣,它沒有將任務維護在運行隊列中,CFS 維護了一個以時間爲順序的紅黑樹(以下圖)。 紅黑樹是一個樹,具備不少有趣、有用的屬性。首先,它是自平衡的,這意味着樹上沒有路徑比任何其餘路徑長兩倍以上。 第二,樹上的運行按 O(log n) 時間發生(其中 n 是樹中節點的數量)。這意味着能夠快速高效地插入或刪除任務。
任務存儲在以時間爲順序的紅黑樹中(由 sched_entity
對象表示),對處理器需求最多的任務 (最低虛擬運行時)存儲在樹的左側,處理器需求最少的任務(最高虛擬運行時)存儲在樹的右側。 爲了公平,調度器而後選取紅黑樹最左端的節點調度爲下一個以便保持公平性。任務經過將其運行時間添加到虛擬運行時, 說明其佔用 CPU 的時間,而後若是可運行,再插回到樹中。這樣,樹左側的任務就被給予時間運行了,樹的內容從右側遷移到左側以保持公平。 所以,每一個可運行的任務都會追趕其餘任務以維持整個可運行任務集合的執行平衡。
task_struct任務結構和紅黑樹的結構層次
樹的根經過 rb_root
元素經過 cfs_rq
結構(在 ./kernel/sched.c 中)引用。紅黑樹的葉子不包含信息,可是內部節點表明一個或多個可運行的任務。紅黑樹的每一個節點都由 rb_node
表示,它只包含子引用和父對象的顏色。 rb_node
包含在 sched_entity
結構中,該結構包含 rb_node
引用、負載權重以及各類統計數據。最重要的是,sched_entity
包含 vruntime
(64 位字段),它表示任務運行的時間量,並做爲紅黑樹的索引。 最後,task_struct
位於頂端,它完整地描述任務幷包含 sched_entity
結構。
就 CFS 部分而言,調度函數很是簡單。 在 ./kernel/sched.c 中,有通用 schedule()
函數,它會先搶佔當前運行任務(除非它經過 yield()
代碼先搶佔本身)。注意 CFS 沒有真正的時間切片概念用於搶佔,由於搶佔時間是可變的。 當前運行任務(如今被搶佔的任務)經過對 put_prev_task
調用(經過調度類)返回到紅黑樹。 當schedule
函數開始肯定下一個要調度的任務時,它會調用 pick_next_task
函數。此函數也是通用的(在 ./kernel/sched.c 中),但它會經過調度器類調用 CFS 調度器。 CFS 中的 pick_next_task
函數能夠在 ./kernel/sched_fair.c(稱爲 pick_next_task_fair()
)中找到。 此函數只是從紅黑樹中獲取最左端的任務並返回相關 sched_entity
。經過此引用,一個簡單的 task_of()
調用肯定返回的 task_struct
引用。通用調度器最後爲此任務提供處理器。
徹底公平運行隊列:描述運行在同一個cpu上的處於TASK_RUNNING狀態的普通進程的各類運行信息
struct cfs_rq { struct load_weight load; //運行隊列總的進程權重 unsigned int nr_running, h_nr_running; //進程的個數 u64 exec_clock; //運行的時鐘 u64 min_vruntime; //該cpu運行隊列的vruntime推動值, 通常是紅黑樹中最小的vruntime值 struct rb_root tasks_timeline; //紅黑樹的根結點 struct rb_node *rb_leftmost; //指向vruntime值最小的結點 //當前運行進程, 下一個將要調度的進程, 立刻要搶佔的進程, struct sched_entity *curr, *next, *last, *skip; struct rq *rq; //系統中有普通進程的運行隊列, 實時進程的運行隊列, 這些隊列都包含在rq運行隊列中 ... };
調度實體:記錄一個進程的運行狀態信息
struct sched_entity { struct load_weight load; //進程的權重 struct rb_node run_node; //運行隊列中的紅黑樹結點 struct list_head group_node; //與組調度有關 unsigned int on_rq; //進程如今是否處於TASK_RUNNING狀態 u64 exec_start; //一個調度tick的開始時間 u64 sum_exec_runtime; //進程從出生開始, 已經運行的實際時間 u64 vruntime; //虛擬運行時間 u64 prev_sum_exec_runtime; //本次調度以前, 進程已經運行的實際時間 struct sched_entity *parent; //組調度中的父進程 struct cfs_rq *cfs_rq; //進程此時在哪一個運行隊列中 };
建立進程,設置新進程的vruntime值,task_fork_fair()函數部分代碼:
static void task_fork_fair(struct task_struct *p) { struct cfs_rq *cfs_rq; struct sched_entity *se = &p->se, *curr; int this_cpu = smp_processor_id(); struct rq *rq = this_rq(); unsigned long flags; raw_spin_lock_irqsave(&rq->lock, flags); update_rq_clock(rq); cfs_rq = task_cfs_rq(current); curr = cfs_rq->curr; rcu_read_lock(); __set_task_cpu(p, this_cpu); //設置新進程在哪一個cpu上運行 rcu_read_unlock(); update_curr(cfs_rq); //更新當前進程的vruntime值 if (curr) se->vruntime = curr->vruntime; //先以父進程的vruntime爲基礎 place_entity(cfs_rq, se, 1); //設置新進程的vruntime值, 1表示是新進程 if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) { //sysctl_sched_child_runs_first值表示是否設置了讓子進程先運行 swap(curr->vruntime, se->vruntime); //當子進程的vruntime值大於父進程的vruntime時, 交換兩個進程的vruntime值 resched_task(rq->curr); //設置從新調度標誌TIF_NEED_RESCHED } se->vruntime -= cfs_rq->min_vruntime; //防止新進程運行時是在其餘cpu上運行的, 這樣在加入另外一個cfs_rq時再加上另外一個cfs_rq隊列的min_vruntime值便可(具體能夠看enqueue_entity函數) raw_spin_unlock_irqrestore(&rq->lock, flags); }
進程的主動調度函數是schedule():
asmlinkage void __sched schedule(void) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq *rq; int cpu; need_resched: preempt_disable(); //在這裏面被搶佔可能出現問題,先禁止它! cpu = smp_processor_id(); rq = cpu_rq(cpu); rcu_qsctr_inc(cpu); prev = rq->curr; switch_count = &prev->nivcsw; release_kernel_lock(prev); need_resched_nonpreemptible: spin_lock_irq(&rq->lock); update_rq_clock(rq); clear_tsk_need_resched(prev); //清除須要調度的位 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { if (unlikely(signal_pending_state(prev->state, prev))) prev->state = TASK_RUNNING; else deactivate_task(rq, prev, 1); //出隊, 此處主要是把prev->on_rq賦值爲0, 由於當前進程原本就沒在紅黑樹中. on_rq爲0後, 後面的put_prev_task函數就不會把當前進程加入紅黑樹了 switch_count = &prev->nvcsw; } if (unlikely(!rq->nr_running)) idle_balance(cpu, rq); prev->sched_class->put_prev_task(rq, prev); //把當前進程加入紅黑樹中 next = pick_next_task(rq, prev); //從紅黑樹中挑選出下一個要運行的進程, 並將其設置爲當前進程 if (likely(prev != next)) { sched_info_switch(prev, next); rq->nr_switches++; rq->curr = next; ++*switch_count; //完成進程切換 context_switch(rq, prev, next); cpu = smp_processor_id(); rq = cpu_rq(cpu); } else spin_unlock_irq(&rq->lock); if (unlikely(reacquire_kernel_lock(current) < 0)) goto need_resched_nonpreemptible; preempt_enable_no_resched(); //這裏新進程也可能有TIF_NEED_RESCHED標誌,若是新進程也須要調度則再調度一次 if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) goto need_resched; }
5.對進程的見解
進程是操做系統最核心的概念,這是對正在運行的程序的一個抽象。全部操做系統其餘的全部內容都是圍繞着進程的概念展開的。進程是操做系統的核心之一,對於 Linux 技術而言,唯一不變的就是永恆的變化,不斷追求更優更有效率的算法,讓計算機給人們提供更好的服務。咱們從進程模型中學習到的理論知識與算法的更新模式等,都應與現實結合,好好實踐。