(進程通信)

進程與線程

導讀

大家好,很高興又和大家見面啦!!!

在前面的內容中,我們一同探討了進程的“內心世界”:從進程作為程序執行實體的基本概念,到其動態變化的生命狀態,以及操作系統如何通過進程控制(如創建、切換、終止)來精準地調度這些“任務單元”。我們看到了每個進程都擁有獨立的內存空間,像一個戒備森嚴的私人辦公室,這保證了系統的穩定與安全。

然而,一個顯而易見的問題隨之產生:如果所有進程都如此“與世隔絕”,我們電腦上那些需要默契配合的應用程序(比如一邊聽音樂一邊寫作)又將如何協同工作?這些獨立的“辦公室”之間,如何才能安全、高效地傳遞信息、協調步伐?

這正是我們接下來要揭曉的答案。進程的獨立性恰恰構成了它們之間需要通信的根本原因。本文將深入講解操作系統是如何搭建起一座座跨越隔離的橋樑,實現進程間通信(IPC) 的。現在,就讓我們一同走進這座連接進程孤島的精密工程。

一、定義

進程通信Inter-Process Communication, IPC)是指運行在不同進程之間的信息交換機制

之所以不同進程之間的通信需要特定的交換機制,是因為進程的獨立性

操作系統在創建每一個進程時,都會為其分配一塊獨立的內存空間,並且其它的進步無法直接訪問該空間。因此進程與進程之間想要進行通信,就需要通過特定的信息交換機制來實現。

根據交換信息量的多少和複雜度,進程通信可分為兩類:

  • 低級通信:主要傳遞狀態標誌或簡單的整數值。

    • 例如用於進程同步與互斥的信號量機制。
    • 低級通信雖然能傳遞信息,但效率較低,通常需要程序員處理複雜的同步細節。
  • 高級通信:是指以較高的效率傳輸大量數據。

    • 操作系統提供了封裝好的通信命令(原語),使得通信過程對程序員更透明,簡化了程序編寫的複雜度。
    • 高級進程通信主要有三種經典模型:共享存儲消息傳遞管道通信

接下來,我們將進一步深入探討這三種經典模型;

二、共享存儲

2.1 定義

共享存儲 指的是在通信的進程之間存在一塊 可直接訪問的共享空間,通過對這片共享空間進行讀/寫操作實現進程之間的信息交換

flowchart LR
	subgraph A[內存空間]
		direction LR
		a[進程1]
		b[進程2]
		c[共享空間]
		a--->c--->a
		b--->c--->b
	end

進程在對共享空間進行讀/寫操作時,需要使用同步互斥工具對共享空間的讀/寫進行控制。如 P操作 和 V操作。

2.2 分類

共享存儲分為兩種:

  • 低級方式的共享:基於數據結構的共享
  • 高級方式的共享:基於存儲區的共享

我們可以將這兩種共享方式簡單的理解為:

  • 低級共享方式規定了共享空間的數據結構
    • 當採取該共享方式進行通信時,進程之間在通信的過程中需要按照數據結構的要求來進行通信
    • 這種通信方式速度慢、限制多,因此是一種低級通信方式
  • 高級共享方式規定了共享空間的大小
    • 當採取該共享方式進行通信時,數據的形式、存放的位置都是由進程控制,而不是操作系統
    • 這種通信方式使得通信雙方更加的方便、快捷,因此是一種高級通信方式

進程通過共享存儲方式進行通信時,其核心在於由通信進程直接通過共享的存儲區域進行數據交換,而數據交換的邏輯、時機與語義則由進程自身控制和解釋

三、消息傳遞

3.1 定義

消息傳遞指的是進程之間以格式化的消息Message) 為單位,通過操作系統提供的發送消息/接收消息這兩個原語 進行數據交換。

flowchart LR
a[進程1]--->b[進程2]--->a

這種通信方式隱藏了通信實現細節,使通信過程對用户透明,簡化了通信程序的設計,是當前應用最廣泛的進程間通信機制。

在微內核操作系統中,微內核與服務器之間的通信就採用了消息傳遞機制。該機制能很好地支持多 CPU 系統、分佈式系統和計算機網絡,因此也成為這些領域最主要的通信工具。

3.2 分類

消息傳遞這種通信方式按照傳遞方式的不同,可以分為兩類:

  • 直接通信方式:發送進程直接將消息發送給接收進程,並將它掛在接收進程的消息緩衝隊列上,接收進程從消息緩衝隊列中取得消息

  • 間接通信方式:發送進程將消息發送到某個中間實體,接收進程從中間實體取得消息。。

    • 這種中間實體一般稱為信箱
    • 該通信方式廣泛應用於計算機網絡中。

簡單的理解就是:

  • 直接通信方式是進程與進程直接進行通信,其中間載體為接收方的消息隊列:
    • 發送方將格式化的消息發送到接收方的消息隊列中
    • 接收方從消息隊列中直接讀取格式化的消息
flowchart LR
	subgraph a[進程1]
		a1[消息隊列]
	end
	subgraph b[進程2]
		b1[消息隊列]
	end
	a--->|格式化消息1|b1
	b--->|格式化消息2|a1
  • 間接通信方式,同樣是進程與進程之間直接通信,但是其中間載體為第三方載體——信箱
flowchart LR
	subgraph a[進程1]
		
	end
	subgraph b[進程2]
		
	end
	subgraph c[信箱]
	end
	a--->|格式化消息1|c--->|格式化消息1|b
	b--->|格式化消息2|c--->|格式化消息2|a

這兩種通信方式從圖示中就可以看到區別:

  • 直接通信:消息直接發送到接收方
  • 間接通信:消息先發送到信箱,接收方再從信箱中讀取消息

這就好比小紅與小明這兩個朋友平時之間的通信方式都是通過書信的形式:

  • 直接通信:小紅在寫好書信後,由郵遞員直接將信送到了小明的手上
  • 間接通信:小紅在寫好書信後,由郵遞員將信先送到了指定的郵箱中,再由小明自己主動去該指定郵箱中取出小紅的書信

四、管道通信

4.1 定義

管道 是一個特殊的共享文件,也稱為 pipe 文件,數據在管道中遵循 先進先出$First _ In _ First _ Out, FIFO$)的原則。

管道通信 是指兩個進程之間通過 管道 完成數據交換。該通信方式允許兩個進程按照 生產者-消費者 的方式進行通信。

  • 只要管道未被填滿,發送方就能將信息發送到 管道
  • 只要管道非空,接收方就能從 管道 中接收信息
flowchart LR
	a[進程1]
	subgraph b[管道]
		b1[消息1]
		b2[消息2]
		b3[...]
	end
	c[進程2]
	a--->|發送消息|b--->|接收消息|c
	

在同一時間內,單個 管道 中只能夠實現單向的通信,即管道通信只能實現半雙工通信:

  • 進程1發送信息到管道中,進程2從管道中讀取信息
  • 進程2發生信息到管道中,進程1從管道中讀取信息

若同一時間內,進程1要向進程2發送信息,進程2也要向進程1發送信息,這時就需要兩條互斥的管道完成:

flowchart LR
	subgraph a[進程1]
		direction LR
		a1[發送信道]
		a2[接收信道]
	end
	subgraph B[管道]
		direction LR
		subgraph b[管道1]
			b1[消息1]
			b2[消息2]
			b3[...]
		end
		subgraph d[管道2]
			d1[消息1]
			d2[消息2]
			d3[...]
		end
	end
	
	subgraph c[進程2]
		direction LR
		c2[接收信道]
		c1[發送信道]
	end
	a--->B--->c
	c--->B--->a

4.2 協調能力

在管道通信中,為了協調雙方的通信,管道機制必須滿足三方面的協調能力:

  • 互斥:當一個進程對管道進行 讀/寫 操作時,其它進程必須等待
  • 同步:讀寫進程保持同步
    • 進程向管道中寫入一定數量的數據後,寫進程阻塞,直到讀進程取走數據後,再將它喚醒
    • 讀進程將管道中的數據取空後,讀進程阻塞,直到寫進程將數據寫入管道後,才將其喚醒
  • 確定對方的存在。
    • 管道通信不可能讓一個進程與另一個根本不存在的進程進行通信。

通信雙方如果是以 管道通信 的方式實現的信息交換,那麼必然滿足下面的條件:

  • 進程必須存在:通信雙方都必須是正在運行的進程
  • 管道端點必須被打開:雙方都必須持有管道的文件描述符
  • 連接必須建立:特別是對於命名管道,需要雙方都打開管道

4.3 Linux 中的管道通信

Linux 中,管道是一種使用十分頻繁的通信機制。從本質上説,管道也是一種文件,但它又和一般的文件有所不同,管道可以克服使用文件進行通信的兩個問題:

  • 限制管道的大小。
    • 管道是一個固定大小的緩衝區,我們可以將其視為一個定長的循環隊列
    • Linux 中,管道的大小為 4KB ,這是的它的大小不像普通文件那樣不加檢驗的增長。
    • 使用單個固定緩衝區也會帶來問題:
      • 管道被寫滿時,隨後對管道的 write() 調用將會默認被阻塞,等待某些數據被讀取,以便騰出足夠的空間供 write() 調用
  • 讀進程也可能工作得比寫進場快。
    • 當管道內不存在任何數據時,即管道變為了 空隊列,此時隨後的 read() 調用會被阻塞,等到某些數據的寫入

4.4 管道的特性

標準的匿名管道只允許存在一個讀進程與一個寫進程

不過對於滿足 $FIFO$ 這種特性的隊列而言,它是能夠天然的支持 多個寫進程 ,這是因為寫入的數據會按寫入的順序進行排列,並不會對管道造成什麼影響。

但是當多個讀進程來同時讀取該管道時,就可能出現數據爭搶的情況,多個讀者讀取同一份數據,這就會導致讀取失敗的問題。

這是因為管道中的數據被讀取後,就會從管道中消失,為新的數據騰出空間,以便進行新的寫入操作,即一個數據只能被一個讀者獲取,在這種情況下,其它讀者就可能出現以下情況:

  • 讀取成功,其它讀者讀取到了正確的信息
  • 讀取失敗,其它讀者未能讀取到正確的信息:
    • 可能讀取到空,或者部分信息
    • 讀取的信息不是當前讀者需要的信息

所以,我們可以得到結論:

  • 同一個管道可以允許多個寫進程,但只允許一個讀進程

但是在實際的使用中,同一個管道是允許 多個寫進程 寫入數據,同時也允許 多個讀進程 來讀取數據。只不過在這種情況下,多個讀進程讀取同一條管道中的數據時,需要經過一些特殊處理,才能夠正常的讀取數據:

  • 給管道進行命名,明確每一個寫入者寫入數據的管道,以及每一個讀者需要讀取數據的管道;
  • 通過廣播式多讀,將管道中的信息複製到其它管道中,每個讀取者都會從對應的管道中讀取到全部的信息;
  • 讓多個讀者輪流讀取管道中的信息;

不管是上述的哪種特殊處理,都能夠實現管道的多寫多讀

管道只能由創建創建進程所訪問

想要成功的創建一個管道,就必須保證:

  • 通信的雙方都是正在運行的進程
  • 雙方都必須持有管道的文件描述符
  • 雙方都需要與管道建立連接

因此當一個父進程創建了一個管道後,子進程會繼承父進程的管道,並可以利用該管道來與父進程進行通信。

結語

今天的內容到這裏就全部結束了。通過今天的學習,我們系統地揭開了進程間通信(IPC)的神秘面紗。進程的獨立性是通信機制存在的根本原因,而操作系統則為我們提供了多種精巧的“橋樑”來跨越這一鴻溝。

我們深入探討了三種經典的高級通信模型:

  • 共享存儲:如同為進程開闢了一個共享的“白板”,通信雙方通過直接讀寫這塊共享區域來交換數據,其核心挑戰與關鍵在於如何通過同步互斥工具(如P/V操作)來協調雙方的訪問,確保數據的一致性。

  • 消息傳遞:這是當前應用最廣泛的機制,進程間通過發送和接收格式化的“消息包”進行通信。無論是消息直接送達的直接通信方式,還是通過“信箱”中轉的間接通信方式,其優勢都在於將複雜的通信細節封裝成原語,對程序員透明,尤其適用於分佈式系統。

  • 管道通信:它模擬了現實中的“管道”,數據遵循FIFO(先進先出)的原則單向流動。我們瞭解了其作為生產者-消費者模型的典型應用,以及為保證通信正確性所必須滿足的互斥、同步和確認對方存在三大協調能力。此外,還特別探討了其在Linux環境下的具體實現與特性。

回顧全文,這三種機制各具特色,但它們共同解決了同一個核心問題:如何在保證進程獨立性的前提下,安全、高效地實現數據交換與進程協同。

理解這些基礎通信模型,不僅是掌握操作系統原理的關鍵一環,也為今後學習更復雜的分佈式系統、網絡編程等知識奠定了堅實的基石。

希望本篇內容能幫助您清晰地構建起進程通信的知識框架。

互動與分享

  • 點贊👍 - 您的認可是我持續創作的最大動力

  • 收藏⭐ - 方便隨時回顧這些重要的基礎概念

  • 轉發↗️ - 分享給更多可能需要的朋友

  • 評論💬 - 歡迎留下您的寶貴意見或想討論的話題

感謝您的耐心閲讀! 關注博主,不錯過更多技術乾貨。我們下一篇再見!