在上一篇博客中,我們已經熟悉並使用了匿名管道,這篇博客我們將講述進程間通信另外兩種常見方式——命名管道與共享內存。
1.命名管道
管道是使用文件的方式,進行進程之間的通信。因此對於管道的操作,實際上還是用諸如write,read等接口實現。
匿名管道應用的一個限制就是只能在具有親緣關系(如父進程與子進程、兄弟進程)之間進行通信。如果想在不相關的進程間進行數據交換,可以使用FIFO文件來做這種工作。
這里的FIFO文件即我們所說的命名管道。必須強調的是,雖然FIFO是一種文件,但實際上數據的讀寫都是在操作系統開辟的內存緩沖區中進行的,並不會真的寫入磁盤中。如果那樣,進程間通信的效率將會極大降低!
1.1 創建命名管道
1.1.1 使用命令行創建
在命令行中使用mkfifo 管道名
的方式來創建命名管道。如下圖
可以看到文件類型為p,即管道類型的文件。文件大小為0,即便寫入到pipe中的數據沒有被另一個進程讀出,依然是0!因為根本不會將數據寫入到磁盤中。
下例中我們將hello world重定向到pipe中,並且從pipe中讀出數據顯示到屏幕。使用兩個shell,進入到同一個目錄下,一個在命令行輸入echo hello world >pipe
,另一個輸入 cat < pipe
, 可以看到在另一個shell的屏幕上出現了hello world。
1.1.2 使用接口創建
在命令行輸入man 3 mkfifo
后可以看到如下內容:
該接口一共有兩個參數,其中第一個創建的fifo文件的路徑,第二個是文件權限,與我們之前學習的文件操作的權限一模一樣。返回值如果等於0則創建成功,-1則創建失敗。
1.2 匿名管道和命名管道的區別
- 匿名管道由pipe函數創建並打開。
- 命名管道由mkfifo函數創建,打開用open。
- FIFO(命名管道)與pipe(匿名管道)之間唯一的區別在它們創建與打開的方式不同,一但這些工作完成之后,它們具有相同的語義。其余操方式與文件的操作沒有區別。
2. system V共享內存
2.1 什么是共享內存?
共享內存是最快的IPC形式,是系統在內存中開辟一塊內存,用於進程之間共享地進行操作。這片內存區域經過頁表映射到通信進程各自的地址空間中,不同的進程可以通過操作自己的地址空間,來操作共享內存。如下圖所示:
2.2 共享內存函數
共享內存的創建及銷毀一般分為以下幾步:
- 操作系統在內存中申請一片內存
- 將共享內存掛接到進程地址空間中
- 使用完畢后,將共享內存和進程地址空間去關聯
- 操作系統銷毀共享內存
因此,操作系統提供了以下幾組接口:
2.2.1 ftok函數
key_t ftok(const char *pathname, int proj_id);
該函數有兩個參數,第一個是路徑名稱,第二個是項目id。兩者配合用於創建唯一的key值,該值可以在系統內標定唯一的通信資源。(為什么這里不說標定唯一的共享內存,是因為system v包含共享內存,消息隊列,信號量三種通信方式。三種通信方式都是使用ftok函數創建唯一的key來標定唯一性的。)
2.2.2 shmget函數
int shmget(key_t key, size_t size, int shmflg);
該函數用來創建共享內存。有三個參數:
key即系統層面上這個共享內存段名字,就是我們用ftok獲取的唯一值。
size是共享內存大小,一般是頁的整數倍。(一個page是4096 bytes
shmflg是由九個權限標志構成的,它們的用法和創建文件時使用的mode模式標志是一樣的。IPC_CREATE 的用法是如果key標定的共享內存不存在則創建,若存在則直接使用。IPC_EXCL一般要配合IPC_CREATE使用,即key標定的共享內存不存在則創建,若存在則報錯,常用於申請開辟共享內存的一端。除此之外,在創建共享內存時,還應該標定該共享內存的權限(與文件中設置權限如出一轍),如0664等。
返回值:成功返回一個非負整數,即該共享內存段的標識碼(注意,該標識碼是用戶層面的標識碼,簡單,方便用戶使用!而key是系統層面的!;失敗返回-1。)
2.2.3 shmat函數
void* shmat(int shmid, const void* shmaddr, int shmflg);
該函數用於將共享內存掛接到進程地址空間中。
shmid參數是使用shmget獲取的用戶層面的共享內存標識符。
shmaddr參數是掛接到進程地址空間中的起始虛擬地址,用於一般不關心,設置為NULL,讓操作系統自己選擇分配。
shmflg參數:0即對該共享內存有讀寫權限,另一個SHM_RDONLY即對該共享內存有只讀權限。
返回值:成功返回一個指針,指向共享內存起始地址;失敗返回-1。
2.2.4 shmdt函數
int shmdt (const void* shmaddr);
該函數用於將共享內存與當前進程去關聯。
唯一的一個參數shmaddr是由shmat所返回的指針。
去關聯成功則返回0,失敗則返回-1。
注意:去關聯與銷毀共享內存是完全不同的兩個概念!
2.2.5 shmctl函數
int shmctl(int shmid, int cmd, struct shmid_ds* buf);
功能:用於控制共享內存(常用於銷毀共享內存)
第一個參數shmid是shmget函數返回的共享內存標識符,cmd有三個可以采取的動作,常使用IPC_RMID來銷毀共享內存,buf指向一個保存着共享內存的模式狀態和訪問權的數據結構,如果是為了銷毀該共享內存,直接置為NULL。
成功返回0,失敗返回-1。
3.共享內存的數據結構
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
最前面的 struct ipc_perm shm_perm
是system v的三種通信方式共有的,該結構體內就有key值。
unsigned short shm_nattch;
這個字段即掛接該共享內存的進程數。
4.使用共享內存的例子
4.1實例代碼
Makefile:
.PHONY: all
all: server client
client: client.c comm.c
gcc -o $@ $^
server: server.c comm.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f client server
comm.h
#ifndef COMM_H
#define COMM_H
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0x66
int createShm(int size);
int destroyShm(int shmid);
int getShm(int size);
#endif
comm.c
#include "comm.h"
static int commShm(int size, int flags)
{
key_t _key = ftok(PATHNAME, PROJ_ID);
if(_key < 0)
{
perror("ftok");
return -1;
}
int shmid = shmget(_key, size, flags);
if(shmid < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int destroyShm(int shmid)
{
if(shmctl(shmid, IPC_RMID, NULL) < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
int createShm(int size)
{
return commShm(size, IPC_CREAT|IPC_EXCL|0666);
}
//creatShm是server端創建shm,getShm是client端獲取創建好的shm
int getShm(int size)
{
return commShm(size, IPC_CREAT);
}
server.c
#include "comm.h"
int main()
{
int shmid = createShm(4096);
char* addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
while(1)
{
printf("client# %s\n",addr);
sleep(1);
}
shmdt(addr);
sleep(2);
destroyShm(shmid);
return 0;
}
client.c
#include "comm.h"
int main()
{
int shmid = getShm(4096);
sleep(1);
char *addr = (char*)shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(1)
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
return 0;
}
4.2 效果展示
在./server運行之前,在命令行輸入ipcs -m命令,查看此時共享內存情況。發現系統中此時沒有共享內存。
./server運行后,此時可以看到nattach變為1,即有一個進程掛接到該共享內存。
運行./client后,此時nattch變為2,即此時有兩個進程掛接到該共享內存。
在./client端ctrl+C之后,發現./server端還在打印,此時打印的數據不再改變,因為沒有進程向共享內存中寫入數據。此時nattch變為1。
在關閉./server之后,發現nattch變為1。但是,標識符為5的共享內存仍然存在!!也就是說,共享內存的生命周期隨內核!!而不是隨進程!!
4.3 共享內存的特性
通過4.2中的例子,我們可以得到共享內存的以下特性:
1.共享內存的生命周期隨內存。
2.系統層面是用key來標識共享內存的,而用戶層面是通過shmid來進行標識,且shmid比key要簡單得多。
3.可以使用ipcs -m
命令來查看共享內存的狀態,使用ipcrm -m +shmid
來刪除共享內存。
4.刪除共享內存是銷毀內存中的內存空間,而去關聯實際上是刪除進程頁表中進程地址空間和對應共享內存的映射關系。
5.如果在刪除的時候,nattch字段為0,那么內核中描述共享內存的結構體也被釋放了。如果在刪除的時候,nattch字段不為0,那么key會變為0x00000000.表示當前共享內存不能被其他進程掛接,共享內存的status變為destroy。如下圖所示:在client運行過程中使用ipcrm -m 6刪除6號共享內存,再使用ipcs -m查看,此時key為0x00000000,status為dest。
在共享內存status為dest時,一旦該共享內存的進程掛接數為0,共享內存將會被立即銷毀。如下圖所示。在./client被crtl C后,使用ipcs -m命令,此時系統中再無共享內存。
6.共享內存不提供同步與互斥機制(回想一下,管道是否提供)。
7.共享內存是最快的進程通信方式。因為共享內存寫是覆蓋寫的方式,讀是直接訪問地址。而我們之前所學習的管道,需要通過系統調用(如write,read)來進行數據的讀寫,相比之下,共享內存的數據拷貝次數更少,因此效率也會更高。
3.其他進程通信方式
除了共享內存,system v還提供了消息隊列和信號量兩種進程通信方式,其中消息隊列還是為了實現進程間的數據交換,而信號量主要是用於實現進程之間的同步與互斥機制。
3.1 system V消息隊列
消息隊列提供了一個從一個進程向另外一個進程發送一塊數據的方法。
每個數據塊都被認為是有一個類型,接收者進程接收的數據塊可以有不同的類型值。
IPC資源必須刪除,否則不會自動清除,除非重啟,所以system V IPC資源的生命周期隨內核。
3.2 system V信號量
信號量主要用於同步和互斥的。
由於各進程要求共享資源,而且有些資源需要互斥使用,因此各進程間競爭使用這些資源,進程的這種關系為進程的互斥。
系統中某些資源一次只允許一個進程使用,稱這樣的資源為臨界資源或互斥資源。在進程中涉及到互斥資源的程序段叫臨界區。
關於同步與互斥,我們將在多線程部分重點學習。