1.概念和差別
概念
從本質上說,一個程序就是一個正在執行的程式,它是系統進行資源配置設定和排程的基本單元,是作業系統結構的基礎。每個程序都有自己的位址空間,包括可執行程式,程式的資料,棧,一組寄存器(程式電腦,棧指針以及其他運作程式需要的資訊)
線程有時被稱為輕量級程序,是程式執行的最小執行流,它是程序的一個實體,是系統獨立排程和配置設定的基本機關。
程序和線程的差別
- 位址空間 :同一程序的線程共享本程序的位址空間,而程序之間則是獨立的位址空間。
- 資源擁有 :同一程序内的線程共享本程序的資源如記憶體、I/O、CPU等。但是程序之間的資源是獨立的。程序切換時,消耗的資源大,效率低,是以涉及到頻繁的切換時,使用線程好于程序。 同樣如果要求同時進行并且又要共享某些變量的并發操作,隻能用線程不能用程序。
- 執行過程 :每個獨立的程序有一個程式運作的入口,順序執行序列和程式入口。但是線程不能獨立執行,必須依存在應用程式中,由應用程式提供多個線程執行控制。
- 線程是處理器排程的進本機關,但程序不是 。
- 兩者都可并發執行。
2.程序和線程占有的資源
首先我們知道線程自己基本上不擁有系統資源,隻擁有一點在運作中必不可少的資源(如程式計數器,一組寄存器和棧),但是它可與同屬一個程序的其他線程共享程序所擁有的全部資源,
不包括堆棧,如果要修改共享空間裡的資源,需要加鎖。
程序占有堆棧。
3.程序
程序的建立和終止
程序的建立主要有四個原因:系統的初始化,正在運作的程序執行了穿件程序的系統調用,使用者請求建立一個程序以及批處理作業的初始化。
常見的就是一個程序調用了fork()函數建立新的程序。
作業系統建立一個新的程序的過程 1.為新程序配置設定一個唯一的程序辨別号,并申請一個空白的PCB(PCB是有限的)。若PCB申請失敗則建立失敗。
2.為程序配置設定資源,為新程序的程式和資料,以及使用者棧配置設定必要的記憶體空間(在PCB中展現)。注意:如果資源不足(比如記憶體空間),并不是建立失敗,而是處于“等待狀态”,或則稱為“阻塞狀态”,等待的是記憶體這個資源。
3.初始化PCB,主要包括初始化标志資訊,初始化處理機狀态資訊和初始化處理機控制資訊,以及設定程序的優先級等。
4.如果程序就緒隊列能夠接納新錦成,或将新程序插入到就緒隊列,等待被排程運作。
注:
PCB是程序存在的唯一辨別,它包含
程序辨別符(内部辨別符:每個程序唯一的一個數字辨別符,外部辨別符:建立者提供),
處理機狀态,
程序排程資訊和
程序控制資訊。
程序終止的一些原因:工作完成正常退出,出錯退出,嚴重錯誤,被其他程序殺死。
程序的狀态和控制原語
程序有三種狀态:運作态,阻塞态,就緒态。這三種狀态的轉換是:
就緒:當一個程序獲得了除處理機以外的一切所需資源,一旦得到處理機即可運作,則稱此程序處于就緒狀态。
阻塞:也稱為等待或睡眠狀态,一個程序正在等待某一事件發生(例如請求I/O而等待I/O完成等)而暫時停止運作,這時即使把處理機配置設定給程序也無法運作,故稱該程序處于阻塞狀态(是一種主動的行為)。
運作:當一個程序在處理機上運作時,則稱該程序處于運作狀态。
注意不可能存在直接從阻塞态轉換到執行态。
除了這三個基本狀态還有一個
挂起狀态,建立狀态,終止狀态。
引起挂起狀态的原因:終端使用者的請求,父程序請求,負荷調節的需要,作業系統的需要。
用于控制程序的原語有:
- 建立原語(
):建立一個就緒狀态的程序,使程序從建立狀态變遷為就緒狀态。Create
- 阻塞原語(
):使程序從執行狀态變遷為阻塞狀态。Block
- 喚醒原語(
):使程序從阻塞狀态變遷為就緒狀态。Wakeup
- 挂起原語(
):将指定的程序或處于阻塞的程序挂起Suspend
4.Java的Runnable狀态與作業系統中程序運作狀态的關系
RUNNABLE
狀态對應了傳統的
ready
,
running
以及部分的
waiting
狀态,也就是上面的三種狀态,但是操作體系中其實是有五種狀态的。
5.程序間通信
每個程序各自有不同的使用者位址空間,任何一個程序的全局變量在另一個程序中都看不到,是以程序之間要交換資料必須通過核心,在核心中開辟一塊緩沖區,
程序A把資料從使用者空間拷貝到核心緩沖區,程序B再從核心緩沖區把資料讀走,核心提供這種機制稱為程序間通信。
管道
所謂管道,就是用于連接配接一個讀程序和一個寫程序以實作他們之間通信的一個共享檔案,又名"pipe"檔案。
管道是由
pipe函數來建立
#include<unistd.h>
int pipe (int fd[2]);
//傳回:成功傳回0,出錯傳回-1
// fd參數傳回兩個檔案描述符,fd[0]指向管道的讀端,fd[1]指向管道的寫端。fd[1]的輸出是fd[0]的輸入。那麼此時這個管道對于使用者程式就是一個檔案,可以通過read(fd [0]);或者write(fd [1])進行操作。pipe函數調用成功傳回0,否則傳回-1。
實作程序通信的方式
- 父程序建立管道,得到兩個檔案描述符指向管道的兩端
- 父程序fork出子程序,子程序也有兩個檔案描述符指向同一管道
- 父程序關閉fd[0],子程序關閉fd[1],即父程序關閉管道讀端,子程序關閉管道寫端,( 因為管道隻支援單向通信 )。父程序可以往管道裡寫,子程序可以往管道裡讀, 管道是用環形隊列實作的 , 資料從寫端流入,從讀端流出 ,這樣就實作了程序間通信。
管道讀取資料的幾種情況(
讀、寫具有互斥性):
- 讀端不讀,寫端一直寫
- 寫端不寫,但是讀端一直讀
- 讀端一直讀,且
保持打開,而寫端寫了一部分資料不寫了,并且關閉fd[0]
fd[1]
- 讀端讀了一部分資料,不讀了且關閉
,寫端一直在寫且fd[0]
還保持打開狀态。fd[1]
對應的處理:
- 如果一個管道的寫端一直在寫,而讀端的引⽤計數是否⼤于0決定管道是否會堵塞,引用計數大于0,隻寫不讀再次調用
會導緻管道堵塞;write
- 如果一個管道的讀端一直在讀,而寫端的引⽤計數是否⼤于0決定管道是否會堵塞,引用計數大于0,隻讀不寫再次調用
會導緻管道堵塞;read
- 而當他們的引用計數等于0時,隻寫不讀會導緻寫端的程序收到一個
信号,導緻程序終止,隻寫不讀會導緻SIGPIPE
傳回0,就像讀到⽂件末尾⼀樣。read
- 如果所有指向管道寫端的檔案描述符都關閉了,而仍然有程序從管道的讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會傳回0,就像讀到檔案末尾一樣
- 如果有指向管道寫端的檔案描述符沒關閉,而持有管道寫端的程序也沒有向管道中寫資料,這時有程序從管道讀端讀資料,那麼管道中剩餘的資料都被讀取後,再次read會阻塞,直到管道中有資料可讀了才讀取資料并傳回。
- 如果所有指向管道讀端的檔案描述符都關閉了,這時有程序指向管道的寫端write,那麼該程序會收到信号SIGPIPE,通常會導緻程序異常終止。
- 如果有指向管道讀端的檔案描述符沒關閉,而持有管道讀端的程序也沒有從管道中讀資料,這時有程序向管道寫端寫資料,那麼在管道被寫滿時再write會阻塞,直到管道中有空位置了才寫入資料并傳回。
管道的特點:
- 管道隻允許具有血緣關系的程序間通信,如父子程序間的通信。
- 管道隻允許單向通信。
- 管道内部保證同步機制,進而保證通路資料的一緻性。
- 面向位元組流
- 管道随程序,程序在管道在,程序消失管道對應的端口也關閉,兩個程序都消失管道也消失。
:指當寫(輸入)程序把一定的數量的資料寫入pipe,便去睡眠等待,直到讀(輸出)程序取走資料後,再把它喚醒。當讀程序讀一空pipe時,也應該睡眠等待,直到寫程序将資料寫入管道後,才将之喚醒。
匿名管道匿名管道主要用于本地父程序和子程序之間的通信,在父程序中的話,首先是要建立一個匿名管道,在建立匿名管道成功後,可以擷取到對這個匿名管道的讀寫句柄,然後父程序就可以向這個匿名管道中寫入資料和讀取資料了,但是如果要實作的是父子程序通信的話,那麼還必須在父程序中建立一個子程序,同時,這個子程序必須能夠繼承和使用父程序的一些公開的句柄,為什麼呢?因為在子程序中必須要使用父程序建立的匿名管道的讀寫句柄,通過這個匿名管道才能實作父子程序的通信,是以必須繼承父程序的公開句柄。同時在建立子程序的時候,必須将子程序的标準輸入句柄設定為父程序中建立匿名管道時得到的讀管道句柄,将子程序的标準輸出句柄設定為父程序中建立匿名管道時得到的寫管道句柄。然後在子程序就可以讀寫匿名管道了。
信号量
信号量本質上是一個計數器(不設定全局變量是因為程序間是互相獨立的,而這不一定能看到,看到也不能保證
++
引用計數為原子操作 ),
用于多程序對共享資料的讀取,它和管道有所不同,他不以傳送資料為主要目的,他主要用來保護共享資源(信号量也屬于臨界資源),使得資源在一個時刻隻有一個程序獨享。工作原理:
由于信号量隻能進行兩種操作等待和發送信号,即
P(sv)
和
V(sv)
,他們的行為是這樣的:
-
:如果P(sv)
的值大于零,就給它減1;如果它的值為零,就挂起該程序的執行sv
-
:如果有其他程序因等待V(sv)
而被挂起,就讓它恢複運作,如果沒有程序因等待sv
而挂起,就給它加1.sv
在信号量進行
PV
操作時都為
原子操作(單條指令的執行是不會被打斷的,因為它需要保護臨界資源)
與信号量相關的函數:
// 建立信号量,傳回:成功傳回信号集ID,出錯傳回-1
int semget(key_t key,int nsems,int flags)
// 删除和初始化信号量
int semctl(int semid, int semnum, int cmd, ...);
// 改變信号量的值
int semop(int semid, struct sembuf *sops, size_t nops);
// 對應的參數的含義:https://blog.csdn.net/skyroben/article/details/72513985
消息隊列
消息隊列是消息的連結表,存放在核心中并由消息隊列辨別符辨別。使用者可以從消息隊列中讀取和添加消息,其中
發送程序添加消息到隊列的末尾,接收程序在隊列的頭部接收消息,消息一旦被接收,就會從隊列中删除。
消息隊列常用的一些函數:
- msgget 建立或者打開消息隊列
- msgsnd 添加消息
- msgrcv 讀取消息
- msgctl 控制消息隊列
- ftok 由于檔案路徑工程ID生成的标準key
共享記憶體
共享記憶體就是允許兩個或多個程序共享一定的存儲區。就如同
malloc()
函數向不同程序傳回了指向同一個實體記憶體區域的指針。
當一個程序改變了這塊位址中的内容的時候,其它程序都會察覺到這個更改。因為資料不需要在客戶機和伺服器端之間複制,資料直接寫到記憶體,不用若幹次資料拷貝。
但是共享記憶體沒有任何的同步與互斥機制,是以要使用信号量來實作對共享記憶體的存取的同步
共享記憶體的涉及到的函數:
// 建立共享記憶體,成功傳回共享記憶體的ID,出錯傳回-1
int shmget(key_t key, size_t size, int shmflg);
// 操作共享記憶體,成功傳回0,出錯傳回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
共享記憶體優缺點:
- 優點:我們可以看到使用共享記憶體進行程序間的通信真的是非常友善,而且函數的接口也簡單,資料的共享還使程序間的資料不用傳送,而是直接通路記憶體,也加快了程式的效率。同時,它也不像匿名管道那樣要求通信的程序有一定的父子關系。
- 缺點:共享記憶體沒有提供互斥同步的機制,這使得我們在使用共享記憶體進行程序間通信時,往往要借助其他的手段比如信号量等來進行程序間的同步工作。
6.為什要程序通信
程序是一個獨立的資源配置設定單元,不同程序(這裡所說的程序通常指的是使用者程序)之間的資源是獨立的,沒有關聯,不能在一個程序中直接通路另一個程序的資源(例如打開的檔案描述符)但是,
程序不是孤立的,不同的程序需要進行資訊的互動和狀态的傳遞等,是以需要程序間通信 。程序間通信的目的:
- 資料傳輸: 一個程序需要将它的資料發送給另一個程序。
- 通知事件: 一個程序需要向另一個或一組程序發送消息,通知它(它們)發生了某種事件(如程序終止時要通知父程序)。
- 資源共享: 多個程序之間共享同樣的資源。為了做到這一點,需要核心提供互斥和同步機制。
- 程序控制: 有些程序希望完全控制另一個程序的執行(如Debug程序),此時控制程序希望能夠攔截另一個程序的所有陷入和異常,并能夠及時知道它的狀态改變。
7.線程間通信
同步
指多個線程通過
Synchronized
關鍵字這種方式來實作線程間的通信。
比方說由于線程
A
和線程
B
持有同一個
MyObject
類的對象
object
,盡管這兩個線程需要調用不同的方法,但是它們是同步執行的,比如:線程B需要等待線程
A
執行完了
methodA()
方法之後,它才能執行
methodB()
方法。這樣,線程
A
和線程
B
就實作了通信。
這種方式,本質上就是“共享記憶體”式的通信。多個線程需要通路同一個共享變量,誰拿到了鎖(獲得了通路權限),誰就可以執行。
while輪詢的方式
參考:Java多線程通信方式
wait/notify機制
通過程序調用對應的函數,通知對應另外的線程進而實作線程的通信。
比方說當條件未滿足時,線程
A
調用
wait()
放棄
CPU
,并進入阻塞狀态,當條件滿足時,線程
B
調用
notify()
通知線程
A
,所謂通知線程
A
,就是喚醒線程
A
,并讓它進入可運作狀态。
8.程序同步
程序同步是一個作業系統級别的概念,有兩種形式的制約:間接性制約(同一個系統的程序需要共享着某種資源),直接性制約(源于程序間的合作)表示是在多道程式的環境下,存在着不同的制約關系,
為了協調這種互相制約的關系,實作資源共享和程序協作,進而避免程序之間的沖突,引入了程序同步。 比如說程序
B
需要從緩沖區讀取程序
A
産生的資訊,當緩沖區為空時,程序
B
因為讀取不到資訊而被阻塞。而當程序
A
産生資訊放入緩沖區時,程序
B
才會被喚醒。
臨界資源在作業系統中,程序是占有資源的最小機關,但對于某些資源來說,其在同一時間隻能被一個程序所占用。
這些一次隻能被一個程序所占用的資源就是所謂的臨界資源。典型的臨界資源比如實體上的列印機
對于臨界資源的通路,必須是互斥進行。也就是當臨界資源被占用時,另一個申請臨界資源的程序會被阻塞,直到其所申請的臨界資源被釋放。
而程序内通路臨界資源的代碼被稱為臨界區。對于臨界區的通路過程分為四個部分:
- 進入區 :檢視臨界區是否可通路,如果可以通路,則轉到步驟二,否則程序會被阻塞
- 臨界區 :在臨界區做操作
- 退出區 :清除臨界區被占用的标志
- 剩餘區 :程序與臨界區不相關部分的代碼
9.程序互斥
程序互斥是程序之間的間接制約關系。當一個程序進入臨界區使用臨界資源時,另一個程序必須等待。隻有當使用臨界資源的程序退出臨界區後,這個程序才會解除阻塞狀态。
比如程序 B 需要通路列印機,但此時程序 A 占有了列印機,程序 B 會被阻塞,直到程序 A 釋放了列印機資源,程序B 才可以繼續執行。
實作臨界區互斥的基本方法- 通過硬體實作臨界區最簡單的辦法就是關 CPU 的中斷
- 信号量實作:常見的 P,V 操作
10.程序同步與程序通信差別
程序同步:控制多個程序按一定的順序執行;
程序通信:程序間傳輸資訊。
程序通信是一種手段,而程序同步是一種目的。
也可以說,為了能夠達到程序同步的目的,需要讓程序進行通信,傳輸一些程序同步所需要的資訊。
11.如何正确的停止一個線程
結束一個線程有一個最基本的方法:Thread.stop() 方法,但是這個方法已經是被建議不要使用的方法(會立即釋放該線程所持有的所有的鎖導緻資料得不到同步的處理,出現資料不一緻的問題;即刻停止 run() 方法中剩餘的全部工作,包括在 catch 或 finally 語句中,并抛出 ThreadDeath 異常(通常情況下此異常不需要顯示的捕獲),是以可能會導緻一些清理性的工作的得不到完成,如檔案,資料庫等的關閉。)。
正确的是使用中斷,也就是使用 Thread.interrupt() 方法,嚴格的講,線程中斷不會使線程立即退出,而是給線程發送一個通知,告訴目标線程,有人需要你退出!至于目标線程接到通知後如何處理,則完全由目标線程自行決定。 是以置為中斷狀态,還需要增加中斷處理邏輯程式,不然就沒有作用。需要使用 Thread.isInterrupted() 判斷線程是否被中斷,然後進入中斷處理邏輯代碼。12.中斷和異常
所謂中斷是指CPU對系統發生的某個事件作出的一種反應:CPU暫停正在執行的程式,保留現場後自動地轉去執行相應的處理程式,處理完該事件後再傳回斷點繼續執行被“打斷”的程式。
引起中斷的事件稱為
中斷源,中斷源向CPU提出進行處理的請求稱為
中斷請求。它是由CPU以外的事件引起的中斷,如I/O中斷、時鐘中斷、控制台中斷等。
引入中斷的目的:實作并發活動,實作實時處理,故障自動處理
異常來自 CPU 的内部事件或程式執行中的事件引起的過程。如由于CPU本身故障、程式故障和請求系統服務的指令引起的中斷等。
13.程序隔離
它是為保護作業系統中程序互不幹擾而設計的一組不同硬體和軟體)的技術。這個技術是為了避免程序 A 寫入程序B 的情況發生。
程序的隔離實作,使用了虛拟位址空間。程序 A 的虛拟位址和程序 B 的虛拟位址不同,這樣就防止程序 A 将資料資訊寫入程序 B。
虛拟記憶體
虛拟記憶體:虛拟記憶體是一種邏輯上擴充實體記憶體的技術。基本思想是用軟、硬體技術把記憶體與外存這兩級存儲器當做一級存儲器來用。虛拟記憶體技術的實作利用了自動覆寫和交換技術。
簡單的說就是将硬碟的一部分作為記憶體來使用。
虛拟位址空間
通過虛拟記憶體的概念,作業系統為每一個程序提供完全一緻的記憶體視圖,這個記憶體視圖的位址空間,叫虛拟位址空間。CPU在尋址的時候,是按照虛拟位址來尋址,然後通過MMU(記憶體管理單元)将虛拟位址轉換為實體位址。因為隻有程式的一部分加入到記憶體中,是以會出現所尋找的位址不在記憶體中的情況(CPU産生缺頁異常),如果在記憶體不足的情況下,就會通過頁面排程算法來将記憶體中的頁面置換出來,然後将在外存中的頁面加入到記憶體中,使程式繼續正常運作。
14.多線程、多程序的差別及适用場景
對比次元多程序多線程總結資料共享、同步資料共享複雜、需要用IPC;資料是分開的,同步簡單因為共享程序資料,資料共享簡單,但也是因為這個原因導緻同步複雜各有優勢記憶體、CPU占用記憶體多,切換複雜,CPU使用率低占用記憶體少,切換簡單,CPU使用率高線程占優建立銷毀、切換建立銷毀、切換複雜,速度慢建立銷毀,切換簡單,速度很快線程占優程式設計、調試程式設計簡單,調試簡單程式設計複雜,調試複雜 程序占優 可靠性程序間不會互相影響一個線程挂掉将導緻整個程序挂掉程序占優分布式适用于多核、多機分布式;如果一台機器不夠,擴充到多态機器比較簡單是英語多核分布式程序占優
多線程相比于多程序占用記憶體少、CPU使用率高,建立銷毀,切換都比較簡單,速度很快。多程序相比于多線程共享資料複雜,需要将程序間通信。但是同步簡單,多線程因為資料共享簡單,導緻同步複雜。多程序程式設計調試都比多線程簡單。程序之間互相不影響,一個線程挂掉将導緻整個程序挂掉。多程序适合多核,多機分布,多線程适合多核分布。
舉個例子,谷歌浏覽器是使用多程序來實作的,浏覽器中你打開的每個頁面,都是一個程序。如果一個頁面崩潰了,不會影響其他頁面(程序互相獨立)。但是谷歌浏覽器占用記憶體相比于其他浏覽器多,實際應用中,打開頁面太多,占用記憶體較大。其他浏覽器采用多線程來實作,每個頁面就是一個線程,是以一個頁面崩潰,會導緻整個浏覽器崩潰。