文/tangsl(簡書作者)
原文鏈接:http://www.lxweimin.com/p/2b993a4b913e
著作權歸作者所有,轉載請聯系作者獲得授權,并標注“簡書作者”。
又來到了一個老生常談的問題,應用層軟件開發的程序員要不要了解和深入學習操作系統呢? 今天就這個問題開始,來談談操作系統中可以說是最重要的一個概念--進程。
操作系統最主要的兩個職能是管理各種資源和為應用程序提供系統調用接口。這其中關鍵的部分是,cpu到進程的抽象,物理內存到地址空間(虛擬內存)的抽象,磁盤到文件的抽象,而其中后兩部分以進程為基礎,所以嘛,咱重點來討論進程,以及與進程密切相關的線程。
一.先說說概念
進程(process)
狹義的定義:進程就是一段程序的執行過程。
廣義定義:進程是一個具有一定獨立功能的程序關于某次數據集合的一次運行活動,它是操作系統分配資源的基本單元。
簡單來講進程的概念主要有兩點:第一,進程是一個實體。每一個進程都有它自己的地址空間,一般情況下,包括文本區域(text region)、數據區域(data region)和堆棧(stack region)。文本區域存儲處理器執行的代碼;數據區域存儲變量和進程執行期間使用的動態分配的內存;堆棧區域存儲著活動過程中調用的指令和本地變量。第二,進程是一個“執行中的程序”。程序是一個沒有生命的實體,只有處理器賦予程序生命時,它才能成為一個活動的實體,我們稱其為進程。
進程狀態:進程有三個狀態,就緒,運行和阻塞。就緒狀態其實就是獲取了除cpu外的所有資源,只要處理器分配資源馬上就可以運行。運行態就是獲取了處理器分配的資源,程序開始執行,阻塞態,當程序條件不夠時,需要等待條件滿足時候才能執行,如等待I/O操作的時候,此刻的狀態就叫阻塞態。
說說程序,程序是指令和數據的有序集合,其本身沒有任何運動的含義,是一個靜態的概念,而進程則是在處理機上的一次執行過程,它是一個動態的概念。進程是包含程序的,進程的執行離不開程序,進程中的文本區域就是代碼區,也就是程序。
線程(thread)
通常在一個進程中可以包含若干個線程,當然一個進程中至少有一個線程,不然沒有存在的意義。線程可以利用進程所擁有的資源,在引入線程的操作系統中,通常都是把進程作為分配資源的基本單位,而把線程作為獨立運行和獨立調度的基本單位,由于線程比進程更小,基本上不擁有系統資源,故對它的調度所付出的開銷就會小得多,能更高效的提高系統多個程序間并發執行的程度。
多線程(multiThread)
在一個程序中,這些獨立運行的程序片段叫作“線程”(Thread),利用它編程的概念就叫作“多線程處理”。多線程是為了同步完成多項任務,不是為了提高運行效率,而是為了提高資源使用效率來提高系統的效率。線程是在同一時間需要完成多項任務的時候實現的。
最簡單的比喻多線程就像火車的每一節車廂,而進程則是火車。車廂離開火車是無法跑動的,同理火車也不可能只有一節車廂。多線程的出現就是為了提高效率。
二、說說區別
1、進程與線程的區別:
進程和線程的主要差別在于它們是不同的操作系統資源管理方式。進程有獨立的地址空間,一個進程崩潰后,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等于整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對于一些要求同時進行并且又要共享某些變量的并發操作,只能用線程,不能用進程。
1) 簡而言之,一個程序至少有一個進程,一個進程至少有一個線程.
2) 線程的劃分尺度小于進程,使得多線程程序的并發性高。
3) 另外,進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。
4) 線程在執行過程中與進程還是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
5) 從邏輯角度來看,多線程的意義在于一個應用程序中,有多個執行部分可以同時執行。但操作系統并沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。
三、說說優缺點
線程和進程在使用上各有優缺點:線程執行開銷小,但不利于資源的管理和保護;而進程正相反。同時,線程適合于在SMP(多核處理機)機器上運行,而進程則可以跨機器遷移。
四、說說進程和線程的細節,底層構成 和 調度
(一)進程相關的數據結構
為了管理進程,內核必須對每個進程所做的事情進行清楚的描述,例如,內核必須知道進程的優先級,它是在CPU上運行還是因為某些事而被阻塞,給它分配了什么樣的地址空間,允許它訪問哪個文件等。
這些正是進程描述符的作用---進程描述符都是task_struct 數據結構,它的字段包含了與一個進程相關的所有信息。下圖顯示了Linux進程描述符
談談進程的基本信息。
1)標識一個進程--PID
每個進程都必須擁有它自己的進程描述符;進程和進程描述符之間有非常嚴格的一一對應關系,所以我們可以方便地使用32位進程描述符地址標識進程。
進程描述符指針(task_struct*)指向這些地址。內核對進程的大部份引用都是通過進程描述符指針進行的。
另一方面,類Unix橾作系統允許用戶使用一個叫做進程標識符processID(PID)的數來標識進程,PID存放在task_struct的pid字段中。PID被順序編號,新創建進程的PID通常是前一個進程的PID加1。不過,PID的值有一個上限,當內核使用的PID達到這個峰值的時候,就必須開始循環使用已閑置的小PID號。在缺省情況下,最大的PID號是32767。
系統管理員可以通過往/proc/sys/kernel/pid_max 這個文件中寫入一個更小的值來減小PID的上限值,使PID的上限小于32767。在64位體系結構中,系統管理員可以把PID的上限擴大到4194304。
Linux只支持輕量級進程,不支持線程,但為了彌補這樣的缺陷,Linux引入線程組的概念。一個線程組中的所有線程使用和該線程組的領頭線程相同的PID,也就是該組中第一個輕量級進程的PID,它被存入進程描述符的tgid字段中。getpid()系統調用返回當前進程的tgid值而不是pid值,因此,一個多線程應用的所有線程共享相同的PID。絕大多數進程都屬于一個線程組;而線程組的領頭線程其tgid與pid的值相同,因而getpid()系統調用對這類進程所起的作用和一般進程是一樣的。
所以,我們得出一個重要的結論,Linux雖不支持線程,但是它有具備支持線程的操作系統的所有特性,后面講解輕量級進程的概念中還會詳細討論。
2)進程描述符定位
進程是動態實體,其生命周期范圍從幾毫秒到幾個月,因此內核必須同時處理很多進程,并把對應的進程描述符放在動態內存中,而不是放在永久分配給內核的內存區(3G之上的線性地址)。
那么,怎么找到被動態分配的進程描述符呢?我們需要在3G之上線性地址的內存區為每個進程設計一個塊—thread_union。
對每個進程來說,我們需要給其分配兩個頁面,即8192個字節的塊,Linux把兩個不同數據結構緊湊地存放在一個單獨為進程分配的存儲區域內:一個是內核態的進程堆棧,另一個是緊挨著進程描述符的小數據結構thread_info,叫做線程描述符。
考慮到效率問題,內核讓這8k的空間占據連續兩個頁框并讓第一個頁框的起始地址是2^13的倍數。當幾乎沒有可用的動態內存空間時,就會很難找到這樣的兩個連續頁框,因為空閑空間可能存在大量的碎片(注意,這里是物理空間,見“伙伴系統算法”博文)。因此,在80x86體系結構中,在編譯時可以進行設置,以使內核棧和線程描述符跨越一個單獨的頁框(因為主要存在的單頁的碎片)。在“Linux中的分段”的博文中我們已經知道,內核態的進程訪問處于內核數據段的棧,也就是我們Linux在3G以上內存空間為每個進程設計這么一個棧的目的,這個棧不同于用戶態的進程所用的棧。因為內核控制路徑使用很少的棧,因此只需要幾千個字節的內核態堆棧。所以,對棧和thread_info來說,8KB足夠了。不過,如果只使用一個頁框存放這兩個結構的話,內核要采用一些額外的棧以防止中斷和異常的深度嵌套而引起的溢出。
下圖顯示了在2頁(8KB)內存區中存放兩種數據結構的方式。線程描述符駐留于這個內存區的開始位置,而棧從末端向下增長。該圖還顯示了如何通過task字段與task_struct結構相互關聯。
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系統中,棧起始于末端,并朝這個內存區的起始方向增長。從用戶態切換到內核態以后,進程的內核棧總是空的,因此,esp寄存器指向這個棧的頂端。
一旦數據寫入堆棧,esp的值就遞減。特別要注意,這里的數據是指內核數據,其實用得很少,所以大多數時候這個內核棧是空的。因為thread_info
結構是52個字節的長度,所以內核棧能擴展到8140個字節。C語言使用下列聯合結構,方便地表示一個進程的線程描述符和內核棧:
union thread_union {
struct thread_info thread_info;
unsigned long stack[2048]; /* 1024 for 4KB stacks */
};
內核使用alloc_thread_info 和 free_thread_info宏分配和釋放存儲thread_info結構和內核棧的內存區。
3)標識當前進程
我們再從效率的觀點來看,剛才所講的thread_info結構與內核態堆棧之間的緊密結合提供的主要好處還在:內核很容易從esp寄存器的值獲得當前在CPU上正在運行進程的thread_info結構的地址。事實上,如果thread_union的長度是8K(213字節),則內核屏蔽掉esp的低13位有效位就可以獲得thread_info結構的基地址;而如果thread_union的長度是4K,內核需要蔽掉esp的低12位有效位。這項工作由current_thread_info()函數來完成,它產生如下一些匯編指令:
movl $0xffffe000,%ecx /* or 0xfffff000 for 4KB stacks */
andl %esp,%ecx
movl %ecx,p
這三條指令執行后,p就是在執行指令的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,所以執行完這三條指令之后,p就是CPU上運行進程的描述符指針。
current宏經常作為進程描述符字段的前綴出現在內核代碼中,例如,current->pid返回在CPU上正在執行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字段描述了進程當前所處的狀態。它由一組標志組成,其中每個標志描述一種可能的進程狀態。在當前的Linux版本中,這些狀態是互斥的,因此,嚴格意義上來說,只能設置一種狀態,其余的標志位將被清除。下面是可能的狀態:
可運行狀態(TASK_RUNNING)
進程要么在CPU上執行,要么準備執行。
可中斷的等待狀態(TASK_INTERRUPTIBLE)
進程被掛起(睡眠),直到某個條件變為真。產生一個硬件中斷、釋放進程正在等待的系統資源、或傳遞一個信號都是可以喚醒進程的條件(把進程狀態放回到TASK_RUNNING)。
不可中斷的等待狀態(TASK_UNINTERRUPTIBLE)
與可中斷的等待狀態類似,但有一個例外,把信號傳遞到該睡眠進程時,不能改變它的狀態。這種狀態很少用到,但在一些特定條件下(進程必須等待,直到一個不能被中斷的時事件發生),這種狀態是很有用的。例如,當進程打開一個設備文件,其相應的設備驅動程序開始探測相應的硬件設備時會用到這種狀態。探測完成以前,設備驅動程序不能被中斷,否則,硬件設備會處于不可預知的狀態。
暫停狀態(TASK_STOPPED)
進程的執行被暫停。當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU信號后,進人暫停狀態。
跟蹤狀態(TASK_TRACED)
進程的執行已由debugger程序暫停。當一個進程被另一個進程監控時(例如debugger執行ptrace()系統調用監控一個測試程序)任何信號都可以把這個進程置于TASK_TRACED狀態。
還有兩個進程狀態既可以存放在進程描述符的state字段啊中,也可以存放在exit_state中字段中。從這兩個字段的名稱可以看出,只有當進程的執行被終止時,進程的狀態才會變成此兩種中的一種:
僵死狀態(EXIT_ZOMBIE)
進程的執行被終止,但是父進程還沒發布wait4()或waitpid()系統調用來返回有關死亡進程的信息。發布wait()類系統調用前,內核不能丟棄包含在死進程描述符中的數據,因為父進程可能還需要它。
僵死撤銷狀態(EXIT_DEAD)
終狀態:由于父進程剛發出wait4()或waitpid()系統調用,因而進程由系統刪除。為了防止其他執行線程在同一個進程上也執行wait()類系統調用(這也是一種競爭條件),而把進程的狀態由僵死(EXIT_ZOMBIE)狀態改為僵死撤銷狀態(EXIT_DEAD)
state字段的值通常用一個簡單的賦值語句設置,例如:
p->state = TASK_RUNNING;
內核也使用set_task_state和set_current_state宏:它們分別設置指定進程的狀態和當前執行進程的狀態。此外,這些宏確保編譯程序或CPU控制單元不把賦值操作和其他指令混合。混合指令的順序有時會導致災難性的后果。
6)TASK_RUNNING狀態的進程鏈表
當內核尋找到一個新進程在CPU上運行時,必須只考慮可運行進程(即處在TASK_RUNNING狀態的進程)。
早先的Linux版本把所有的可運行進程都放在同一個叫做運行隊列(runqueue)的鏈表中,由于維持鏈表中的進程優先級排序的開銷過大,因此,早期的調度程序不得不為選擇“最佳”可運行進程而掃描整個隊列。
Linux 2.6實現的運行隊列有所不同。其目的是讓調度程序能在固定的時間內選出“最佳”可運行隊列,與進程中可運行的進程數無關。
提高調度程序運行速度的訣竅是建立多個可運行進程鏈表,每種進程優先級對應一個不同的鏈表。每個task_struct描述符包含一個list_head類型的字段run_list。如果進程的優先權等于k(其取值范圍從0到139),run_list字段就把該進程的優先級鏈入優先級為k的可運行進程的鏈表中。此外,在多處理器系統中,每個CPU都有它自己的運行隊列,即它自己的進程鏈表集。這是一個通過使數據結構更復雜來改善性能的典型例子:調度程序的操作效率的確更高了,但運行隊列的鏈表卻為此被拆分成140個不同的隊列!
內核必須為系統中每個運行隊列保存大量的數據,不過運行隊列的主要數據結構還是組成運行隊列的進程描述符鏈表,所有這些鏈表都由一個單獨的prio_array_t數據結構來實現。
enqueue_task(p,array)函數把進程描述符(p參數)插入到某個運行隊列的鏈表(基于prio_array_t結構的array參數),其代碼本質上等同于如下代碼:
list_add_tail(&p->run_list, &array->queue[p->prio]);
__set_bit(p->prio, array->bitmap);
array->nr_active++;
p->array = array;
進程描述符的prio字段存放進程的動態優先權,而array字段是一個指針,指向當前運行隊列的proo_array_t數據結構。類似地,dequeue_task(p,array)函數從運行隊列的鏈表中刪除一個進程的描述符。
7)進程間關系
父子兄弟關系:
程序創建的進程具有父/子關系。如果一個進程創建多個子進程時,則子進程之間具有兄弟關系。進程0和進程1是由內核創建的;進程1(init)是所有進程的祖先。
在進程描述符中引入幾個字段來表示這些關系,我們假設擁有該task_struct結構的這個進程叫P:
real_parent——指向創建了P進程的描述符,如果進程P的父進程不存在,就指向進程1的描述符(因此,如果用戶運行了一個后臺進程而且退出了shell,后臺進程就會變成init的子進程)。
parent——指向P的當前父進程(這種進程的子進程終止時,必須向父進程發信號)。它的值通常與reak_parent一致,但偶爾也可以不同,例如,當另一個進程發出監控P的ptrace系統調用請求時。
children——鏈表的頭部,鏈表中所有的元素都是P創建的子進程。
sibling——指向兄弟進程鏈表中的下一個元素或前一個元素的指針,這些兄弟進程的父進程跟P是一樣的。
下圖顯示了一組進程間的親屬關系,進程P0創建了P1,P2,P3,進程P3又創建了P4。
need-to-insert-img
其他關系:此外,進程之間還存在其他關系:一個進程可能是一個進程組或登錄會話的領頭進程,也可能是一個線程組的領頭進程,他還可能跟蹤其他進程的執行,下面就列出進程描述符中的一些字段,這些字段建立起了進程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()系統調用提供服務時就會發生這種情況:當進程P1希望向另一個進程P2發送一個信號時,P1調用kill()系統調用,其參數為P2的PID,內核從這個PID導出其對應的進程描述符,然后從該task_struct中取出記錄掛起信號的數據結構指針。
那么如何得到這個task_struct呢?首先想到for_each_process(p)。不行,雖然順序掃描進程鏈表并檢查進程描述符的pid字段是可行的,但相當低效。為了加速查找,Linux內核引入了4個散列表。需要4個散列表是因為進程描述符包含了表示不同類型PID的字段,而且每種類型的PID需要它自己的散列表:
PIDTYPE_PID??? pid??? 進程的PID
PIDTYPE_TGID??? tgid??? 線程組領頭進程的PID
PIDTYPE_PGID??? pgrp??? 進程組領頭進程的PID
PIDTYPE_SID??? session??? 會話領頭的PID
內核初始化期間動態地為4個散列表分配空間,并把它們的地址存入pid_hash數組。一個散列表的長度依賴于可用的RAM的容量,例如:一個系統擁有512MB的RAM,那么每個散列表就被存在4個頁框中,可擁有2048個表項。
用pid_hashfn宏把PID轉化為表索引:
#define pid_hashfn(x) hash_long((unsigned long) x, pidhash_shift)
變量pidhash_shift用來存放表索引的長度(以位為單位的長度,在我們這里是11位)。很多散列函數都使用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。
正如計算機科學的基礎課程所闡述的那樣,散列函數并不總能確保PID與表的索引一一對應。兩個不同的PID散列到相同的表索引稱為沖突(colliding)。Linux利用鏈表來處理沖突的PID:每個表項是由沖突的進程描述符組成的雙向循環鏈表
(二)進程調度
1)進程調度的目標
1.高效性:高效意味著在相同的時間下要完成更多的任務,調度程序會被頻繁的執行,所以調度程序要盡可能高效。
2.加強交互性能:在系統相當的負載下,也要保證系統的響應時間
3.保證公平和避免饑渴
4.SMP調度:調度程序必須支持多處理系統
5.軟實時調度:系統必須有效的調用實時進程,但不保證一定滿足其要求。
2)進程優先級
進程提供了兩種優先級,一種是普通的進程優先級,一種是實時進程優先級。
前者適用SCHED_NORMAL調度策略,后者可選SCHED_FIFO或SCHED_RR調度策略,任何時候,實時進程的優先級都高于普通進程,實時進程只會被更高級的實時進程搶占,同級實時進程之間是按照FIFO(一次機會做完)或者RR(多次輪轉)規則調度的。
實時進程,只有靜態優先級,因為內核不會再根據休眠等因素對其靜態優先級做調整,其范圍在0~MAX_RT_PRIO-1間。默認MAX_RT_PRIO配置為100,也即,默認的實時優先級范圍是0~99。而nice值,影響的是優先級在MAX_RT_PRIO~MAX_RT_PRIO+40范圍內的進程。
不同與普通進程,系統調度時,實時優先級高的進程總是先于優先級低的進程執行,直到實時優先級高的實時進程無法執行。實時進程總是被認為處于活動狀態。如果有數個 優先級相同的實時進程,那么系統就會按照進程出現在隊列上的順序選擇進程,假設當前CPU運行的實時進程A的優先級為a,而此時有個優先級為b的實時進程B進入可運行狀態,那么只要b
不同調度策略的實時進程只有在相同優先級時才有可比性:
1. 對于FIFO的進程,意味著只有當前進程執行完畢才會輪到其他進程執行。由此可見相當霸道。
2. 對于RR的進程。一旦時間片消耗完畢,則會將該進程置于隊列的末尾,然后運行其他相同優先級的進程,如果沒有其他相同優先級的進程,則該進程會繼續執行。
總而言之,對于實時進程,高優先級的進程就是大爺。它執行到沒法執行了,才輪到低優先級的進程執行。
普通進程的調度
Linux對于普通的進程,根據動態優先級進行調度,而動態優先級是由靜態優先級調整而來,Linux下,靜態優先級是用戶不可見的,隱藏在內核中,而內核提供給用戶一個可以影響靜態優先級的接口,那就是nice值。
關系如下:
static_prio =MAX_RT_PRIO+nice+20
nice值的范圍是-20~19,因而靜態優先級范圍在100~139之間,nice數值越大就使得static_prio越大,最終進程優先級就越低。
我們前面也說了,系統調度時,還會考慮其他因素,因而會計算出一個叫進程動態優先級的東西,根據此來實施調度。因為,不僅要考慮靜態優先級,也要考慮進程
的屬性。例如如果進程屬于交互式進程,那么可以適當的調高它的優先級,使得界面反應地更加迅速,從而使用戶得到更好的體驗。Linux2.6
在這方面有了較大的提高。Linux2.6認為,交互式進程可以從平均睡眠時間這樣一個measurement進行判斷。進程過去的睡眠時間越多,則越有
可能屬于交互式進程。則系統調度時,會給該進程更多的獎勵(bonus),以便該進程有更多的機會能夠執行。獎勵(bonus)從0到10不等。
系統會嚴格按照動態優先級高低的順序安排進程執行。動態優先級高的進程進入非運行狀態,或者時間片消耗完畢才會輪到動態優先級較低的進程執行。動態優先級的計算主要考慮兩個因素:靜態優先級,進程的平均睡眠時間也即bonus。計算公式如下,
dynamic_prio?= max (100, min (static_prio - bonus?+ 5, 139))
為什么根據睡眠和運行時間確定獎懲分數是合理的
睡眠和CPU耗時反應了進程IO密集和CPU密集兩大瞬時特點,不同時期,一個進程可能即是CPU密集型也是IO密集型進程。對于表現為IO密集的進程,應該經常運行,但每次時間片不要太長。對于表現為CPU密集的進程,CPU不應該讓其經常運行,但每次運行時間片要長。交互進程為例,假如之前其其大部分時間在于等待CPU,這時為了調高相應速度,就需要增加獎勵分。另一方面,如果此進程總是耗盡每次分配給它的時間片,為了對其他進程公平,就要增加這個進程的懲罰分數。可以參考CFS的virtutime機制.
3)現代方法CFS
不再單純依靠進程優先級絕對值,而是參考其絕對值,綜合考慮所有進程的時間,給出當前調度時間單位內其應有的權重,也就是,每個進程的權重X單位時間=應獲cpu時間,但是這個應得的cpu時間不應太小(假設閾值為1ms),否則會因為切換得不償失。但是,當進程足夠多時候,肯定有很多不同權重的進程獲得相
同的時間——最低閾值1ms,所以,CFS只是近似完全公平。
4)Linux進程狀態機
進程是通過fork系列的系統調用(fork clone,vfork)來創建的,內核,內核模塊也可以通過kernel_thread函數創建內核進程,這些創建子進程的函數本質上都完成了相同的功能——將調用進程復制一份,得到子進程。(可以通過選項參數來決定各種資源是共享、還是私有。)那么既然調用進程處于TASK_RUNNING狀態(否則,它若不是正在運行,又怎么進行調用?),則子進程默認也處于TASK_RUNNING狀態。
另外,在系統調用clone和內核函數kernel_thread也接受CLONE_STOPPED選項,從而將子進程的初始狀態置為 TASK_STOPPED。
進程創建后,狀態可能發生一系列的變化,直到進程退出。而盡管進程狀態有好幾種,但是進程狀態的變遷卻只有兩個方向——從TASK_RUNNING狀態變為非TASK_RUNNING狀態、或者從非TASK_RUNNING狀態變為TASK_RUNNING狀態。總之,TASK_RUNNING是必經之路,不可能兩個非RUN狀態直接轉換。
也就是說,如果給一個TASK_INTERRUPTIBLE狀態的進程發送SIGKILL信號,這個進程將先被喚醒(進入TASK_RUNNING狀態),然后再響應SIGKILL信號而退出(變為TASK_DEAD狀態)。并不會從TASK_INTERRUPTIBLE狀態直接退出。
進程從非TASK_RUNNING狀態變為TASK_RUNNING狀態,是由別的進程(也可能是中斷處理程序)執行喚醒操作來實現的。執行喚醒的
進程設置被喚醒進程的狀態為TASK_RUNNING,然后將其task_struct結構加入到某個CPU的可執行隊列中。于是被喚醒的進程將有機會被
調度執行。
而進程從TASK_RUNNING狀態變為非TASK_RUNNING狀態,則有兩種途徑:
1、響應信號而進入TASK_STOPED狀態、或TASK_DEAD狀態;
2、執行系統調用主動進入TASK_INTERRUPTIBLE狀態(如nanosleep系統調用)、或TASK_DEAD狀態(如exit系統調用);或由于執行系統調用需要的資源得不到滿 ?足,而進入TASK_INTERRUPTIBLE狀態或TASK_UNINTERRUPTIBLE狀態(如select系統調用)。
顯然,這兩種情況都只能發生在進程正在CPU上執行的情況下。
通過ps命令我們能夠查看到系統中存在的進程,以及它們的狀態:R(TASK_RUNNING),可執行狀態。
只有在該狀態的進程才可能在CPU上運行。而同一時刻可能有多個進程處于可執行狀態,這些進程的task_struct結構(進程控制塊)被放入對應CPU的可執行隊列中(一個進程最多只能出現在一個CPU的可執行隊列中)。進程調度器的任務就是從各個CPU的可執行隊列中分別選擇一個進程在該CPU上運行。
只要可執行隊列不為空,其對應的CPU就不能偷懶,就要執行其中某個進程。一般稱此時的CPU“忙碌”。對應的,CPU“空閑”就是指其對應的可執行隊列為空,以致于CPU無事可做。
有人問,為什么死循環程序會導致CPU占用高呢?因為死循環程序基本上總是處于TASK_RUNNING狀態(進程處于可執行隊列中)。除非一些非常極端情況(比如系統內存嚴重緊缺,導致進程的某些需要使用的頁面被換出,并且在頁面需要換入時又無法分配到內存……),否則這個進程不會睡眠。所以CPU的可執行隊列總是不為空(至少有這么個進程存在),CPU也就不會“空閑”。
很多操作系統教科書將正在CPU上執行的進程定義為RUNNING狀態、而將可執行但是尚未被調度執行的進程定義為READY狀態,這兩種狀態在linux下統一為 TASK_RUNNING狀態。
S(TASK_INTERRUPTIBLE),可中斷的睡眠狀態。
處于這個狀態的進程因為等待某某事件的發生(比如等待socket連接、等待信號量),而被掛起。這些進程的task_struct結構被放入對應事件的等待隊列中。當這些事件發生時(由外部中斷觸發、或由其他進程觸發),對應的等待隊列中的一個或多個進程將被喚醒。
通過ps命令我們會看到,一般情況下,進程列表中的絕大多數進程都處于TASK_INTERRUPTIBLE狀態(除非機器的負載很高)。畢竟CPU就這么一兩個,進程動輒幾十上百個,如果不是絕大多數進程都在睡眠,CPU又怎么響應得過來。
D(TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。
與TASK_INTERRUPTIBLE狀態類似,進程處于睡眠狀態,但是此刻進程是不可中斷的。不可中斷,指的并不是CPU不響應外部硬件的中斷,而是指進程不響應異步信號。
絕大多數情況下,進程處在睡眠狀態時,總是應該能夠響應異步信號的。否則你將驚奇的發現,kill -9竟然殺不死一個正在睡眠的進程了!于是我們也很好理解,為什么ps命令看到的進程幾乎不會出現TASK_UNINTERRUPTIBLE狀態,而總是TASK_INTERRUPTIBLE狀態。
而TASK_UNINTERRUPTIBLE狀態存在的意義就在于,內核的某些處理流程是不能被打斷的。如果響應異步信號,程序的執行流程中就會被插入一段用于處理異步信號的流程(這個插入的流程可能只存在于內核態,也可能延伸到用戶態),于是原有的流程就被中斷了(參見《linux異步信號handle淺析》)。
在進程對某些硬件進行操作時(比如進程調用read系統調用對某個設備文件進行讀操作,而read系統調用最終執行到對應設備驅動的代碼,并與對應的物理設備進行交互),可能需要使用TASK_UNINTERRUPTIBLE狀態對進程進行保護,以避免進程與設備交互的過程被打斷,造成設備陷入不可控的狀態。(比如read系統調用觸發了一次磁盤到用戶空間的內存的DMA,如果DMA進行過程中,進程由于響應信號而退出了,那么DMA正在訪問的內存可能就要被釋放了。)這種情況下的TASK_UNINTERRUPTIBLE狀態總是非常短暫的,通過ps命令基本上不可能捕捉到。
linux系統中也存在容易捕捉的TASK_UNINTERRUPTIBLE狀態。執行vfork系統調用后,父進程將進入TASK_UNINTERRUPTIBLE狀態,直到子進程調用exit或exec。
通過下面的代碼就能得到處于TASK_UNINTERRUPTIBLE狀態的進程:
#include
void main() {
if (!vfork()) sleep(100);
}
向進程發送一個SIGSTOP信號,它就會因響應該信號而進入TASK_STOPPED狀態(除非該進程本身處于TASK_UNINTERRUPTIBLE狀態而不響應信號)。(SIGSTOP與SIGKILL信號一樣,是非常強制的。不允許用戶進程通過signal系列的系統調用重新設置對應的信號處理函數。)
向進程發送一個SIGCONT信號,可以讓其從TASK_STOPPED狀態恢復到TASK_RUNNING狀態。
當進程正在被跟蹤時,它處于TASK_TRACED這個特殊的狀態。“正在被跟蹤”指的是進程暫停下來,等待跟蹤它的進程對它進行操作。比如在gdb中對被跟蹤的進程下一個斷點,進程在斷點處停下來的時候就處于TASK_TRACED狀態。而在其他時候,被跟蹤的進程還是處于前面提到的那些狀態。
對于進程本身來說,TASK_STOPPED和TASK_TRACED狀態很類似,都是表示進程暫停下來。
而TASK_TRACED狀態相當于在TASK_STOPPED之上多了一層保護,處于TASK_TRACED狀態的進程不能響應SIGCONT信號而被喚醒。只能等到調試進程通過ptrace系統調用執行PTRACE_CONT、PTRACE_DETACH等操作(通過ptrace系統調用的參數指定操作),或調試進程退出,被調試的進程才能恢復TASK_RUNNING狀態。
Z(TASK_DEAD - EXIT_ZOMBIE),退出狀態,進程成為僵尸進程。
進程在退出的過程中,處于TASK_DEAD狀態。
在這個退出過程中,進程占有的所有資源將被回收,除了task_struct結構(以及少數資源)以外。于是進程就只剩下task_struct這么個空殼,故稱為僵尸。
之所以保留task_struct,是因為task_struct里面保存了進程的退出碼、以及一些統計信息。而其父進程很可能會關心這些信息。比如在shell中,$?變量就保存了最后一個退出的前臺進程的退出碼,而這個退出碼往往被作為if語句的判斷條件。
當然,內核也可以將這些信息保存在別的地方,而將task_struct結構釋放掉,以節省一些空間。但是使用task_struct結構更為方便,因為在內核中已經建立了從pid到task_struct查找關系,還有進程間的父子關系。釋放掉task_struct,則需要建立一些新的數據結構,以便讓父進程找到它的子進程的退出信息。
父進程可以通過wait系列的系統調用(如wait4、waitid)來等待某個或某些子進程的退出,并獲取它的退出信息。然后wait系列的系統調用會順便將子進程的尸體(task_struct)也釋放掉。
子進程在退出的過程中,內核會給其父進程發送一個信號,通知父進程來“收尸”。這個信號默認是SIGCHLD,但是在通過clone系統調用創建子進程時,可以設置這個信號。
通過下面的代碼能夠制造一個EXIT_ZOMBIE狀態的進程:
#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
只要父進程不退出,這個僵尸狀態的子進程就一直存在。那么如果父進程退出了呢,誰又來給子進程“收尸”?
當進程退出的時候,會將它的所有子進程都托管給別的進程(使之成為別的進程的子進程)。托管給誰呢?可能是退出進程所在進程組的下一個進程(如果存在的話),或者是1號進程。所以每個進程、每時每刻都有父進程存在。除非它是1號進程。
1號進程,pid為1的進程,又稱init進程。
linux系統啟動后,第一個被創建的用戶態進程就是init進程。它有兩項使命:
1、執行系統初始化腳本,創建一系列的進程(它們都是init進程的子孫);
2、在一個死循環中等待其子進程的退出事件,并調用waitid系統調用來完成“收尸”工作;
init進程不會被暫停、也不會被殺死(這是由內核來保證的)。它在等待子進程退出的過程中處于TASK_INTERRUPTIBLE狀態,“收尸”過程中則處于TASK_RUNNING狀態。
X(TASK_DEAD - EXIT_DEAD),退出狀態,進程即將被銷毀。
而進程在退出過程中也可能不會保留它的task_struct。比如這個進程是多線程程序中被detach過的進程(進程?線程?參見《linux線程淺析》)。或者父進程通過設置SIGCHLD信號的handler為SIG_IGN,顯式的忽略了SIGCHLD信號。(這是posix的規定,盡管子進程的退出信號可以被設置為SIGCHLD以外的其他信號。)
此時,進程將被置于EXIT_DEAD退出狀態,這意味著接下來的代碼立即就會將該進程徹底釋放。所以EXIT_DEAD狀態是非常短暫的,幾乎不可能通過ps命令捕捉到。
5)調度觸發的時機
調度的觸發主要有如下幾種情況:
1、當前進程(正在CPU上運行的進程)狀態變為非可執行狀態。
進程執行系統調用主動變為非可執行狀態。比如執行nanosleep進入睡眠、執行exit退出、等等;
進程請求的資源得不到滿足而被迫進入睡眠狀態。比如執行read系統調用時,磁盤高速緩存里沒有所需要的數據,從而睡眠等待磁盤IO;
進程響應信號而變為非可執行狀態。比如響應SIGSTOP進入暫停狀態、響應SIGKILL退出、等等;
2、搶占。進程運行時,非預期地被剝奪CPU的使用權。這又分兩種情況:進程用完了時間片、或出現了優先級更高的進程。
優先級更高的進程受正在CPU上運行的進程的影響而被喚醒。如發送信號主動喚醒,或因為釋放互斥對象(如釋放鎖)而被喚醒;
內核在響應時鐘中斷的過程中,發現當前進程的時間片用完;
內核在響應中斷的過程中,發現優先級更高的進程所等待的外部資源的變為可用,從而將其喚醒。比如CPU收到網卡中斷,內核處理該中斷,發現某個socket可讀,于是喚醒正在等待讀這個socket的進程;再比如內核在處理時鐘中斷的過程中,觸發了定時器,從而喚醒對應的正在nanosleep系統調用中睡眠的進程;
6)內核搶占
理想情況下,只要滿足“出現了優先級更高的進程”這個條件,當前進程就應該被立刻搶占。但是,就像多線程程序需要用鎖來保護臨界區資源一樣,內核中也存在很多這樣的臨界區,不大可能隨時隨地都能接收搶占。
linux 2.4時的設計就非常簡單,內核不支持搶占。進程運行在內核態時(比如正在執行系統調用、正處于異常處理函數中),是不允許搶占的。必須等到返回用戶態時才會觸發調度(確切的說,是在返回用戶態之前,內核會專門檢查一下是否需要調度);
linux 2.6則實現了內核搶占,但是在很多地方還是為了保護臨界區資源而需要臨時性的禁用內核搶占。
也有一些地方是出于效率考慮而禁用搶占,比較典型的是spin_lock。spin_lock是這樣一種鎖,如果請求加鎖得不到滿足(鎖已被別的進程占有),則當前進程在一個死循環中不斷檢測鎖的狀態,直到鎖被釋放。
為什么要這樣忙等待呢?因為臨界區很小,比如只保護“i+=j++;”這么一句。如果因為加鎖失敗而形成“睡眠-喚醒”這么個過程,就有些得不償失了。
那么既然當前進程忙等待(不睡眠),誰又來釋放鎖呢?其實已得到鎖的進程是運行在另一個CPU上的,并且是禁用了內核搶占的。這個進程不會被其他進程搶占,所以等待鎖的進程只有可能運行在別的CPU上。(如果只有一個CPU呢?那么就不可能存在等待鎖的進程了。)
而如果不禁用內核搶占呢?那么得到鎖的進程將可能被搶占,于是可能很久都不會釋放鎖。于是,等待鎖的進程可能就不知何年何月得償所望了。
對于一些實時性要求更高的系統,則不能容忍spin_lock這樣的東西。寧可改用更費勁的“睡眠-喚醒”過程,也不能因為禁用搶占而讓更高優先級的進程等待。比如,嵌入式實時linux montavista就是這么干的。
由此可見,實時并不代表高效。很多時候為了實現“實時”,還是需要對性能做一定讓步的。
7)多處理器下的負載均衡
前面我們并沒有專門討論多處理器對調度程序的影響,其實也沒有什么特別的,就是在同一時刻能有多個進程并行地運行而已。那么,為什么會有“多處理器負載均衡”這個事情呢?
如果系統中只有一個可執行隊列,哪個CPU空閑了就去隊列中找一個最合適的進程來執行。這樣不是很好很均衡嗎?
的確如此,但是多處理器共用一個可執行隊列會有一些問題。顯然,每個CPU在執行調度程序時都需要把隊列鎖起來,這會使得調度程序難以并行,可能導致系統性能下降。而如果每個CPU對應一個可執行隊列則不存在這樣的問題。
另外,多個可執行隊列還有一個好處。這使得一個進程在一段時間內總是在同一個CPU上執行,那么很可能這個CPU的各級cache中都緩存著這個進程的數據,很有利于系統性能的提升。
所以,在linux下,每個CPU都有著對應的可執行隊列,而一個可執行狀態的進程在同一時刻只能處于一個可執行隊列中。
于是,“多處理器負載均衡”這個麻煩事情就來了。內核需要關注各個CPU可執行隊列中的進程數目,在數目不均衡時做出適當調整。什么時候需要調整,以多大力度進程調整,這些都是內核需要關心的。當然,盡量不要調整最好,畢竟調整起來又要耗CPU、又要鎖可執行隊列,代價還是不小的。
另外,內核還得關心各個CPU的關系。兩個CPU之間,可能是相互獨立的、可能是共享cache的、甚至可能是由同一個物理CPU通過超線程技術虛擬出來的……CPU之間的關系也是實現負載均衡的重要依據。關系越緊密,進程在它們之間遷移的代價就越小。參見《linux內核SMP負載均衡淺析》。
優先級繼承
由于互斥,一個進程(設為A)可能因為等待進入臨界區而睡眠。直到正在占有相應資源的進程(設為B)退出臨界區,進程A才被喚醒。
可能存在這樣的情況:A的優先級非常高,B的優先級非常低。B進入了臨界區,但是卻被其他優先級較高的進程(設為C)搶占了,而得不到運行,也就無法退出臨界區。于是A也就無法被喚醒。
A有著很高的優先級,但是現在卻淪落到跟B一起,被優先級并不太高的C搶占,導致執行被推遲。這種現象就叫做優先級反轉。
出現這種現象是很不合理的。較好的應對措施是:當A開始等待B退出臨界區時,B臨時得到A的優先級(還是假設A的優先級高于B),以便順利完成處理過程,退出臨界區。之后B的優先級恢復。這就是優先級繼承的方法。
中斷處理線程化
在linux下,中斷處理程序運行于一個不可調度的上下文中。從CPU響應硬件中斷自動跳轉到內核設定的中斷處理程序去執行,到中斷處理程序退出,整個過程是不能被搶占的。
一個進程如果被搶占了,可以通過保存在它的進程控制塊(task_struct)中的信息,在之后的某個時間恢復它的運行。而中斷上下文則沒有task_struct,被搶占了就沒法恢復了。
中斷處理程序不能被搶占,也就意味著中斷處理程序的“優先級”比任何進程都高(必須等中斷處理程序完成了,進程才能被執行)。但是在實際的應用場景中,可能某些實時進程應該得到比中斷處理程序更高的優先級。
于是,一些實時性要求更高的系統就給中斷處理程序賦予了task_struct以及優先級,使得它們在必要的時候能夠被高優先級的進程搶占。但是顯然,做這些工作是會給系統造成一定開銷的,這也是為了實現“實時”而對性能做出的一種讓步。
(三)進程同步與互斥
多進程系統中避免不了進程之間的相互關系,最主要是兩種關系--同步和互斥。
進程同步 是進程間直接的相互作用,是合作進程間的有意識的行為。我們也要有一定的同步機制保證它們的執行次序。
進程互斥是進程之間發生的一種間接性作用,一般是程序不希望的。通常的情況是兩個或兩個以上的進程需要同時訪問某個共享變量。我們一般將發生能夠問共享變量的程序段稱為臨界區。兩個進程不能同時進入臨界區,否則就會導致數據的不一致,產生與時間有關的錯誤。解決互斥問題應該滿足互斥和公平兩個原則,即任意時刻只能允許一個進程處于同一共享變量的臨界區,而且不能讓任一進程無限期地等待。互斥問題可以用硬件方法解決,也可以用軟件方法。
同步是說進程的合作關系,互斥是說進程對資源的競爭關系。
信號量、管程
二,管程:參考自http://hi.baidu.com/zucenaa/blog/item/e63d22277c9d9c09918f9de2.html
信號量機制功能強大,但使用時對信號量的操作分散,而且難以控制,讀寫和維護都很困難。因此后
來又提出了一種集中式同步進程——管程。其基本思想是將共享變量和對它們的操作集中在一個模塊中,操作系統或并發程序就由這樣的模塊構成。這樣模塊之間聯
系清晰,便于維護和修改,易于保證正確性。
管程作為一個模塊,它的類型定義如下:
monitor_name = MoNITOR;
共享變量說明;
define 本管程內部定義、外部可調用的函數名表;
use 本管程外部定義、內部可調用的函數名表;
內部定義的函數說明和函數體
{
共享變量初始化語句;
}
從語言的角度看,管程主要有以下特性:
(1)模塊化。管程是一個基本程序單位,可以單獨編譯;
(2)抽象數據類型。管程是中不僅有數據,而且有對數據的操作;
(3)信息掩蔽。管程外可以調用管程內部定義的一些函數,但函數的具體實現外部不可見;
對于管程中定義的共享變量的所有操作都局限在管程中,外部只能通過調用管程的某些函數來間接訪問這些變量。因此管程有很好的封裝性。
為了保證共享變量的數據一致性,管程應互斥使用。 管程通常是用于管理資源的,因此管程中有進程等待隊列和相應的等待和喚醒操作。在管程入口有一個等待隊列,稱為入口等待隊列。當一個已進入管程的進程等待時,就釋放管程的互斥使用權;當已進入管程的一個進程喚醒另一個進程時,兩者必須有一個退出或停止使用管程。在管程內部,由于執行喚醒操作,可能存在多個等待進程(等待使用管程),稱為緊急等待隊列,它的優先級高于入口等待隊列。
因此,一個進程進入管程之前要先申請,一般由管程提供一個enter過程;離開時釋放使用權,如果緊急等待隊列不空,則喚醒第一個等待者,一般也由管程提供外部過程leave。
管程內部有自己的等待機制。管程可以說明一種特殊的條件型變量:var c:condition;實際上是一個指針,指向一個等待該條件的PCB隊列。對條件型變量可執行wait和signal操作:(聯系P和V; take和give)
wait(c):若緊急等待隊列不空,喚醒第一個等待者,否則釋放管程使用權。執行本操作的進程進入C隊列尾部;
signal(c):若C隊列為空,繼續原進程,否則喚醒隊列第一個等待者,自己進入緊急等待隊列尾部。
(四)進程間通信(IPC)
進程間通信主要包括 管道,系統IPC(包括消息隊列,信號量,共享內存), SOCKET.
管道分為有名管道和無名管道,無名管道只能用于親屬進程之間的通信,而有名管道則可用于無親屬關系的進程之間。
消息隊列用于運行于同一臺機器上的進程間通信,與管道相似;
消息隊列用于運行于同一臺機器上的進程間通信,與管道相似;
共享內存通常由一個進程創建,其余進程對這塊內存區進行讀寫。得到共享內存有兩種方式:映射/dev/mem設備和內存映像文件。前一種方式不給系統帶來額外的開銷,但在現實中并不常用,因為它控制存取的是實際的物理內存;
本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。一般說來,為了獲得共享資源,進程需要執行下列操作:
(1)測試控制該資源的信號量;
(2)若此信號量的值為正,則允許進行使用該資源,進程將進號量減1;
(3)若此信號量為0,則該資源目前不可用,進程進入睡眠狀態,直至信號量值大于0,進程被喚醒,轉入步驟(1);
(4)當進程不再使用一個信號量控制的資源時,信號量值加1,如果此時有進程正在睡眠等待此信號量,則喚醒此進程。
套接字通信并不為Linux所專有,在所有提供了TCP/IP協議棧的操作系統中幾乎都提供了socket,而所有這樣操作系統,對套接字的編程方法幾乎是完全一樣的。
管道:速度慢,容量有限,只有父子進程能通訊
FIFO(命名管道):任何進程間都能通訊,但速度慢,命名管道可用于非父子進程,命名管道就是FIFO,管道是先進先出的通訊方式
消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題
信號量:不能傳遞復雜消息,只能用來同步
共享內存區:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當于線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存。
線程
線程是CPU調度的最小單位,多個線程共享一個進程的地址空間。
線程包含線程ID,程序計數器,寄存器和棧。
(一)線程調度
(二)線程同步
作者:簡書的王布斯
鏈接:http://www.lxweimin.com/p/7ce30a806c51
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權并注明出處。