並發與並行
並發程序是指可以被同事發起執行的程序,並行程序可以在並行的硬件上執行的並發程序,這兩者稍有不同,並發程序代表了所有可以實現並發行為的程序,其中包含了並行程序。
串行程序所有代碼的先后順序都是確定的,並發程序中只有部分代碼有序,其中有一些代碼的執行順序無明確指定,這被稱為不確定性。
並發程序內部會被划分為多個部分,每個部分都是一個串行程序,這些串行程序中又會存在交互需求,我們需要協調他們的執行,就涉及到同步。
同步
同步的作用是避免在並發訪問共享資源時可能發生的沖突,以及確保有條不紊的傳遞數據。如果程序想使用一個共享資源,就必須請求該資源並獲取到對它的訪問權,如果不需要時,應該放棄對該資源的訪問權。即同一時刻某個資源應該只被一個程序所占用。
異步
傳遞數據是並發程序內部的一種交互方式,也成為並發程序內部的通信,協調這種內部通信的方式不只“同步”一種,我們也可使用異步的方式進行。這種方式可使數據不加延遲的發送給數據接收方。數據將會被臨時存放在一個稱為數據通信緩存的數據結構中。通信緩存是一種特殊的共享資源,他可同時被多個程序使用,數據結合搜房可以在准備就緒后按照數據的寫入順序進行接收。
多進程編程
進程間通信的方式用來支持多個進程間協作完成任務,這種通信也被稱為IPC(Inter-Process Communication),Linux中支持的IPC有多種,可分為三類:
-
基於通信的IPC方法
其中又分為以數據傳送為手段的IPC方法和以共享內存為手段的IPC方法,前者包括管道(PIPE)和消息隊列(message queue),管道可以用來傳送字節流,消息隊列可以用來傳送結構化的消息對象。以共享內存為手段的IPC方法主要以共享內存區(shared memory)為代表,它是最快的一種IPC方法。
-
基於信號的IPC方式
即操作系統的信號(signal)機制,它是唯一的一種異步IPC的方法。 -
基於同步的IPC方式,
即信號量(semaphore)
進程的定義
一個程序的執行叫做進程,進程也是操作系統進行資源分配的一個基本單位。
進程可使用(fork)創建若干個新的進程,前者稱為后者的父進程,后者稱為前者的子進程。每個子進程都源自父進程的一個副本,他會獲得父進程的數據段、堆和棧的副本,並與父進程共享代碼段。每一份副本都是獨立的。子進程對副本的修改對其父進程和兄弟進程都是不可見的,反之亦然。Linux 操作系統內核使用寫時復制(Copy on Write)等技術來提高進程創建的效率。
Unix/Linux中每一個進程都有父進程,所有的進程共同組成了一個樹狀結構。內核啟動進程作為進程樹的根,負責系統的初始化操作。
為了管理進程,內核必須對每個進程的屬性和行為進行詳細記錄,包括進程的優先級、狀態、虛擬地址范圍以及各種訪問權限等等,這些信息都會被記錄在每個進程的進程描述符中。進程描述符是一個復雜的數據結構。保存在進程描述符中的進程ID(也被稱為PID)是進程在操作系統的唯一標識。其中進程ID為1的進程就是內核啟動進程。進程ID為一個非負整數。此外,進程描述符中海油當前進程的父進程的ID也被稱為(PPID)。
我們可通過Go標准庫代碼查看當前進程的PID和PPID:
pid := os.Getpid()
ppid := os.Getppid()
進程的狀態
Linux中每個進程在每個時刻都有狀態,可能的狀態有6個:可運行狀態、可中斷的睡眠狀態、不可中斷的睡眠狀態、暫停狀態或跟蹤狀態、僵屍狀態和退出狀態,具體每個狀態的輪轉可見參考文章
進程的空間
用戶進程(程序的執行實例)總會生存在用戶空間,無法與其所在計算機的硬件進行交互。內核可與硬件交互,但它生存在內核空間。用戶進程無法直接訪問內核空間。用戶空間和內核空間瓜分了操作系統可支配的內存區域。
用戶空間范圍為0~TASK_SIZE,內核空間則占據了剩下的空間。TASK_SIZE由所在計算機體系結構確定,是一個常數,如圖:

內存區域中的每一個單元都是有地址的,地址由指針來標識和定位。通過指針來尋找內存單元的操作也稱為內存尋址。指針是一個正整數,由若干個二進制位來表示,具體的二進制位的數量由計算機(CPU)的字長來決定。如在32位計算機中可以有效標識2的32次方個內存單元,64位計算機中可以有效標識2的64次方個內存單元。
此處所說的地址並非物理地址,而是虛擬地址,而由虛擬地址來標識的內存區域又稱為虛擬地址空間,也被稱為虛擬內存。虛擬內存的最大容量與實際可用的物理內存的大小無關。CPU和內核會負責維護虛擬內存與物理內存之間的關系。另外,內核會為每個用戶進程分配的是虛擬內存而不是物理內存。每個用戶進程分配到的虛擬內存總是在用戶空間中,而內核空間為內核專用。
內核會將進程的虛擬內存划分為若干頁(page),而物理內存單元的划分由CPU負責。一個物理內存單元被稱為一個葉框(page frame)。不同進程的大多數頁都會與不同的葉框對應。

系統調用
用戶進程使用用戶空間和內核空間之間橋梁(允許用戶進程使用內核功能的接口)的行為稱為系統調用,與普通函數相比,系統調用是向內核空間發出的一個明確請求,普通函數只定義了如何獲取一個給定的服務。系統調用會導致內核空間中數據的存取和指令的執行,而普通函數卻只能在用戶空間進行。此外,系統調用是內核的一部分。
內核態與用戶態
為了確保系統安全,內核依據由CPU提供的、可以讓進城駐留的特權級別建立了兩個特權狀態——內核態與用戶態。大部分情況下,CPu都處於用戶態,這時CPU只能對用戶空間進行訪問。換而言之,CPU在用戶態下運行的用戶進程是不能與內核接觸的。
當用戶進程發出一個系統調用時,內核會把CPU從內核態切換到用戶態,而后讓CPU執行對應的內核函數。這就相當於用戶進程可以通過系統調用使用內核提供的功能,當內核函數執行完畢后,內核會把CPU從內核態切回用戶態,並把執行結果返回給用戶進程。
進程切換和調度
Linux操作系統可憑借CPU的威力快速地在多個進程之間進行切換,這被稱為進程間的上下文切換,在進程換入換出期間必須要做的任務統稱為進程切換。
為了使各個生存着的進程都有運行的機會,內核還要考慮下次切換時運行哪個進程、何時切換、換下的進程何時換上等等。解決類似問題的方案和任務統稱為進程調度。常見的進程調度方法有:
1. 先來先去服務
2. 時間片輪轉法
3. 多級反饋隊列算法
4. 最短進程優先
5. 最短剩余時間優先
6. 最高響應比優先
7. 多級反饋隊列調度算法
同步
同步這塊有幾個高頻出現的概念:
- 執行過程中不能中斷的操作稱為原子操作(atomic operation)
- 只能被串行化訪問或執行的某個資源或某段代碼稱為臨界區(critical section)
PS: 所有系統調用都屬於原子操作。
*原子操作不能中斷,而臨界區對是否可以被中斷卻無強制性規定,原子操作僅適合細粒度的簡單操作*
在單核程序並發時,原子操作是一種很好的解決方案,不過讓串行化執行的若干代碼形成臨界區的這種做法更加通用。保證只有一個進程或線程在臨界區之內的做法有一個官方謂稱——互斥(mutual exclusion,簡稱mutex)。實現互斥的方法必須確保排他原則(exclusion principle),並且保證不能依賴於任何計算機硬件來實現。作為IPC方法之一的信號量就屬於實現互斥的可行方式之一。
Go語言中支持的IPC方法有管道、信號和socket
管道
管道(pipe)是一種半雙工的通信方式,只能用於父進程與子進程以及通祖先的子進程之間的通信。
如:Shell命令中的管道,shell 為每個命令都創建一個進程,然后把左邊命令的標准輸出用管道與右邊命令的標准輸入連接起來。
ps aux|grep go
如上是匿名管道,與之對應的是命名管道(named pipe)。與匿名管道不同的是,任何進程都可以通過命名管道交換數據。

如上圖,我們創建了一個名為myfifo1的命名管道,從src.log中讀入數據,最后寫入dst.log中
PS:
1.命名管道默認是阻塞式的。具體來說,只有在對這個命名管道的讀操作和寫操作都已准備就緒后,數據才開始流轉。
2.匿名管道會在管道緩沖區被寫滿后使寫數據的進程阻塞,命名管道會在其中一段未就緒前阻塞另一端的進程。
3.命名管道可以被多路復用。
go語言中創建管道的語句
reader, writer := io.Pipe()
信號
信號(signal)是IPC中唯一一種異步的通信方式,他的本質使用軟件來模擬硬件的中斷機制。信號用來通知有某個事件發生了。
在Linux下,我們可使用kill命令來查看當前系統所支持的信號。

可以看到,每一個信號都有一個以“SIG”為前綴的名字。如SIGINT、SIGQUIT等。Linux支持的信號有62種(無32和33的信號)。其中,編號從1到31的信號屬於標准信號(不可靠信號),編號34到64的信號屬於實時信號(也稱可靠信號)。對於同一個進程來說,每種標准信號只會被記錄並處理一次。並且如果發送給某一個進程的標准信號的種類有多個,那么他們的處理順序也是完全不確定的。而實時信號解決了標准信號的這兩個問題,即多個同種類的實時信號都可以記錄在案,並且它們可以按照信號的發送順序被處理。雖然實時信號在功能上更加強大,但已成為事實標准的標准信號也無法被替換掉。因而這兩種信號一直存在。
信號的來源有鍵盤輸入、硬件故障、系統函數、軟件中的非法運算。進程響應信號的方式有三種:忽略、捕捉和執行默認操作。
Linux對每一個標准信號都有默認的操作方式。包括:終止進程、忽略該信號、終止進程並保存內存信息、停止進程、恢復進程。
在Go語言中,Go命令會對其中一些以鍵盤輸入為來源的標准信號作出響應,這是通過標准庫代碼包os/signal中的一些API實現的。即Go命令指定了需要被處理的信號並用一種很優雅的方式來監聽信號的到來。
接口os.Signal
type Signal interface{
String() string
Signal()
}
利用Go提供的信號處理接口,我們可以處理信號傳來的數據
Socket
套接字,可以通過網絡連接讓多進程建立通信並相互傳遞數據,這使得通信雙方是否在同一台計算機上變得無關緊要。即實現了讓通信端的位置透明化。
在Linux系統中,存在一個名為socket的系統調用,其聲明如下:
int socket(int domain, int type, int protocal);
該系統調用的功能是創建一個socket實例。接受了3個參數,分別代表這個socket的通信域、類型和所用協議。
