漫談進程和線程

爲了幫助你們理解什麼是進程,以廚師作蛋糕爲例。廚師作蛋糕,首先須要廚師(CPU),其次,須要食譜(程序)和原料(輸入數據),而用原料作蛋糕的一些列動做的總和就是進程。某天廚師正在後廚作着蛋糕,突來聽到兒子哭着跑進後廚,說本身被蜜蜂蟄了 ,廚師放下手中工具,並記錄下當前作到哪一步了(保存上下文信息) ,而後拿出急救手冊,按其中的說明爲兒子進行處理(開始另一個進程)。前端

進程概覽

咱們知道文件是對I/O設備的抽象,虛擬存儲器是對文件和主存的抽象,指令集是對CPU的抽象,進程是對指令集和虛擬存儲器的抽象。以下圖所示 。java

image.png

 

進程在內存的邏輯佈局

從上可知,進程包括指令集和虛擬存儲器。咱們着重介紹進程在虛擬存儲器中的邏輯佈局,它包括用戶棧、堆、程序數據和程序代碼,其中,用戶棧從上往下生長,堆從下往上生長,程序數據和程序代碼從可執行文件加載而來,將程序代碼改寫成彙編指令就是相似於movl、imul、addl等指令。以下圖所示後端

image.png

 

此時,CPU運行到地址爲304的指令, 假設CPU時間片恰好用完,就須要進行進程切換,在進行進程切換以前,須要保護現場,即保存寄存器信息、PC、打開的文件, 代碼段地址、數據地址、堆棧信息等,這些信息稱爲進程的上下文。當操做系統切換到進程時,首先將進程2的上下文信息加載到操做系統中,找到PC,而後接着執行就能夠了。緩存

進程控制塊(PCB)

進程的上下文信息是以某個數據結構保存在內存中的,而這種數據結構就是PCB。在Linux操做系統中PCB對應的數據結構就是task_struct,它保存着進程的重要信息。bash

struct task_struct{ pid_t pid://進程號 long state;//狀態 cputime_t utime,stime;//cpu在用戶態和 核心態下執行的時間 struct files_struct *files;//打開的文件 struct mm_struct *mm;//進程使用的內存 ... } 

進程的狀態

  • 進程的狀態包括新建態、就緒態、等待態、運行態、退出態
  • 流程:首先進程被新建,而後進入到就緒狀態,此時,進程並無進入到運行狀態,而是等待CPU調度,若是被CPU調度則進入到運行態,而當時間片用完時,進程從運行態返回到就緒態,而當等待I/O操做時,則由運行態進入阻塞態。須要注意的是:只有運行態的進程擁有CPU,而處於就緒態和等待態的進程只是處於內存,等待CPU調度,所以CPU調度是一個很關鍵的流程。
    image.png

CPU調度

像上文描述的那樣,CPU調度就是到底哪一個進程佔有CPU,它能夠分爲非搶佔式和搶佔式。非搶佔式是指調度程序一旦把CPU分配給某一進程後便讓它一直運行下去,直到進程完成或發生某件事件而不能運行時,纔將CPU分配給其餘進程。它適合批處理系統,簡單、系統開銷小。搶佔式是指當一個進程正在執行時,系統能夠基於某種策略剝奪CPU給其餘進程。剝奪的原則有優先權原則、端進程優先原則、時間片原則,它適用於交互式系統。微信

  • 評價標準
  1. 公平:合理的分配CPU
  2. 響應時間:從用戶輸入到產生反映的時間
  3. 吞吐量:單位時間完成的任務數量
  4. 可是這些目標是矛盾的,例如:咱們但願前端進程可以快速獲得響應,這樣一來後端進程就不能獲得快速響應。
  • 批處理系統中的調度
  1. 先來先服務(公平、FIFO隊列、非搶佔式)
  2. 最短做業優先(系統的平均等待時間最短,可是須要預先知道每一個任務的運行時間)
  • 交互式調度策略
  1. 輪轉,每一個進程分配一個固定的時間片,可是定義時間片長度是個問題,假設進程切換一次的開銷爲1ms,若是時間片過短,那麼不少時間都浪費在切換上,例如時間片爲4ms,那麼20%的時間浪費在切換上;若是時間片太長,浪費時間就減小了,可是最後一個常常等待的時間就很是久,譬如,時間片100ms,浪費的時間1%,假設有50個進程,最後一個須要等待5s。
  2. 靜態優先級,給每一個進程賦予優先級,優先級高的先執行,優先級低的後執行,可是該方法存在必定問題:低優先級的進程存在被餓死的狀況,例如新來的進程的優先級都比原來的高,怎麼辦呢?咱們根據等待時間的增長而調整優先級大小---多級反饋隊列
  3. 動態優先級---多級反饋隊列,即進程的優先級會隨着等待時間的增加而增加。

進程間同步

咱們知道,打印機有一個緩存,叫作打印隊列,以下圖所示,打印隊列有5個空格,就是說這個打印隊列最多能夠容納5個待打印文件,打印機進程就是消費者,而其餘待打印進程是生產者,生產者不斷地向隊列中放數據,例如:A.java、B.doc等。數據結構

  • 臨界區:多個進程須要互斥的訪問共享資源,共享資源能夠是變量、表和文件等,例如打印隊列就是共享資源。多線程

  • 當生產者將隊列放滿時,須要等待消費者;若是消費者把全部文件都打印完了,則須要等待生產者,這就是進程間的同步問題。函數

image.png
  • 進程間同步的本質
  1. 進程調度是不可控的
  2. 在機器層面,count++,count--並非原子操做,即一條代碼,對應彙編層面多條指令。二者缺一不可,若是進程調度是可控的,那麼,即便count++對應多條指令,當執行完第一條指令時,發生CPU切換,進程調度控制接下來的進程仍是原來的進程控制CPU。
  • 解決方案
  1. 關閉中斷
    缺點:把中斷操做(CPU收到時鐘中斷之後,會檢查當前進程的時間片是否用完,用完則切換)開放給應用程序,這是極其危險的事情,例如:當某個程序關閉中斷以後,執行完畢以後,忘記打開中斷,致使整個系統都終止了。工具

  2. 用硬件指令來實現鎖

boolean TestAndSet(boolean *lock){ boolean rv = *lock; *lock = TRUE; return rv; } // 使用TestAndSet boolean lock = false; do{ while(TestAndSet(&lock)){ ...//什麼也不作 } 臨界區 lock = false; 剩餘區 }while(true); 
  • 注意:操做系統會鎖住系統總線,防止其餘CPU訪問內存變量
  • 注意TestAndSet函數中的三條指令是原子執行的
  1. 信號量
  • 信號量S是個整形變量,除了初始化外,有兩個操做,wait()、signal()。
  • 爲了確保信號量操做,須要用一種原子的方式實現它,操做系統來實現原子性。
wait(S){ while(S<=0){ ...//啥也不作 } S--; } signal(S){ S++; } // semaphore mutext = 1; wait(mutex); 進入臨界區 signal(mutex); 剩餘區 
  1. 不能忙等問題

用硬件指令實現鎖的方案和信號量方案都有忙等問題,即某個進程得到了CPU時間片,可是啥事幹不了,while(S < = 0){...}

  • 新增進程隊列,當發現value<0,將當前隊列加入到阻塞隊列中,同時,阻塞進程,而不像以前的方法那樣無限等待下去
typedef struct{ int value; struct process *list; } semaphore; wait(semaphore *s){ s -> value--; if(s->value<0){ //把當前進程加入到s->list中 block(); } signal(semaphore *s){ s -> value++; if(s -> value <=0){ //從s->list取出一個進程p wakeup(p); } } 

線程

因爲進程之間是相互獨立的,因此進程間數據共享只能經過內核實現,開銷很麻煩,所以咱們提出了線程這個概念。線程之間的數據是共享的;一個進程能夠只有一個線程,也能夠有多個線程(一個進程至少有一個線程);當一個進程有多個線程時,每一個線程都有一套獨立的寄存器和堆棧信息,而代碼、數據和文件是共享的,以下圖所示。

image.png

 

線程的實現

  1. 徹底在用戶層實現(當用戶要執行硬件設備,必須從用戶空間到內核空間,這是一種保護措施,保護操做系統不被惡意程序所破壞),線程在應用層實現有一個優勢就是線程切換不用內核介入,線程切換會很是的快。也就是說線程的調度策略是本身實現的。可是這裏也有一個巨大的缺陷:因爲內核只知道進程而不知道線程,那麼進程1中的任何一個線程被阻塞,致使進程1中的其餘線程也被阻塞

  2. 內核實現線程和用戶空間一一對應,能夠有效的解決方案一中的缺點,可是因爲在內核中實現用戶空間相同數量的線程數,開銷比較大

  3. 用戶空間中多個線程映射到內核中的一個線程,這樣一來,內核中的線程就不用建立那麼多, 並且阻塞的機率也下降了,這是一種平衡和折中的方式。JVM就是實現了這種方式 。JVM自己就是一個進程,JVM能夠建立不少線程,而後對應內核中的線程,內核中的線程調度CPU。


image

歡迎關注微信公衆號:木可大大,全部文章都將同步在公衆號上。

相關文章
相關標籤/搜索