操作系統相關知識整理(二)

進程和線程

進程空間

Linux 對進程地址空間有個標準佈局,地址空間中由各個不同的內存段組成 (Memory Segment),主要的內存段如下:
- 程序段 (Text Segment):可執行文件代碼的內存映射
- 數據段 (Data Segment):可執行文件的已初始化全局變量的內存映射
- BSS段 (BSS Segment):未初始化的全局變量或者靜態變量(用零頁初始化)
- 堆區 (Heap) : 存儲動態內存分配,匿名的內存映射
- 棧區 (Stack) : 進程用戶空間棧,由編譯器自動分配釋放,存放函數的參數值、局部變量的值等
- 映射段(Memory Mapping Segment):任何內存映射文件

線程空間

線程之間共享進程空間,在內存映射區域開闢一塊獨有的線程棧。

線程共享的環境包括: 進程代碼段、進程的公有數據( 利用這些共享的數據,線程很容易的實現相互之間的通訊) 、進程打開的文件描述符、信號的處理器、進程的當前目錄和進程用戶 ID 與進程組 ID 。
線程之間特有的:每個線程都有自己獨立的線程上下文,包括線程 ID 、棧、棧指針、程序計數器、條件碼和通用目的寄存器值。
線程之間共有的:共享進程上下文的剩餘部分,包括只讀文本(代碼)、讀/ 寫數據、堆以及所有的共享庫代碼和數據區域。線程也共享相同的打開文件的集合。

進程的創建

在 在 linux 中主要提供了 fork ,vfork ,clone 三個進程創建方法。
fork:現在 Linux 中是採取了 copy-on-write(COW 寫時複製) 技術,爲了降低開銷,fork 最初並不會真的產生兩個不同的拷貝,因爲在那個時候,大量的數據其實完全是一樣的。寫時複製是在推遲真正的數據拷貝。若後來確實發生了寫入,那意味着 parent 和 child 的數據不一致了,於是產生複製動作,每個進程拿到屬於自己的那一份,這樣就可以降低系統調用的開銷。


vfork():vfork 系統調用不同於 fork,用 用 vfork 創建的子進程與父進程共享地址空間,也就是說子進程完全運行在父進程的地址空間上,如果這時子進程修改了某個變量,這將影響到父進程。但此處有一點要注意的是用 vfork()創建的子進程必須顯示調用 exit()來結束,否則子進程將不能結束,而 fork()則不存在這個情況。


用 vfork 創建子進程後,父進程會被阻塞直到子進程調用 exec(exec,將一個新的可執行文件載入到地址空間並執行之。)或exit。vfork 的好處是在子進程被創建後往往僅僅是爲了調用 exec 執行另一個程序,因爲它就不會對父進程的地址空間有任何引用,所以對地址空間的複製是多餘的 ,因此通過 vfork 共享內存可以減少不必要的開銷。

clone():系統調用 fork()和 vfork()是無參數的,而 clone()則帶有參數。fork()是全部複製,vfork()是共享內存,而 clone() 是則可以將父進程資源有選擇地複製給子進程,而沒有複製的數據結構則通過指針的複製讓子進程共享。

Unix環境高級編程中8.3節中說,“子進程是父進程的副本。例如,子進程獲得父進程數據空間、堆和棧的副本。注意,這是子進程所擁有的副本。父進程和子進程並不共享這些存儲空間部分。父進程和子進程共享正文段。”

線程的創建

實際上是調用了進程創建的clone()方法,對進程描述符task_struct中的內存描述符mm_struct作淺拷貝,再調用mmap在內存映射區域創建線程棧。

進程狀態

R (TASK_RUNNING),可執行狀態。

S (TASK_INTERRUPTIBLE),可中斷的睡眠狀態。

D (TASK_UNINTERRUPTIBLE),不可中斷的睡眠狀態。

TASK_UNINTERRUPTIBLE狀態存在的意義就在於,內核的某些處理流程是不能被打斷的。

T (TASK_STOPPED or TASK_TRACED),暫停狀態或跟蹤狀態。

Z (TASK_DEAD - EXIT_ZOMBIE),退出狀態,進程成爲殭屍進程。

在這個退出過程中,進程佔有的所有資源將被回收,除了task_struct結構(以及少數資源)以外。於是進程就只剩下task_struct這麼個空殼,故稱爲殭屍。父進程可以通過wait系列的系統調用(如wait4、waitid)來等待某個或某些子進程的退出,並獲取它的退出信息。然後wait系列的系統調用會順便將子進程的屍體(task_struct)也釋放掉。

X (TASK_DEAD - EXIT_DEAD),退出狀態,進程即將被銷燬。

線程狀態

進程調度

參考這篇博客https://www.cnblogs.com/newjiang/p/7479965.html

進程間通信

IPC 的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket 、Streams 等。其中 Socket 和 Streams 支持不同主機上的兩個進程 IPC

管道

int pipe(int fd[2]); // 返回值:若成功返回 0,失敗返回-1,半雙工

有名管道

FIFO 可以在無關的進程之間交換數據,與無名管道不同。

int mkfifo(const char *pathname, mode_t mode);

消息隊列

消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID )來標識。
消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。
消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。

信號量

信號量(semaphore )與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。
每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
支持信號量組。

共享內存

共享內存(Shared Memory ),指兩個或多個進程共享一個給定的存儲區。
共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。
因爲多個進程可以同時操作,所以需要進行同步。
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。

各種IPC的優缺點

五種通訊方式總結
管 道:速度慢,容量有限,只有父子進程能通訊
FIFO :任何進程間都能通訊,但速度慢
消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題
信號量:不能傳遞複雜消息,只能用來同步

共享內存區:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時
候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存

線程同步

條件變量爲什麼要搭配互斥鎖
1 條件變量的改變一般是臨界資源來完成的,  那麼修改臨界資源首先應該加鎖, 而線程在條件不滿足的情況下要阻塞,等待別人喚醒, 那麼在阻塞後一定要把鎖放開,等到合適的線程拿到鎖去修改臨界資源,否則會出現死鎖
2 在線程被喚醒後第一件事也應該是爭取拿到鎖, 恢復以前加鎖的狀態。 否則在執行條件變量成立後的代碼也法保證其原子性。
所以條件變量和鎖是相輔相成的:條件變量需要鎖的保護鎖需要條件變量成立後,後重新上鎖。 

內存池,進程池,線程池

內存池是池化技術中的一種形式。通常我們在編寫程序的時候回使用 new delete 這些關鍵字來向操作系統申請內存,而這樣造成的後果就是每次申請內存和釋放內存的時候,都需要和操作系統的系統調用打交道,從堆中分配所需的內存。如果這樣的操作太過頻繁,就會找成大量的內存碎片進而降低內存的分配性能,甚至出現內存分配失敗的情況。


而內存池就是爲了解決這個問題而產生的一種技術。從內存分配的概念上看,內存申請無非就是向內存分配方索要一個指針, 當向操作系統申請內存時,操作系統需要進行復雜的內存管理調度之後,才能正確的分配出一個相應的指針。而這個分配的過程中,我們還面臨着分配失敗的風險。


所以,每一次進行內存分配,就會消耗一次分配內存的時間,設這個時間爲 T,那麼進行 n 次分配總共消耗的時間就是 nT;如果我們一開始就確定好我們可能需要多少內存,那麼 在最初的時候就分配好這樣的一塊內存區域, 當我們需要內存的時候,直接從這塊已經分配好的內存中使用即可,那麼總共需要的分配時間僅僅只有 T。當 n 越大時,節約的時間就越多。

線程池 是一種多線程處理形式,將任務添加到隊列中,然後在創建線程後,  自動啓動這些任務。 線程池線程都是後臺線程。
使用線程池理由:1. 頻繁創建、 銷燬加大系統開銷,影響處理效率。2. 限制線程最大併發數。3. 管理線程

孤兒進程,殭屍進程,守護進程

父進程在調用 fork 接口之後和子進程獨立開,之後子進程和父進程就以未知的順序向下執行(異步過程)。所以父進程和子進程都有可能先執行完。 當父進程先結束,子進程此時就會變成孤兒進程 ,被 不過這種情況問題不大,孤兒進程會自動向上被 init 進程收養,init進程完成對狀態收集工作。而且這種過繼的方式也是守護進程能夠實現的因素。

如果子進程先結束,父進程並未調用 wait 或者 waitpid 獲取進程狀態信息,那麼子進程描述符就會一直保存在系統裏,這種進程稱爲殭屍進程。

守護進程:
定義:守護進程是脫離終端並在後臺運行的進程,執行過程中信息不會顯示在終端上並且也不會被終端發出的信號打斷。

自旋鎖

自旋鎖是一種用於保護多線程共享資源的鎖,與一般的互斥鎖(mutex)不同之處在於當自旋鎖嘗試獲取鎖的所有權時會以忙等待(busy waiting)的形式不斷的循環檢查鎖是否可用。

在多處理器環境中對 持有鎖時間 較短的程序來說使用自旋鎖代替一般的互斥鎖往往能提高程序的性能。
自旋鎖有兩種基本狀態:
1. 鎖定狀態
鎖定狀態又稱不可用狀態,當自旋鎖被某個線程持有時就是鎖定狀態,在自旋鎖被釋放之前其他線程不能獲得鎖的所有權。
2. 可用狀態
當自選鎖未被任何線程持有時的狀態就是可用狀態。假設某自旋鎖內部使用 bool 類型的 flag 變量來標識自旋鎖的狀態。當 flag 爲 true 表示鎖定狀態,爲 false 表示可用狀態。

協程

協程,英文 Coroutines,是一種比線程更加輕量級的存在。正如一個進程可以擁有多個線程一樣,一個線程也可以擁有多個協程。協程的調度完全由用戶控制。協程擁有自己的寄存器上下文和棧。協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文和棧,直接操作棧則基本沒有內核切換的開銷,可以不加鎖的訪問全局變量,所以上下文的切換非常快。

協程與線程主要區別是它將不再被內核調度,而是交給了程序自己而線程是將自己交給內核調度,最重要的是,協程不是被
操作系統內核所管理,而完全是由程序所控制(也就是在用戶態執行)。協程暫停和線程的阻塞是有本質區別的。協程的暫停完全由程序控制,線程的阻塞狀態是由操作系統內核來進行切換。因此,協程的開銷遠遠小於線程的開銷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章