進程和線程的區(qū)別(轉)

文/tangsl(簡書作者)

原文鏈接:http://m.itdecent.cn/p/2b993a4b913e

著作權歸作者所有,轉載請聯(lián)系作者獲得授權,并標注“簡書作者”。

又來到了一個老生常談的問題,應用層軟件開發(fā)的程序員要不要了解和深入學習操作系統(tǒng)呢? 今天就這個問題開始,來談談操作系統(tǒng)中可以說是最重要的一個概念--進程。

操作系統(tǒng)最主要的兩個職能是管理各種資源和為應用程序提供系統(tǒng)調用接口。這其中關鍵的部分是,cpu到進程的抽象,物理內存到地址空間(虛擬內存)的抽象,磁盤到文件的抽象,而其中后兩部分以進程為基礎,所以嘛,咱重點來討論進程,以及與進程密切相關的線程。

.先說說概念

進程(process)

狹義的定義:進程就是一段程序的執(zhí)行過程。

廣義定義:進程是一個具有一定獨立功能的程序關于某次數(shù)據(jù)集合的一次運行活動,它是操作系統(tǒng)分配資源的基本單元。

簡單來講進程的概念主要有兩點:第一,進程是一個實體。每一個進程都有它自己的地址空間,一般情況下,包括文本區(qū)域(text region)、數(shù)據(jù)區(qū)域(data region)和堆棧(stack region)。文本區(qū)域存儲處理器執(zhí)行的代碼;數(shù)據(jù)區(qū)域存儲變量和進程執(zhí)行期間使用的動態(tài)分配的內存;堆棧區(qū)域存儲著活動過程中調用的指令和本地變量。第二,進程是一個“執(zhí)行中的程序”。程序是一個沒有生命的實體,只有處理器賦予程序生命時,它才能成為一個活動的實體,我們稱其為進程。

進程狀態(tài):進程有三個狀態(tài),就緒,運行和阻塞。就緒狀態(tài)其實就是獲取了除cpu外的所有資源,只要處理器分配資源馬上就可以運行。運行態(tài)就是獲取了處理器分配的資源,程序開始執(zhí)行,阻塞態(tài),當程序條件不夠時,需要等待條件滿足時候才能執(zhí)行,如等待I/O操作的時候,此刻的狀態(tài)就叫阻塞態(tài)。

說說程序,程序是指令和數(shù)據(jù)的有序集合,其本身沒有任何運動的含義,是一個靜態(tài)的概念,而進程則是在處理機上的一次執(zhí)行過程,它是一個動態(tài)的概念。進程是包含程序的,進程的執(zhí)行離不開程序,進程中的文本區(qū)域就是代碼區(qū),也就是程序。

線程(thread)

通常在一個進程中可以包含若干個線程,當然一個進程中至少有一個線程,不然沒有存在的意義。線程可以利用進程所擁有的資源,在引入線程的操作系統(tǒng)中,通常都是把進程作為分配資源的基本單位,而把線程作為獨立運行和獨立調度的基本單位,由于線程比進程更小,基本上不擁有系統(tǒng)資源,故對它的調度所付出的開銷就會小得多,能更高效的提高系統(tǒng)多個程序間并發(fā)執(zhí)行的程度。

多線程(multiThread)

在一個程序中,這些獨立運行的程序片段叫作“線程”(Thread),利用它編程的概念就叫作“多線程處理”。多線程是為了同步完成多項任務,不是為了提高運行效率,而是為了提高資源使用效率來提高系統(tǒng)的效率。線程是在同一時間需要完成多項任務的時候實現(xiàn)的。

最簡單的比喻多線程就像火車的每一節(jié)車廂,而進程則是火車。車廂離開火車是無法跑動的,同理火車也不可能只有一節(jié)車廂。多線程的出現(xiàn)就是為了提高效率。

二、說說區(qū)別

1、進程與線程的區(qū)別:

進程和線程的主要差別在于它們是不同的操作系統(tǒng)資源管理方式。進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等于整個進程死掉所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些要求同時進行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進程。

1) 簡而言之,一個程序至少有一個進程,一個進程至少有一個線程.

2) 線程的劃分尺度小于進程,使得多線程程序的并發(fā)性高。

3) 另外,進程在執(zhí)行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。

4) 線程在執(zhí)行過程中與進程還是有區(qū)別的。每個獨立的線程有一個程序運行的入口、順序執(zhí)行序列和程序的出口。但是線程不能夠獨立執(zhí)行,必須依存在應用程序中,由應用程序提供多個線程執(zhí)行控制。

5) 從邏輯角度來看,多線程的意義在于一個應用程序中,有多個執(zhí)行部分可以同時執(zhí)行。但操作系統(tǒng)并沒有將多個線程看做多個獨立的應用,來實現(xiàn)進程的調度和管理以及資源分配。這就是進程和線程的重要區(qū)別。

三、說說優(yōu)缺點

線程和進程在使用上各有優(yōu)缺點:線程執(zhí)行開銷小,但不利于資源的管理和保護;而進程正相反。同時,線程適合于在SMP(多核處理機)機器上運行,而進程則可以跨機器遷移。

四、說說進程和線程的細節(jié),底層構成 和 調度

(一)進程相關的數(shù)據(jù)結構

為了管理進程,內核必須對每個進程所做的事情進行清楚的描述,例如,內核必須知道進程的優(yōu)先級,它是在CPU上運行還是因為某些事而被阻塞,給它分配了什么樣的地址空間,允許它訪問哪個文件等。

這些正是進程描述符的作用---進程描述符都是task_struct 數(shù)據(jù)結構,它的字段包含了與一個進程相關的所有信息。下圖顯示了Linux進程描述符

談談進程的基本信息。

1)標識一個進程--PID

每個進程都必須擁有它自己的進程描述符;進程和進程描述符之間有非常嚴格的一一對應關系,所以我們可以方便地使用32位進程描述符地址標識進程。

進程描述符指針(task_struct*)指向這些地址。內核對進程的大部份引用都是通過進程描述符指針進行的。

另一方面,類Unix橾作系統(tǒng)允許用戶使用一個叫做進程標識符processID(PID)的數(shù)來標識進程,PID存放在task_struct的pid字段中。PID被順序編號,新創(chuàng)建進程的PID通常是前一個進程的PID加1。不過,PID的值有一個上限,當內核使用的PID達到這個峰值的時候,就必須開始循環(huán)使用已閑置的小PID號。在缺省情況下,最大的PID號是32767。

系統(tǒng)管理員可以通過往/proc/sys/kernel/pid_max 這個文件中寫入一個更小的值來減小PID的上限值,使PID的上限小于32767。在64位體系結構中,系統(tǒng)管理員可以把PID的上限擴大到4194304。

Linux只支持輕量級進程,不支持線程,但為了彌補這樣的缺陷,Linux引入線程組的概念。一個線程組中的所有線程使用和該線程組的領頭線程相同的PID,也就是該組中第一個輕量級進程的PID,它被存入進程描述符的tgid字段中。getpid()系統(tǒng)調用返回當前進程的tgid值而不是pid值,因此,一個多線程應用的所有線程共享相同的PID。絕大多數(shù)進程都屬于一個線程組;而線程組的領頭線程其tgid與pid的值相同,因而getpid()系統(tǒng)調用對這類進程所起的作用和一般進程是一樣的。

所以,我們得出一個重要的結論,Linux雖不支持線程,但是它有具備支持線程的操作系統(tǒng)的所有特性,后面講解輕量級進程的概念中還會詳細討論。

2)進程描述符定位

進程是動態(tài)實體,其生命周期范圍從幾毫秒到幾個月,因此內核必須同時處理很多進程,并把對應的進程描述符放在動態(tài)內存中,而不是放在永久分配給內核的內存區(qū)(3G之上的線性地址)。

那么,怎么找到被動態(tài)分配的進程描述符呢?我們需要在3G之上線性地址的內存區(qū)為每個進程設計一個塊—thread_union。

對每個進程來說,我們需要給其分配兩個頁面,即8192個字節(jié)的塊,Linux把兩個不同數(shù)據(jù)結構緊湊地存放在一個單獨為進程分配的存儲區(qū)域內:一個是內核態(tài)的進程堆棧,另一個是緊挨著進程描述符的小數(shù)據(jù)結構thread_info,叫做線程描述符。

考慮到效率問題,內核讓這8k的空間占據(jù)連續(xù)兩個頁框并讓第一個頁框的起始地址是2^13的倍數(shù)。當幾乎沒有可用的動態(tài)內存空間時,就會很難找到這樣的兩個連續(xù)頁框,因為空閑空間可能存在大量的碎片(注意,這里是物理空間,見“伙伴系統(tǒng)算法”博文)。因此,在80x86體系結構中,在編譯時可以進行設置,以使內核棧和線程描述符跨越一個單獨的頁框(因為主要存在的單頁的碎片)。在“Linux中的分段”的博文中我們已經知道,內核態(tài)的進程訪問處于內核數(shù)據(jù)段的棧,也就是我們Linux在3G以上內存空間為每個進程設計這么一個棧的目的,這個棧不同于用戶態(tài)的進程所用的棧。因為內核控制路徑使用很少的棧,因此只需要幾千個字節(jié)的內核態(tài)堆棧。所以,對棧和thread_info來說,8KB足夠了。不過,如果只使用一個頁框存放這兩個結構的話,內核要采用一些額外的棧以防止中斷和異常的深度嵌套而引起的溢出。

下圖顯示了在2頁(8KB)內存區(qū)中存放兩種數(shù)據(jù)結構的方式。線程描述符駐留于這個內存區(qū)的開始位置,而棧從末端向下增長。該圖還顯示了如何通過task字段與task_struct結構相互關聯(lián)。

struct thread_info {

struct task_struct??? *task;??? ??? /* main task structure */

struct exec_domain??? *exec_domain;??? /* execution domain */

unsigned long??? ??? flags;??? ??? /* low level flags */

unsigned long??? ??? status;??? ??? /* thread-synchronous flags */

__u32??? ??? ??? cpu;??? ??? /* current CPU */

__s32??? ??? ??? preempt_count; /* 0 => preemptable, <0 => BUG */

mm_segment_t??? ??? addr_limit;??? /* thread address space:0-0xBFFFFFFF for user-thead? 0-0xFFFFFFFF for kernel-thread*/

struct restart_block??? restart_block;

unsigned long?????????? previous_esp;?? /* ESP of the previous stack in caseof nested (IRQ) stacks*/

__u8??? ??? ??? supervisor_stack[0];

};

esp為CPU棧指針寄存器,用來存放棧頂單元的地址。在80x86系統(tǒng)中,棧起始于末端,并朝這個內存區(qū)的起始方向增長。從用戶態(tài)切換到內核態(tài)以后,進程的內核棧總是空的,因此,esp寄存器指向這個棧的頂端。

一旦數(shù)據(jù)寫入堆棧,esp的值就遞減。特別要注意,這里的數(shù)據(jù)是指內核數(shù)據(jù),其實用得很少,所以大多數(shù)時候這個內核棧是空的。因為thread_info

結構是52個字節(jié)的長度,所以內核棧能擴展到8140個字節(jié)。C語言使用下列聯(lián)合結構,方便地表示一個進程的線程描述符和內核棧:

union thread_union {

struct thread_info thread_info;

unsigned long stack[2048]; /* 1024 for 4KB stacks */

};

內核使用alloc_thread_info 和 free_thread_info宏分配和釋放存儲thread_info結構和內核棧的內存區(qū)。

3)標識當前進程

我們再從效率的觀點來看,剛才所講的thread_info結構與內核態(tài)堆棧之間的緊密結合提供的主要好處還在:內核很容易從esp寄存器的值獲得當前在CPU上正在運行進程的thread_info結構的地址。事實上,如果thread_union的長度是8K(213字節(jié)),則內核屏蔽掉esp的低13位有效位就可以獲得thread_info結構的基地址;而如果thread_union的長度是4K,內核需要蔽掉esp的低12位有效位。這項工作由current_thread_info()函數(shù)來完成,它產生如下一些匯編指令:

movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */

andl %esp,%ecx

movl %ecx,p

這三條指令執(zhí)行后,p就是在執(zhí)行指令的CPU上運行的當前進程的thread_info結構的指針。不過,進程最常用的是進程描述符的地址,而不是thread_info結構的地址。為了獲得當前在CPU上運行進程的描述符指針,內核要調用current宏,該宏本質上等價于current_thread_info( )->task,它產生如下匯編指令:

movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */

andl %esp,%ecx

movl (%ecx),p

因為task字段在thread_info結構中的偏移量為0,所以執(zhí)行完這三條指令之后,p就是CPU上運行進程的描述符指針。

current宏經常作為進程描述符字段的前綴出現(xiàn)在內核代碼中,例如,current->pid返回在CPU上正在執(zhí)行CPU的進程的PID。

4)進程鏈表

Linux內核把進程鏈表把所有進程的描述符鏈接起來。每個task_struct結構都包含一個list_head類型的tasks字段,這個類型的prev和next字段分別指向前面和后面的的task_struct元素。

進程鏈表的頭是init_task描述符,它是所謂的0進程或swapper進程的進程描述符。init_task的tasks.prev字段指向鏈表中最后插入的進程描述符的tasks字段。

SET_LINKS 和 REMOVE_LINKS 宏分別用于從進程鏈表中插入和刪除一個進程描述符。這些宏考慮了進程間的父子關系。

另外,還有一個很有用的宏就是for_each_process,它的功能是掃描整個進程鏈表,其定義如下:

#define for_each_process(p) /

for (p=&init_task; (p=list_entry((p)->tasks.next, /

struct task_struct, tasks) /

) != &init_task; )

5)state字段

進程描述符task_struct結構的state字段描述了進程當前所處的狀態(tài)。它由一組標志組成,其中每個標志描述一種可能的進程狀態(tài)。在當前的Linux版本中,這些狀態(tài)是互斥的,因此,嚴格意義上來說,只能設置一種狀態(tài),其余的標志位將被清除。下面是可能的狀態(tài):

可運行狀態(tài)(TASK_RUNNING)

進程要么在CPU上執(zhí)行,要么準備執(zhí)行。

可中斷的等待狀態(tài)(TASK_INTERRUPTIBLE)

進程被掛起(睡眠),直到某個條件變?yōu)檎?。產生一個硬件中斷、釋放進程正在等待的系統(tǒng)資源、或傳遞一個信號都是可以喚醒進程的條件(把進程狀態(tài)放回到TASK_RUNNING)。

不可中斷的等待狀態(tài)(TASK_UNINTERRUPTIBLE)

與可中斷的等待狀態(tài)類似,但有一個例外,把信號傳遞到該睡眠進程時,不能改變它的狀態(tài)。這種狀態(tài)很少用到,但在一些特定條件下(進程必須等待,直到一個不能被中斷的時事件發(fā)生),這種狀態(tài)是很有用的。例如,當進程打開一個設備文件,其相應的設備驅動程序開始探測相應的硬件設備時會用到這種狀態(tài)。探測完成以前,設備驅動程序不能被中斷,否則,硬件設備會處于不可預知的狀態(tài)。

暫停狀態(tài)(TASK_STOPPED)

進程的執(zhí)行被暫停。當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信號后,進人暫停狀態(tài)。

跟蹤狀態(tài)(TASK_TRACED)

進程的執(zhí)行已由debugger程序暫停。當一個進程被另一個進程監(jiān)控時(例如debugger執(zhí)行ptrace()系統(tǒng)調用監(jiān)控一個測試程序)任何信號都可以把這個進程置于TASK_TRACED狀態(tài)。

還有兩個進程狀態(tài)既可以存放在進程描述符的state字段啊中,也可以存放在exit_state中字段中。從這兩個字段的名稱可以看出,只有當進程的執(zhí)行被終止時,進程的狀態(tài)才會變成此兩種中的一種:

僵死狀態(tài)(EXIT_ZOMBIE)

進程的執(zhí)行被終止,但是父進程還沒發(fā)布wait4()或waitpid()系統(tǒng)調用來返回有關死亡進程的信息。發(fā)布wait()類系統(tǒng)調用前,內核不能丟棄包含在死進程描述符中的數(shù)據(jù),因為父進程可能還需要它。

僵死撤銷狀態(tài)(EXIT_DEAD)

終狀態(tài):由于父進程剛發(fā)出wait4()或waitpid()系統(tǒng)調用,因而進程由系統(tǒng)刪除。為了防止其他執(zhí)行線程在同一個進程上也執(zhí)行wait()類系統(tǒng)調用(這也是一種競爭條件),而把進程的狀態(tài)由僵死(EXIT_ZOMBIE)狀態(tài)改為僵死撤銷狀態(tài)(EXIT_DEAD)

state字段的值通常用一個簡單的賦值語句設置,例如:

p->state = TASK_RUNNING;

內核也使用set_task_state和set_current_state宏:它們分別設置指定進程的狀態(tài)和當前執(zhí)行進程的狀態(tài)。此外,這些宏確保編譯程序或CPU控制單元不把賦值操作和其他指令混合?;旌现噶畹捻樞蛴袝r會導致災難性的后果。

6)TASK_RUNNING狀態(tài)的進程鏈表

當內核尋找到一個新進程在CPU上運行時,必須只考慮可運行進程(即處在TASK_RUNNING狀態(tài)的進程)。

早先的Linux版本把所有的可運行進程都放在同一個叫做運行隊列(runqueue)的鏈表中,由于維持鏈表中的進程優(yōu)先級排序的開銷過大,因此,早期的調度程序不得不為選擇“最佳”可運行進程而掃描整個隊列。

Linux 2.6實現(xiàn)的運行隊列有所不同。其目的是讓調度程序能在固定的時間內選出“最佳”可運行隊列,與進程中可運行的進程數(shù)無關。

提高調度程序運行速度的訣竅是建立多個可運行進程鏈表,每種進程優(yōu)先級對應一個不同的鏈表。每個task_struct描述符包含一個list_head類型的字段run_list。如果進程的優(yōu)先權等于k(其取值范圍從0到139),run_list字段就把該進程的優(yōu)先級鏈入優(yōu)先級為k的可運行進程的鏈表中。此外,在多處理器系統(tǒng)中,每個CPU都有它自己的運行隊列,即它自己的進程鏈表集。這是一個通過使數(shù)據(jù)結構更復雜來改善性能的典型例子:調度程序的操作效率的確更高了,但運行隊列的鏈表卻為此被拆分成140個不同的隊列!

內核必須為系統(tǒng)中每個運行隊列保存大量的數(shù)據(jù),不過運行隊列的主要數(shù)據(jù)結構還是組成運行隊列的進程描述符鏈表,所有這些鏈表都由一個單獨的prio_array_t數(shù)據(jù)結構來實現(xiàn)。

enqueue_task(p,array)函數(shù)把進程描述符(p參數(shù))插入到某個運行隊列的鏈表(基于prio_array_t結構的array參數(shù)),其代碼本質上等同于如下代碼:

list_add_tail(&p->run_list, &array->queue[p->prio]);

__set_bit(p->prio, array->bitmap);

array->nr_active++;

p->array = array;

進程描述符的prio字段存放進程的動態(tài)優(yōu)先權,而array字段是一個指針,指向當前運行隊列的proo_array_t數(shù)據(jù)結構。類似地,dequeue_task(p,array)函數(shù)從運行隊列的鏈表中刪除一個進程的描述符。

7)進程間關系

父子兄弟關系:

程序創(chuàng)建的進程具有父/子關系。如果一個進程創(chuàng)建多個子進程時,則子進程之間具有兄弟關系。進程0和進程1是由內核創(chuàng)建的;進程1(init)是所有進程的祖先。

在進程描述符中引入幾個字段來表示這些關系,我們假設擁有該task_struct結構的這個進程叫P:

real_parent——指向創(chuàng)建了P進程的描述符,如果進程P的父進程不存在,就指向進程1的描述符(因此,如果用戶運行了一個后臺進程而且退出了shell,后臺進程就會變成init的子進程)。

parent——指向P的當前父進程(這種進程的子進程終止時,必須向父進程發(fā)信號)。它的值通常與reak_parent一致,但偶爾也可以不同,例如,當另一個進程發(fā)出監(jiān)控P的ptrace系統(tǒng)調用請求時。

children——鏈表的頭部,鏈表中所有的元素都是P創(chuàng)建的子進程。

sibling——指向兄弟進程鏈表中的下一個元素或前一個元素的指針,這些兄弟進程的父進程跟P是一樣的。

下圖顯示了一組進程間的親屬關系,進程P0創(chuàng)建了P1,P2,P3,進程P3又創(chuàng)建了P4。

need-to-insert-img

其他關系:此外,進程之間還存在其他關系:一個進程可能是一個進程組或登錄會話的領頭進程,也可能是一個線程組的領頭進程,他還可能跟蹤其他進程的執(zhí)行,下面就列出進程描述符中的一些字段,這些字段建立起了進程P和其他進程之間的關系:

group_leader——P所在進程組的領頭進程的描述符指針

signal->pgrp——P所在進程組的領頭進程的PID

tgid——P所在線程組的領頭進程的PID

signal->session——P的登錄會話領頭進程的PID

ptrace_children——鏈表的頭,該鏈表包含所有被debugger程序跟蹤的P的子進程

ptrace_list——指向所跟蹤進程其實際父進程鏈表的前一個和下一個元素(用于P被跟蹤的時候)

8)PID定位task_struct

再來,內核必須能從進程的PID導出對應的進程描述符指針。例如,為kill()系統(tǒng)調用提供服務時就會發(fā)生這種情況:當進程P1希望向另一個進程P2發(fā)送一個信號時,P1調用kill()系統(tǒng)調用,其參數(shù)為P2的PID,內核從這個PID導出其對應的進程描述符,然后從該task_struct中取出記錄掛起信號的數(shù)據(jù)結構指針。

那么如何得到這個task_struct呢?首先想到for_each_process(p)。不行,雖然順序掃描進程鏈表并檢查進程描述符的pid字段是可行的,但相當?shù)托?。為了加速查找,Linux內核引入了4個散列表。需要4個散列表是因為進程描述符包含了表示不同類型PID的字段,而且每種類型的PID需要它自己的散列表:

PIDTYPE_PID??? pid??? 進程的PID

PIDTYPE_TGID??? tgid??? 線程組領頭進程的PID

PIDTYPE_PGID??? pgrp??? 進程組領頭進程的PID

PIDTYPE_SID??? session??? 會話領頭的PID

內核初始化期間動態(tài)地為4個散列表分配空間,并把它們的地址存入pid_hash數(shù)組。一個散列表的長度依賴于可用的RAM的容量,例如:一個系統(tǒng)擁有512MB的RAM,那么每個散列表就被存在4個頁框中,可擁有2048個表項。

用pid_hashfn宏把PID轉化為表索引:

#define pid_hashfn(x) hash_long((unsigned long) x, pidhash_shift)

變量pidhash_shift用來存放表索引的長度(以位為單位的長度,在我們這里是11位)。很多散列函數(shù)都使用hash_long(),在32位體系結構中它基本等價于:

unsigned long hash_long(unsigned long val, unsigned int bits)

{

unsigned long hash = val * 0x9e370001UL;

return hash >> (32 - bits);

}

因為我們這里的pidhash_shift等于11,所以pid_hashfn的取值范圍是0到2^11 - 1=2047。

正如計算機科學的基礎課程所闡述的那樣,散列函數(shù)并不總能確保PID與表的索引一一對應。兩個不同的PID散列到相同的表索引稱為沖突(colliding)。Linux利用鏈表來處理沖突的PID:每個表項是由沖突的進程描述符組成的雙向循環(huán)鏈表

(二)進程調度

1)進程調度的目標

1.高效性:高效意味著在相同的時間下要完成更多的任務,調度程序會被頻繁的執(zhí)行,所以調度程序要盡可能高效。

2.加強交互性能:在系統(tǒng)相當?shù)呢撦d下,也要保證系統(tǒng)的響應時間

3.保證公平和避免饑渴

4.SMP調度:調度程序必須支持多處理系統(tǒng)

5.軟實時調度:系統(tǒng)必須有效的調用實時進程,但不保證一定滿足其要求。

2)進程優(yōu)先級

進程提供了兩種優(yōu)先級,一種是普通的進程優(yōu)先級,一種是實時進程優(yōu)先級。

前者適用SCHED_NORMAL調度策略,后者可選SCHED_FIFO或SCHED_RR調度策略,任何時候,實時進程的優(yōu)先級都高于普通進程,實時進程只會被更高級的實時進程搶占,同級實時進程之間是按照FIFO(一次機會做完)或者RR(多次輪轉)規(guī)則調度的。

實時進程,只有靜態(tài)優(yōu)先級,因為內核不會再根據(jù)休眠等因素對其靜態(tài)優(yōu)先級做調整,其范圍在0~MAX_RT_PRIO-1間。默認MAX_RT_PRIO配置為100,也即,默認的實時優(yōu)先級范圍是0~99。而nice值,影響的是優(yōu)先級在MAX_RT_PRIO~MAX_RT_PRIO+40范圍內的進程。

不同與普通進程,系統(tǒng)調度時,實時優(yōu)先級高的進程總是先于優(yōu)先級低的進程執(zhí)行,直到實時優(yōu)先級高的實時進程無法執(zhí)行。實時進程總是被認為處于活動狀態(tài)。如果有數(shù)個 優(yōu)先級相同的實時進程,那么系統(tǒng)就會按照進程出現(xiàn)在隊列上的順序選擇進程,假設當前CPU運行的實時進程A的優(yōu)先級為a,而此時有個優(yōu)先級為b的實時進程B進入可運行狀態(tài),那么只要b

不同調度策略的實時進程只有在相同優(yōu)先級時才有可比性:

1. 對于FIFO的進程,意味著只有當前進程執(zhí)行完畢才會輪到其他進程執(zhí)行。由此可見相當霸道。

2. 對于RR的進程。一旦時間片消耗完畢,則會將該進程置于隊列的末尾,然后運行其他相同優(yōu)先級的進程,如果沒有其他相同優(yōu)先級的進程,則該進程會繼續(xù)執(zhí)行。

總而言之,對于實時進程,高優(yōu)先級的進程就是大爺。它執(zhí)行到沒法執(zhí)行了,才輪到低優(yōu)先級的進程執(zhí)行。

普通進程的調度

Linux對于普通的進程,根據(jù)動態(tài)優(yōu)先級進行調度,而動態(tài)優(yōu)先級是由靜態(tài)優(yōu)先級調整而來,Linux下,靜態(tài)優(yōu)先級是用戶不可見的,隱藏在內核中,而內核提供給用戶一個可以影響靜態(tài)優(yōu)先級的接口,那就是nice值。

關系如下:

static_prio =MAX_RT_PRIO+nice+20

nice值的范圍是-20~19,因而靜態(tài)優(yōu)先級范圍在100~139之間,nice數(shù)值越大就使得static_prio越大,最終進程優(yōu)先級就越低。

我們前面也說了,系統(tǒng)調度時,還會考慮其他因素,因而會計算出一個叫進程動態(tài)優(yōu)先級的東西,根據(jù)此來實施調度。因為,不僅要考慮靜態(tài)優(yōu)先級,也要考慮進程

的屬性。例如如果進程屬于交互式進程,那么可以適當?shù)恼{高它的優(yōu)先級,使得界面反應地更加迅速,從而使用戶得到更好的體驗。Linux2.6

在這方面有了較大的提高。Linux2.6認為,交互式進程可以從平均睡眠時間這樣一個measurement進行判斷。進程過去的睡眠時間越多,則越有

可能屬于交互式進程。則系統(tǒng)調度時,會給該進程更多的獎勵(bonus),以便該進程有更多的機會能夠執(zhí)行。獎勵(bonus)從0到10不等。

系統(tǒng)會嚴格按照動態(tài)優(yōu)先級高低的順序安排進程執(zhí)行。動態(tài)優(yōu)先級高的進程進入非運行狀態(tài),或者時間片消耗完畢才會輪到動態(tài)優(yōu)先級較低的進程執(zhí)行。動態(tài)優(yōu)先級的計算主要考慮兩個因素:靜態(tài)優(yōu)先級,進程的平均睡眠時間也即bonus。計算公式如下,

dynamic_prio?= max (100, min (static_prio - bonus?+ 5, 139))

為什么根據(jù)睡眠和運行時間確定獎懲分數(shù)是合理的

睡眠和CPU耗時反應了進程IO密集和CPU密集兩大瞬時特點,不同時期,一個進程可能即是CPU密集型也是IO密集型進程。對于表現(xiàn)為IO密集的進程,應該經常運行,但每次時間片不要太長。對于表現(xiàn)為CPU密集的進程,CPU不應該讓其經常運行,但每次運行時間片要長。交互進程為例,假如之前其其大部分時間在于等待CPU,這時為了調高相應速度,就需要增加獎勵分。另一方面,如果此進程總是耗盡每次分配給它的時間片,為了對其他進程公平,就要增加這個進程的懲罰分數(shù)。可以參考CFS的virtutime機制.

3)現(xiàn)代方法CFS

不再單純依靠進程優(yōu)先級絕對值,而是參考其絕對值,綜合考慮所有進程的時間,給出當前調度時間單位內其應有的權重,也就是,每個進程的權重X單位時間=應獲cpu時間,但是這個應得的cpu時間不應太?。僭O閾值為1ms),否則會因為切換得不償失。但是,當進程足夠多時候,肯定有很多不同權重的進程獲得相

同的時間——最低閾值1ms,所以,CFS只是近似完全公平。

4)Linux進程狀態(tài)機

進程是通過fork系列的系統(tǒng)調用(fork clone,vfork)來創(chuàng)建的,內核,內核模塊也可以通過kernel_thread函數(shù)創(chuàng)建內核進程,這些創(chuàng)建子進程的函數(shù)本質上都完成了相同的功能——將調用進程復制一份,得到子進程。(可以通過選項參數(shù)來決定各種資源是共享、還是私有。)那么既然調用進程處于TASK_RUNNING狀態(tài)(否則,它若不是正在運行,又怎么進行調用?),則子進程默認也處于TASK_RUNNING狀態(tài)。

另外,在系統(tǒng)調用clone和內核函數(shù)kernel_thread也接受CLONE_STOPPED選項,從而將子進程的初始狀態(tài)置為 TASK_STOPPED。

進程創(chuàng)建后,狀態(tài)可能發(fā)生一系列的變化,直到進程退出。而盡管進程狀態(tài)有好幾種,但是進程狀態(tài)的變遷卻只有兩個方向——從TASK_RUNNING狀態(tài)變?yōu)榉荰ASK_RUNNING狀態(tài)、或者從非TASK_RUNNING狀態(tài)變?yōu)門ASK_RUNNING狀態(tài)??傊琓ASK_RUNNING是必經之路,不可能兩個非RUN狀態(tài)直接轉換。

也就是說,如果給一個TASK_INTERRUPTIBLE狀態(tài)的進程發(fā)送SIGKILL信號,這個進程將先被喚醒(進入TASK_RUNNING狀態(tài)),然后再響應SIGKILL信號而退出(變?yōu)門ASK_DEAD狀態(tài))。并不會從TASK_INTERRUPTIBLE狀態(tài)直接退出。

進程從非TASK_RUNNING狀態(tài)變?yōu)門ASK_RUNNING狀態(tài),是由別的進程(也可能是中斷處理程序)執(zhí)行喚醒操作來實現(xiàn)的。執(zhí)行喚醒的

進程設置被喚醒進程的狀態(tài)為TASK_RUNNING,然后將其task_struct結構加入到某個CPU的可執(zhí)行隊列中。于是被喚醒的進程將有機會被

調度執(zhí)行。

而進程從TASK_RUNNING狀態(tài)變?yōu)榉荰ASK_RUNNING狀態(tài),則有兩種途徑:

1、響應信號而進入TASK_STOPED狀態(tài)、或TASK_DEAD狀態(tài);

2、執(zhí)行系統(tǒng)調用主動進入TASK_INTERRUPTIBLE狀態(tài)(如nanosleep系統(tǒng)調用)、或TASK_DEAD狀態(tài)(如exit系統(tǒng)調用);或由于執(zhí)行系統(tǒng)調用需要的資源得不到滿    ?足,而進入TASK_INTERRUPTIBLE狀態(tài)或TASK_UNINTERRUPTIBLE狀態(tài)(如select系統(tǒng)調用)。

顯然,這兩種情況都只能發(fā)生在進程正在CPU上執(zhí)行的情況下。

通過ps命令我們能夠查看到系統(tǒng)中存在的進程,以及它們的狀態(tài):R(TASK_RUNNING),可執(zhí)行狀態(tài)。

只有在該狀態(tài)的進程才可能在CPU上運行。而同一時刻可能有多個進程處于可執(zhí)行狀態(tài),這些進程的task_struct結構(進程控制塊)被放入對應CPU的可執(zhí)行隊列中(一個進程最多只能出現(xiàn)在一個CPU的可執(zhí)行隊列中)。進程調度器的任務就是從各個CPU的可執(zhí)行隊列中分別選擇一個進程在該CPU上運行。

只要可執(zhí)行隊列不為空,其對應的CPU就不能偷懶,就要執(zhí)行其中某個進程。一般稱此時的CPU“忙碌”。對應的,CPU“空閑”就是指其對應的可執(zhí)行隊列為空,以致于CPU無事可做。

有人問,為什么死循環(huán)程序會導致CPU占用高呢?因為死循環(huán)程序基本上總是處于TASK_RUNNING狀態(tài)(進程處于可執(zhí)行隊列中)。除非一些非常極端情況(比如系統(tǒng)內存嚴重緊缺,導致進程的某些需要使用的頁面被換出,并且在頁面需要換入時又無法分配到內存……),否則這個進程不會睡眠。所以CPU的可執(zhí)行隊列總是不為空(至少有這么個進程存在),CPU也就不會“空閑”。

很多操作系統(tǒng)教科書將正在CPU上執(zhí)行的進程定義為RUNNING狀態(tài)、而將可執(zhí)行但是尚未被調度執(zhí)行的進程定義為READY狀態(tài),這兩種狀態(tài)在linux下統(tǒng)一為 TASK_RUNNING狀態(tài)。

S(TASK_INTERRUPTIBLE),可中斷的睡眠狀態(tài)。

處于這個狀態(tài)的進程因為等待某某事件的發(fā)生(比如等待socket連接、等待信號量),而被掛起。這些進程的task_struct結構被放入對應事件的等待隊列中。當這些事件發(fā)生時(由外部中斷觸發(fā)、或由其他進程觸發(fā)),對應的等待隊列中的一個或多個進程將被喚醒。

通過ps命令我們會看到,一般情況下,進程列表中的絕大多數(shù)進程都處于TASK_INTERRUPTIBLE狀態(tài)(除非機器的負載很高)。畢竟CPU就這么一兩個,進程動輒幾十上百個,如果不是絕大多數(shù)進程都在睡眠,CPU又怎么響應得過來。

D(TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態(tài)。

與TASK_INTERRUPTIBLE狀態(tài)類似,進程處于睡眠狀態(tài),但是此刻進程是不可中斷的。不可中斷,指的并不是CPU不響應外部硬件的中斷,而是指進程不響應異步信號。

絕大多數(shù)情況下,進程處在睡眠狀態(tài)時,總是應該能夠響應異步信號的。否則你將驚奇的發(fā)現(xiàn),kill -9竟然殺不死一個正在睡眠的進程了!于是我們也很好理解,為什么ps命令看到的進程幾乎不會出現(xiàn)TASK_UNINTERRUPTIBLE狀態(tài),而總是TASK_INTERRUPTIBLE狀態(tài)。

而TASK_UNINTERRUPTIBLE狀態(tài)存在的意義就在于,內核的某些處理流程是不能被打斷的。如果響應異步信號,程序的執(zhí)行流程中就會被插入一段用于處理異步信號的流程(這個插入的流程可能只存在于內核態(tài),也可能延伸到用戶態(tài)),于是原有的流程就被中斷了(參見《linux異步信號handle淺析》)。

在進程對某些硬件進行操作時(比如進程調用read系統(tǒng)調用對某個設備文件進行讀操作,而read系統(tǒng)調用最終執(zhí)行到對應設備驅動的代碼,并與對應的物理設備進行交互),可能需要使用TASK_UNINTERRUPTIBLE狀態(tài)對進程進行保護,以避免進程與設備交互的過程被打斷,造成設備陷入不可控的狀態(tài)。(比如read系統(tǒng)調用觸發(fā)了一次磁盤到用戶空間的內存的DMA,如果DMA進行過程中,進程由于響應信號而退出了,那么DMA正在訪問的內存可能就要被釋放了。)這種情況下的TASK_UNINTERRUPTIBLE狀態(tài)總是非常短暫的,通過ps命令基本上不可能捕捉到。

linux系統(tǒng)中也存在容易捕捉的TASK_UNINTERRUPTIBLE狀態(tài)。執(zhí)行vfork系統(tǒng)調用后,父進程將進入TASK_UNINTERRUPTIBLE狀態(tài),直到子進程調用exit或exec。

通過下面的代碼就能得到處于TASK_UNINTERRUPTIBLE狀態(tài)的進程:

#include

void main() {

if (!vfork()) sleep(100);

}

向進程發(fā)送一個SIGSTOP信號,它就會因響應該信號而進入TASK_STOPPED狀態(tài)(除非該進程本身處于TASK_UNINTERRUPTIBLE狀態(tài)而不響應信號)。(SIGSTOP與SIGKILL信號一樣,是非常強制的。不允許用戶進程通過signal系列的系統(tǒng)調用重新設置對應的信號處理函數(shù)。)

向進程發(fā)送一個SIGCONT信號,可以讓其從TASK_STOPPED狀態(tài)恢復到TASK_RUNNING狀態(tài)。

當進程正在被跟蹤時,它處于TASK_TRACED這個特殊的狀態(tài)?!罢诒桓櫋敝傅氖沁M程暫停下來,等待跟蹤它的進程對它進行操作。比如在gdb中對被跟蹤的進程下一個斷點,進程在斷點處停下來的時候就處于TASK_TRACED狀態(tài)。而在其他時候,被跟蹤的進程還是處于前面提到的那些狀態(tài)。

對于進程本身來說,TASK_STOPPED和TASK_TRACED狀態(tài)很類似,都是表示進程暫停下來。

而TASK_TRACED狀態(tài)相當于在TASK_STOPPED之上多了一層保護,處于TASK_TRACED狀態(tài)的進程不能響應SIGCONT信號而被喚醒。只能等到調試進程通過ptrace系統(tǒng)調用執(zhí)行PTRACE_CONT、PTRACE_DETACH等操作(通過ptrace系統(tǒng)調用的參數(shù)指定操作),或調試進程退出,被調試的進程才能恢復TASK_RUNNING狀態(tài)。

Z(TASK_DEAD - EXIT_ZOMBIE),退出狀態(tài),進程成為僵尸進程。

進程在退出的過程中,處于TASK_DEAD狀態(tài)。

在這個退出過程中,進程占有的所有資源將被回收,除了task_struct結構(以及少數(shù)資源)以外。于是進程就只剩下task_struct這么個空殼,故稱為僵尸。

之所以保留task_struct,是因為task_struct里面保存了進程的退出碼、以及一些統(tǒng)計信息。而其父進程很可能會關心這些信息。比如在shell中,$?變量就保存了最后一個退出的前臺進程的退出碼,而這個退出碼往往被作為if語句的判斷條件。

當然,內核也可以將這些信息保存在別的地方,而將task_struct結構釋放掉,以節(jié)省一些空間。但是使用task_struct結構更為方便,因為在內核中已經建立了從pid到task_struct查找關系,還有進程間的父子關系。釋放掉task_struct,則需要建立一些新的數(shù)據(jù)結構,以便讓父進程找到它的子進程的退出信息。

父進程可以通過wait系列的系統(tǒng)調用(如wait4、waitid)來等待某個或某些子進程的退出,并獲取它的退出信息。然后wait系列的系統(tǒng)調用會順便將子進程的尸體(task_struct)也釋放掉。

子進程在退出的過程中,內核會給其父進程發(fā)送一個信號,通知父進程來“收尸”。這個信號默認是SIGCHLD,但是在通過clone系統(tǒng)調用創(chuàng)建子進程時,可以設置這個信號。

通過下面的代碼能夠制造一個EXIT_ZOMBIE狀態(tài)的進程:

#include

void main() {

if (fork())

while(1) sleep(100);

}

編譯運行,然后ps一下:

kouu@kouu-one:~/test$ ps -ax | grep a\.out

10410 pts/0? ? ? S+? ? ?? 0:00 ./a.out

10411 pts/0? ? ? Z+? ? ?? 0:00 [a.out]

10413 pts/1? ? ? S+? ? ?? 0:00 grep a.out

只要父進程不退出,這個僵尸狀態(tài)的子進程就一直存在。那么如果父進程退出了呢,誰又來給子進程“收尸”?

當進程退出的時候,會將它的所有子進程都托管給別的進程(使之成為別的進程的子進程)。托管給誰呢?可能是退出進程所在進程組的下一個進程(如果存在的話),或者是1號進程。所以每個進程、每時每刻都有父進程存在。除非它是1號進程。

1號進程,pid為1的進程,又稱init進程。

linux系統(tǒng)啟動后,第一個被創(chuàng)建的用戶態(tài)進程就是init進程。它有兩項使命:

1、執(zhí)行系統(tǒng)初始化腳本,創(chuàng)建一系列的進程(它們都是init進程的子孫);

2、在一個死循環(huán)中等待其子進程的退出事件,并調用waitid系統(tǒng)調用來完成“收尸”工作;

init進程不會被暫停、也不會被殺死(這是由內核來保證的)。它在等待子進程退出的過程中處于TASK_INTERRUPTIBLE狀態(tài),“收尸”過程中則處于TASK_RUNNING狀態(tài)。

X(TASK_DEAD - EXIT_DEAD),退出狀態(tài),進程即將被銷毀。

而進程在退出過程中也可能不會保留它的task_struct。比如這個進程是多線程程序中被detach過的進程(進程?線程?參見《linux線程淺析》)?;蛘吒高M程通過設置SIGCHLD信號的handler為SIG_IGN,顯式的忽略了SIGCHLD信號。(這是posix的規(guī)定,盡管子進程的退出信號可以被設置為SIGCHLD以外的其他信號。)

此時,進程將被置于EXIT_DEAD退出狀態(tài),這意味著接下來的代碼立即就會將該進程徹底釋放。所以EXIT_DEAD狀態(tài)是非常短暫的,幾乎不可能通過ps命令捕捉到。

5)調度觸發(fā)的時機

調度的觸發(fā)主要有如下幾種情況:

1、當前進程(正在CPU上運行的進程)狀態(tài)變?yōu)榉强蓤?zhí)行狀態(tài)。

進程執(zhí)行系統(tǒng)調用主動變?yōu)榉强蓤?zhí)行狀態(tài)。比如執(zhí)行nanosleep進入睡眠、執(zhí)行exit退出、等等;

進程請求的資源得不到滿足而被迫進入睡眠狀態(tài)。比如執(zhí)行read系統(tǒng)調用時,磁盤高速緩存里沒有所需要的數(shù)據(jù),從而睡眠等待磁盤IO;

進程響應信號而變?yōu)榉强蓤?zhí)行狀態(tài)。比如響應SIGSTOP進入暫停狀態(tài)、響應SIGKILL退出、等等;

2、搶占。進程運行時,非預期地被剝奪CPU的使用權。這又分兩種情況:進程用完了時間片、或出現(xiàn)了優(yōu)先級更高的進程。

優(yōu)先級更高的進程受正在CPU上運行的進程的影響而被喚醒。如發(fā)送信號主動喚醒,或因為釋放互斥對象(如釋放鎖)而被喚醒;

內核在響應時鐘中斷的過程中,發(fā)現(xiàn)當前進程的時間片用完;

內核在響應中斷的過程中,發(fā)現(xiàn)優(yōu)先級更高的進程所等待的外部資源的變?yōu)榭捎茫瑥亩鴮⑵鋯拘?。比如CPU收到網卡中斷,內核處理該中斷,發(fā)現(xiàn)某個socket可讀,于是喚醒正在等待讀這個socket的進程;再比如內核在處理時鐘中斷的過程中,觸發(fā)了定時器,從而喚醒對應的正在nanosleep系統(tǒng)調用中睡眠的進程;

6)內核搶占

理想情況下,只要滿足“出現(xiàn)了優(yōu)先級更高的進程”這個條件,當前進程就應該被立刻搶占。但是,就像多線程程序需要用鎖來保護臨界區(qū)資源一樣,內核中也存在很多這樣的臨界區(qū),不大可能隨時隨地都能接收搶占。

linux 2.4時的設計就非常簡單,內核不支持搶占。進程運行在內核態(tài)時(比如正在執(zhí)行系統(tǒng)調用、正處于異常處理函數(shù)中),是不允許搶占的。必須等到返回用戶態(tài)時才會觸發(fā)調度(確切的說,是在返回用戶態(tài)之前,內核會專門檢查一下是否需要調度);

linux 2.6則實現(xiàn)了內核搶占,但是在很多地方還是為了保護臨界區(qū)資源而需要臨時性的禁用內核搶占。

也有一些地方是出于效率考慮而禁用搶占,比較典型的是spin_lock。spin_lock是這樣一種鎖,如果請求加鎖得不到滿足(鎖已被別的進程占有),則當前進程在一個死循環(huán)中不斷檢測鎖的狀態(tài),直到鎖被釋放。

為什么要這樣忙等待呢?因為臨界區(qū)很小,比如只保護“i+=j++;”這么一句。如果因為加鎖失敗而形成“睡眠-喚醒”這么個過程,就有些得不償失了。

那么既然當前進程忙等待(不睡眠),誰又來釋放鎖呢?其實已得到鎖的進程是運行在另一個CPU上的,并且是禁用了內核搶占的。這個進程不會被其他進程搶占,所以等待鎖的進程只有可能運行在別的CPU上。(如果只有一個CPU呢?那么就不可能存在等待鎖的進程了。)

而如果不禁用內核搶占呢?那么得到鎖的進程將可能被搶占,于是可能很久都不會釋放鎖。于是,等待鎖的進程可能就不知何年何月得償所望了。

對于一些實時性要求更高的系統(tǒng),則不能容忍spin_lock這樣的東西。寧可改用更費勁的“睡眠-喚醒”過程,也不能因為禁用搶占而讓更高優(yōu)先級的進程等待。比如,嵌入式實時linux montavista就是這么干的。

由此可見,實時并不代表高效。很多時候為了實現(xiàn)“實時”,還是需要對性能做一定讓步的。

7)多處理器下的負載均衡

前面我們并沒有專門討論多處理器對調度程序的影響,其實也沒有什么特別的,就是在同一時刻能有多個進程并行地運行而已。那么,為什么會有“多處理器負載均衡”這個事情呢?

如果系統(tǒng)中只有一個可執(zhí)行隊列,哪個CPU空閑了就去隊列中找一個最合適的進程來執(zhí)行。這樣不是很好很均衡嗎?

的確如此,但是多處理器共用一個可執(zhí)行隊列會有一些問題。顯然,每個CPU在執(zhí)行調度程序時都需要把隊列鎖起來,這會使得調度程序難以并行,可能導致系統(tǒng)性能下降。而如果每個CPU對應一個可執(zhí)行隊列則不存在這樣的問題。

另外,多個可執(zhí)行隊列還有一個好處。這使得一個進程在一段時間內總是在同一個CPU上執(zhí)行,那么很可能這個CPU的各級cache中都緩存著這個進程的數(shù)據(jù),很有利于系統(tǒng)性能的提升。

所以,在linux下,每個CPU都有著對應的可執(zhí)行隊列,而一個可執(zhí)行狀態(tài)的進程在同一時刻只能處于一個可執(zhí)行隊列中。

于是,“多處理器負載均衡”這個麻煩事情就來了。內核需要關注各個CPU可執(zhí)行隊列中的進程數(shù)目,在數(shù)目不均衡時做出適當調整。什么時候需要調整,以多大力度進程調整,這些都是內核需要關心的。當然,盡量不要調整最好,畢竟調整起來又要耗CPU、又要鎖可執(zhí)行隊列,代價還是不小的。

另外,內核還得關心各個CPU的關系。兩個CPU之間,可能是相互獨立的、可能是共享cache的、甚至可能是由同一個物理CPU通過超線程技術虛擬出來的……CPU之間的關系也是實現(xiàn)負載均衡的重要依據(jù)。關系越緊密,進程在它們之間遷移的代價就越小。參見《linux內核SMP負載均衡淺析》。

優(yōu)先級繼承

由于互斥,一個進程(設為A)可能因為等待進入臨界區(qū)而睡眠。直到正在占有相應資源的進程(設為B)退出臨界區(qū),進程A才被喚醒。

可能存在這樣的情況:A的優(yōu)先級非常高,B的優(yōu)先級非常低。B進入了臨界區(qū),但是卻被其他優(yōu)先級較高的進程(設為C)搶占了,而得不到運行,也就無法退出臨界區(qū)。于是A也就無法被喚醒。

A有著很高的優(yōu)先級,但是現(xiàn)在卻淪落到跟B一起,被優(yōu)先級并不太高的C搶占,導致執(zhí)行被推遲。這種現(xiàn)象就叫做優(yōu)先級反轉。

出現(xiàn)這種現(xiàn)象是很不合理的。較好的應對措施是:當A開始等待B退出臨界區(qū)時,B臨時得到A的優(yōu)先級(還是假設A的優(yōu)先級高于B),以便順利完成處理過程,退出臨界區(qū)。之后B的優(yōu)先級恢復。這就是優(yōu)先級繼承的方法。

中斷處理線程化

在linux下,中斷處理程序運行于一個不可調度的上下文中。從CPU響應硬件中斷自動跳轉到內核設定的中斷處理程序去執(zhí)行,到中斷處理程序退出,整個過程是不能被搶占的。

一個進程如果被搶占了,可以通過保存在它的進程控制塊(task_struct)中的信息,在之后的某個時間恢復它的運行。而中斷上下文則沒有task_struct,被搶占了就沒法恢復了。

中斷處理程序不能被搶占,也就意味著中斷處理程序的“優(yōu)先級”比任何進程都高(必須等中斷處理程序完成了,進程才能被執(zhí)行)。但是在實際的應用場景中,可能某些實時進程應該得到比中斷處理程序更高的優(yōu)先級。

于是,一些實時性要求更高的系統(tǒng)就給中斷處理程序賦予了task_struct以及優(yōu)先級,使得它們在必要的時候能夠被高優(yōu)先級的進程搶占。但是顯然,做這些工作是會給系統(tǒng)造成一定開銷的,這也是為了實現(xiàn)“實時”而對性能做出的一種讓步。

(三)進程同步與互斥

多進程系統(tǒng)中避免不了進程之間的相互關系,最主要是兩種關系--同步和互斥。

進程同步 是進程間直接的相互作用,是合作進程間的有意識的行為。我們也要有一定的同步機制保證它們的執(zhí)行次序。

進程互斥是進程之間發(fā)生的一種間接性作用,一般是程序不希望的。通常的情況是兩個或兩個以上的進程需要同時訪問某個共享變量。我們一般將發(fā)生能夠問共享變量的程序段稱為臨界區(qū)。兩個進程不能同時進入臨界區(qū),否則就會導致數(shù)據(jù)的不一致,產生與時間有關的錯誤。解決互斥問題應該滿足互斥和公平兩個原則,即任意時刻只能允許一個進程處于同一共享變量的臨界區(qū),而且不能讓任一進程無限期地等待?;コ鈫栴}可以用硬件方法解決,也可以用軟件方法。

同步是說進程的合作關系,互斥是說進程對資源的競爭關系。

信號量、管程

二,管程:參考自http://hi.baidu.com/zucenaa/blog/item/e63d22277c9d9c09918f9de2.html

信號量機制功能強大,但使用時對信號量的操作分散,而且難以控制,讀寫和維護都很困難。因此后

來又提出了一種集中式同步進程——管程。其基本思想是將共享變量和對它們的操作集中在一個模塊中,操作系統(tǒng)或并發(fā)程序就由這樣的模塊構成。這樣模塊之間聯(lián)

系清晰,便于維護和修改,易于保證正確性。

管程作為一個模塊,它的類型定義如下:

monitor_name = MoNITOR;

共享變量說明;

define 本管程內部定義、外部可調用的函數(shù)名表;

use 本管程外部定義、內部可調用的函數(shù)名表;

內部定義的函數(shù)說明和函數(shù)體

{

共享變量初始化語句;

}

從語言的角度看,管程主要有以下特性:

(1)模塊化。管程是一個基本程序單位,可以單獨編譯;

(2)抽象數(shù)據(jù)類型。管程是中不僅有數(shù)據(jù),而且有對數(shù)據(jù)的操作;

(3)信息掩蔽。管程外可以調用管程內部定義的一些函數(shù),但函數(shù)的具體實現(xiàn)外部不可見;

對于管程中定義的共享變量的所有操作都局限在管程中,外部只能通過調用管程的某些函數(shù)來間接訪問這些變量。因此管程有很好的封裝性。

為了保證共享變量的數(shù)據(jù)一致性,管程應互斥使用。 管程通常是用于管理資源的,因此管程中有進程等待隊列和相應的等待和喚醒操作。在管程入口有一個等待隊列,稱為入口等待隊列。當一個已進入管程的進程等待時,就釋放管程的互斥使用權;當已進入管程的一個進程喚醒另一個進程時,兩者必須有一個退出或停止使用管程。在管程內部,由于執(zhí)行喚醒操作,可能存在多個等待進程(等待使用管程),稱為緊急等待隊列,它的優(yōu)先級高于入口等待隊列。

因此,一個進程進入管程之前要先申請,一般由管程提供一個enter過程;離開時釋放使用權,如果緊急等待隊列不空,則喚醒第一個等待者,一般也由管程提供外部過程leave。

管程內部有自己的等待機制。管程可以說明一種特殊的條件型變量:var c:condition;實際上是一個指針,指向一個等待該條件的PCB隊列。對條件型變量可執(zhí)行wait和signal操作:(聯(lián)系P和V; take和give)

wait(c):若緊急等待隊列不空,喚醒第一個等待者,否則釋放管程使用權。執(zhí)行本操作的進程進入C隊列尾部;

signal(c):若C隊列為空,繼續(xù)原進程,否則喚醒隊列第一個等待者,自己進入緊急等待隊列尾部。

(四)進程間通信(IPC)

進程間通信主要包括 管道,系統(tǒng)IPC(包括消息隊列,信號量,共享內存), SOCKET.

管道分為有名管道和無名管道,無名管道只能用于親屬進程之間的通信,而有名管道則可用于無親屬關系的進程之間。

消息隊列用于運行于同一臺機器上的進程間通信,與管道相似;

消息隊列用于運行于同一臺機器上的進程間通信,與管道相似;

共享內存通常由一個進程創(chuàng)建,其余進程對這塊內存區(qū)進行讀寫。得到共享內存有兩種方式:映射/dev/mem設備和內存映像文件。前一種方式不給系統(tǒng)帶來額外的開銷,但在現(xiàn)實中并不常用,因為它控制存取的是實際的物理內存;

本質上,信號量是一個計數(shù)器,它用來記錄對某個資源(如共享內存)的存取狀況。一般說來,為了獲得共享資源,進程需要執(zhí)行下列操作:

(1)測試控制該資源的信號量;

(2)若此信號量的值為正,則允許進行使用該資源,進程將進號量減1;

(3)若此信號量為0,則該資源目前不可用,進程進入睡眠狀態(tài),直至信號量值大于0,進程被喚醒,轉入步驟(1);

(4)當進程不再使用一個信號量控制的資源時,信號量值加1,如果此時有進程正在睡眠等待此信號量,則喚醒此進程。

套接字通信并不為Linux所專有,在所有提供了TCP/IP協(xié)議棧的操作系統(tǒng)中幾乎都提供了socket,而所有這樣操作系統(tǒng),對套接字的編程方法幾乎是完全一樣的。

管道:速度慢,容量有限,只有父子進程能通訊

FIFO(命名管道):任何進程間都能通訊,但速度慢,命名管道可用于非父子進程,命名管道就是FIFO,管道是先進先出的通訊方式

消息隊列:容量受到系統(tǒng)限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數(shù)據(jù)的問題

信號量:不能傳遞復雜消息,只能用來同步

共享內存區(qū):能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當于線程中的線程安全,當然,共享內存區(qū)同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存。

線程

線程是CPU調度的最小單位,多個線程共享一個進程的地址空間。

線程包含線程ID,程序計數(shù)器,寄存器和棧。

(一)線程調度

(二)線程同步

作者:簡書的王布斯

鏈接:http://m.itdecent.cn/p/7ce30a806c51

來源:簡書

簡書著作權歸作者所有,任何形式的轉載都請聯(lián)系作者獲得授權并注明出處。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 又來到了一個老生常談的問題,應用層軟件開發(fā)的程序員要不要了解和深入學習操作系統(tǒng)呢? 今天就這個問題開始,來談談操...
    tangsl閱讀 4,332評論 0 23
  • 1.內存的頁面置換算法 (1)最佳置換算法(OPT)(理想置換算法):從主存中移出永遠不再需要的頁面;如無這樣的...
    杰倫哎呦哎呦閱讀 3,607評論 1 9
  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進行...
    月亮是我踢彎得閱讀 6,182評論 3 28
  • 極簡生活,大家都已經不再陌生。 追求極致簡單,追求品質,減少身邊不必要的物品,只留下自己需要的,將自己需要的物品縮...
    辰girl閱讀 433評論 2 1
  • 天氣格外的悶熱,屋里屋外仿佛兩個季節(jié)。即受不了空調下的冷風吹,又不愿感受外面的熱浪襲,于是只能用文字來舒緩內心,心...
    藍尋歡閱讀 349評論 0 2

友情鏈接更多精彩內容