進程,線程 ->iOS 多線程 runloop

?????? 又來到了一個老生常談的問題频敛,應(yīng)用層軟件開發(fā)的程序員要不要了解和深入學(xué)習(xí)操作系統(tǒng)呢? 今天就這個問題開始馅扣,來談?wù)劜僮飨到y(tǒng)中可以說是最重要的一個概念--進程斟赚。

????? 操作系統(tǒng)最主要的兩個職能是管理各種資源和為應(yīng)用程序提供系統(tǒng)調(diào)用接口。這其中關(guān)鍵的部分是岂嗓,cpu到進程的抽象汁展,物理內(nèi)存到地址空間(虛擬內(nèi)存)的抽象,磁盤到文件的抽象厌殉,而其中后兩部分以進程為基礎(chǔ)食绿,所以嘛,咱重點來討論進程公罕,以及與進程密切相關(guān)的線程器紧。

.先說說概念

進程(process)

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

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

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

進程狀態(tài):進程有三個狀態(tài),就緒赋咽,運行和阻塞旧噪。就緒狀態(tài)其實就是獲取了除cpu外的所有資源,只要處理器分配資源馬上就可以運行冬耿。運行態(tài)就是獲取了處理器分配的資源,程序開始執(zhí)行萌壳,阻塞態(tài)亦镶,當(dāng)程序條件不夠時日月,需要等待條件滿足時候才能執(zhí)行,如等待I/O操作的時候缤骨,此刻的狀態(tài)就叫阻塞態(tài)爱咬。

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

線程(thread)

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

多線程(multiThread)

在一個程序中帖旨,這些獨立運行的程序片段叫作“線程”(Thread),利用它編程的概念就叫作“多線程處理”灵妨。多線程是為了同步完成多項任務(wù)解阅,不是為了提高運行效率,而是為了提高資源使用效率來提高系統(tǒng)的效率泌霍。線程是在同一時間需要完成多項任務(wù)的時候?qū)崿F(xiàn)的货抄。

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

二怪与、說說區(qū)別

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

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

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

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

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

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

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

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

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

四、說說進程和線程的細節(jié)王暗,底層構(gòu)成 和 調(diào)度

(一)進程相關(guān)的數(shù)據(jù)結(jié)構(gòu)

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

這些正是進程描述符的作用---進程描述符都是task_struct 數(shù)據(jù)結(jié)構(gòu)隐绵,它的字段包含了與一個進程相關(guān)的所有信息之众。下圖顯示了Linux進程描述符

談?wù)勥M程的基本信息。

1)標(biāo)識一個進程--PID

每個進程都必須擁有它自己的進程描述符依许;進程和進程描述符之間有非常嚴(yán)格的一一對應(yīng)關(guān)系棺禾,所以我們可以方便地使用32位進程描述符地址標(biāo)識進程。

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

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

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

Linux只支持輕量級進程,不支持線程醉锄,但為了彌補這樣的缺陷乏悄,Linux引入線程組的概念。一個線程組中的所有線程使用和該線程組的領(lǐng)頭線程相同的PID恳不,也就是該組中第一個輕量級進程的PID檩小,它被存入進程描述符的tgid字段中。getpid()系統(tǒng)調(diào)用返回當(dāng)前進程的tgid值而不是pid值烟勋,因此规求,一個多線程應(yīng)用的所有線程共享相同的PID筐付。絕大多數(shù)進程都屬于一個線程組;而線程組的領(lǐng)頭線程其tgid與pid的值相同阻肿,因而getpid()系統(tǒng)調(diào)用對這類進程所起的作用和一般進程是一樣的瓦戚。

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

2)進程描述符定位

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

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

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

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

下圖顯示了在2頁(8KB)內(nèi)存區(qū)中存放兩種數(shù)據(jù)結(jié)構(gòu)的方式肴捉。線程描述符駐留于這個內(nèi)存區(qū)的開始位置,而棧從末端向下增長叔收。該圖還顯示了如何通過task字段與task_struct結(jié)構(gòu)相互關(guān)聯(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)中,棧起始于末端跺株,并朝這個內(nèi)存區(qū)的起始方向增長复濒。從用戶態(tài)切換到內(nèi)核態(tài)以后,進程的內(nèi)核椘故。總是空的巧颈,因此,esp寄存器指向這個棧的頂端袖扛。

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

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

union thread_union {

struct thread_info thread_info;

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

};

內(nèi)核使用alloc_thread_info 和 free_thread_info宏分配和釋放存儲thread_info結(jié)構(gòu)和內(nèi)核棧的內(nèi)存區(qū)琢融。

3)標(biāo)識當(dāng)前進程

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

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

andl %esp,%ecx

movl %ecx,p

這三條指令執(zhí)行后泊碑,p就是在執(zhí)行指令的CPU上運行的當(dāng)前進程的thread_info結(jié)構(gòu)的指針坤按。不過,進程最常用的是進程描述符的地址馒过,而不是thread_info結(jié)構(gòu)的地址臭脓。為了獲得當(dāng)前在CPU上運行進程的描述符指針腹忽,內(nèi)核要調(diào)用current宏,該宏本質(zhì)上等價于current_thread_info( )->task摔竿,它產(chǎn)生如下匯編指令:

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

andl %esp,%ecx

movl (%ecx),p

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

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


4)進程鏈表

Linux內(nèi)核把進程鏈表把所有進程的描述符鏈接起來蒂教。每個task_struct結(jié)構(gòu)都包含一個list_head類型的tasks字段,這個類型的prev和next字段分別指向前面和后面的的task_struct元素脆荷。

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

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

另外,還有一個很有用的宏就是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結(jié)構(gòu)的state字段描述了進程當(dāng)前所處的狀態(tài)。它由一組標(biāo)志組成退子,其中每個標(biāo)志描述一種可能的進程狀態(tài)岖妄。在當(dāng)前的Linux版本中,這些狀態(tài)是互斥的寂祥,因此荐虐,嚴(yán)格意義上來說,只能設(shè)置一種狀態(tài)丸凭,其余的標(biāo)志位將被清除福扬。下面是可能的狀態(tài):

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

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

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

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

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

與可中斷的等待狀態(tài)類似莉御,但有一個例外撇吞,把信號傳遞到該睡眠進程時,不能改變它的狀態(tài)礁叔。這種狀態(tài)很少用到牍颈,但在一些特定條件下(進程必須等待,直到一個不能被中斷的時事件發(fā)生)琅关,這種狀態(tài)是很有用的煮岁。例如,當(dāng)進程打開一個設(shè)備文件涣易,其相應(yīng)的設(shè)備驅(qū)動程序開始探測相應(yīng)的硬件設(shè)備時會用到這種狀態(tài)人乓。探測完成以前,設(shè)備驅(qū)動程序不能被中斷都毒,否則色罚,硬件設(shè)備會處于不可預(yù)知的狀態(tài)。

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

進程的執(zhí)行被暫停账劲。當(dāng)進程接收到SIGSTOP戳护、SIGTSTP金抡、SIGTTIN或SIGTTOU信號后,進人暫停狀態(tài)腌且。

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

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

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

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

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

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

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

state字段的值通常用一個簡單的賦值語句設(shè)置删掀,例如:

p->state = TASK_RUNNING;

內(nèi)核也使用set_task_state和set_current_state宏:它們分別設(shè)置指定進程的狀態(tài)和當(dāng)前執(zhí)行進程的狀態(tài)翔冀。此外,這些宏確保編譯程序或CPU控制單元不把賦值操作和其他指令混合披泪∠俗樱混合指令的順序有時會導(dǎo)致災(zāi)難性的后果。

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

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

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

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

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

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

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

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)先權(quán)荆秦,而array字段是一個指針篱竭,指向當(dāng)前運行隊列的proo_array_t數(shù)據(jù)結(jié)構(gòu)。類似地萄凤,dequeue_task(p,array)函數(shù)從運行隊列的鏈表中刪除一個進程的描述符室抽。


7)進程間關(guān)系

父子兄弟關(guān)系:

程序創(chuàng)建的進程具有父/子關(guān)系。如果一個進程創(chuàng)建多個子進程時靡努,則子進程之間具有兄弟關(guān)系坪圾。進程0和進程1是由內(nèi)核創(chuàng)建的;進程1(init)是所有進程的祖先惑朦。

在進程描述符中引入幾個字段來表示這些關(guān)系兽泄,我們假設(shè)擁有該task_struct結(jié)構(gòu)的這個進程叫P:

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

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

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

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

下圖顯示了一組進程間的親屬關(guān)系,進程P0創(chuàng)建了P1遣臼,P2性置,P3,進程P3又創(chuàng)建了P4揍堰。


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

group_leader——P所在進程組的領(lǐng)頭進程的描述符指針

signal->pgrp——P所在進程組的領(lǐng)頭進程的PID

tgid——P所在線程組的領(lǐng)頭進程的PID

signal->session——P的登錄會話領(lǐng)頭進程的PID

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

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

8)PID定位task_struct

再來凰萨,內(nèi)核必須能從進程的PID導(dǎo)出對應(yīng)的進程描述符指針继控。例如,為kill()系統(tǒng)調(diào)用提供服務(wù)時就會發(fā)生這種情況:當(dāng)進程P1希望向另一個進程P2發(fā)送一個信號時胖眷,P1調(diào)用kill()系統(tǒng)調(diào)用武通,其參數(shù)為P2的PID,內(nèi)核從這個PID導(dǎo)出其對應(yīng)的進程描述符珊搀,然后從該task_struct中取出記錄掛起信號的數(shù)據(jù)結(jié)構(gòu)指針冶忱。

那么如何得到這個task_struct呢?首先想到for_each_process(p)境析。不行囚枪,雖然順序掃描進程鏈表并檢查進程描述符的pid字段是可行的,但相當(dāng)?shù)托Ю拖榱思铀俨檎伊凑樱琇inux內(nèi)核引入了4個散列表。需要4個散列表是因為進程描述符包含了表示不同類型PID的字段沛鸵,而且每種類型的PID需要它自己的散列表:

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

PIDTYPE_TGID??? tgid??? 線程組領(lǐng)頭進程的PID

PIDTYPE_PGID??? pgrp??? 進程組領(lǐng)頭進程的PID

PIDTYPE_SID??? session??? 會話領(lǐng)頭的PID

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

用pid_hashfn宏把PID轉(zhuǎn)化為表索引:

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

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

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房官。

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

(二)進程調(diào)度

1)進程調(diào)度的目標(biāo)

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

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

3.保證公平和避免饑渴

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

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

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

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

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

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

不同與普通進程挎袜,系統(tǒng)調(diào)度時,實時優(yōu)先級高的進程總是先于優(yōu)先級低的進程執(zhí)行肥惭,直到實時優(yōu)先級高的實時進程無法執(zhí)行盯仪。實時進程總是被認為處于活動狀態(tài)。如果有數(shù)個 優(yōu)先級相同的實時進程蜜葱,那么系統(tǒng)就會按照進程出現(xiàn)在隊列上的順序選擇進程全景,假設(shè)當(dāng)前CPU運行的實時進程A的優(yōu)先級為a,而此時有個優(yōu)先級為b的實時進程B進入可運行狀態(tài)牵囤,那么只要b<a,系統(tǒng)將中斷A的執(zhí)行爸黄,而優(yōu)先執(zhí)行B,直到B無法執(zhí)行(無論A揭鳞,B為何種實時進程)炕贵。

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

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

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

總而言之焰轻,對于實時進程,高優(yōu)先級的進程就是大爺昆雀。它執(zhí)行到?jīng)]法執(zhí)行了鹦马,才輪到低優(yōu)先級的進程執(zhí)行。


普通進程的調(diào)度

Linux對于普通的進程忆肾,根據(jù)動態(tài)優(yōu)先級進行調(diào)度荸频,而動態(tài)優(yōu)先級是由靜態(tài)優(yōu)先級調(diào)整而來,Linux下客冈,靜態(tài)優(yōu)先級是用戶不可見的旭从,隱藏在內(nèi)核中,而內(nèi)核提供給用戶一個可以影響靜態(tài)優(yōu)先級的接口场仲,那就是nice值和悦。

關(guān)系如下:

static_prio =MAX_RT_PRIO+nice+20

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

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

的屬性徒探。例如如果進程屬于交互式進程瓦呼,那么可以適當(dāng)?shù)恼{(diào)高它的優(yōu)先級,使得界面反應(yīng)地更加迅速测暗,從而使用戶得到更好的體驗央串。Linux2.6

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

可能屬于交互式進程挫掏。則系統(tǒng)調(diào)度時侦另,會給該進程更多的獎勵(bonus)秩命,以便該進程有更多的機會能夠執(zhí)行尉共。獎勵(bonus)從0到10不等褒傅。

系統(tǒng)會嚴(yán)格按照動態(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耗時反應(yīng)了進程IO密集和CPU密集兩大瞬時特點,不同時期鸠按,一個進程可能即是CPU密集型也是IO密集型進程礼搁。對于表現(xiàn)為IO密集的進程,應(yīng)該經(jīng)常運行目尖,但每次時間片不要太長馒吴。對于表現(xiàn)為CPU密集的進程,CPU不應(yīng)該讓其經(jīng)常運行瑟曲,但每次運行時間片要長饮戳。交互進程為例,假如之前其其大部分時間在于等待CPU洞拨,這時為了調(diào)高相應(yīng)速度扯罐,就需要增加獎勵分。另一方面烦衣,如果此進程總是耗盡每次分配給它的時間片歹河,為了對其他進程公平,就要增加這個進程的懲罰分數(shù)花吟。可以參考CFS的virtutime機制.

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

不再單純依靠進程優(yōu)先級絕對值启泣,而是參考其絕對值,綜合考慮所有進程的時間示辈,給出當(dāng)前調(diào)度時間單位內(nèi)其應(yīng)有的權(quán)重寥茫,也就是,每個進程的權(quán)重X單位時間=應(yīng)獲cpu時間矾麻,但是這個應(yīng)得的cpu時間不應(yīng)太猩闯堋(假設(shè)閾值為1ms),否則會因為切換得不償失险耀。但是弄喘,當(dāng)進程足夠多時候,肯定有很多不同權(quán)重的進程獲得相

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

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


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

另外裆熙,在系統(tǒng)調(diào)用clone和內(nèi)核函數(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)∑兜欤總之唬血,TASK_RUNNING是必經(jīng)之路,不可能兩個非RUN狀態(tài)直接轉(zhuǎn)換唤崭。

也就是說拷恨,如果給一個TASK_INTERRUPTIBLE狀態(tài)的進程發(fā)送SIGKILL信號,這個進程將先被喚醒(進入TASK_RUNNING狀態(tài))谢肾,然后再響應(yīng)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í)行喚醒的

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

調(diào)度執(zhí)行分预。

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

1薪捍、響應(yīng)信號而進入TASK_STOPED狀態(tài)笼痹、或TASK_DEAD狀態(tài)配喳;

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

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

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

只有在該狀態(tài)的進程才可能在CPU上運行馋贤。而同一時刻可能有多個進程處于可執(zhí)行狀態(tài),這些進程的task_struct結(jié)構(gòu)(進程控制塊)被放入對應(yīng)CPU的可執(zhí)行隊列中(一個進程最多只能出現(xiàn)在一個CPU的可執(zhí)行隊列中)畏陕。進程調(diào)度器的任務(wù)就是從各個CPU的可執(zhí)行隊列中分別選擇一個進程在該CPU上運行配乓。

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

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

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

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

處于這個狀態(tài)的進程因為等待某某事件的發(fā)生(比如等待socket連接择同、等待信號量)第焰,而被掛起。這些進程的task_struct結(jié)構(gòu)被放入對應(yīng)事件的等待隊列中腻格。當(dāng)這些事件發(fā)生時(由外部中斷觸發(fā)串慰、或由其他進程觸發(fā))栋盹,對應(yīng)的等待隊列中的一個或多個進程將被喚醒绞灼。

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

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

與TASK_INTERRUPTIBLE狀態(tài)類似跃洛,進程處于睡眠狀態(tài),但是此刻進程是不可中斷的终议。不可中斷汇竭,指的并不是CPU不響應(yīng)外部硬件的中斷,而是指進程不響應(yīng)異步信號穴张。

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

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

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

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

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

#include

void main() {

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

}

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

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

當(dāng)進程正在被跟蹤時,它處于TASK_TRACED這個特殊的狀態(tài)多搀∑缃叮“正在被跟蹤”指的是進程暫停下來,等待跟蹤它的進程對它進行操作康铭。比如在gdb中對被跟蹤的進程下一個斷點惯退,進程在斷點處停下來的時候就處于TASK_TRACED狀態(tài)。而在其他時候从藤,被跟蹤的進程還是處于前面提到的那些狀態(tài)催跪。

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

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

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

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

在這個退出過程中逆害,進程占有的所有資源將被回收藏古,除了task_struct結(jié)構(gòu)(以及少數(shù)資源)以外。于是進程就只剩下task_struct這么個空殼忍燥,故稱為僵尸拧晕。

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

當(dāng)然机久,內(nèi)核也可以將這些信息保存在別的地方臭墨,而將task_struct結(jié)構(gòu)釋放掉,以節(jié)省一些空間膘盖。但是使用task_struct結(jié)構(gòu)更為方便胧弛,因為在內(nèi)核中已經(jīng)建立了從pid到task_struct查找關(guān)系,還有進程間的父子關(guān)系侠畔。釋放掉task_struct结缚,則需要建立一些新的數(shù)據(jù)結(jié)構(gòu),以便讓父進程找到它的子進程的退出信息软棺。

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

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

通過下面的代碼能夠制造一個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)的子進程就一直存在服球。那么如果父進程退出了呢,誰又來給子進程“收尸”颠焦?

當(dāng)進程退出的時候斩熊,會將它的所有子進程都托管給別的進程(使之成為別的進程的子進程)。托管給誰呢伐庭?可能是退出進程所在進程組的下一個進程(如果存在的話)粉渠,或者是1號進程分冈。所以每個進程、每時每刻都有父進程存在霸株。除非它是1號進程雕沉。

1號進程,pid為1的進程去件,又稱init進程坡椒。

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

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

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

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

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

而進程在退出過程中也可能不會保留它的task_struct功咒。比如這個進程是多線程程序中被detach過的進程(進程愉阎?線程?參見《linux線程淺析》)力奋“竦或者父進程通過設(shè)置SIGCHLD信號的handler為SIG_IGN,顯式的忽略了SIGCHLD信號景殷。(這是posix的規(guī)定溅呢,盡管子進程的退出信號可以被設(shè)置為SIGCHLD以外的其他信號。)

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

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

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

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

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

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

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

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

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

內(nèi)核在響應(yīng)時鐘中斷的過程中佛寿,發(fā)現(xiàn)當(dāng)前進程的時間片用完幌墓;

內(nèi)核在響應(yīng)中斷的過程中,發(fā)現(xiàn)優(yōu)先級更高的進程所等待的外部資源的變?yōu)榭捎眉叫海瑥亩鴮⑵鋯拘殉B隆1热鏑PU收到網(wǎng)卡中斷,內(nèi)核處理該中斷弹渔,發(fā)現(xiàn)某個socket可讀胳施,于是喚醒正在等待讀這個socket的進程;再比如內(nèi)核在處理時鐘中斷的過程中肢专,觸發(fā)了定時器舞肆,從而喚醒對應(yīng)的正在nanosleep系統(tǒng)調(diào)用中睡眠的進程;

6)內(nèi)核搶占

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

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

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

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

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

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

而如果不禁用內(nèi)核搶占呢模捂?那么得到鎖的進程將可能被搶占捶朵,于是可能很久都不會釋放鎖。于是狂男,等待鎖的進程可能就不知何年何月得償所望了综看。

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

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

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

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

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

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

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

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

于是,“多處理器負載均衡”這個麻煩事情就來了胀滚。內(nèi)核需要關(guān)注各個CPU可執(zhí)行隊列中的進程數(shù)目趟济,在數(shù)目不均衡時做出適當(dāng)調(diào)整。什么時候需要調(diào)整咽笼,以多大力度進程調(diào)整顷编,這些都是內(nèi)核需要關(guān)心的。當(dāng)然剑刑,盡量不要調(diào)整最好媳纬,畢竟調(diào)整起來又要耗CPU、又要鎖可執(zhí)行隊列,代價還是不小的钮惠。

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

優(yōu)先級繼承

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

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

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

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

中斷處理線程化

在linux下拴测,中斷處理程序運行于一個不可調(diào)度的上下文中。從CPU響應(yīng)硬件中斷自動跳轉(zhuǎn)到內(nèi)核設(shè)定的中斷處理程序去執(zhí)行府蛇,到中斷處理程序退出集索,整個過程是不能被搶占的。

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

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

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

(三)進程同步與互斥

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

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

進程互斥是進程之間發(fā)生的一種間接性作用冕末,一般是程序不希望的姆另。通常的情況是兩個或兩個以上的進程需要同時訪問某個共享變量喇肋。我們一般將發(fā)生能夠問共享變量的程序段稱為臨界區(qū)。兩個進程不能同時進入臨界區(qū)迹辐,否則就會導(dǎo)致數(shù)據(jù)的不一致蝶防,產(chǎn)生與時間有關(guān)的錯誤。解決互斥問題應(yīng)該滿足互斥和公平兩個原則明吩,即任意時刻只能允許一個進程處于同一共享變量的臨界區(qū)间学,而且不能讓任一進程無限期地等待∮±螅互斥問題可以用硬件方法解決低葫,也可以用軟件方法。

同步是說進程的合作關(guān)系仍律,互斥是說進程對資源的競爭關(guān)系嘿悬。


信號量、管程

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

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

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

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

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

monitor_name = MoNITOR;

共享變量說明;

define 本管程內(nèi)部定義、外部可調(diào)用的函數(shù)名表;

use 本管程外部定義呻此、內(nèi)部可調(diào)用的函數(shù)名表;

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

{

共享變量初始化語句;

}

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

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

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

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

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

為了保證共享變量的數(shù)據(jù)一致性,管程應(yīng)互斥使用吨些。 管程通常是用于管理資源的搓谆,因此管程中有進程等待隊列和相應(yīng)的等待和喚醒操作。在管程入口有一個等待隊列豪墅,稱為入口等待隊列泉手。當(dāng)一個已進入管程的進程等待時,就釋放管程的互斥使用權(quán)偶器;當(dāng)已進入管程的一個進程喚醒另一個進程時斩萌,兩者必須有一個退出或停止使用管程。在管程內(nèi)部屏轰,由于執(zhí)行喚醒操作颊郎,可能存在多個等待進程(等待使用管程),稱為緊急等待隊列霎苗,它的優(yōu)先級高于入口等待隊列姆吭。

因此,一個進程進入管程之前要先申請唁盏,一般由管程提供一個enter過程猾编;離開時釋放使用權(quán)瘤睹,如果緊急等待隊列不空升敲,則喚醒第一個等待者驴党,一般也由管程提供外部過程leave倔既。

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

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

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


(四)進程間通信(IPC)

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

管道分為有名管道和無名管道肠鲫,無名管道只能用于親屬進程之間的通信导饲,而有名管道則可用于無親屬關(guān)系的進程之間。

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

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

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

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

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

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

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

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

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

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

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

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

信號量:不能傳遞復(fù)雜消息夺鲜,只能用來同步

共享內(nèi)存區(qū):能夠很容易控制容量杆麸,速度快搁进,但要保持同步,比如一個進程在寫的時候昔头,另一個進程要注意讀寫的問題饼问,相當(dāng)于線程中的線程安全,當(dāng)然揭斧,共享內(nèi)存區(qū)同樣可以用作線程間通訊莱革,不過沒這個必要峻堰,線程間本來就已經(jīng)共享了同一進程內(nèi)的一塊內(nèi)存。


線程

線程是CPU調(diào)度的最小單位盅视,多個線程共享一個進程的地址空間捐名。

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

(一)線程調(diào)度

(二)線程同步

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市赏半,隨后出現(xiàn)的幾起案子贺归,更是在濱河造成了極大的恐慌,老刑警劉巖断箫,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拂酣,死亡現(xiàn)場離奇詭異,居然都是意外死亡仲义,警方通過查閱死者的電腦和手機婶熬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埃撵,“玉大人赵颅,你說我怎么就攤上這事≡萘酰” “怎么了性含?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鸳惯。 經(jīng)常有香客問我,道長叠萍,這世上最難降的妖魔是什么芝发? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮苛谷,結(jié)果婚禮上辅鲸,老公的妹妹穿的比我還像新娘。我一直安慰自己腹殿,他們只是感情好独悴,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锣尉,像睡著了一般刻炒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上自沧,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天坟奥,我揣著相機與錄音,去河邊找鬼。 笑死爱谁,一個胖子當(dāng)著我的面吹牛晒喷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播访敌,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼凉敲,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寺旺?” 一聲冷哼從身側(cè)響起爷抓,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎迅涮,沒想到半個月后废赞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡叮姑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年唉地,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片传透。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡耘沼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出朱盐,到底是詐尸還是另有隱情群嗤,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布兵琳,位于F島的核電站狂秘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏躯肌。R本人自食惡果不足惜者春,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望清女。 院中可真熱鬧钱烟,春花似錦、人聲如沸嫡丙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曙博。三九已至拥刻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間羊瘩,已是汗流浹背泰佳。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工盼砍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逝她。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓浇坐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親黔宛。 傳聞我的和親對象是個殘疾皇子近刘,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容