Android跨進程通信IPC整體內容如下
- 1、Android跨進程通信IPC之1——Linux基礎
- 2、Android跨進程通信IPC之2——Bionic
- 3、Android跨進程通信IPC之3——關于"JNI"的那些事
- 4、Android跨進程通信IPC之4——AndroidIPC基礎1
- 4、Android跨進程通信IPC之4——AndroidIPC基礎2
- 5、Android跨進程通信IPC之5——Binder的三大接口
- 6、Android跨進程通信IPC之6——Binder框架
- 7、Android跨進程通信IPC之7——Binder相關結構體簡介
- 8、Android跨進程通信IPC之8——Binder驅動
- 9、Android跨進程通信IPC之9——Binder之Framework層C++篇1
- 9、Android跨進程通信IPC之9——Binder之Framework層C++篇2
- 10、Android跨進程通信IPC之10——Binder之Framework層Java篇
- 11、Android跨進程通信IPC之11——AIDL
- 12、Android跨進程通信IPC之12——Binder補充
- 13、Android跨進程通信IPC之13——Binder總結
- 14、Android跨進程通信IPC之14——其他IPC方式
- 15、Android跨進程通信IPC之15——感謝
由于Android系統是基于Linux系統的,所以有必要簡單的介紹下Linux的跨進程通信,對大家后續了解Android的跨進程通信是有幫助的,本篇的主要內容如下:
- 1、Linux介紹
1.1、Unix操作系統
1.2、GNU
1.3、Linux的誕生
1.4、開源發展實驗室和Linux基金
1.5、Linux的全局圖
1.6、Linux的源碼目錄結構- 2、內核態與用戶態
2.1、內核態與用戶態簡介
3.2、為什么要有用戶態和內核態
2.3、用戶態與內核態的切換- 3、紅黑樹
3.1、二叉搜索樹
3.2、紅黑樹
3.3、數據結構設計
3.4、樹的旋轉知識- 4、Linux的跨進程通信
4.1、匿名管道(pipe)
4.2、命名管道(FIFO)
4.3、信號(signal)
4.4、信號量(semaphore)
4.5、消息隊列(message queue)
4.6、共享內存(share memory)
4.7、套接字(Socket)
4.8、Linux的幾種跨進程通信的方式的比較
的旋轉知識
一、Linux介紹
說到Linux操作系統,不的不說下Unix系統
(一)、Unix操作系統
Unix因為其安全可靠,高效強大的特點在服務器領域得到了廣發的應用。直到GNU/Linux流行開始前,Unix也是科學計算、大型機、超級電腦等所用操作系統的主流。
1、Unix的誕生
湯普遜和里奇最早是在貝爾實驗室開發Unix,此后10年,Unix在學術機構和大型企業中得到了廣泛的應用,當Unix擁有者AT&T公司以低廉甚至免費的許可將Unix源碼授權給學術機構做研究或教學之用,許多機構在此源碼的基礎加以擴展和優化,形成了所謂的"Unix變種",這種變種反過來也促進了Unix的發展,其中最著名的變種就是BSD產品。BSD在后續的發展中也衍生出了3個主要分支:FresBSD、OpenBSD和NetBSD。
2、Unix名字的由來
Unix的誕生和Multics(Multiplexed Information android Computing System) 是有一定淵源的。Multics是由麻省理工學院,AT&T貝爾實驗室和通用電氣合作進行的操作系統項目。由于Multics的失敗,AT&T撤出了Multics投入的資源,其中一位開發者——肯.湯普遜則繼續開發軟件,并最終編寫一個太空旅行游戲,但是他發現游戲速度很慢,并且成本高,在丹尼斯.里奇的幫助下,湯普遜用PDP-7的匯編語言重寫了這個游戲,并讓其再DEC PDP-7上運行起來。這次經歷加上Multics項目的經驗,湯普遜和里奇領導一組開發者開發了一個新的多任務操作系統,這個系統包括命令解釋器和一些實用程序。1970年那部PDP-7卻只能支持兩個用戶,所以他們就開玩笑的稱他們的系統其實是"UNiplexed Information and Computing System",縮寫為"UNICS",于是這個項目被稱為UnICS(Uniplexed Information and Computing System)。后來,大家取其諧音這個名字就改為UNIX
3、Unix的文化
UNIX is not jus an operating system , but a way of life. (UNIX 不僅僅是一個操作系統,更是一種生活方式。) 經過幾十年的發展,UNIX在技術上日臻成熟的過程中,他獨特的設計哲學和美學也深深地吸引了一大批技術人員,他們在維護、開發、使用UNIX的同時,UNIX也影響了他們的思考方式和看待世界的角度。這些人自然而然地形成了一個社團
4、Unix的重要設計原則:
- 簡潔至上
- 提供機制而非策略
(二)、GNU
GNU又稱革奴計劃,是由查理德.斯托曼在1983年9月27日發起的,它的目標是創建一套完全自由的操作系統。
《GNU宣言》是解釋為什么發起該計劃的文章,其中一個重要理由就是要"重現當年軟件界合作互助的團結精神"。為了保證GNU軟件可以自由地"使用、復制、修改和發布",所有GNU軟件都有一份在禁止其他人添加任何限制的情況下授權所所有權利了給任何人的協議條款,GNU通用的公共許可證(GNU General Public License GPL)。即"反版權"(或者稱copyleft)概念。
GNU是“GNU is Not Unix”的縮寫。斯托曼宣布GNU應當發音為Guh-NOO以避免與new這個單詞混淆(注:Gnu在英文中原意為非洲牛羚,發音與new相同)。UNIX是一種廣泛使用的商業操作系統的名稱。由于GNU將要實現UNIX系統的接口標準,因此GNU計劃可以分別開發不同的操作系統部件。GNU計劃采用了部分當時已經可自由使用的軟件。不過GNU計劃也開發了大批其他的自由軟件。
(三)、Linux的誕生
1、Linux的誕生
1991年,在赫爾辛基,Linus Toral開始寫了一個項目,目的是用來訪問大學里面的大型Unix服務器的虛擬終端。他專門寫了一個用于他當時正在用的硬件的,與系統操作系統無關的程序,開發是在Minix,用的編譯器是GCC來完成的,這個項目后面逐漸轉變為Linux內核。
2、Linux的誕生
Linus Torvalds本要把他的發時叫做Freax——“fread”,“free”和“x”(暗指Unix)的合成詞。在開發系統的前半年里,他把文件以文件名“Freax”存儲。Torvalds考慮過Linux這個名字,但是因為覺得它過于自我本位而放棄了使用它。為便于開發,后面,他把那些文件上傳到了赫爾辛基工業大學(HUT)的FTP服務器。Torvalds在HUT負責管理那個服務器的同事,覺得“Freax”這個名字不是很好,就在不咨詢Torvalds的情況下,把項目的名字改成了“Linux”。但是之后,Torvalds也同意“Linux”這個名字了:“經過多次討論,他承認Linux這個名字更好。在0.01版本Linux的源代碼的makefile里仍然使用‘Freax’這個名字,在之后‘Linux’這個名字才被使用。所以,Linux這個名字并不是預先想好的,只是它被廣泛接受了而已”。
(四)、開源發展實驗室和Linux基金
開源碼發展實驗室(Open Source Development Lab)創立于2000年。它是一個獨立的非營利性組織。它的目標是優化Linux以應用于數據中心和運營商的領域。它是Linus Torvalds和Andrew Morton工作的贊助來源。2006年年中,Morton去了Google(Google也是使用Linux內核的);Torvalds全職為OSDL開發Linux內核。非商業性運營機制的資金主要來源于Red Hat,Novell,三菱,英特爾, IBM ,戴爾和惠普等幾家大公司。
2007年1月22日,OSDL和自由標準組織合并為Linux基金會,把它們的工作焦點集中在改進GNU/Linux以與Windows競爭。
(五)、Linux的全局圖
下面是一幅Linux kernel map:
這是makeLinux網站提供的一幅非常經典的Linux內核圖,涵蓋了內核最為核心的方法,Linux除了驅動開發外,還有很多通用子系統,比如CPU,memory,file system等核心模塊,即便不做底層驅動開發,掌握這些模塊對于加深理解整個系統運轉還是很有幫助的。
(六)、Linux的源碼目錄結構
目錄 | 解釋 | 部分子子目錄 |
---|---|---|
kernel | 內核管理相關,進程調度等 | sched/fork等 |
fs | 文件子系統 | ext4/f2fs/fuse/debugfs/proc等 |
mm | 內存子系統 | |
drivers | 設備驅動 | staging/cpufreq/gpu 等 |
arch | 所有CPU系統結構相關的代碼 | arm/x86等 |
include | 頭文件 | linux/uapi/asm_generic等 |
lib | 標準通用的C庫 | |
ipc | 進程通信相關 | |
init | 初始化過程(非系統引導) | |
block | 塊設備驅動程序 | |
crypto | 加密、解密、校驗算法 | |
Documentation | 說明文檔 |
二、內核態與用戶態
這部分是臨時加進來的,是在后面的Binder驅動里面會用到,原來是打算加到"Android跨進程通信IPC之1——Linux基礎"里面,不過由于簡書的篇幅限制,我加到這里來了。
1、內核態和用戶態的簡介
- ** 內核態 **:CPU可以訪問內存所有數據,包括外圍設備,例如硬盤、網卡,CPU可以將自己從一個程序切換到另外一個程序。
- ** 用戶態 **: 只能受限的訪問內存,且不允許訪問外圍設備,占用CPU的能力被剝削,CPU資源可以被其他程序獲取。
2、為什么要有用戶態和內核態
由于需要限制不同的程序之間的訪問能力,防止他們獲取別的程序的內存數據,或者獲取外圍設備的數據,并發送網絡,CPU劃分出兩個權限等級 ----用戶態 和 內核態。
3、用戶態與內核態的切換
3.1 切換簡介
所有用戶程序都是運行在用戶態的,但是有時候程序確實需要做一些內核態的事情,例如從硬盤讀取數據,或者從鍵盤獲取輸入等。而唯一可以這這些事情的就是 操作系統 ,所以這時候 程序 就需要先向 操作系統 請求,以 程序 的名字來執行這些操作。這時候就需要一個這樣的機制:用戶態 切換到 內核態,但是不能控制內核態中執行的執行這種機制叫做** 系統調用 **,在CPU中的實現稱之為 "陷阱指令(Trap Instruction)"
3.2 系統調用機制流程:
- 1、用戶態程序將一些數據值放在寄存器中,或者使用參數創建一個堆棧(stack frame),以表明需要操作系統提供的服務。
- 2、用戶態程序執行陷阱指令
- 3、CUP切換到內核態,并跳到內存指定位置的指令,這些指令是操作系統的一部分,他們具有內存保護,不可被用戶態程序訪問
- 4、這些指令稱之為 陷阱 (trap) 或者新系統調用處理器 ( system call hanlder )。他們會讀取程序放入內存的數據參數,并執行程序請求的服務。
- 5、系統調用完成后,操作系統會重置CPU為用戶態并返回系統調用的結果。
三、紅黑樹
紅黑樹是60年代中期計算機科學界尋找一種算法復雜度穩定,容易實現的數據存儲算法的產物。紅黑樹是常用的一種數據結構,它使得對數據的索引,插入和刪除操作都能保持在O(lgn)的時間復雜度。在優先級隊列、字典等實用領域都有廣泛地應用,更是70年代提出的關系數據庫模型——B樹的鼻祖。當然,相比于一般的數據結構,紅黑樹實現的難度有所增加。** 關鍵是后面要講解的Binder驅動里面用到了紅黑樹 **
(一)二叉搜索樹
在具體實現紅黑樹之前, 必須弄清楚它的基本含義。紅黑樹本質上是一顆二叉搜索樹,它滿足二叉搜索樹的基本性質——即樹中的任何節點的值大于它的左子節點,且小于它的右子節點。
按照二叉搜索樹組織數據,使得對元素的查找非常快捷。比如上圖的中的二叉搜索樹,如果查詢值為48的節點,只需要遍歷4個節點即可完成。理論上,一顆平衡的二叉樹搜索樹的任意節點平均查找效率為樹的高度h,即O(lgn)。但是如果二叉搜索樹的失去平衡(元素在一側),搜索效率就退化為O(n),因此二叉搜索樹的平衡是搜索效率的關鍵所在。為了維護樹的平衡性,數據結構內出現了各種各樣的樹,比如AVL樹通過維持任何節點的左右子樹的高度差 不大于1保持樹的平衡,而紅黑樹使用顏色維持樹的平衡,使二叉搜索樹的左右子樹的高度差 保持在固定的范圍。相比于其他二叉搜索樹,紅黑樹對二叉搜索樹的平衡性維持著自身的優勢
(二) 紅黑樹
紅黑樹,顧名思義,紅黑樹的節點是有顏色概念的,即非紅即黑,通過顏色的語速,紅黑樹為支持著二叉搜索樹的平衡性。一個紅黑樹必須有下面5個特征
- 1、節點是紅色或黑色
- 2、根是黑色
- 3、所有葉子是黑色(葉子是NIL節點)
- 4、每個紅色節點的兩個子節點都是黑色的(從每個葉子到跟的所有路徑不能有兩個連續的紅色節點)
- 5、從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。
如下圖
這些特征強制約束了紅黑樹的關鍵性質:從跟到葉子的最長可能路徑不多于最短可能路徑的兩倍長(特征4保證了路徑最長的情況為1紅1黑,最短的情況為全黑,再結合特征5,可以推導出)。結果是這個樹大致上是平衡的。因為比如插入、刪除和查找操作中,操作某個值的最壞情況的時間都要求與樹的高度成比例,這個高度上的理論上限允許紅黑樹在最壞的情況都是高效的,而不同于普通的(二叉搜索樹)
(三) 數據結構設計
- 和一般的數據結構設計類似,我們用抽象數據類型表示紅黑樹的節點,使用指針保存節點之間的相互關系。
- 作為紅黑樹的節點,其基本屬性有:節點的顏色,左節點的指針,右節點的指針、父節點的指針、節點的值
如下圖
為了方便紅黑樹關鍵算法的實現,還定義了一些簡單的操作(都是內聯函數)。
//紅黑樹節點
template<class T>
class rb_tree_node
{
typedef rb_tree_node_color node_color;
typedef rb_tree_node<T> node_type;
public:
node_color color;//顏色
node_type*parent;//父節點
node_type*left;//左子節點
node_type*right;//右子節點
T value;//值
rb_tree_node(T&v);//構造函數
inline node_type*brother();//獲取兄弟節點
inline bool on_left();//自身是左子節點
inline bool on_right();//自身是右子節點
inline void set_left(node_type*node);//設置左子節點
inline void set_right(node_type*node);//設置左子節點
};
為了表示紅黑樹節點的顏色,我們定義了一個簡單的枚舉類型。
//紅黑樹節點顏色
enum rb_tree_node_color
{
red=false,
black=true
};
有了節點,剩下的就是實現紅黑樹的構造、插入、搜索、刪除等關鍵算法了。
//紅黑樹
template<class T>
class rb_tree
{
public:
typedef rb_tree_node<T> node_type;
rb_tree();
~rb_tree();
void clear();
void insert(T v);//添加節點
bool insert_unique(T v);//添加唯一節點
node_type* find(T v);//查詢節點
bool remove(T v);//刪除節點
inline node_type* maximum();//最大值
inline node_type* minimum();//最小值
inline node_type* next(node_type*node);//下一個節點
inline node_type* prev(node_type*node);//上一個節點
void print();//輸出
int height();//高度
unsigned count();//節點數
bool validate();//驗證
unsigned get_rotate_times();//獲取旋轉次數
private:
node_type*root;//樹根
unsigned rotate_times;//旋轉的次數
unsigned node_count;//節點數
void __clear(node_type*sub_root);//清除函數
void __insert(node_type*&sub_root,node_type*parent,node_type*node);//內部節點插入函數
node_type* __find(node_type*sub_root,T v);//查詢
inline node_type* __maximum(node_type*sub_root);//最大值
inline node_type* __minimum(node_type*sub_root);//最小值
void __rebalance(node_type*node);//新插入節點調整平衡
void __fix(node_type*node,node_type*parent,bool direct);//刪除節點調整平衡
void __rotate(node_type*node);//自動判斷類型旋轉
void __rotate_left(node_type*node);//左旋轉
void __rotate_right(node_type*node);//右旋轉
void __print(node_type*sub_root);//輸出
int __height(node_type*&sub_root);//高度
bool __validate(node_type*&sub_root,int& count);//驗證紅黑樹的合法性
};
在紅黑樹類中,定義了** 樹根(root) ** 和** 節點數 (count) ,其中還記錄了紅黑樹插入、刪除時執行的旋轉次數 rotate_times。其中核心操作有 插入操作(insert) , 搜索操作 (find) , ** 刪除操作(remove) , 遞減操作(prev) ** ——尋找比當前節點較小的節點, 遞增操作(next) ** ——尋找比當前節點比較大的節點, ** 最大值(maximum) ** 和 ** 最小值(minimum) ** 。** 其中驗證操作(__ validate) ** 通過遞歸操作紅黑樹,驗證紅黑樹的基本顏色約束,用于操縱紅黑樹驗證紅黑樹是否保持平衡。
(四) 樹的旋轉知識
當我們在對紅黑樹進行插入和刪除等操作時,對樹做了修改,那么可能會違背紅黑樹的性質。所以為了繼續保持紅黑樹的性質,我們可以通過對節點進行重新著色,以及對樹進行相關的旋轉操作,即修改樹中某些節點的顏色及指針結構,來達到對紅黑樹進行插入和刪除結點等操作后,繼續保持它的性質或平衡。
樹的旋轉,分為左旋和右旋,借助下圖來做介紹:
1、左旋
如上圖所示:
- 當在某個結點pivot上,做左旋操作時,我們假設它的右孩子不是NIL,pivot可以為任何不是不是NIL的左孩子的結點。
- 左旋以pivot到y之間的鏈為"支軸"進行,它使y成為該孩子樹新的根,而y的左孩子b則成為pivot的右孩子。
2、右旋
對于樹的旋轉,能保持不變的只有原樹和搜索性質,而原樹的紅黑性質則不能保持,在紅黑樹的數據插入和刪除后可利用旋轉和顏色重涂來恢復樹的紅黑性質。
這樣大家對紅黑樹就有了初步了解。這里就不詳細介紹了,如果大家有興趣,可以自行去了解。
四、Linux的跨進程通信(IPC)概述
(一)、跨進程通信(IPC)的目的
跨進程通信(IPC)的目的主要如下:
- 數據傳遞
一個進程需要將它的數據發送給另外一個進程,發送的數據量在一個字節到幾M字節之間- 共享數據
多個進程想要操作共享數據。- 通知事件
一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)- 資源共享
多個進程之間共享資源。為了做到這一點,需要內核提供鎖和同步機制- 進程控制
有些進程希望完全控制另一個進程的執行(如debug進程),此時控制進程希望能夠攔截另一個進程的所有步驟和異常,并能夠及時知道它的狀態改變。
(二)、Linux 進程間通信(IPC)的發展
** Linux **下的跨進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來。而對Unix發展做出大量貢獻的量大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校伯克利軟件發布中心)在進程間通信方面的側重點有所不同。前者對Unix早期的進程間通信手段進行了系統的改進和擴充,形成了"system V IPC",通信進程局限在單個計算機內;而后者則跳過了這個限制,形成了基于套接字(socket)的進程間通信機制。
** Linux **則把兩者繼承了下來。
所以可以把Linux中的進程間通信大體分為4類
- 基于早期Unix的進程間通信:管道和信號
- 基于System V的進程間通信:System V消息隊列、System V 信號燈、System V 共享內存
- 基于Socket 的進程間通信:socket
- POSIX進程間通信:posix 消息隊列、posix信號燈、posix共享內存
這里說下PSOIX:
由于Unix版本的多樣性,電子電器工程協會(IEEE) 開發了一個獨立的Unix標準,這個心的ANSI Unix標準被稱為計算機環境的可移植性操作系統?,F有的大部分Unix和流行版本都是遵循POSIX標準的,而Linux從一開始就是遵循POSIX標準。
三、Linux的跨進程通信詳解
在Linux下進程通信有以下七種:
- 1、匿名管道(pipe)
- 2、命名管道(FIFO)
- 3、信號(signal)
- 4、信號量(semaphore)
- 5、消息隊列(message queue)
- 6、共享內存(share memory)
- 7、套接字(Socket)
那我們就來詳細的了解下相關的內容
(一)、匿名管道(pipe)
1、什么是匿名管道?
匿名管道(pipe)是Linux支持的最初Unix IPC形式之一,具有以下特點:
- 匿名管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立兩個管道;
- 只能作用于父子進程或者兄弟進程之間(具有親緣關系的進程)
- 單獨構成的一種獨立的文件系統:匿名管道對于管道兩端的進程而言,就是一個文件,但它不是普通文件,它不屬于某種文件系統,而是自理門戶,單獨構成一種文件系統,去并且只存在于內存中。
- 數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,并且每次都是從緩存區的頭部讀出數據。
2、匿名管道的實現機制
匿名管道是右內核管理的一個緩沖區,相當于我們放入內存的中一個紙條。匿名管道的一端連接一個進程的輸出。這個進程會向管道中放入信息。匿名管道的另一端連接一個進程的輸入,這個進程取出被放入管道的信息。一個緩存區不需要很大,它被設計成為喚醒的數據結構,以便管道可以被循環利用。當管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。當管道被放滿信息的時候,嘗試放入信息的進程就會等待,直到另一端的進程取出信息。兩個進程都終結的時候,管道也會自動消失。如下圖
從原理上,匿名管道利用fork機制建立,從而讓兩個進程可以連接到同一個PIPE上。最開始的時候,上面的兩個箭頭都連接到同一個進程Process 1上(連接在Process 1上的兩個箭頭)。當fork復制進程的時候,會將這兩個連接也復制到新的進程(Process 2)。隨后,每個進程關閉在自己不需要的一個連接(兩個黑色的箭頭被關閉;Process 1關閉從PIPE來的輸入連接,Process 2關閉輸出到PIPE的連接),這樣,剩下的紅色連接就構成了上圖的PIPE。
示例圖如下:
3、匿名管道實現細節
在Linux中,匿名管道的實現并沒有使用專門的數據結構,而是借助了文件系統的file結構,和VFS的索引節點inode。通過將兩個file結構指向同一個臨時的VFS節點,而這個VFS索引節點又指向了一個物理頁面而實現的。如下圖
4、關于匿名管道的讀寫
匿名管道的實現的源代碼在fs/pipe.c中,在pipe.c中有很多函數,其中有兩個函數比較重要,即匿名管道pipe_read()讀函數和匿名管道寫函數pipe_write()。匿名管道寫函數通過將字節復制到VFS索引節點指向物理內存而寫入數據,而匿名管道讀函數則通過復制物理內存而讀出數據。當然,內核必須利用一定的 同步機制 對管道的訪問,為此內核使用了 鎖 、等待隊列、和 信號。
當寫入進程向匿名管道中寫入時,它利用標準的庫函數write(),系統根據庫函數傳遞的文件描述符,可找到該文件的file結構。file結構中制定了用來進行寫操作的函數(即寫入函數)地址,于是,內核調用該函數完成寫操作。寫入函數在向內存中寫入數據之前,必須首先檢查VFS索引節點中的信息,同時滿足如下條件時,才能進行實際的內存復制工作:
- 內存中有足夠的空間可以容納所有要寫入的數據。
- 內存沒有被讀程序鎖定。
如果同時滿足上述條件,寫入函數首先會鎖定內存,然后從寫進程的地址空間中復制數據到內存。否則,寫進程就休眠在VFS索引節點的等待隊列中,接下來,內核將調用調度程序,而調度程序會選擇其他進程運行。寫進程實際處于可中斷的等待狀態,當內存中有足夠的空間可以容納寫入數據,或內存被解鎖時,讀取進程會喚醒寫入進程,這時,寫入進程將接受到信號。當數據寫入內存之后,內存被解鎖,而所有休眠在索引節點的讀取進程會被喚醒。
匿名管道的讀取過程和寫入過程類型。但是,進程可以在沒有數據或者內存被鎖定時立即返回錯誤信息,而不是阻塞該進程,這一來于文件或管道的打開模式。反之,進程可以休眠在索引節點的等待隊列中等待寫入進程寫入數據。當所有的進程完成了管道操作之后,管道的索引節點被丟棄,而共享數據頁被釋放。
PS:有些同學可能不清楚VFS,我這里就簡單介紹下;
VFS(virtual File System/虛擬文件系統):是Linux文件系統對外的接口。任何要使用文件系統的程序都必須經由這層接口來使用它。它是采用標準的Unix系統調用讀寫位于不同物理介質上的不同文件系統。VFS是一個可以讓open()、read()、write()等系統調用不用關系底層的存儲介質和文件系統類型就可以工作的粘合層。在Linux中,VFS采用的是面向對象的編程方法。
(二)、命名管道(FIFO/named PIPE)
在上面,我們介紹了匿名管道(pipe),我們知道了如何匿名管道在進程之間傳遞數據,同時也是看到了這個方式的一個缺陷,就是這些就進程都是由一個共同的祖先進程啟動,這給我們在不相關的進程之間交換數據帶來了不方便。這里我將會介紹另一種通信方式——命名管道,來解決不相關進程之間的問題。
1、什么是命名管道?
- 命名管道也被稱為FIFO或者named pipe,它是一種特殊類型的文件,它在文件系統中以文件名的形式存在,但是它的行為卻和之前所講的匿名管道類似。
- FIFO(First in, First out) 為一種特殊的文件類型,它在文件系統中有對應的路徑。當一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那么內核就會在兩個進程之間建立管道,所以FIFO實際上也由內核管理,不與硬盤打交道。之所以叫FIFO,因為管道本質上是一個** 先進先出的隊列數據結構 ,最早放入的數據被最先讀出來,從而保證信息交流的順序。FIFO只是借用了文件系統(file system,命名管道是一種特殊類型的文件,因為Linux中所有事物都是文件,它是在文件系統中以文件名的形式存在。)來為管道命名。寫模式的進程向FIFO中寫入,而讀模式的進程從FIFO文件中讀出。當刪除FIFO文件時,管道連接也隨之小時。FIFO的好處在于我們可以通過 文件的路徑來識別管道,從而讓沒有親緣關系的進程之間建立連接 **。
2、命名管道的讀寫規則:
- 1、從FIFO中讀取數據的約定:如果一個進程為了從FIFO中讀取數據而阻塞打開了FIFO,那么該進程內的讀操作 為設置了阻塞標志的讀操作。
- 2、從FIFO中寫入數據的約定:如果一個進程為了想FIFO中寫入數據而阻塞打開了FIFO,那么該進程內的寫操作 為設置了阻塞標志的寫操作。
3、命名管道的安全問題:
大家想一下,只使用一個FIFO文件,如果有多個進程同時向同一個FIFO文件寫數據,而只有一個讀FIFO進程在同一個FIFO文件讀取數據時,會發生怎么樣的情況呢,會發生數據塊的相互交錯是很正常的?而且個人認為多個不同進程向一個FIFO讀取進程發送數據是很正常的情況。
為了解決這個問題,就是讓寫操作原子化,怎么才能使寫操作原子化呢?答案其實很簡單:系統規定,在一個以O_WRONLY(即阻塞方式)打開的FIFO中,如果寫入的數據長度小于等于PIPE_BUF,那么或者寫入全部字節,或者一個字節都不寫入。如果所有的寫請求都是發往一個阻塞的FIFO的,并且每個寫請求的數據長度小于等于PIPE_BUF字節,系統就可以確保數據絕不會交錯在一起。
(三)、信號(Signal)
1、什么是信號?
信號是比較復雜的通信方式,用于通知接受進程有某種事件發生,除了用于進程間通信外,進程還可以發送信號給進程本身;Linux除了支持Unix早期信號語義函數sigal外,還支持語義服務Posix.1標準的信號函數sigaction(實際上,該函數是基于BSD的,BSD為了實現可靠信號機制,有能夠統一對外接口,用sigaction函數重新實現了signal函數)
2、信號的種類
如下圖:
每種信號類型都有對應的信號處理程序(也叫信號的操作),就好像每個中斷都有一個中斷服務例程一樣。大多數信號的默認操作是結束接受信號的進程;然而一個進程通常可以請求系統采取某些代替的操作,各種代替操作是:
- 忽略信號。隨著這一選項的設置,進程將忽略信號的出現。有兩個信號不可以被忽略:SIGKILL,它將結束進程:SIGSTOP,它是作業控制機制的一部分,將掛起作業的執行。
- 恢復信號的默認操作
- 執行一個預先安排的信號處理函數。進程可以登記特殊的信號處理函數。當進程收到信號時,信號處理函數將像中斷服務例程一樣被調用,當從信號處理函數返回時,控制被返回給主程序,并且繼續正常執行。
但是,信號和中斷有所不同。中斷的響應和處理都發生在內核空間,而信號的響應發生在內核空間,信號處理程序的執行卻發生在用戶空間。
那么什么時候檢測和響應信號?通常發生在兩種情況下:
- 當前進程由于系統調用、中斷或異常而進入內核空間以后,從內核空間返回到用戶空間前戲
- 當前進程在內核進入睡眠以后剛被喚醒的時候,由于檢測到信號的存在而提前返回到用戶空間
3、信號的本質
信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。
信號是進程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進程有哪些事情發生了。信號機制經過POSIX實時擴展后,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。
4、信號來源
信號事件的發生有兩個來源:硬件來源(比如我們按下鍵盤或者其他硬件故障);潤健來源,最常用發送信號的系統函數是kill,raise,alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。
5、關于信號處理機制的原理(內核角度)
內核給一個進程發送中斷信號,是在進程所在的進程表項的信號域設置對應于該信號的位。這里要補充的是,如果信號發送給一個正在睡眠的進程,那么要看該進程進入睡眠的優先級,如果進程睡眠在可被中斷的優先級上,則喚醒正在睡眠的進程;否則僅設置進程表中信號域相應的位,而不是喚醒進程。這一點比較重要,因為進程檢查是否收到信號的時機是:一個進程在即將從內核態返回到用戶態時;或者,在一個進程進入或離開一個適當的低調度優先級睡眠狀態時。
內核處理一個進程吸收的信號的時機是在一個進程從內核態返回用戶態時。所以,當一個進程在內核態下運行時,軟中斷信號并不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號才會返回用戶態,進程在用戶態下不會有未處理完的信號。
內核處理一個進程收到的軟中斷信號是在該進程的上下文中。因此,進程必須處于運行狀態。如果進程收到一個要捕捉的信號,那么進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數出,從函數返回再彈出棧頂時,才返回原先進入內核的地方,接著原來的地方繼續運行。這樣做的原因是用戶定義的處理函數不能切不允許在內核態下執行。
6、信號的生命周期
(四)、信號量(semaphore)
1、什么是信號量
信號量又稱為信號燈,它用來協調不用進程間的數據對象,而最主要的應用是共享內存方式的進程間通信。本質上,信號量時一個計數器,他用來記錄某個資源(如共享內存)的存取狀況。信號量的使用,主要是用來保護共享資源,使得資源在一個時刻只有一個進程(線程)所擁有。
信號量的值為正的時候,說明它空閑。所有的線程可以鎖定而使用它。若為0,說明它被占用,測試的線程要進入睡眠隊列中,等待被喚醒。
2、信號量的注意事項
- 為了防止出現因多個程序同時訪問一個共享資源而引發的一系列問題,我們需要這一種方法,它可以通過生成并使用令牌來授權,在任一時刻只能由一個執行線程訪問代碼的臨界區域
- 臨界區域是指執行數據更新的代碼需要獨占式地執行。而信號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就說信號量臨界區是指執行數據更新的代碼需要獨占式地執行。而信號量就可以提供這樣的一種訪問機制,讓一個臨界區同一時間只有一個線程在訪問它,也就說信號量是用來協調進程對共享資源的訪問。
- 信號量時一個特殊的變量,程序對其訪問都是原子操作,且只允許對它進行等待(即P-信號變量)和發送(即V信號變量)信息操作。
- 最簡單的信號量只能取0和1的變量,這也是信號量最常見的一種形式,叫做二進制信號量。而可以取多個正整數的信號量被稱為通用信號量
3、信號量的原理
由于信號量只能進行兩種操作即"等待"和"發送",即P(sv)和V(sv),他們的行為是這樣:
- P(sv):如果sv的值大于零,就給它減1;如果它的值為零,就掛起該進程的執行
- V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1。
舉個例子,就是兩個進程共享信號量sv,一旦其中一個進程執行了P(sv)操作,他將得到信號量,并可以進如臨界區,使sv減1。而第二個進程將被阻止進入臨界區,因為當它試圖執行P(sv)時,sv為0,它會掛起以等待第一個進程離開臨界區并執行(sv)釋放信號。
3、信號量的分類
Linux提供兩種信號量
- 內核信號量:由內核控制路徑使用
- 用戶態進程使用的信號量:這種信號量又分為POSIX信號量和SYSTEM V信號量
POSIX辛信號量又分為有名信號量和無名信號量
- 有名信號量:其值保存在文件中,所以它可以用于線程也可以用于進程間同步
- 無名信號量:其值保存在內存。
POSIX信號量和SYSTEM V信號量的比較
- 對POSIX來說,信號量是個非負數。常用于線程間同步
而SYSTEM V信號量則是一個或者多個信號量集合,他對應的是一個信號量的結構體,這個結構體為SYSTEM V IPC服務的,信號量只不過是它的一部分。常用語進程間同步。- POSIX信號量的引用頭文件是<semaphore,h>,而SYSTEM V信號量的引用頭文件是<sys/sem.h>
- 從使用的角度,System V信號量是簡單的。比如,POSIX信號量的創建和初始化PV操作就很方便。
(五)、消息隊列(message)
1、消息隊列也稱為報文隊列:
消息隊列也成為報文隊列,消息隊列是隨內核持續的,只有在內核重其或者顯示刪除一個消息隊列時,該消息隊列才會真正刪除系統中記錄消息隊列的數據結構體 struct ipc_ids_msg_ids位于內核中,系統中所有消息隊列都可以在結構msg_ids中找到訪問入口。
2、消息隊列的原理及注意事項:
- 消息隊列其實就是一個消息的鏈表,每個消息都有一個隊列頭,稱為struct_msg_queue,這個隊列頭描述了消息隊列的key值,用戶ID,組ID等信息,但它存于內核中而結構體struct msqid_ds能夠返回或設置消息隊列的信息,這個結構體位于用戶空間中,與msg_queue結構相似的消息隊列允許一個或多個進程向它寫入或讀取消息,消息隊列是消息的鏈表。
- 消息是按消息類型訪問,進程必須指定消息類型來讀取消息,同樣,當向消息隊列中寫入消息事業必須給出消息的類型,如果讀隊列使用消息的類型為0,則讀取隊列中的第一條消息。
- 內核空間的結構體msg_queue描述了對應key值消息隊列的情況,而對應用戶空間的msqid_ds這個結構體,因此,可以操作msgid_ds這個結構體來操作消息隊列。
(六)、共享內存(share memory)
共享內存是進程間通信中最簡單的方式之一。
1、什么是共享內存?
共享內存是系統處于多個進程之間通訊的考慮,而預留的一塊內存區。共享內存允許兩個或更多的進程訪問同一塊內存,就如同malloc()函數向不同進程返回了指向同一個物理內存區域的指針。當一個進程改變了這塊地址中的內容的時候,其他進程都會覺察到這個更改。
2、關于共享內存
- 當一個程序加載進內存后,它就被分成叫做頁的塊。通信將存在內存的兩個頁之間或者兩個獨立的進程之間??傊?,當一個程序想和另外一個程序通信的時候,那內存將會為這兩個程序生成一塊公共的內存區域。這塊被兩個進程分享的內存區域叫做共享內存。
- 由于所有進程共享同一塊內存,共享內存在各種進程間通信方式中具有最高的效率。訪問共享內存區域和訪問進程獨有的內存區域一樣快,并不需要通過系統調用或者其他需要切入內核的過程來完成。同時它也也避免了對數據的跟中不必要的復制。
- 如果沒有共享內存的概念,那一個進程不能存取另外一個進程的內存部分,因而導致共享數據或者通信失效。因為系統內核沒有對訪問共享內存進行同步,開發者必須提供自己的同步措施。
- 解決了這些問題的常用方法是是通過信號量進行同步。不過通常我們程序只有一個進程訪問了共享內存,因此在集中展示了共享內存機制的同時,我們避免了讓代碼被同步邏輯搞的混亂不堪。
為了簡化共享數據的完整性和避免同時存取數據,內核提供了一種專門存取共享內存資源的機制。這稱為互斥體或者Mutex對象。
3、Mutex對象
例如,在數據被寫入前不允許進程從共享內存中讀取信息、不允許兩個進程同時向一個共享內存地址寫入數據等。
當一個基礎想和兩一個進程通信的時候,它將按以下順序運行:
- 1、獲取Mutex對象,鎖定共享區域
- 2、將要通信的數據寫入共享區域
- 3、釋放Mutex對象
當一個進程從這個區域讀取數據的時候,它將重復同樣的步驟,只是將第二步變成讀取。
4、內存模型
要使用一塊共享內存
- 進程必須首先分配它
- 隨后需要訪問這個共享內存塊的每一個進程都必須將這個共享內存綁定到自己的地址空間中。
- 當完成通信之后,所有進程都脫離共享內存,并且由一個進程釋放該共享內存塊。
在/proc/sys/kernel/目錄下,記錄著共享內存的一些限制,如一個共享內存區的最大字節數shmmax,系統范圍內最大的共享內存區標志符數shmmni等。
5、Linux系統內存模型
- 在Linux系統中,每個進程的虛擬內存是被分為許多頁面的。這些內存頁面中包含了實際的數據。每個進程都會維護一個從內存地址到虛擬內存頁面之間的映射關系。盡管每個進程都有自己的內存地址,不同的進程可以同時將同一個頁面頁面映射到自己的地址空間,從而達到共享內存的目的。
- 分配一個新的共享內存塊會創建新的內存頁面。因為所有進程都希望共享對同一塊內存的訪問,只應由一個進程創建一塊新的共享內存。再次分配一塊已經存在的內存塊不會創建新的頁面,而只是會返回一個標示該內存塊的標識符。
- 一個進程如需使用這個共享內存塊,則首先需要將它綁定到自己的地址空間中。
- 這樣會創建一個從進程本身虛擬地址到共享頁面的映射關系。當對共享內存的使用結束之后,這個映射關系將被刪除。
- 當再也沒有京城需要使用這個共享內存塊的時候,必須有一個(有且只有一個)進程負責釋放這個被共享的內存頁面。
- 所有共享內存塊的大小必須是系統頁面大小的整數倍。系統頁面大小指的是系統中單個內存頁面包含的字節數。在Linux系統中,內存頁面大小是4KB,不過您仍然應高通過調用getPageSize獲取這個值。
6、Linux共享內存的實現步驟
共享內存的實現分為兩個步驟:
- 創建共享內存,使用shmget函數
- 映射共享內存,將這段創建的共享內存映射到具體的進程空間中,使用shmat函數
(七)、套接字(socket)
套接字也是一種進程間通信機制,與其他通信機制不同的是,它可用于不同機器間的進程通信。
更為一般的進程間通信機制,可用于不同機器之間的進程間通信。起初是右Unix系統的BSD分之開發出來的,但是現在一般可以移植其他類Unix系統上。比如Linux和System V的變種都支持套接字。
(八) Linux的幾種跨進程通信的方式的比較
1、效率比較
類型 | 無連接 | 可靠 | 流控制 | 優先級 |
---|---|---|---|---|
匿名PIPE | N | Y | Y | N |
命名PIPE(FIFO) | N | Y | Y | N |
信號量 | N | Y | Y | Y |
消息隊列 | N | Y | Y | Y |
共享內存 | N | Y | Y | Y |
UNIX流SOCKET | N | Y | Y | N |
UNIX數據包SOCKET | Y | Y | N | N |
PS:無連接是指無需調用某種行動是OPEN,就有發送消息的能力流控制,如果系統資源短缺或者不能接受更多的消息,則發送進程能進進行流量控制。
2、優缺點比較
- 匿名管道(pipe):速度慢,容量有限,只有父子進程能通訊
- 有名管道(FIFO): 任務進程都能通訊,但速度慢
- 消息隊列(message queue):容量收到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據問題。
- 信號量:不能傳遞復雜消息,只能用來同步
- 共享內存區:能夠容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題。相當于線程中的線程安全,當然,共享內存區同樣可以做線程間通訊,不過沒有這個必要,線程間本來就已經共享了同一進程內的一塊內存
3、使用場景
- 如果用戶傳遞的信息較少或是需要通過信號來出發某些行為,上面提到的軟中斷限號機制不失為一種簡潔有效的一種進程間通信方式。但若是進程間要求傳遞的信息量比較大或者進程間存在交換數據的要求,那就需要考慮別的通信方式。
- 匿名管道簡單方便,但局限于單向通信的工作方式,并且只能創建它的進程及其子孫進程之間實現管道的共享。
- 有名管道雖然可以提供給任意關系的進程使用,但是由于其長期存在于系統之中,使用不當容易出錯。所以不建議初級開發者使用。
- 消息緩存可以不再局限于父子進程,而允許任意進程間通過共享消息隊列來實現進程間通信,并由系統調用函數來實現消息發送和接受方之間的同步,從而使得用戶在使用消息緩沖進行通信時不再需要考慮同步問題,使用方便,但是信息的復制需要額外的消耗CPU的時間,不適宜信息量大或操作頻繁的場合。
- 共享內存針對消息緩存的缺點而改進,利用了內存緩存區直接交換信息,無需復制,快捷、信息量大的是其優點。但是共享內存的通信方式是通過將共享內存緩存直接附加到進程的虛擬地址空間中來實現的。因此這些進程之間的讀寫操作的同步問題操作系統無法實現。必須由各進程利用其它同步工具解決。另外, 由于內存實體存在于計算機系統中,所以只能由處于同一個計算機系統中的其它進程共享,不方便網絡通信。
補充一點:
共享內存塊提供了在任意數量的進程之間進行高效雙向通信的機制。每個使用者都可以讀取寫入數據,但是所有程序之間必須達成并遵守一定的協議,以防止諸如在讀取信息之前覆寫內存空間等競爭狀態的出現。不行的是,Linux無法嚴格保證提供對共享內存塊的獨占訪問,同時,多個使用共享內存塊的進程之間必須協調使用同一個鍵值。