進程和執行緒

為什麼要引入進程?

我們知道,最早出現的OS是單道批處理系統,由於它是順序執行程式的,即一個一個地按先到先執行的順序依次執行。因此,CPU的高速性與I/O的低速性之間的矛盾很明顯(IO低速是指這個程式在運行過程中需要頻繁的訪問IO資源,如需要輸入數據,如讀取硬碟文件,使用印表機,在這個程式進行IO操作的時候,一般來說不佔用CPU資源,此時把CPU資源分配給其他的進程,這樣就提高了CPU的利用率)。為了緩解這個矛盾,人們引入了多道批處理系統,該系統讓程式並發執行,即在一個程式發起I/O請求時CPU不再選擇等待I/O完成,而是轉去執行下一個程式。

並發執行特點:

  • 間斷性:多個程式宏觀上並發,微觀上輪流使用CPU,所以每一個程式都是走走停停,具有間斷性,這樣做提高了資源的利用率
  • 失去封閉性:多個程式共享系統的資源,所以資源的狀態不專屬於一個程式,失去了封閉性
  • 不可再現性:由於多個程式共享資源,每個程式都有對資源修改的權利,所以程式的輸出結果是不一定的

作業系統設計的最終目的是不但要提高資源的利用率,而且還要具備封閉性和可再現性,如何解決封閉性的問題?

解決封閉性有兩種方法

1.Bernstein條件:既然所有運行的程式不能並發運行,那麼能否讓一部分互不干擾的程式並發運行,在一部分程式上實現並發

具體內容:

其實要表達的意思是這樣的,將程式的所有讀的集合和寫的集合挪列出來,如果兩個程式僅僅只有讀集有交集,說明兩個程式是可以並發的,也就是說兩個程式可以同時讀,但一個讀一個寫和同時寫都是不能並發的

2.引入進程

那麼能否有一個東西將資源封閉起來呢?

進程就被引入了,進程是系統進行資源分配和處理機調度的獨立單位,所以進程之間是可以並發的。(意思就是以前每個程式都共享所有的資源,現在就是讓每個程式都只能擁有一部分資源,這就解決了封閉性,解決了封閉性也就實現了再現性)

進程 = 數據段 + 程式段 + PCB

為什麼要引入執行緒?

進程是一個可擁有資源的獨立單位,進程同時又是一個可獨立調度和分派的基本單位。正是由於進程有這兩個基本屬性,才使進程成為一個能獨立運行的基本單位,從而也就構成了進程並發執行的基礎。

為使程式能並發執行,系統必須對進程進行以下的一系列操作:創建進程、撤銷進程以及進程間切換。據此可知,由於進程是一個資源的擁有者,因而在創建、撤消和切換中,系統必須為之付出較大的時空開銷。這就限制了系統中所設置進程的數目,而且進程切換也不宜過於頻繁,從而限制了並發程度的進一步提高。

要設法將進程的上述兩個屬性分開,由OS分開處理,亦即並不把作為調度和分派的基本單位也同時作為擁有資源的單位,以做到「輕裝上陣」,而對於擁有資源的基本單位,又不對之施以頻繁的切換。正是在這種思想的指導下,形成了執行緒的概念。

自己理解:

  1. 儘可能使資源分配和CPU調度分開,因為,因為進程間切換需要保存進程CPU環境(棧、暫存器、頁表和文件句柄等)以及新調度的進程CPU環境的設置,而執行緒切換保存和設置程式計數器、少量暫存器和棧的內容
  2. 應用程式功能越來越多,可以用多執行緒來實現(比如QQ可以用一個執行緒來接受文件,一個執行緒來進行語音通話,其實也可以通過創建新的進程來實現,第一點才是引入的根本目的)

進程、執行緒和協程的區別和聯繫

image

一個進程可以創建多少個執行緒?

進程的虛擬記憶體空間大小為4G,其中 3G-4G為內核空間,用戶可用的的空間為3G,具體可以創建多少個要看執行緒的大小,Linux下一個執行緒大約為10M,可以創建300個執行緒左右。

虛擬地址

對於每一個進程都會對應一個虛擬地址空間,對於32位的作業系統(其指令的位數最大為32位,因此地址碼最多32位),虛擬地址空間的大小為B即0~4GB的虛擬地址空間,其中內核空間為1GB,如下所示:

//cdn.nlark.com/yuque/0/2022/png/28379192/1651891572065-7eb9f3ca-713e-45ad-af25-9a30b3bf5a6d.png

每一個進程的進程式控制制塊PCB都位於內核區,在每一個進程的PCB中有一個文件描述符表(是一個數組),用於標記該進程所打開的所有文件。從文件描述符表可以看出每一個進程最多能打開1024個文件,其中有三個文件默認是一直處於打開狀態的(即進程創建完成時就處於打開狀態),分別是:標準輸入 STDIN_FILENO,其文件描述符為0;標準輸出 STDOUT_FILENO,其文件描述符為1;錯誤輸出 STDERR_FILENO,其文件描述符為2,其中文件描述符0和1可以省略不寫。供我們用戶打開的文件,只能夠佔據從3開始的位置(即其文件描述符為3以後的數字,3~1023)。每打開一個文件就會佔用一個文件描述符,且使用的是空閑的最小的一個文件描述符。

//cdn.nlark.com/yuque/0/2022/png/28379192/1651891587692-ad97a754-de67-44af-ac42-70112c32ea76.png

Linux下可執行文件的格式為ELF:[root@localhost Calc]# file zsx

zsx: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=0x14ef2d34126e7c54141b73c31968bd825ca522ba, not stripped //可以看出zsx為64位(即機器指令位數為64位,OS位數)的可執行文件,其格式為ELF。

對於每一個程式在執行時(如上圖中的a.out),此時會產生一個相應的進程,系統都會自動為其分配一個04G的虛擬地址空間,其中1G的內核空間用於:進程管理、記憶體管理、設備管理和虛擬文件系統等。下面詳細介紹03G的用戶空間。

強調一點:以下說明的各段都是與編程相關的,不包括虛擬地址空間的全部。

0~3G的用戶空間。從小到大(從下往上)依次為:保留區(受保護的地址)、程式碼段、數據段(.data段)、.bss段、堆空間、記憶體映射段、棧空間、命令行參數和環境變數。下面依次對每一個段做簡單的介紹:

1.保留區(受保護的地址)

保留區即為受保護的地址,大小為0~4K,位於虛擬地址空間的最低部分,未賦予物理地址(不會與記憶體地址相對應,因此其不會放任何內容)。任何對它的引用都是非法的,用於捕捉使用空指針和小整型值指針引用記憶體的異常情況。大多數作業系統中,極小的地址通常都是不允許訪問的,如NULL。C語言將無效指針賦值為0也是出於這種考慮,因為0地址上正常情況下不會存放有效的可訪問數據。將指針賦值為0,意味著該指針將永遠不會被使用,從而不會出現野指針情況。#define NULL 0 與 #define NULL (void)0 在C語言中是等效的,而在C++中,只能用#define NULL 0,後面 #define NULL (void)0的使用會出錯。

2.程式碼段

程式碼段也稱正文段或文本段,通常用於存放程式執行程式碼(即CPU執行的機器指令)。一般C語言執行語句都編譯成機器程式碼保存在程式碼段。通常程式碼段是可共享的,因此頻繁執行的程式只需要在記憶體中擁有一份拷貝即可。程式碼段通常屬於只讀,以防止其他程式意外地修改其指令(對該段的寫操作將導致段錯誤)。某些架構也允許程式碼段為可寫,即允許修改程式。

3.數據段(.data段)

數據段通常用於存放程式中已初始化的全局變數和靜態局部變數。數據段屬於靜態記憶體分配(靜態存儲區),可讀可寫。由於全局變數未初始化時,其默認值為0,因此值為0的全局變數位於.bbs段(不位於數據段)。對於未初始化的局部變數,其值是不可預測的。注意:在程式碼段和數據段之間還包括其它段:只讀數據段和符號段等。

4..bbs段

該段用於存放未初始化的全局變數和靜態局部變數,包括值為0的全局變數。 數據段和.bbs段又稱為全局數據區,前者初始化,後者未初始化。

ELF段包括:程式碼段、其它段(只讀數據段和符號段等)、.data段(數據段)和.bbs段,都屬於可執行程式部分。

5.堆空間

new( )和malloc( )函數分配的空間就屬於對空間,用於記憶體空間的分配,其從下往上。 堆用於存放進程運行時動態分配的記憶體段,可動態擴張或縮減。堆中內容是匿名的,不能按名字直接訪問,只能通過指針間接訪問。當進程調用malloc(C) 和new (C++)等函數分配記憶體時,新分配的記憶體動態添加到堆上(擴張);當調用free(C)/delete(C++)等函數釋放記憶體時,被釋放的記憶體從堆中剔除(縮減) 。

6.記憶體映射段(共享庫)

此處,內核將硬碟文件的內容直接映射到記憶體, 任何應用程式都可通過Linux的mmap()系統調用請求這種映射。記憶體映射是一種方便高效的文件I/O方式, 因而被用於裝載動態共享庫。如C標準庫函數(fread、fwrite、fopen等)和Linux系統I/O函數,它們都是動態庫函數,其中C標準庫函數都被封裝在了/lib/libc.so庫文件中,都是二進位文件。這些動態庫函數都是與位置無關的程式碼,即每次被載入進入記憶體映射區時的位置都是不一樣的,因此使用的是其本身的邏輯地址,經過變換成線性地址(虛擬地址),然後再映射到記憶體。而靜態庫不一樣,由於靜態庫被鏈接到可執行文件中,因此其位於程式碼段,每次在地址空間中的位置都是固定的。

7.棧空間

用於存放局部變數(非靜態局部變數,C語言稱為自動變數),分配存儲空間時從上往下。棧和堆都是後進先出的數據結構。

8.命令行參數

該段用於存放命令行參數的內容:argc和argv。

9.環境變數

用於存放當前的環境變數,在Linux中用env命令可以查看其值。

10.虛擬地址空間的作用(好處)

1.方面編譯器和作業系統安排程式的地址;2.方便實現各個進程空間之間的隔離,互不干擾,因為每個進程都對應自己的虛擬地址空間;3.實現虛擬存儲,從邏輯上擴大了記憶體。

補充內容:

程式碼段(.text段)與只讀數據段和符號段(.rodata段),都屬於只能讀的部分,在鏈接的時候這兩部分會鏈接成為一個整體;而.data段和.bbs段屬於可讀可寫RW的部分。這四個部分都是以頁(每頁4KB)的形式存放在記憶體中。進程式控制制塊PCB(又叫進程描述符)放於內核空間。

多個進程在並發執行時,這些進程的用戶空間都是彼此獨立的,因此各個進程的用戶空間在映射為記憶體空間使都是獨立的,互不干擾,這是MMU地址變換必須要能夠保證的。例如,各個進程的.text段、只讀數據段和符號段、.data段和.bbs段等在用戶空間中使用到的其它數據資訊,都會與頁為基本單位放在記憶體中,各個進程的映射是獨立的。而對於內核空間,由於只有一個作業系統,內核空間主要是 機器指令、作業系統內核的各個模組等,它們是公用的,因此每個進程的映射方式一樣。強調一點:每個進程用到或即將用到的數據才會調入記憶體,其餘都在磁碟上。但是各個進程內核空間的進程式控制制塊(進程描述符)映射的地點是不一樣的,也是相互獨立的。共用的模組才是一樣的。 這些都是MMU的實現機制所決定的。如果感興趣,可以看看MMU的實現機制。

記憶體碎片

記憶體碎片是什麼?關於記憶體碎片

記憶體碎片通常分為內部碎片和外部碎片。

所謂內部碎片指的就是,系統為某項功能分派了一定的記憶體,但是該功能的實現沒有用完所有系統分配的。餘下的部分就被成為記憶體碎片的內部碎片。

//cdn.nlark.com/yuque/0/2022/png/28379192/1651891862718-f99a5370-ad90-4d6b-9414-bf23a3db9556.png

外部記憶體指的是有一些連續型記憶體太小了沒辦法被系統分配到某個功能所導致的浪費。

//cdn.nlark.com/yuque/0/2022/png/28379192/1651891881680-6cb5fff1-b6f5-4972-94d9-c22a71326f53.png

內部碎片如何產生?

個人理解:

在請求分頁式記憶體申請中,請求的記憶體是按照頁面劃分的,那麼申請的記憶體很大概率大於需要用到的記憶體,這樣就產生了內部碎片

外部碎片如何產生?

比如某個記憶體地址被A進程使用,A進程結束運行之後,該記憶體就空閑出來,但是空閑出來的記憶體可能由於大小(或者太大),無法分配給其他進程使用,就產生外部碎片。

進程通訊

進程之間的通訊方式以及優缺點

  • 管道(PIPE)
    • 有名管道:一種半雙工的通訊方式,它允許無親緣關係進程間的通訊
      • 優點:可以實現任意關係的進程間的通訊
      • 缺點:
        1. 長期存於系統中,使用不當容易出錯
        2. 緩衝區有限
    • 無名管道:一種半雙工的通訊方式,只能在具有親緣關係的進程間使用(父子進程)
      • 優點:簡單方便
      • 缺點:
        1. 局限於單向通訊
        2. 只能創建在它的進程以及其有親緣關係的進程之間
        3. 緩衝區有限
  • 訊號量(Semaphore):一個計數器,可以用來控制多個執行緒對共享資源的訪問
    • 優點:可以同步進程
    • 缺點:訊號量有限
  • 訊號(Signal):一種比較複雜的通訊方式,用於通知接收進程某個事件已經發生
  • 消息隊列(Message Queue):是消息的鏈表,存放在內核中並由消息隊列標識符標識
    • 優點:可以實現任意進程間的通訊,並通過系統調用函數來實現消息發送和接收之間的同步,無需考慮同步問題,方便
    • 缺點:資訊的複製需要額外消耗 CPU 的時間,不適宜於資訊量大或操作頻繁的場合
  • 共享記憶體(Shared Memory):映射一段能被其他進程所訪問的記憶體,這段共享記憶體由一個進程創建,但多個進程都可以訪問
    • 優點:無須複製,快捷,資訊量大
    • 缺點:
      1. 通訊是通過將共享空間緩衝區直接附加到進程的虛擬地址空間中來實現的,因此進程間的讀寫操作的同步問題
      2. 利用記憶體緩衝區直接交換資訊,記憶體的實體存在於電腦中,只能同一個電腦系統中的諸多進程共享,不方便網路通訊
  • 套接字(Socket):可用於不同電腦間的進程通訊
    • 優點:
      1. 傳輸數據為位元組級,傳輸數據可自定義,數據量小效率高
      2. 傳輸數據時間短,性能高
      3. 適合於客戶端和伺服器端之間資訊實時交互
      4. 可以加密,數據安全性強
    • 缺點:需對傳輸的數據進行解析,轉化成應用級的數據。

執行緒之間的通訊方式

  • 鎖機制:包括互斥鎖/量(mutex)、讀寫鎖(reader-writer lock)、自旋鎖(spin lock)、條件變數(condition)

  • 互斥鎖/量(mutex):提供了以排他方式防止數據結構被並發修改的方法。

  • 讀寫鎖(reader-writer lock):允許多個執行緒同時讀共享數據,而對寫操作是互斥的。

  • 自旋鎖(spin lock)與互斥鎖類似,都是為了保護共享資源。互斥鎖是當資源被佔用,申請者進入睡眠狀態;而自旋鎖則循環檢測保持者是否已經釋放鎖。

  • 條件變數(condition):可以以原子的方式阻塞進程,直到某個特定條件為真為止。對條件的測試是在互斥鎖的保護下進行的。條件變數始終與互斥鎖一起使用。

  • 訊號量機制(Semaphore)

    • 無名執行緒訊號量
    • 命名執行緒訊號量
  • 訊號機制(Signal):類似進程間的訊號處理

  • 屏障(barrier):屏障允許每個執行緒等待,直到所有的合作執行緒都達到某一點,然後從該點繼續執行。

    執行緒間的通訊目的主要是用於執行緒同步,所以執行緒沒有像進程通訊中的用於數據交換的通訊機制

什麼是阻塞與非阻塞

  • 阻塞模式就是如果調用某個函數(如read函數進行文件讀取),沒有相應的結果就一直等待,直到結果出現(直到讀緩衝區有內容)
  • 非阻塞模式就是調用某個函數(如read函數讀取緩衝區內容),如果緩衝區為空,就立即返回,不會等待

epoll的水平模式的邊緣模式

  • 水平模式(LT),只要文件描述符關聯的讀緩衝區(和寫緩衝區)有數據可讀(可寫),就一直發出讀(寫)訊號,支援阻塞和非阻塞
  • 邊緣模式(ET),文件描述符關聯的讀(寫)緩衝區有數據到來(可寫),就發出讀(寫)的訊號,但是只發出一次,之後就不再發出,支援阻塞和非阻塞

執行緒同步,非同步

  • 同步,意思就是在發出功能調用的時候,必須等待這個結果,才能執行下一步的操作

  • 非同步,和同步相對,在發出功能調用的時候,不需要等待調用結果,可以繼續往下執行,該功能的調用結果會通過狀態,通知的回調告訴調用者

執行緒安全

  • 多執行緒在並行執行並且訪問共享數據的時候,如果每次都能得到確定的結果,那麼就是執行緒安全的,即每次得到的結果和執行緒執行的順序無關。

  • 多執行緒對全局變數,靜態變數,堆區數據進行操作的時候,一般來說的不安全的

  • 產生執行緒不安全的原因,是因為執行緒對程式碼執行的過程中,並不是原子操作,有執行緒的切換

  • 保證執行緒安全策略:用mutex(互斥鎖),用原子操作

  • vector保證執行緒安全:1.用mutex(互斥鎖) 2. 固定vector的大小