Linux程式通訊機制

fhzmWJ發表於2020-12-04


Linux管道通訊機制

管道是所有Unix及Linux都提供的一種程式間的通訊機制,它是程式之間的一個單向資料流,一個程式可向管道寫入資料,另一個程式可從管道中讀取資料,從而達到資料交換的目的。

Liunx的管道通訊機制有無名管道PIPE有名管道FIFO兩種機制。


無名管道

  • 無名管道只能用於具有親緣關係的程式之間的通訊。
  • 無名管道是半雙工的,具有固定的讀寫端。雖然pipe()系統呼叫返回了兩個檔案描述符,但每個程式在使用一個檔案描述符之前應該先將另外一個檔案描述符關閉。如果需要雙向的資料流,則必須通過兩次pipe()建立起兩個管道。
  • 無名管道可以看作是一種特殊的檔案,由一組VFS物件(虛擬檔案系統)來實現,有對應的磁碟映像,只存在於記憶體的快取記憶體中。Linux在2.6之後的版本中,把管道相關的VFS物件組織成一種特殊檔案系統pipefs進行管理,但它在系統目錄樹中沒有安裝點,所以使用者看不到。
    對管道的讀寫和對普通檔案讀寫差不多,使用通用的read()、write()等,但核心最終會呼叫管道檔案的讀寫操作函式

建立無名管道:

pipe()

int pipe(int fileds[2]);
 需要包含標頭檔案<unistd.h>

引數:
	fileds[2] 是一個輸出引數,返回兩個檔案描述符
	0 用於讀管道
	1 用於寫管道
	
功能:
* 在內緩衝區建立一個管道,主要是建立相關 VFS 物件,
* 並將讀寫該管道的一對檔案描述符儲存在filedes[2]中。
* 不再使用管道時,只需關閉兩個檔案描述符即可。

返回值:
	成功返回:0
	失敗返回:-1,並且在error中存入錯誤碼

從管道中讀取資料

程式使用 read() 系統呼叫從管道中讀取資料,核心最終會呼叫 pipe_read() 函式來實現。

#include <unistd>
ssize_t read(int filedes, void *buf, size_t nbytes);

返回:若成功則返回讀到的位元組數,若已到檔案末尾則返回0,若出錯則返回-1
filedes:檔案描述符
buf:讀取資料快取區
nbytes:要讀取的位元組數

Linux2.6.10之前,每個管道僅有一個緩衝區(4KB);而在2.6.11之後,每個管道最多可有16個緩衝區(64KB)。

從管道中讀取資料有兩種方式:

  • 阻塞型讀取資料
    管道大小(管道緩衝區中待讀的位元組數)為p,使用者程式請求讀取n個位元組,則阻塞型讀取情況如表所示:
管道大小至少有一個寫程式沒有寫程式
p=01. 如果有睡眠寫程式,讀取n個位元組並返回n,當管道緩衝區為空時等待寫程式寫資料。 2. 如果沒有睡眠寫程式,等待寫程式寫資料,然後讀取資料返回0
0<p<n1. 有睡眠寫程式同上; 2. 無睡眠寫程式,讀取p個位元組並返回p,管道緩衝區中還剩0個位元組讀取p個位元組並返回p;管道緩衝區中還剩0個位元組。
p>=n讀取n個位元組,返回n,管道緩衝區中還剩 p-n 個位元組讀取n個位元組,返回n,管道緩衝區中還剩 p-n 個位元組

簡單總結一下,阻塞的情況主要是產生在管道大小p小於n的情況,如果待讀的位元組數p已經大於等於n也不至於阻塞。

  • 非阻塞型讀取資料
    非阻塞操作通常都是在open()系統呼叫中指定O_NONBLOCK(非阻塞方式)標誌進行請求,但這個方法不適合無名管道,因為它沒有open()操作,不過程式可以通過對相應的檔案描述符發出 fcntl() 系統呼叫來請求對管道執行非阻塞操作。在非阻塞情況下,如果管道大小p小於n,則讀取p個位元組並返回p,讀操作完成;否則讀取n個位元組並返回n,讀操作完成。

fcntl
檔案控制函式

檔案控制函式          fcntl -- file control
標頭檔案:
#include <unistd.h>
#include <fcntl.h>

函式原型:          
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);         
int fcntl(int fd, int cmd, struct flock *lock);

描述:
fcntl()針對(檔案)描述符提供控制.引數fd是被引數cmd操作(如下面的描述)的描述符.            
針對cmd的值,fcntl能夠接受第三個引數(arg)

fcntl函式有5種功能:
1.複製一個現有的描述符(cmd=F_DUPFD).
2.獲得/設定檔案描述符標記(cmd=F_GETFD或F_SETFD).
3.獲得/設定檔案狀態標記(cmd=F_GETFL或F_SETFL).
4.獲得/設定非同步I/O所有權(cmd=F_GETOWN或F_SETOWN).
5.獲得/設定記錄鎖(cmd=F_GETLK,F_SETLK或F_SETLKW).

F_SETFL     
設定給arg描述符狀態標誌,可以更改的幾個標誌是:O_APPEND, O_NONBLOCK,O_SYNC和O_ASYNC


向管道中寫入資料

程式使用 write() 系統呼叫向管道中寫入資料,核心最終會呼叫pipe_write() 函式來實現。

#include <unistd>
ssize_t write(int filedes, const void *buf, size_t nbytes);
 返回:若成功則返回寫入的位元組數,若出錯則返回-1
 filedes:檔案描述符
 buf:待寫入資料快取區
 nbytes:要寫入的位元組數

函式說明:write()會把引數buf所指的記憶體寫入count個位元組到引數放到所指的檔案內。
返回值:如果順利write()會返回實際寫入的位元組數。當有錯誤發生時則返回-1,錯誤程式碼存入errno中。

POSIX標準要求涉及少量位元組數(<=4096B)的寫操作必須“原子”地進行,更確切的說,如果兩個或多個程式併發地寫同一個管道,那麼任何不超過4096B(PIPE_BUF,管道緩衝區)的寫操作必須單獨完成,不能與其他程式的寫操作交叉進行。但是超過4096B的寫操作是可以分割的。

向管道中寫入資料也有兩種方式,阻塞型和非阻塞型。
若管道緩衝區還有u位元組空閒空間,程式請求寫入n個位元組

緩衝區剩餘空間至少有一個讀程式 、阻塞寫至少有一個讀程式、非阻塞寫沒有讀程式
u<n<=4096等待,直到n-u個位元組被讀出為止,寫入n個位元組並返回n返回-EAGAIN,提醒以後再寫寫入失敗,核心向寫程式傳送SIGPIPE訊號,並返回-EPIPE
n>4096寫入n個位元組(必要時等待)並返回n如果u>0,則寫入u位元組並返回u;否則,就返回-EAGAIN同上
u>=n寫入n個位元組並返回n同左同上

關閉管道

不再使用管道時,只需關閉兩個檔案描述符即可。
用close函式把兩個檔案描述符關閉。
close

int close(int fd);
fd就是之前用open()獲得的一個檔案描述符。

在核心中,開啟的檔案會被維護一個引用計數,每次close()會把檔案的引用計數減一,
引用計數減少到0的檔案才會從核心中釋放資源。

有名管道

無名管道應用的一個很大的限制是只能用於具有親緣關係的程式間通訊,而有名管道(named pipe或FIFO)克服了該限制。有名管道不同於無名管道之處在於它是有檔名的。以FIFO檔案形式真實地存在於磁碟上的檔案系統中。這樣即使與有名管道的建立程式不存在親緣關係的程式,只要可以訪問該檔案,就能夠彼此通過有名管道相互通訊,從而實現不相關程式間的資料交換。

此外,有名管道是一種雙向通訊管道,程式能以讀寫模式開啟一個有名管道檔案,但一般不建議這麼做,因為可能導致程式讀取自己寫入的資料。有名管道也嚴格遵循先進先出的原則,對管道的讀總是從開始處返回資料,對管道的寫則把資料新增到末尾。有名管道和無名管道都不支援諸如lseek()等檔案定位操作


建立有名管道:

mkfifo()

	int mkfifo(const char* pathname, mode_t mode);
	需要包含標頭檔案<sys/types.h><sys/stat.h>
	
引數:
	pathname是路徑名(含有名管道的檔名)
	mode是檔案的許可權

開啟有名管道:

open()
與普通檔案類似,有名管道在使用之前必須先進行open操作。
函式原型:

	int open(const char* pathname, int flags);
	需要包含標頭檔案<sys/types.h> <sys/stat.h> <fcntl.h>
	pathname是路徑名
	flags是開啟方式,有O_RDONLY O_WRONLY O_RDWR O_NONBLOCK(非阻塞方式)
	成功返回檔案描述符,失敗返回-1



POSIX通訊

POSIX : Portable Operating System Interface 可移植作業系統介面
IPC:Inter-Process Communication 程式間通訊

POSIX IPC 包括semaphore(訊號量)shared memory(共享記憶體)message queue(訊息佇列)


POSIX訊號量

用於程式間同步。
分為有名訊號量無名訊號量

Linux實現有名訊號量的方式是建立一個同名檔案,程式間通訊的載體就是該檔案。有名訊號量通過IPC名字進行程式間的同步。

無名訊號量又稱為基於記憶體的訊號量,常用於多執行緒間的同步,也可用於相關程式間的同步。記憶體訊號量沒有名字這種情況下,通訊的程式需要共享存放訊號量的記憶體。如果是多執行緒通訊,一個全域性變數即可。如果是程式間通訊,需要記憶體對映。

注意!!!
使用Posix訊號量時必須包含標頭檔案#include<semaphore.h>,
且在編譯程式的時候,應該加上 -pthread選項,
gcc -o test1 -pthread test1.c


有名訊號量

有名訊號量的特點是把訊號量的值儲存在檔案中,所以對於相關程式來說,子程式繼承了父程式的檔案描述符,自然共享了儲存在檔案中的有名訊號量。因此,可以使用有名訊號量實現相關程式間的同步。

可以通過sem_open建立,通過sem_unlink銷燬。
函式(使用者空間)的原型如下:
建立有名訊號量:

sem_open

開啟一個已經存在的有名訊號量,或建立並初始化一個有名訊號量,並將其引用計數加1
sem_t * sem_open(const char* name, int oflag)
sem_t * sem_open(const char* name, int oflag, mode_t mode, unsigned int value)

name : 有名訊號量的名稱,
       【在linux下,sem都是建立在/dev/shm目錄下,所以name不能寫路徑,直接"mysem"這樣就好】
oflag : 說明建立方式
       O_CREATE : 若name指定的訊號量不存在,則建立一個,且後面的mode和value引數必須有效;
                  若指定的訊號量已經存在,直接開啟,忽略後面兩個引數
       O_CREATE|O_EXCL : 若name指定的訊號量不存在,則建立一個;
       					 否則直接返回error
mode_t : 控制新建訊號量的訪問許可權,如0644
value : 新建訊號量的初始值   
返回值 : 成功時返回指向有名訊號量的指標,出錯時為SEM_FAILED(常量=2,表示失敗)

有名訊號量的刪除需要兩個步驟:
sem_close

1. 關閉訊號量
將訊號量引用計數-1,但並沒有刪除它,還是可以使用sem_open()開啟				
int sem_close(sem_t * sem)
>>>
sem : 指向欲關閉的訊號量的指標,即呼叫sem_open()的返回值
返回值 : 成功0,失敗-1
>>>

sem_unlink

2. 刪除訊號量
從系統中徹底刪除該訊號量,注意,sem_unlink()只對引用計數為0的訊號量有用,
對引用計數不為0的訊號量不會有任何作用
【因為這是計數型訊號量】
int sem_unlink(const char * name)
>>>
name : 有名訊號量的標示符
返回值 : 成功0,失敗-1
>>>

無名訊號量

無名訊號量本質上就是一塊足夠存放sem_t型別的變數的共享記憶體,不需要open,但必須使用sem_init初始化,釋放記憶體前需要呼叫sem_destroy銷燬。


訊號量的使用

對訊號量的使用上,兩種方式並無差別,使用sem_post釋放訊號量,sem_wait獲取訊號量。

sem_post

int sem_post(sem_t *sem);
功能:釋放資源操作,將指定訊號量的值加1,若有執行緒/程式在等待,則會喚醒其中的一個執行緒/程式
引數:sem 訊號量的名稱
返回值:成功0,錯誤-1並且不改訊號量的值

sem_wait

int sem_wait(sem_t *sem);
功能:阻塞型申請資源操作:測試訊號量sem的值,若大於0,則將sem的值減1後返回;若等於0,
則呼叫執行緒/程式會進入阻塞狀態直到另一個相關執行緒/程式執行sem_post, ……解除阻塞,然後
立即將sem減1,然後返回。
引數:sem 指定的訊號量名稱
返回值:函式執行成功返回0;錯誤時返回-1,訊號量的值不改動。

補充說明
sem 是訊號量名稱,無名有名都有
name 是有名訊號量名稱,就有名有,其實是檔案路徑


IPC訊息佇列

在IPC訊息佇列通訊機制中,若干個程式可以共享一個訊息佇列,系統允許其中的一個或多個程式向訊息佇列寫入訊息,同時也允許一個或多個程式從訊息佇列中讀取訊息,從而完成程式之間的資訊交換,這種通訊機制被稱為訊息佇列通訊機制。
訊息佇列服務通訊機制是客戶\伺服器模型中常用的程式通訊方式:客戶向伺服器傳送請求資訊,伺服器讀取訊息並執行相應的請求。訊息可以是命令也可以是資料。

相關的資料結構:

訊息緩衝區
msgbuf,IPC機制中的訊息緩衝區是由固定大小的首部和可變長度的正文組成,系統只是給出了緩衝區的基本定義模板。程式設計師可以根據這個重新定義。
該結構的第一個成員mtype必須是一個大於0的長整數,表示對應訊息的型別,以允許程式有選擇地從訊息佇列中獲取訊息。
可以像下面這樣自定義:

struct my_msgbuf{
   long mtype;
   long sender_id, receiver_id;
   char mtext[1024];
}

Linux沒有限定mtext的型別,但是限定了最大長度MSGMAX(一個巨集 常量 8192 不同版本值不一定一樣)。

訊息結構
訊息列表中的每個訊息節點由msg_msg結構來描述,定義在include/linux/msg.h檔案中。

m_list 指向訊息佇列中的下一條訊息
m_type 訊息型別
m_ts 訊息正文的大小
next 訊息的下一部分

每條訊息分開存放在一個或多個動態分配的記憶體頁中,第一頁起始部分存放訊息頭,即上面的msg_msg結構體,之後緊接著存放訊息正文。如果訊息正文超出4072B(第一頁剩下空間),就繼續存放在第二頁,其地址存放在msg_msg結構中的next欄位中;第二頁以msg_msgseg結構體開始,該結構體只有一個成員:next指標,指向可選的第三頁。

訊息佇列結構
msg_queue:系統中每個訊息佇列由一個msg_queue結構描述,定義在include/linux/msg.h檔案中,,,不過這個結構體在ipc/msg.c檔案裡找到

Linux為避免資源耗盡,給出了幾個限制:IPC訊息佇列數最多為16個,每個訊息大小最大為8192B,一個訊息佇列中全部訊息大小最大為16384B。不過系統管理員可以通過修改/proc/sys/kernel路徑下的msgmni檔案、msgmax檔案及msgmnb檔案來調整這些值。


IPC訊息佇列相關的系統呼叫
<sys/types.h> <sys/ipc.h> <sys/msg.h>

msgget
建立訊息佇列

int msgget(key_t key,int msgflg);

引數:
key: 訊息佇列的鍵值,若為0(IPC_PRIVATE),則建立一個新的訊息佇列;
若大於0(通常是通過ftok()函式生成的),則進一步依據msgflag引數確定本函式的行為

msgflg: 對訊息佇列的訪問許可權和控制命令的組合。
訪問許可權用三個八進位制整數分別表示屬主、同組使用者和其他使用者的許可權
IPC_CREAT:如果key對應的訊息佇列不存在,則建立它;
如果已經存在,則返回其識別符號
IPC_EXCL|IPC_CREATE:如果key對應的訊息佇列不存在,則建立它;
如果已經存在,則出錯返回-1

功能:如果引數msgflag為IPC_CREATE,則msgget()新建立一個訊息佇列,
並返回其識別符號。
或者返回具有相同鍵值的已存在的訊息佇列的識別符號……

msgsnd
向訊息佇列傳送訊息

int msgsnd(int msqid, struct msgbuf* msgp, size_t msgsz, int msgflg);
引數:
msqid: 訊息佇列的識別符號
msgp: 存放欲傳送訊息內容的訊息緩衝區指標
msgsz: 訊息正文(而非整個訊息結構)的長度
msgflg: 傳送標誌
* 0:訊息佇列滿時,呼叫程式(傳送程式)將會阻塞,直到訊息佇列可寫入該訊息
* IPC_NOWAIT: 訊息佇列滿時,呼叫程式立即返回-1
* MSG_NOERROR: 訊息正文長度超過msgsz時,不報錯,而是直接截去多餘的部分,
並只將前面的msgsz位元組傳送出去

msgrcv
從訊息佇列接收訊息

int msgrcv(int msqid, struct msgbuf* msgp, size_t msgsz, long msgtyp, int msgflg);
引數: 
msqid: 訊息佇列的識別符號
msgp: 存放欲傳送訊息內容的訊息緩衝區指標
msgsz: 訊息正文(而非整個訊息結構)的長度
msgtyp: 接收的訊息型別
* 0: 接收訊息佇列中的第一個訊息
* >0: 接收第一個型別為msgtyp的訊息
* <0: 接收第一個型別小於等於msgtyp的絕對值的訊息
msgflg: 接收訊息時的標誌
* 0: 沒有可以接收的訊息時,呼叫程式(接收程式)阻塞
* IPC_NOWAIT: 沒有可以接收的訊息時,立即返回-1
* MSG_EXCEPT: 返回第一個型別不是msgtyp的訊息
* MSG_NOERROR: 訊息正文長度超過msgsz位元組時,將直接擷取多餘的部分

返回值: 接收成功,返回實際接收到訊息正文的位元組數; 否則返回-1

msgctl
獲取或設定訊息佇列的屬性資訊,或刪除訊息佇列

int msgctl(int msqid, int cmd, struct msqid_ds * buf);
引數: 
msqid: 訊息佇列的識別符號
cmd: 將要在訊息佇列上執行的命令,包括IPC_STAT、IPC_SET和IPC_RMID
IPC_RMID是最常用的,刪除訊息佇列,並且喚醒該訊息佇列上等待讀或等待寫的程式。
呼叫者必須有相應的許可權。
buf: 使用者空間中的一個快取,接收或提供狀態資訊

返回值: 執行成功返回0; 否則返回-1

補充說明
Linux的訊息佇列(queue)實質上是一個連結串列, 它有訊息佇列識別符號(queue ID). msgget建立一個新佇列或開啟一個存在的佇列; msgsnd向佇列末端新增一條新訊息; msgrcv從佇列中取訊息, 取訊息是不一定遵循先進先出的, 也可以按訊息的型別欄位取訊息.

識別符號(des)和鍵(key):

訊息佇列, 訊號量和共享儲存段, 都屬於核心中的IPC結構, 它們都用識別符號來描述. 這個識別符號是一個非負整數, 與檔案描述符不同的是, 建立時並不會重複利用通過刪除回收的整數, 而是每次+1, 直到整數最大值迴轉到0.

識別符號是IPC物件的內部名, 而它的外部名則是key(鍵), 它的基本型別是key_t, 在標頭檔案<sys/types.h>中定義為長整型. 鍵由核心變換成識別符號.




IPC共享記憶體通訊

共享記憶體實際上是一段特殊的記憶體區域,它可以被兩個或以上的程式對映到自身的地址空間中,就好像它是由C中的malloc()分配的記憶體一樣。一個程式寫入共享記憶體中的資訊,可以被其他使用這個共享記憶體的程式讀出,從而實現了程式間的通訊。
這塊虛擬共享記憶體的頁面在每一個共享它的程式的頁表中都有頁表項引用,但是不需要在所有程式的虛擬記憶體中都有相同的地址。
Linux系統並沒有為共享記憶體機制提供同步機制,程式設計師需要使用同步機制保證程式間的同步關係。

系統為每個共享記憶體區都設定了一個shmid_kernel結構,用來描述該共享記憶體區的屬性及使用資訊。

struct shmid_kernel{

struct kern_ipc_perm shm_perm; //共享記憶體區的kern_ipc_perm結構
struct file* shm_file; //共享記憶體區的特殊檔案
unsigned long shm_nattch; //共享記憶體區當前的共享計數
unsigned long shm_segsz; //共享記憶體區位元組數
time_t shm_atim; //最後訪問時間
time_t shm_dtim; //最後分離時間
time_t shm_ctim; //最後修改時間
pid_t shm_cprid; //建立者的PID
pid_t shm_lprid; //最後訪問程式的PID
struct user_struct* mlock_user; //使用共享記憶體區的使用者的user_struct指標

}

共享記憶體機制的相關係統呼叫
Linux系統中的每個程式,都有很大的虛擬地址空間,其中有一部分放著程式碼、資料、堆和堆疊,剩餘部分在初始化時是空閒的。一塊共享記憶體一旦被連結attach,就會被對映到程式空閒的虛擬地址空間中。隨後,程式就可以像對待普通記憶體區域那樣讀、寫共享記憶體。

共享記憶體有四個相關係統呼叫,使用時需要包含以下兩個標頭檔案:
<sys/ipc.h>以及<sys/shm.h>

shmget

int shmget(key_t key, int size, int shmflg);
引數:
key: 標識共享記憶體的鍵值,可以是0(IPC_PRIVATE)或大於0(通常是由ftok()生成)
size: 所需共享記憶體的最小尺寸(以位元組為單位)
shmflg: 共享記憶體的建立方式標誌
IPC_CREAT  如果key對應的共享記憶體不存在,則建立它;如果已經存在則返回其識別符號;
IPC_CREAT|IPC_EXCL 如果key對應的共享記憶體不存在,則建立它;如果已經存在則出錯返回-1

功能:
建立一塊共享記憶體;若已經存在,則返回其識別符號

返回值:
若成功,則返回共享記憶體的識別符號;否則返回-1

shmat

void *shmat(int shmid, const void * shmaddr, int shmgflg);
引數:
shmid : 共享記憶體的識別符號
shmaddr : 指定共享記憶體對映到程式虛擬地址空間的位置,
若設定為NULL0,則讓系統確定一個合適的地址位置
shmflg : 程式對共享記憶體的讀寫屬性,SHM_RDONLY為只讀模式,其他為讀寫模式。

功能:
把指定共享記憶體區對映到呼叫程式的虛擬地址空間。若成功則返回對映的起始地址,
並對shmid_kernel結構中的共享計數shm_nattch加1.

返回值:若成功,則返回已對映到的起始地址;否則返回-1

shmdt

int shmdt(const void* shmaddr);
引數:shmaddr表示欲斷開對映的共享記憶體的起始地址。

功能:斷開共享記憶體在呼叫程式中的對映,禁止本程式訪問此共享記憶體。若成功,
則會對shmid_kernel結構中共享計數shm_nattch減1,
當shm_nattch為0時,系統才真正刪除該共享記憶體。

返回值:若成功,則返回0,否則-1

shmctl

int shmctl(int shmid, int cmd, struct shmid_ds * buff);
引數:
shmid:欲處理的共享記憶體的識別符號
cmd: 要進行的操作,包括IPC_STAT(獲取狀態)IPC_SET(設定狀態)IPC_RMID(刪除共享記憶體)。
最常用的是IPC_RMID,實際操作是把共享記憶體置為刪除標記,當共享計數shm_nattch為0時,才真正刪除。
buf: 使用者空間中的一個快取,接收或提供狀態資訊。

功能:獲取或設定共享記憶體的屬性資訊或銷燬一塊共享記憶體。

返回值:若成功,則返回0,否則返回-1.

相關文章