通信機制在linux
A. linux系統的進程間通信有哪幾種方式
一、方式
1、管道(Pipe)及有名管道( mkpipe):
管道可用於具有親緣關系進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關系進程間的通信;
2、信號(Signal):
信號是比較復雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身。
linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction。
實際上,該函數是基於BSD的,BSD為了實現可靠信號機制,又能夠統一對外介面,用sigaction函數重新實現了signal函數。
3、消息隊列(Message):
消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠許可權的進程可以向隊列中添加消息,被賦予讀許可權的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式位元組流以及緩沖區大小受限等缺點。
4、共享內存:
使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
5、信號量(semaphore):
主要作為進程間以及同一進程不同線程之間的同步手段。
6、套介面(Socket):
更為一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。
二、概念
進程間通信概念:
IPC—-InterProcess Communication
每個進程各自有不同的用戶地址空間,任何一個進程的全局變數在另一個進程中都看不到所以進程之間要交換數據必須通過內核。
在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信。
(1)通信機制在linux擴展閱讀
1)無名管道:
管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;只能用於父子進程或者兄弟進程之間(具有親緣關系的進程)。
管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,構成兩進程間通信的一個媒介。
數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩沖區的末尾,並且每次都是從緩沖區的頭部讀出數據。
2)有名管道:
不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即使與FIFO的創建進程不存在親緣關系的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信(能夠訪問該路徑的進程以及FIFO的創建進程之間)。
因此,通過FIFO不相關的進程也能交換數據。值得注意的是,FIFO嚴格遵循先進先出(first in first out),對管道及FIFO的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操作。
B. Linux進程間通信
linux下進程間通信的幾種主要手段簡介:
一般文件的I/O函數都可以用於管道,如close、read、write等等。
實例1:用於shell
管道可用於輸入輸出重定向,它將一個命令的輸出直接定向到另一個命令的輸入。比如,當在某個shell程序(Bourne shell或C shell等)鍵入who│wc -l後,相應shell程序將創建who以及wc兩個進程和這兩個進程間的管道。
實例二:用於具有親緣關系的進程間通信
管道的主要局限性正體現在它的特點上:
有名管道的創建
小結:
管道常用於兩個方面:(1)在shell中時常會用到管道(作為輸入輸入的重定向),在這種應用方式下,管道的創建對於用戶來說是透明的;(2)用於具有親緣關系的進程間通信,用戶自己創建管道,並完成讀寫操作。
FIFO可以說是管道的推廣,克服了管道無名字的限制,使得無親緣關系的進程同樣可以採用先進先出的通信機制進行通信。
管道和FIFO的數據是位元組流,應用程序之間必須事先確定特定的傳輸"協議",採用傳播具有特定意義的消息。
要靈活應用管道及FIFO,理解它們的讀寫規則是關鍵。
信號生命周期
信號是進程間通信機制中唯一的非同步通信機制,可以看作是非同步通知,通知接收信號的進程有哪些事情發生了。信號機制經過POSIX實時擴展後,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。
可以從兩個不同的分類角度對信號進行分類:(1)可靠性方面:可靠信號與不可靠信號;(2)與時間的關繫上:實時信號與非實時信號。
(1) 可靠信號與不可靠信號
不可靠信號 :Linux下的不可靠信號問題主要指的是信號可能丟失。
可靠信號 :信號值位於SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問題。Linux在支持新版本的信號安裝函數sigation()以及信號發送函數sigqueue()的同時,仍然支持早期的signal()信號安裝函數,支持信號發送函數kill()。
對於目前linux的兩個信號安裝函數:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以後的信號都支持排隊。這兩個函數的最大區別在於,經過sigaction安裝的信號都能傳遞信息給信號處理函數(對所有信號這一點都成立),而經過signal安裝的信號卻不能向信號處理函數傳遞信息。對於信號發送函數來說也是一樣的。
(2) 實時信號與非實時信號
前32種信號已經有了預定義值,每個信號有了確定的用途及含義,並且每種信號都有各自的預設動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。後32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。實時信號是POSIX標準的一部分,可用於應用進程。非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。
發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
調用成功返回 0;否則,返回 -1。
sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。
sigqueue的第一個參數是指定接收信號的進程ID,第二個參數確定即將發送的信號,第三個參數是一個聯合數據結構union sigval,指定了信號傳遞的參數,即通常所說的4位元組值。
sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號。sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個進程發送信號。
inux主要有兩個函數實現信號的安裝: signal() 、 sigaction() 。其中signal()在可靠信號系統調用的基礎上實現, 是庫函數。它只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實時信號的安裝。sigaction()優於signal()主要體現在支持信號帶有參數。
消息隊列就是一個消息的鏈表。可以把消息看作一個記錄,具有特定的格式以及特定的優先順序。對消息隊列有寫許可權的進程可以向中按照一定的規則添加新消息;對消息隊列有讀許可權的進程則可以從消息隊列中讀走消息。消息隊列是隨內核持續的
消息隊列的內核持續性要求每個消息隊列都在系統范圍內對應唯一的鍵值,所以,要獲得一個消息隊列的描述字,只需提供該消息隊列的鍵值即可;
消息隊列與管道以及有名管道相比,具有更大的靈活性,首先,它提供有格式位元組流,有利於減少開發人員的工作量;其次,消息具有類型,在實際應用中,可作為優先順序使用。這兩點是管道以及有名管道所不能比的。同樣,消息隊列可以在幾個進程間復用,而不管這幾個進程是否具有親緣關系,這一點與有名管道很相似;但消息隊列是隨內核持續的,與有名管道(隨進程持續)相比,生命力更強,應用空間更大。
信號燈與其他進程間通信方式不大相同,它主要提供對進程間共享資源訪問控制機制。相當於內存中的標志,進程可以根據它判定是否能夠訪問某些共享資源,同時,進程也可以修改該標志。除了用於訪問控制外,還可用於進程同步。信號燈有以下兩種類型:
int semop(int semid, struct sembuf *sops, unsigned nsops); semid是信號燈集ID,sops指向數組的每一個sembuf結構都刻畫一個在特定信號燈上的操作。
int semctl(int semid,int semnum,int cmd,union semun arg)
該系統調用實現對信號燈的各種控制操作,參數semid指定信號燈集,參數cmd指定具體的操作類型;參數semnum指定對哪個信號燈操作,只對幾個特殊的cmd操作有意義;arg用於設置或返回信號燈信息。
進程間需要共享的數據被放在一個叫做IPC共享內存區域的地方,所有需要訪問該共享區域的進程都要把該共享區域映射到本進程的地址空間中去。系統V共享內存通過shmget獲得或創建一個IPC共享內存區域,並返回相應的標識符。內核在保證shmget獲得或創建一個共享內存區,初始化該共享內存區相應的shmid_kernel結構注同時,還將在特殊文件系統shm中,創建並打開一個同名文件,並在內存中建立起該文件的相應dentry及inode結構,新打開的文件不屬於任何一個進程(任何進程都可以訪問該共享內存區)。所有這一切都是系統調用shmget完成的。
shmget()用來獲得共享內存區域的ID,如果不存在指定的共享區域就創建相應的區域。shmat()把共享內存區域映射到調用進程的地址空間中去,這樣,進程就可以方便地對共享區域進行訪問操作。shmdt()調用用來解除進程對共享內存區域的映射。shmctl實現對共享內存區域的控制操作。這里我們不對這些系統調用作具體的介紹,讀者可參考相應的手冊頁面,後面的範例中將給出它們的調用方法。
註:shmget的內部實現包含了許多重要的系統V共享內存機制;shmat在把共享內存區域映射到進程空間時,並不真正改變進程的頁表。當進程第一次訪問內存映射區域訪問時,會因為沒有物理頁表的分配而導致一個缺頁異常,然後內核再根據相應的存儲管理機制為共享內存映射區域分配相應的頁表。
C. linux系統上信號發送和信號接收講解
用於進程間通信,通信機制由操作系統保證,比較穩定。
在linux中可以通過kill -l查看所有信號的類型。
kill -信號類型 進程ID
int kill(pid_t pid, int sig);
入參pid :
pid > 0: 發送信號給指定的進程。
pid = 0: 發送信號給 與調用kill函數進程屬於同一進程組的所有進程。
pid < 0: 取|pid|發給對應進程組。
pid = -1:發送給進程有許可權發送的系統中所有進程。
sig :信號類型。
返回值 :成功:0;失敗:-1 (ID非法,信號非法,普通用戶殺init進程等權級問題),設置errno
以OpenHarmony源碼為例,應用ANR後,AbilityManagerService會通知應用mp堆棧信息,就是通過信號量做的。
頭文件位置 :
include <signal.h>
函數解釋 :
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
當接收到指定的信號signum時,就會跳轉到參數handler指定的函數執行。其中handler的入參是信號值。
函數原型 :
signum參數指出要捕獲的信號類型,act參數指定新的信號處理方式,oldact參數輸出先前信號的處理方式(如果不為NULL的話)。
sigaction結構體
sa_handler 信號處理函數
sa_mask 在處理該信號時可以暫時將sa_mask 指定的信號集擱置
sa_flags 指定一組修改信號行為的標志。 它由以下零個或多個的按位或組成
SA_RESETHAND:當調用信號處理函數時,將信號的處理函數重置為預設值SIG_DFL
SA_RESTART:如果信號中斷了進程的某個系統調用,則系統自動啟動該系統調用
SA_NODEFER :一般情況下, 當信號處理函數運行時,內核將阻塞該給定信號。但是如果設置了 SA_NODEFER標記, 那麼在該信號處理函數運行時,內核將不會阻塞該信號
sa_restorer 是一個替代的信號處理程序,當設置SA_SIGINFO時才會用它。
相關函數
int sigemptyset( sigset_t *set);
sigemptyset()用來將參數set信號集初始化並清空。
執行成功則返回0,如果有錯誤則返回-1。
完整示例
D. Linux的幾種進程間通信方法簡介
管道(pipe) 管道是Linux支持的最初IPC方式,管道可分為無名管道,有名管道等。 (一)無名管道,它具有幾個特點: 1) 管道是半雙工的,只能支持數據的單向流動;兩進程間需要通信時需要建立起兩個管道; 2) 無名管道使用pipe()函數創建,只能用於父子進程或者兄弟進程之間; 3) 管道對於通信的兩端進程而言,實質上是一種獨立的文件,只存在於內存中; 4) 數據的讀寫操作:一個進程向管道中寫數據,所寫的數據添加在管道緩沖區的尾部;另一個進程在管道中緩沖區的頭部讀數據。 (二)有名管道 有名管道也是半雙工的,不過它允許沒有親緣關系的進程間進行通信。具體點說就是,有名管道提供了一個路徑名與之進行關聯,以FIFO(先進先出)的形式存在於文件系統中。這樣即使是不相乾的進程也可以通過FIFO相互通信,只要他們能訪問已經提供的路徑。 值得注意的是,只有在管道有讀端時,往管道中寫數據才有意義。否則,向管道寫數據的進程會接收到內核發出來的SIGPIPE信號;應用程序可以自定義該信號處理函數,或者直接忽略該信號。 二。信號量(semophore) 信號量是一種計數器,可以控制進程間多個線程或者多個進程對資源的同步訪問,它常實現為一種鎖機制。實質上,信號量是一個被保護的變數,並且只能通過初始化和兩個標準的原子操作(P/V)來訪問。(P,V操作也常稱為wait(s),signal(s)) 三。信號(Signal) 信號是Unix系統中使用的最古老的進程間通信的方法之一。操作系統通過信號來通知某一進程發生了某一種預定好的事件;接收到信號的進程可以選擇不同的方式處理該信號,一是可以採用默認處理機制-進程中斷或退出,一是忽略該信號,還有就是自定義該信號的處理函數,執行相應的動作。 內核為進程生產信號,來響應不同的事件,這些事件就是信號源。信號源可以是:異常,其他進程,終端的中斷(Ctrl-C,Ctrl+\等),作業的控制(前台,後台進程的管理等),分配額問題(cpu超時或文件過大等),內核通知(例如I/O就緒等),報警(計時器)。 四。消息隊列(Message Queue) 消息隊列就是消息的一個鏈表,它允許一個或者多個進程向它寫消息,一個或多個進程向它讀消息。Linux維護了一個消息隊列向量表:msgque,來表示系統中所有的消息隊列。 消息隊列克服了信號傳遞信息少,管道只能支持無格式位元組流和緩沖區受限的缺點。 五。共享內存(shared memory) 共享內存映射為一段可以被其他進程訪問的內存。該共享內存由一個進程所創建,然後其他進程可以掛載到該共享內存中。共享內存是最快的IPC機制,但由於linux本身不能實現對其同步控制,需要用戶程序進行並發訪問控制,因此它一般結合了其他通信機制實現了進程間的通信,例如信號量。 五。套接字(socket) socket也是一種進程間的通信機制,不過它與其他通信方式主要的區別是:它可以實現不同主機間的進程通信。
E. 進程間通信方式
在操作系統中,一個進程可以理解為是關於計算機資源集合的一次運行活動,其就是一個正在執行的程序的實例。從概念上來說,一個進程擁有它自己的虛擬CPU和虛擬地址空間,任何一個進程對於彼此而言都是相互獨立的,這也引入了一個問題 —— 如何讓進程之間互相通信?
由於進程之間是互相獨立的,沒有任何手段直接通信,因此我們需要藉助操作系統來輔助它們。舉個通俗的例子,假如A與B之間是獨立的,不能彼此聯系,如果它們想要通信的話可以藉助第三方C,比如A將信息交給C,C再將信息轉交給B —— 這就是進程間通信的主要思想 —— 共享資源。
這里要解決的一個重要的問題就是如何避免競爭,即避免多個進程同時訪問臨界區的資源。
共享內存是進程間通信中最簡單的方式之一。共享內存允許兩個或更多進程訪問同一塊內存。當一個進程改變了這塊地址中的內容的時候,其它進程都會察覺到這個更改。
你可能會想到,我直接創建一個文件,然後進程不就都可以訪問了?
是的,但這個方法有幾個缺陷:
Linux下採用共享內存的方式來使進程完成對共享資源的訪問,它將磁碟文件復制到內存,並創建虛擬地址到該內存的映射,就好像該資源本來就在進程空間之中,此後我們就可以像操作本地變數一樣去操作它們了,實際的寫入磁碟將由系統選擇最佳方式完成,例如操作系統可能會批量處理加排序,從而大大提高IO速度。
如同上圖一樣,進程將共享內存映射到自己的虛擬地址空間中,進程訪問共享進程就好像在訪問自己的虛擬內存一樣,速度是非常快的。
共享內存的模型應該是比較好理解的:在物理內存中創建一個共享資源文件,進程將該共享內存綁定到自己的虛擬內存之中。
這里要解決的一個問題是如何將同一塊共享內存綁定到自己的虛擬內存中,要知道在不同進程中使用 malloc 函數是會順序分配空閑內存,而不會分配同一塊內存,那麼要如何去解決這個問題呢?
Linux操作系統已經想辦法幫我們解決了這個問題,在 #include <sys/ipc.h> 和 #include <sys/shm.h> 頭文件下,有如下幾個shm系列函數:
通過上述幾個函數,每個獨立的進程只要有統一的共享內存標識符便可以建立起虛擬地址到物理地址的映射,每個虛擬地址將被翻譯成指向共享區域的物理地址,這樣就實現了對共享內存的訪問。
還有一種相像的實現是採用mmap函數,mmap通常是直接對磁碟的映射——因此不算是共享內存,存儲量非常大,但訪問慢; shmat與此相反,通常將資源保存在內存中創建映射,訪問快,但存儲量較小。
不過要注意一點,操作系統並不保證任何並發問題,例如兩個進程同時更改同一塊內存區域,正如你和你的朋友在線編輯同一個文檔中的同一個標題,這會導致一些不好的結果,所以我們需要藉助信號量或其他方式來完成同步。
信號量是迪傑斯特拉最先提出的一種為解決 同步不同執行線程問題 的一種方法,進程與線程抽象來看大同小異,所以 信號量同樣可以用於同步進程間通信 。
信號量 s 是具有非負整數值的全局變數,由兩種特殊的 原子操作 來實現,這兩種原子操作稱為 P 和 V :
信號量並不用來傳送資源,而是用來保護共享資源,理解這一點是很重要的,信號量 s 的表示的含義為 同時允許最大訪問資源的進程數量 ,它是一個全局變數。來考慮一個上面簡單的例子:兩個進程同時修改而造成錯誤,我們不考慮讀者而僅僅考慮寫者進程,在這個例子中共享資源最多允許一個進程修改資源,因此我們初始化 s 為1。
開始時,A率先寫入資源,此時A調用P(s),將 s 減一,此時 s = 0,A進入共享區工作。
此時,進程B也想進入共享區修改資源,它調用P(s)發現此時s為0,於是掛起進程,加入等待隊列。
A工作完畢,調用V(s),它發現s為0並檢測到等待隊列不為空,於是它隨機喚醒一個等待進程,並將s加1,這里喚醒了B。
B被喚醒,繼續執行P操作,此時s不為0,B成功執行將s置為0並進入工作區。
此時C想要進入工作區......
可以發現,在無論何時只有一個進程能夠訪問共享資源,這就是信號量做的事情,他控制進入共享區的最大進程數量,這取決於初始化s的值。此後,在進入共享區之前調用P操作,出共享區後調用V操作,這就是信號量的思想。
在Linux下並沒有直接的P&V函數,而是需要我們根據這幾個基本的sem函數族進行封裝:
正如其名,管道就如同生活中的一根管道,一端輸送,而另一端接收,雙方不需要知道對方,只需要知道管道就好了。
管道是一種最 基本的進程間通信機制。 管道由pipe函數來創建: 調用pipe函數,會在內核中開辟出一塊緩沖區用來進行進程間通信,這塊緩沖區稱為管道,它有一個讀端和一個寫端。管道被分為匿名管道和有名管道。
匿名管道通過pipe函數創建,這個函數接收一個長度為2的Int數組,並返回1或0表示成功或者失敗:
int pipe(int fd[2])
這個函數打開兩個文件描述符,一個讀端文件,一個寫端,分別存入fd[0]和fd[1]中,然後可以作為參數調用 write 和 read 函數進行寫入或讀取,注意fd[0]只能讀取文件,而fd[1]只能用於寫入文件。
你可能有個疑問,這要怎麼實現通信?其他進程又不知道這個管道,因為進程是獨立的,其他進程看不到某一個進程進行了什麼操作。
是的,『其他』進程確實是不知道,但是它的子進程卻可以!這里涉及到fork派生進程的相關知識,一個進程派生一個子進程,那麼子進程將會復制父進程的內存空間信息,注意這里是復制而不是共享,這意味著父子進程仍然是獨立的,但是在這一時刻,它們所有的信息又是相等的。因此子進程也知道該全局管道,並且也擁有兩個文件描述符與管道掛鉤,所以 匿名管道只能在具有親緣關系的進程間通信。
還要注意,匿名管道內部採用環形隊列實現,只能由寫端到讀端,由於設計技術問題,管道被設計為半雙工的,一方要寫入則必須關閉讀描述符,一方要讀出則必須關閉寫入描述符。因此我們說 管道的消息只能單向傳遞。
注意管道是堵塞的,如何堵塞將依賴於讀寫進程是否關閉文件描述符。如果讀管道,如果讀到空時,假設此時寫埠還沒有被完全關閉,那麼操作系統會假設還有數據要讀,此時讀進程將會被堵塞,直到有新數據或寫埠被關閉;如果管道為空,且寫埠也被關閉,此時操作系統會認為已經沒有東西可讀,會直接退出,並關閉管道。
對於寫一個已經滿了的管道同理而言。
管道內部由內核管理,在半雙工的條件下,保證數據不會出現並發問題。
了解了匿名管道之後,有名管道便很好理解了。在匿名管道的介紹中,我們說其他進程不知道管道和文件描述符的存在,所以匿名管道只適用於具有親緣關系的進程,而命名管道則很好的解決了這個問題 —— 現在管道有一個唯一的名稱了,任何進程都可以訪問這個管道。
注意,操作系統將管道看作一個抽象的文件,但管道並不是普通的文件,管道存在於內核空間中而不放置在磁碟(有名管道文件系統上有一個標識符,沒有數據塊),訪問速度更快,但存儲量較小,管道是臨時的,是隨進程的,當進程銷毀,所有埠自動關閉,此時管道也是不存在的,操作系統將所有IO抽象的看作文件,例如網路也是一種文件,這意味著我們可以採用任何文件方法操作管道,理解這種抽象是很重要的,命名管道就利用了這種抽象。
Linux下,採用mkfifo函數創建,可以傳入要指定的『文件名』,然後其他進程就可以調用open方法打開這個特殊的文件,並進行write和read操作(那肯定是位元組流對吧)。
注意,命名管道適用於任何進程,除了這一點不同外,其餘大多數都與匿名管道相同。
消息隊列亦稱報文隊列,也叫做信箱,是Linux的一種通信機制,這種通信機制傳遞的數據會被拆分為一個一個獨立的數據塊,也叫做消息體,消息體中可以定義類型與數據,克服了無格式承載位元組流的缺陷(現在收到void*後可以知道其原本的格式惹):
同管道類似,它有一個不足就是每個消息的最大長度是有上限的,整個消息隊列也是長度限制的。
內核為每個IPC對象維護了一個數據結構struct ipc_perm,該數據結構中有指向鏈表頭與鏈表尾部的指針,保證每一次插入取出都是O(1)的時間復雜度。
一個進程可以發送信號給另一個進程,一個信號就是一條消息,可以用於通知一個進程組發送了某種類型的事件,該進程組中的進程可以採取處理程序處理事件。
Linux下 unistd.h 頭文件下定義了如圖中的常量,當你在shell命令行鍵入 ctrl + c 時,內核就會前台進程組的每一個進程發送 SIGINT 信號,中止進程。
我們可以看到上述只有30個信號,因此操作系統會為每一個進程維護一個int類型變數sig,利用其中30位代表是否有對應信號事件,每一個進程還有一個int類型變數block,與sig對應,其30位表示是否堵塞對應信號(不調用處理程序)。如果存在多個相同的信號同時到來,多餘信號會被存儲在一個等待隊列中等待。
我們要理解進程組是什麼,每個進程屬於一個進程組,可以有多個進程屬於同一個組。每個進程擁有一個進程ID,稱為 pid ,而每個進程組擁有一個進程組ID,稱為 pgid ,默認情況下,一個進程與其子進程屬於同一進程組。
軟體方面(諸如檢測鍵盤輸入是硬體方面)可以利用kill函數發送信號,kill函數接受兩個參數,進程ID和信號類型,它將該信號類型發送到對應進程,如果該pid為0,那麼會發送到屬於自身進程組的所有進程。
接收方可以採用signal函數給對應事件添加處理程序,一旦事件發生,如果未被堵塞,則調用該處理程序。
Linux下有一套完善的函數用以處理信號機制。
Socket套接字是用與網路中不同主機的通信方式,多用於客戶端與伺服器之間,在Linux下也有一系列C語言函數,諸如socket、connect、bind、listen與accept,我們無需花太多時間研究這些函數,因為我們可能一輩子都不會與他們打交道,對於原理的學習,後續我會對Java中的套接字socket源碼進行剖析。
對於工作而言,我們可能一輩子都用不上這些操作,但作為對於操作系統的學習,認識到進程間是如何通信還是很有必要的。
面試的時候對於這些方法我們不需要掌握到很深的程度,但我們必須要講的來有什麼通信方式,這些方式都有什麼特點,適用於什麼條件,大致是如何操作的,能說出這些,基本足以讓面試官對你十分滿意了。
F. linux 進程間通信的幾種方式
1管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關系進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關系進程間的通信;
2信號(Signal):信號是比較復雜的通信方式,用於通知接受進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身;linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD為了實現可靠信號機制,又能夠統一對外介面,用sigaction函數重新實現了signal函數);
3報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠許可權的進程可以向隊列中添加消息,被賦予讀許可權的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式位元組流以及緩沖區大小受限等缺點。
4共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
5信號量(semaphore):主要作為進程間以及同一進程不同線程之間的同步手段。
6套介面(Socket):更為一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。
G. Linux 進程間套接字通信(Socket)基礎知識
姓名:羅學元 學號:21181214375 學院:廣州研究院
【嵌牛導讀】Linux進程間套接字通信基礎
【嵌牛鼻子】Linux 進程間套接字及通信介紹
【嵌牛提問】Linux進程間套接字包含哪些內容,如何實現通信
一、套接字(Socket)通信原理
套接字通信允許互聯的位於不同計算機上的進程之間實現通信功能。
二、套接字的屬性
套接字的特性由3個屬性確定,它們分別是:域、類型和協議。
1. 套接字的域
它指定套接字通信中使用的網路介質,最常見的套接字域是AF_INET,它指的是Internet網路。當客戶使用套接字進行跨網路的連接時,它就需要用到伺服器計算機的IP地址和埠來指定一台聯網機器上的某個特定服務,所以在使用socket作為通信的終點,伺服器應用程序必須在開始通信之前綁定一個埠,伺服器在指定的埠等待客戶的連接。
另一個域AF_UNIX表示UNIX文件系統,就是文件輸入/輸出,它的地址就是文件名。
2. 套接字類型
網際網路提供了兩種通信機制:流(stream)和數據報(datagram),因而套接字的類型也就分為流套接字和數據報套接字。我們主要看流套接字。
流套接字由類型SOCK_STREAM指定,它們是在AF_INET域中通過TCP/IP連接實現,同時也是AF_UNIX中常用的套接字類型。
流套接字提供的是一個有序、可靠、雙向位元組流的連接,因此發送的數據可以確保不會丟失、重復或亂序到達,而且它還有一定的出錯後重新發送的機制。
與流套接字相對的是由類型SOCK_DGRAM指定的數據報套接字,它不需要建立連接和維持一個連接,它們在AF_INET中通常是通過UDP/IP實現的。它對可以發送的數據的長度有限制,數據報作為一個單獨的網路消息被傳輸,它可能丟失、復制或錯亂到達,UDP不是一個可靠的協議,但是它的速度比較高,因為它並不需要總是要建立和維持一個連接。
3.套接字協議
只要底層的傳輸機制允許不止一個協議來提供要求的套接字類型,我們就可以為套接字選擇一個特定的協議。通常只需要使用默認值。
三、套接字地址
每個套接字都有其自己的地址格式,對於AF_UNIX域套接字來說,它的地址由結構sockaddr_un來描述,該結構定義在頭文件
struct sockaddr_un{
sa_family_t sun_family; //AF_UNIX,它是一個短整型
char sum_path[]; //路徑名
};
對於AF_INET域套接字來說,它的地址結構由sockaddr_in來描述,它至少包括以下幾個成員:
struct sockaddr_in{
short int sin_family; //AN_INET
unsigned short int sin_port; //埠號
struct in_addr sin_addr; //IP地址
}
而in_addr被定義為:
struct in_addr{
unsigned long int s_addr;
}
四、基於流套接字的客戶/伺服器的工作流程
使用socket進行進程通信的進程採用的客戶/伺服器系統是如何工作的呢?
1.伺服器端
首先,伺服器應用程序用系統調用socket來創建一個套接字,它是系統分配給該伺服器進程的類似文件描述符的資源,它不能與其他的進程共享。
接下來,伺服器進程會給套接字起個名字,我們使用系統調用bind來給套接字命名。然後伺服器進程就開始等待客戶連接到這個套接字。
然後,系統調用listen來創建一個隊列,並將其用於存放來自客戶的進入連接。
最後,伺服器通過系統調用accept來接受客戶的連接。它會創建一個與原有的命名套接不同的新套接字,這個套接字只用於與這個特定客戶端進行通信,而命名套接字(即原先的套接字)則被保留下來繼續處理來自其他客戶的連接。
2.客戶端
基於socket的客戶端比伺服器端簡單。同樣,客戶應用程序首先調用socket來創建一個未命名的套接字,然後講伺服器的命名套接字作為一個地址來調用connect與伺服器建立連接。
一旦連接建立,我們就可以像使用底層的文件描述符那樣用套接字來實現雙向數據的通信。
H. linux內核態和用戶態的通信機制包括哪些
究竟什麼是用戶態,什麼是內核態,這兩個基本概念以前一直理解得不是很清楚,根本原因個人覺得是在於因為大部分時候我們在寫程序時關注的重點和著眼的角度放在了實現的功能和代碼的邏輯性上,先看一個例子:
1)例子
C代碼
1. void testfork(){
2. if(0 = = fork()){
3. printf(「create new process success!\n」);
4. }
5. printf(「testfork ok\n」);
6. }
這段代碼很簡單,從功能的角度來看,就是實際執行了一個fork(),生成一個新的進程,從邏輯的角度看,就是判斷了如果fork()返回的是則列印相關語句,然後函數最後再列印一句表示執行完整個testfork()函數。代碼的執行邏輯和功能上看就是如此簡單,一共四行代碼,從上到下一句一句執行而已,完全看不出來哪裡有體現出用戶態和進程態的概念。
如果說前面兩種是靜態觀察的角度看的話,我們還可以從動態的角度來看這段代碼,即它被轉換成CPU執行的指令後載入執行的過程,這時這段程序就是一個動態執行的指令序列。而究竟載入了哪些代碼,如何載入就是和操作系統密切相關了。
2)特權級
熟悉Unix/Linux系統的人都知道,fork的工作實際上是以系統調用的方式完成相應功能的,具體的工作是由sys_fork負責實施。其實無論是不是Unix或者Linux,對於任何操作系統來說,創建一個新的進程都是屬於核心功能,因為它要做很多底層細致地工作,消耗系統的物理資源,比如分配物理內存,從父進程拷貝相關信息,拷貝設置頁目錄頁表等等,這些顯然不能隨便讓哪個程序就能去做,於是就自然引出特權級別的概念,顯然,最關鍵性的權力必須由高特權級的程序來執行,這樣才可以做到集中管理,減少有限資源的訪問和使用沖突。
特權級顯然是非常有效的管理和控製程序執行的手段,因此在硬體上對特權級做了很多支持,就Intel x86架構的CPU來說一共有0~3四個特權級,0級最高,3級最低,硬體上在執行每條指令時都會對指令所具有的特權級做相應的檢查,相關的概念有 CPL、DPL和RPL,這里不再過多闡述。硬體已經提供了一套特權級使用的相關機制,軟體自然就是好好利用的問題,這屬於操作系統要做的事情,對於 Unix/Linux來說,只使用了0級特權級和3級特權級。也就是說在Unix/Linux系統中,一條工作在級特權級的指令具有了CPU能提供的最高權力,而一條工作在3級特權級的指令具有CPU提供的最低或者說最基本權力。
3)用戶態和內核態
現在我們從特權級的調度來理解用戶態和內核態就比較好理解了,當程序運行在3級特權級上時,就可以稱之為運行在用戶態,因為這是最低特權級,是普通的用戶進程運行的特權級,大部分用戶直接面對的程序都是運行在用戶態;反之,當程序運行在級特權級上時,就可以稱之為運行在內核態。
雖然用戶態下和內核態下工作的程序有很多差別,但最重要的差別就在於特權級的不同,即權力的不同。運行在用戶態下的程序不能直接訪問操作系統內核數據結構和程序,比如上面例子中的testfork()就不能直接調用 sys_fork(),因為前者是工作在用戶態,屬於用戶態程序,而sys_fork()是工作在內核態,屬於內核態程序。
當我們在系統中執行一個程序時,大部分時間是運行在用戶態下的,在其需要操作系統幫助完成某些它沒有權力和能力完成的工作時就會切換到內核態,比如testfork()最初運行在用戶態進程下,當它調用fork()最終觸發 sys_fork()的執行時,就切換到了內核態。
2. 用戶態和內核態的轉換
1)用戶態切換到內核態的3種方式
a. 系統調用
這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作,比如前例中fork()實際上就是執行了一個創建新進程的系統調用。而系統調用的機制其核心還是使用了操作系統為用戶特別開放的一個中斷來實現,例如Linux的int 80h中斷。
b. 異常
當CPU在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。
c. 外圍設備的中斷
當外圍設備完成用戶請求的操作後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬碟讀寫操作完成,系統會切換到硬碟讀寫的中斷處理程序中執行後續操作等。
這3種方式是系統在運行時由用戶態轉到內核態的最主要方式,其中系統調用可以認為是用戶進程主動發起的,異常和外圍設備中斷則是被動的。
2)具體的切換操作
從觸發方式上看,可以認為存在前述3種不同的類型,但是從最終實際完成由用戶態到內核態的切換操作上來說,涉及的關鍵步驟是完全一致的,沒有任何區別,都相當於執行了一個中斷響應的過程,因為系統調用實際上最終是中斷機制實現的,而異常和中斷的處理機制基本上也是一致的,關於它們的具體區別這里不再贅述。關於中斷處理機制的細節和步驟這里也不做過多分析,涉及到由用戶態切換到內核態的步驟主要包括:
[1] 從當前進程的描述符中提取其內核棧的ss0及esp0信息。
[2] 使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個
過程也完成了由用戶棧到內核棧的切換過程,同時保存了被暫停執行的程序的下一
條指令。
[3] 將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始
執行中斷處理程序,這時就轉到了內核態的程序執行了。
I. Linux 進程間通信方式有哪些
進程間通信(IPC,Interprocess
communication)是一組編程介面,讓程序員能夠協調不同的進程,使之能在一個操作系統里同時運行,並相互傳遞、交換信息。這使得一個程序能夠在同一時間里處理許多用戶的要求。因為即使只有一個用戶發出要求,也可能導致一個操作系統中多個進程的運行,進程之間必須互相通話。IPC介面就提供了這種可能性。每個IPC方法均有它自己的優點和局限性,一般,對於單個程序而言使用所有的IPC方法是不常見的。
1、無名管道通信
無名管道(pipe):管道是一種半雙工的通信方式,數據只能單向流動,而且只能在具有親緣關系的進程間使用,進程的親緣關系通常是指父子進程關系。
2、高級管道通信
高級管道(popen):將另一個程序當做一個新的進程在當前程序進程中啟動,則它算是當前程序的子進程,這種方式我們稱為高級管道方式。
3、有名管道通信
有名管道(named pipe):有名管道也是半雙工的通信方式,但是它允許無親緣關系進程間的通信。
4、消息隊列通信
消息隊列(message
queue):消息隊列是由消息的鏈表,存放在內核中並由消息隊列標識符標識,消息隊列克服了信號傳遞信息少、管道只能承載無格式位元組流以及緩沖區大小受限等缺點。
5、信號量通信
信號量(semophore):信號量是一個計數器,可以用來控制多個進程對共享資源的訪問,它常作為一種鎖機制,防止某進程正在訪問共享資源時,其他進程訪問該資源。因此,主要作為進程間以及同一進程內不同線程之間的同步手段。
6、信號
信號(sinal):信號是一種比較復雜的通信方式,用於通知接收進程某個事件已經發生。
7、共享內存通信
共享內存(shared
memory):共享內存就是映射一段能被其他進程所訪問的內存,這段共享內存由一個進程創建,但多個進程都可以訪問。共享內存是最快的IPC方式,它是針對其他進程間通信方式運行效率低而專門設計的。它往往與其他通信機制,如信號量,配合使用,來實現進程間的同步和通信。
8、套接字通信
套接字(socket):套接字也是一種進程間通信機制,與其他通信機制不同的是,它可用於不同機器間的進程通信。
J. Linux fd 系列|信號編程(signal)竟能這樣做漲姿勢
來源 | 奇點雲存儲(id : qiyacloud)授權轉載
如若轉載請聯系原公眾號
信號是什麼?
首先說,信號(signal)是什麼?
信號( signal )本質是 Linux 進程間通信的一種機制,也叫 軟中斷信號 。既然是通信機制,那麼就是傳遞信息用的,信號傳遞的信息很簡單,就是一個整數,一般用於配合系統管理任務,比如進程的終結、恢復、熱載入等。
信號都用整數常量表示,命名以 SIG 為前綴,比如 SIGINT( ctrl-c 觸發),SIGKILL( kill -9 觸發 )。
信號一般怎麼產生?
信號處理分為兩個階段 :
signalfd 是什麼?
了解了什麼是信號( signal ),那 signalfd 又會是什麼呢?
是一個跟信號關聯的 文件描述符 ,能夠以 io 的行為獲取到系統信號,屬性上來講 signalfd 也是一個匿名 fd 類型。
signalfd 長什麼樣子?
奇點按照 man signalfd 裡面的例子,寫了個 demo,跑在 Linux 機器上,按照慣例去看下 fd 的樣子。
從這里可以得到簡單的信息:
signalfd 使用姿勢?
其實信號是很講究的,甚至有信號編程一說,Linux 的 signalfd 為信號的處理提供了一種新的方法, 統一到文件的 io 模式,契合一切接文件的理念 。
系統調用:
該系統調用返回一個整數類型 signalfd,這個句柄跟信號行為綁定,當發生信號的時候,句柄觸發可讀事件。
第一個參數也可以傳入一個有效的信號 fd 的句柄, 如果傳入的是 -1 ,那麼內核會自動創建一個新的 fd 。
完整的代碼例子,在 Linux 機器上,通過 man signalfd 就可以獲取到。
上面的例子,signalfd 沒有信號(沒有可讀事件)的時候會阻塞在 read 調用上,運行效果如下:
可以看到每一次 ctrl + c 觸發的信號被捕捉到,並且列印出來。用文件 io 的方式來接收信號,牛。
怎麼做到的呢?照例,我們淺析一下內核的代碼,位於 fs/signalfd.c ,這是一個很小的文件,正是這個文件完成了對信號「文件化」的封裝。
上面最重要的兩個調用:
signalfd 原理剖析
1 signalfd
看一下 signalfd 支持的介面調用:
通過這個可以知道 signalfd 支持的特性:
2 signalfd_poll
這個函數做的事情非常簡單,就是把 等待對象 掛到當前進程的信號結構的鏈表上。表頭是: current->sighand->signalfd_wqh ,這個就有意思了,這里直接掛到當前進程的結構上。換句話說,喚醒也是自此表頭開始。
回憶一下 timerfd ,是掛在 timerfd_ctx->wqh 的欄位上。這里的差別是因為信號是對進程來說的。
3 signalfd_read
讀一個 signalfd 的操作非常簡單,主要邏輯:
簡要的代碼注釋如下:
這里就能非常清晰的看到, 進程有信號的時候,signalfd 句柄就是可讀的 。
signal 和 epoll 的配合
1 熟悉的 epoll_ctl
epoll_ctl 注冊 signalfd 的時候,調用 signalfd_poll , signalfd_poll 會把 epoll 創建的 wait entry 掛到 current->sighand 上。喚醒的時候調用這個 wait 鏈表的回調。
2 什麼時候喚醒呢?
喚醒的操作其實不在 signalfd.c 文件中,而是在原有的信號軟中斷的流程中。
為了知識的完整性,說個點, signalfd_notify 其實在 timer 定時器的流程中也有調用,但跟我們本次主幹沒啥關系,這里忽略。
信號的發送喚醒的簡要示意圖:
所有的信號發送都會調用到 send_signal ,在這個裡面實現了喚醒 sighand->signalfd_wqh 鏈表的操作。從而使得 epoll 感知到 signalfd 可讀了(因為來信號了),使得 epoll 從 epoll_wait 出喚醒,然後調用 read 操作,把信號的相關信息從句柄中讀出來。
劃重點:喚醒在 信號發送 的過程。
總結