基本概念
狀態、地址空間
- 三種基本狀態 —— 就緒、運行、阻塞
-
進程控制塊PCB(Process Control Block)
- 進程描述信息(如PID);
- 進程控制&管理信息(狀態、優先級等);
- 源分配清單(地址空間狀況、fd等);
- 處理其相關信息(各寄存器的值等)
進程存在的標識,在Linux系統中是task_struct,task_struct在內核棧(Linux進程氛圍用戶棧和內核棧)的尾端分配。
- 進程地址空間
從低地址到高地址:
- text 代碼段 —— 代碼段,一般是只讀的區域;
- static_data 段 =
- stack 棧區 —— 局部變量,函數的參數,返回值等,由編譯器自動分配釋放;
- heap 堆區 —— 動態內存分配,由程序員分配釋放;
- 進程與線程
進程是擁有資源的基本單位,進程的地址空間相互獨立;
線程是獨立調度的基本單位,共享同一個進程內的資源(線程有自己的棧),減少了程序並發時所付出的時空開銷,並且可以高效的共享數據,有效地利用多處理器和多核計算機,提高os的並發度。
一個進程異常退出不會引起另外的進程運行異常;但是線程若異常退出一般是會引起整個進程奔潰。
創建/撤銷/切換 進程的開銷遠大於線程的(創建線程比創建進程快10~100倍 UNPv2/P406)。
- 僵屍、孤兒、守護
孤兒進程 —— 父進程退出,而它的一個或多個子進程還在運行,那么那些子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)所收養,並由init進程對它們完成狀態收集工作。
僵屍進程 —— 一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中。
守護進程 —— 守護進程的父進程是init進程,因為它真正的父進程在fork出子進程后就先於子進程exit退出了,所以它是一個由init繼承的孤兒進程。不需要用戶輸入就能運行而且提供某種服務,不是對整個系統就是對某個用戶程序提供服務。常見的守護進程包括系統日志進程syslogd、 web服務器httpd、郵件服務器sendmail和數據庫服務器mysqld等。
最大進程數以及單進程內的最大線程數?
最大進程數受以下3方面限制:
- 不能超過pid_t類型的最大值
- 使用命令ulimit -u查看系統中限制的最大進程數。/etc/security/limits.conf里面是硬限制,ulimit -u是軟限制,內核參數kernel.pid_max也做了限制。
- 受系統資源限制,創建一個新進程會消耗系統資源,最主要的就是內存。
IPC (interprocess communication)
UNP 分了以下幾中形式的IPC:
- 消息傳遞 —— 管道、FIFO、消息隊列
- 共享內存
- 同步 ——信號量、互斥量、條件變量、讀寫鎖、文件和記錄鎖、
- 遠程過程調用 —— solaris 門、Sun RPC
- 跨網絡 IPC —— 套接字
- 域套接字
- 信號
進程間通信方式:匿名管道,有名管道,消息隊列,共享內存,信號量,套接字,域套接字,信號
線程同步方式:互斥量,條件變量,讀寫鎖
進程間通信
匿名管道
半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端;是一種特殊的文件(pipefs,掛載在內核中),有固定大小,只存在於內存中;
實現原理:
管道是由內核管理的一個緩沖區,被設計成為環形的數據結構,以便管道可以被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另一端的進程取出信息。當兩個進程都終結的時候,管道也自動消失。
在 Linux 中,管道的實現借助了文件系統的file結構和VFS的索引節點inode。通過將兩個file結構指向同一個臨時的VFS索引節點,而這個VFS索引節點又指向一個物理頁面而實現的。
內核會利用一定的機制同步對管道的訪問。
有名管道
半雙工,可在無關進程使用;FIFO有路徑名與之相關聯,以一種特殊設備文件形式存在於文件系統中
實現原理:
Linux中設立了一個專門的特殊文件系統--管道文件,FIFO在文件系統中有對應的路徑。當一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那么內核就會在這兩個進程之間建立管道,雖然FIFO在VFS的目錄樹下可見,但是它並不對應disk上的文件。
本質上是一個先進先出的隊列數據結構,最早放入的數據被最先讀出來,從而保證信息交流的順序。FIFO只是借用了文件系統來為管道命名。當刪除FIFO文件時,管道連接也隨之消失。當進程終止時,管道內的數據會被刪除。
消息隊列
- 面向記錄的,其中的消息具有特定的格式以及特定的優先級;
- 獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除(隨內核的持性)。
- 可以實現消息的隨機查詢,不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
為什么不使用消息隊列
- 進程終止時,消息隊列及其內容並不會被刪除(隨內核的持性)。
- 在文件系統中沒有名字,不能使用IO。
共享內存和信號量
共享內存是最快的:
通常往管道、FIFO或消息隊列寫入數據時,這些IPC需要將數據從進程復制到內核,通常總共需要復制4次,而共享內存則只拷貝2次數據;如圖:
信號(Signal)
用於通知接收進程,有某種事件發生。
信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達。
- SIGRTMIN之前的信號是非排隊(不可靠)的,多次連續發生的相同信號,只遞交一次;SIGRTMIN之后的信號不會丟失,會遞交多次。
- 在信號處理函數
- singal:只阻塞當前正在處理的信號,后續信號會排隊(也會區分可靠和不可靠)
- sigaction:可以設置阻塞正在處理的信號和其他型號
Pipe 與 FIFO 比較:
- pipe在特殊文件系統pipefs中(內核中),VFS 目錄樹下不可見;FIFO 在目錄樹下可見;
- 都有 inode,但沒有磁盤鏡像(disk image);
- Pipe 用於親緣關系進程通信;FIFO 無此要求;
- 限制都包含兩個:OPEN_MAX(一個進程在任意時刻打開的最大描述符,默認1024);還有PIPE_BUF(可原子性地往一個管道/FIFO 的最大數據量,默認 4K)
線程間同步
互斥鎖(Mutex)
加鎖原語,排他性訪問共享數據,用於保護臨界區。可細分為遞歸鎖/非遞歸鎖。
如果存在某個線程依然使用原先的程序
(即不嘗試獲得mutex,而直接修改共享變量),互斥鎖不能阻止其修改。所以,互斥鎖機制需要程序員自己來寫出完善的程序來實現互斥鎖的功能(以下鎖 一樣)。
條件變量(Condition Variable)
互斥鎖用於上鎖,條件變量用於等待,條件變量的使用是與互斥鎖共通使用的。
條件變量學名叫管程(monitor)【From muduo P40】。
讀寫鎖
讀寫鎖也叫做 共享-獨占鎖,允許更高的並發度。
互斥量要么是鎖住狀態,要么是不加鎖狀態,而且一次只有一個線程對其加鎖。
讀寫鎖可以有三種狀態:讀模式下加鎖狀態,寫模式下加鎖狀態,不加鎖狀態。一次只有一個線程可以占有寫模式的讀寫鎖,但是多個線程可用同時占有讀模式的讀寫鎖。
讀寫鎖可以通過使用互斥鎖和條件變量來實現。
自旋鎖(spinlock)
在獲取鎖之前一直處於忙等(自旋)阻塞狀態;常用於
鎖被持有的時間短,且線程並不希望在重新調度上花費太多成本。當線程自旋等待鎖變為可用時,CPU不能做其他事情。
故而自旋鎖常作為底層原語,用於實現其他類型的鎖。
記錄鎖
記錄鎖是讀寫鎖的一種擴展類型,可用於親緣關系或無親緣關系的進程之間共享某個文件的讀與寫。被鎖住的文件通過文件描述符進行訪問,執行上鎖的操作函數是fcntl,這種類型的鎖通常在內核中維護。
記錄鎖的功能是:一個進程正在讀或修改文件的某個部分時,可以阻止其他進程修改同一文件區,即其鎖定的是文件的一個區域或整個文件。
記錄鎖有兩種類型:共享讀鎖,獨占寫鎖。基本規則是:多個進程在一個給定的字節上可以有一把共享的讀鎖,但在一個給定字節上的寫鎖只能有一個進程獨用。即:如果在一個給定的字節上已經有一把讀或多把讀鎖,則不能在該字節上再加寫鎖;如果在一個字節上已經有一把獨占性的寫鎖,則不能再對它加任何讀鎖。
死鎖
- 死鎖 —— 就是兩個或多個進程被無限期地阻塞、相互等待的一種狀態。
- 死鎖的四個條件
- 競爭同一個資源
- 持有資源不釋放
- 不能搶占資源
- 循環使用資源
處理死鎖的策略:
一般來說,打破循環使用資源最容易,即順序加減鎖
銀行家算法(死鎖避免算法)。在資源動態分配過程中,防止系統進入不安全狀態,以避免發生死鎖。
數據庫中會用到等待圖進行死鎖檢測
- 死鎖定理:
通過將資源分配圖簡化的方法,來檢測系統狀態是否為死鎖狀態。
當且僅當S狀態的資源分配圖不可完全簡化時,S為死鎖狀態。
經典問題
- 生產者-消費者問題
- 讀者-寫者問題
Futex
在Linux下,信號量和線程互斥鎖的實現都是通過futex系統調用。