Bo2SS

Bo2SS

5 進程間通信

課程內容#

IPC—— 進程間通信

—— 共享內存 ——#

  • 父親生孩子之前約定好共享的數據位置
  • 相關接口:shm*

shmget#

分配 System V 類型的共享內存段 [段式內存]

[PS] System V 的通信方式,還在沿用;其啟動方式已經被摈棄

  • man shmget
  • 原型
    • 圖片
    • 返回值類型:int
    • 形參類型:key_t,size_t,int
    • ipc→進程間通信
  • 描述
    • 圖片
    • 傳入的 key 和返回的 id 是聯繫在一起的
    • 新的共享內存段的大小,對應整數頁大小 [上取整]
    • 創建共享內存需要有標誌:IPC_CREAT
    • 圖片
    • 如果沒有 IPC_CREAT,會檢查用戶對 key 對應段的訪問權限
    • 圖片
    • 注意:還需要通過至少 9 個 bit 明確權限,如 0600,前面的 0 不能少
  • 返回值
    • 圖片
    • 成功,返回一個有效的內存標識符 [id];錯誤,返回 - 1 並設置 errno
  • 通過 key 拿到 id 後,如何找到對應的地址呢?shmat 👇

shmat、shmdt#

共享內存操作 [attach,附著;detach,分離]

  • man shmat
  • 原型
    • 圖片
    • 注意 shmat 的返回值為:void *
  • 描述
    • shmat
      • 圖片
      • 將 id 指定的共享內存段附著到調用進程自己的地址空間
        • 每一個進程都認為自己用的是一塊獨立的連續內存 [虛擬內存技術]
      • shmaddr 明確附著地址:
        • NULL:系統自動附著 [常用]
        • 否則,附著自動下取整的地址 [shmflg 中定義了 SHM_RND]或者手動保證頁對齊的地址
      • 圖片
      • 成功的調用會更新共享內存的構造體 shmid_ds
        • shm_nattach:附著數量。共享內存對應的真實物理空間可能被多個進程附著
    • shmdt
      • 圖片
      • 是 shmat 的相反操作
      • 操作的必須是當前被附著的地址
  • 返回值
    • 圖片
    • 成功,shmat 返回地址,shmdt 返回 0
    • 錯誤,返回 - 1,並設置 errno
  • id 在整個系統中是唯一的,同一個 id 一定對應同一塊物理內存
    • 【注意】相同的 id 通過 shmat 返回的地址不同,這是因為在不同的進程中表現為獨立的地址空間 [虛擬內存的概念]
  • 話說回來,shmget 通過 key 拿到 id,shmat 通過 id 拿到內存地址,但【key】又是如何得到的呢?👇

ftok#

將一個路徑名和一個項目標識符轉換為 System V IPC 的 key

  • man ftok
  • 原型
    • 圖片
    • 需要一個路徑名和一個 int 類型變量完成轉換
  • 描述
    • 圖片
    • 文件需要存在,並可訪問
    • proj_id 有至少 8 個有效位,不能是 0
    • 相同的文件名和 proj_id 對應相同的返回值
      • 所以固定的傳入參數,返回的 key 是固定的
  • 返回值
    • 圖片
    • 成功,返回 key;失敗,返回 - 1,並設置 errno

shmctl#

共享內存的控制

  • man shmctl
  • 原型
    • 圖片
    • cmd:命令,int 類型。可預測是一些大寫的宏定義
  • 描述
    • 圖片
    • 可以看到 shmid_ds 結構體的細節信息
    • 圖片
    • 一些命令
      • IPC_STAT:拷貝 shmid_ds 結構體信息到 buf 中 [需有讀權限]
      • IPC_SET:修改 shmid_id 結構體
      • IPC_RMID:標記段被銷毀
        • 只有當共享內存段沒有附著時,段才會被真正的銷毀
        • 不需要 buf 變量,設為 NULL 即可
        • [PS] 必須檢查是否真的被銷毀,否則可能會有殘留 [可以通過返回值]
      • IPC_INFO [Linux 特有,為了兼容性,盡量避免使用]
  • 返回值
    • 圖片
    • 一般地:0,成功 [除了 INFO、STAT];-1,出錯

—— 線程相關 ——#

來自線程庫 [pthread]

互斥量、條件變量

[PS] 多線程、高並發,使用共享內存時都需要考慮互斥

pthread_mutex_*#

【互斥量】操作 mutex

  • man pthread_mutex_init 等
  • 圖片
  • lock:使用 lock 時,會判斷是否已加鎖,若加了鎖,則會掛起直到解鎖 [可能發生阻塞的地方]
  • init:動態初始化方式,需要用到屬性變量
  • 屬性接口:pthread_mutexattr_*
    • init:初始化
      • 圖片
      • 初始化函數中,attr 是一個傳出參數,返回值為 0
    • setpshared:設置進程間共享
      • 圖片
      • pshared 變量為 int 類型,實際上就是一個標誌位
      • 圖片
      • 0:進程間私有;1:進程間共享 → 分別對應宏 PTHREAD_PROCESS_PRIVATE、PTHREAD_PROCESS_SHARED
  • 互斥量的基本操作:創建屬性變量 👉 初始化互斥量 👉 加 / 解鎖操作,詳見代碼演示 —— 共享內存 [互斥鎖]

pthread_cond_*#

【條件變量】控制條件

  • man pthread_cond_init 等
  • 圖片
  • 結構很像 mutex
  • 圖片
  • 條件變量是一種同步設備,允許線程掛起釋放處理器資源,直到滿足某種條件
  • 基本操作:發送條件信號,等待條件信號
  • ⭐必須關聯一個互斥量 mutex,來避免競爭條件:可能在一個線程準備好等待條件之前,另一個線程就已經發出信號了,從而導致消息遺漏
  • init:初始化可以使用屬性變量 [但實際上在 Linux 線程實現中,已忽略屬性變量]
  • 圖片
  • signal:只會重啟正在等待的線程中的1個;如果沒有等待中的線程,就啥事沒有
  • broadcast:則會重啟所有等待中的線程
  • wait
    • ① 解鎖 mutex 並等待條件變量激活 [真正的原子操作]
    • ② 在 wait 返回前 [線程重啟之前 / 等到了信號],會重新將 mutex 上鎖
    • [PS]
      • 在調用 wait 的時候,互斥量必須是上鎖狀態
      • 等待掛起的時候,不會消耗 CPU
  • ❗ 圖中黃框所在段
    • 保證在 wait 解鎖 mutex-> 準備等待期間 [原子操作],其他線程不會發信號
    • ❓ 那這個時候這個 mutex 已經解鎖了,所以這個原子操作可能還需要用到一個類似互斥量的家伙
    • ❓ 感覺上 wait 可以先準備好等待 -> 再解鎖 mutex,這是正常邏輯,而上面解鎖了,又進行一個原子操作,那先解鎖有什麼講究呢

代碼演示#

共享內存 [未加鎖]#

5 個進程進行 1~10000 的累加和演示

  • 圖片
  • 圖片
  • ❗ 這裡是子孩子直接用從父進程那繼承的共享內存地址 share_memory [虛擬的地址]
    • 可見,不同進程中的相同虛擬地址都是指向同一片共享內存的 [物理內存]
    • 如果子孩子通過繼承來的 shmat 和 id 獲得共享地址,會對應子進程中新的虛擬地址,但是同樣指向同一片共享內存,原繼承的共享地址 share_memory 同樣可用
  • ❗ 如何在使用完共享內存後實現真正的銷毀
    • 使用 shmdt 分離所有進程中附著的共享內存 [經試驗,這一步沒有也可以,因為進程結束會自動分離]
    • 使用 shmctl 配合 IPC_RMID 銷毀共享內存段 [移除 shmid]
    • [PS]
      • 每個進程會算一個附著,當附著為 0 時,shmctl 才會真正銷毀內存段
      • 也可以去掉 shmget 中的 IPC_EXCL 標誌,不檢測是否已存在,但不是根本方法
  • 如果不進行上述的 shmctl 操作
    • 第一次執行沒問題,代表成功創建了共享內存段
    • 但第二次執行顯示文件已存在
      • 圖片
      • 說明上次執行後,共享內存並沒有自動銷毀,第二次執行還是使用相同的 key 創建共享內存 [每次相同的 key 對應的 shmid 不相同]
    • [探索過程]
      • ipcs:展示 IPC 相關的資源信息
        • 圖片
        • 可以看到未銷毀的共享內存,還有消息隊列、信號量
        • 以及它們的 key、id、權限 perms
        • 也可以看到它們的 nattch 都為 0,說明已經沒有附著了
      • ipcrm:刪除 IPC 資源
        • 圖片
        • 參數的使用通過 --help 查看
        • 可通過 key 或者 id 移除資源
        • 手動刪除資源再運行程序:ipcs | grep [owner] | awk '{print $2}' | xargs ipcrm -m && ./a.out
  • ❗ 出錯如下錯誤的本質原因 [個人理解]
    • 圖片
    • 1~10000 的正確累加和,應為 50005000
    • 多個進程同時操作共享內存 [或者] 一個進程的讀寫操作未完全完成,另一個進程開始新的操作了。如:
      • 一個進程剛對 now++,寫入內存,此時另一個進程拿到新的 now 再 ++,再用加了 2 次的 now 和舊 sum 相加
      • 也就是 now++ 和 sum 累加兩個操作不是原子操作
    • 但一般 CPU 速度太快了,所以很大概率進行的就是原子操作 [now++ 和 sum 累加寫入內存],很小概率出錯
    • 多核比單核更容易出現計算錯誤,因為一個處理器只能同時運行一個進程
  • [PS]
    • 設置權限時 0600 的第一個 0 表示使用 8 進制
    • usleep (100):讓進程休眠 100ms,讓一個進程計算一次就阻塞,讓其它進程繼續,單純為了體現分工
    • 頭文件根據 man 手冊自行添加

共享內存 [互斥量]#

在內存中更高效的鎖,類似文件鎖 [可前往例子] 的思想

  • 圖片
  • 圖片
  • 創建互斥量:創建屬性變量👉初始化互斥量
  • 互斥量在進程間使用的兩個條件
    • ① 互斥量變量放在共享內存中,共享給每個進程,從而控制每個進程
    • ② 在父進程中創建互斥量時,創建的屬性變量設置進程共享 [默認只在線程間起作用]
      • 但現在一些內核可能不需要做此操作也可以,不過為了兼容性,建議設置一下 ❗
  • [PS]
    • 計算完累加和後,也要記得解鎖
    • 系統中最慢的操作是 IO 操作,所以在 printf 前解鎖,讓互斥量更早開放,會更高效
    • fflush 可以手動刷新緩衝區,避免輸出混亂

共享內存 [條件變量]#

每個進程輪流算 100 次

  • 圖片
  • 圖片
  • 圖片
  • cond 條件變量的初始化,類似 mutex 互斥量
    • Linux 線程的實現,其實不需要 attr 屬性變量;mutex 同
  • 對於單核機器,每次在發送條件信號之前,需要 usleep 一會,保證子進程準備好 wait,等待信號。否則,
    • 對於父進程,其優先執行,發送信號,此時還沒輪到子進程運行,就會錯過信號
    • 對於子進程之間,也類似,發信號前,先讓其他子進程先運行到 wait 狀態 [實際上 usleep 也不能保證]
  • ❗ 注意
    • wait 前要有加鎖的 mutex
    • 每次操作完記得解鎖 + 發信號操作 [100 次計算 / 10000 的累加完成]
    • 發信號操作的順序有兩種:
      • ① 加鎖 —— 發信號 —— 解鎖
        • 缺:等待線程由於收到信號從內核中被喚醒 [→用戶態],但是因為沒有可加鎖的 mutex,很遺憾又 [從用戶態] 回到內核空間,直到有可加鎖的 mutex
          • 兩次上下文切換 [內核態和用戶態之間] 損耗性能。
        • 優:
          • 保證了線程的優先級 [❓]
          • 並且在 Linux 線程實現中,有 cond_wait 隊列和 mutex_lock 隊列,所以不會返回到用戶態,從而不會有性能損耗
      • ② 加鎖 —— 解鎖 —— 發信號 [本文採用]
        • 優:保證 cond_wait 隊列中的線程有可加鎖的 mutex 使用
        • 缺:可能搶到 mutex 的低優先級的線程優先執行
      • 參考:條件變量 pthread_cond_signal、pthread_cond_wait——CSDN
  • 實現效果
    • 圖片
    • 每個進程輪流計算 100 次
    • [PS] 即使是 usleep 也不能完全避免消息遺漏,可以使用共享內存中的一個變量記錄信號的發出;此外,還可能有虛假喚醒的情況發生
if (!count) {                   // ->消息遺漏 [還沒有在wait狀態的]
  pthread_mutex_lock(&lock);
  while (condition_is_false) {  // ->虛假喚醒 [同時喚醒多個]
    pthread_cond_wait(&cond, &lock);
  }
  //...
  pthread_mutex_unlock(&lock);
}

簡易聊天室#

  • chat.h
    • 圖片
    • 用戶名 + 消息 + 使用共享內存的標配
  • 1.server.c
    • 圖片
    • 邏輯與前面的代碼演示基本類似;注意後面的清空操作,主要為了數據安全
  • 2.client.c
    • 圖片
    • 圖片
    • 關注第 41 行的 while 循環的作用:防止客戶端搶鎖,導致服務端收不到 signal
      • 還存在隱患:可能某客戶端搶不到鎖,發生阻塞
    • [注意] gcc 編譯 2.client.c 時,一定要加 - lpthread;否則不會報錯,但運行時,服務端收不到信號
  • 效果演示
    • 圖片
    • 左為服務端,右為兩個客戶端

附加知識點#

  • 有親緣關係的進程:父子之間、兄弟之間

思考點#

  • 進程之間搶著算 VS. 每個進程各算一百次,哪種方式更高效?
    • 後者更高效,往極端想,一個進程計算全部的更高效
    • 這涉及到 CPU 的吞吐量問題,這種累加屬於 CPU 密集型,而不是 IO 密集型

Tips#

  • 查看 IPC 相關的資源信息命令:ipcs
  • 對於用戶線程,一個進程中的一個線程崩潰,整個進程裡的所有線程都會崩潰
  • 課前電影推薦:《與狼共舞》 1990

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。