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

進程概覽

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

進程在內存的邏輯布局

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

此時,CPU運行到地址為304的指令, 假設CPU時間片剛好用完,就需要進行進程切換,在進行進程切換之前,需要保護現場,即保存寄存器信息、PC、打開的文件, 代碼段地址、數據地址、堆棧信息等,這些信息稱為進程的上下文。當操作系統切換到進程時,首先將進程2的上下文信息載入到操作系統中,找到PC,然後接著執行就可以了。

進程式控制制塊(PCB)

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

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調度是一個很關鍵的流程。

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等。

  • 臨界區:多個進程需要互斥的訪問共享資源,共享資源可以是變數、表和文件等,例如列印隊列就是共享資源。
  • 當生產者將隊列放滿時,需要等待消費者;如果消費者把所有文件都列印完了,則需要等待生產者,這就是進程間的同步問題
  • 進程間同步的本質
  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);
}
}

線程

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

線程的實現

  1. 完全在用戶層實現(當用戶要執行硬體設備,必須從用戶空間到內核空間,這是一種保護措施,保護操作系統不被惡意程序所破壞),線程在應用層實現有一個優點就是線程切換不用內核介入,線程切換會非常的快。也就是說線程的調度策略是自己實現的。但是這裡也有一個巨大的缺陷:由於內核只知道進程而不知道線程,那麼進程1中的任何一個線程被阻塞,導致進程1中的其他線程也被阻塞
  2. 內核實現線程和用戶空間一一對應,可以有效的解決方案一中的缺點,但是由於在內核中實現用戶空間相同數量的線程數,開銷比較大
  3. 用戶空間中多個線程映射到內核中的一個線程,這樣一來,內核中的線程就不用創建那麼多, 而且阻塞的概率也降低了,這是一種平衡和折中的方式。JVM就是實現了這種方式 。JVM本身就是一個進程,JVM可以創建很多線程,然後對應內核中的線程,內核中的線程調度CPU。

weixin.qq.com/r/Uyjp8TH (二維碼自動識別)

歡迎關注微信公眾號:木可大大,所有文章都將同步在公眾號上。

推薦閱讀:

相关文章