IPC的方式通常有管道(包括無名管道和命名管道(FIFO))、消息隊列、信號量、、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。
1.管道,通常指無名管道,是 UNIX 系統IPC最古老的形式。
它是半雙工的(即數據只能在一個方向上流動),具有固定的讀端和寫端。
它只能用於具有親緣關係的進程之間的通信(也是父子進程或者兄弟進程之間--有相同的祖先)。
它可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的read、write 等函數。但是它不是普通的文件,並不屬於其他任何文件系統,並且只存在於內存中。
FIFO沒有第二個限制;unix域臺階自沒有上述兩個限制。
#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1
當一個管道建立時,它會創建兩個文件描述符:fd[0]爲讀而打開,fd[1]爲寫而打開。單個進程中的管道幾乎沒有任何用處。所以,通常調用 pipe 的進程接着調用 fork,這樣就創建了父進程與子進程之間的 IPC 通道。
若要數據流從父進程流向子進程,則關閉父進程的讀端(fd[0])與子進程的寫端(fd[1]);反之,則可以使數據流從子進程流向父進程。
#include<stdio.h>
#include<unistd.h>
int main()
{
int fd[2]; // 兩個文件描述符
pid_t pid;
char buff[20];
if(pipe(fd) < 0) // 創建管道
printf("Create Pipe Error!\n");
if((pid = fork()) < 0) // 創建子進程
printf("Fork Error!\n");
else if(pid > 0) // 父進程
{
close(fd[0]); // 關閉讀端
write(fd[1], "hello world\n", 12);
}
else
{
close(fd[1]); // 關閉寫端
read(fd[0], buff, 20);
printf("%s", buff);
}
return 0;
}
2. FIFO,也稱爲命名管道,它是一種文件類型。
特點
1.FIFO可以在無關的進程之間交換數據,與無名管道不同。
2.FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中。
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);// 返回值:成功返回0,出錯返回-1
其中的 mode 參數與open函數中的 mode 相同。一旦創建了一個 FIFO,就可以用一般的文件I/O函數操作它。
當 open 一個FIFO時,是否設置非阻塞標誌(O_NONBLOCK)的區別:
1 . 若沒有指定O_NONBLOCK(默認),只讀 open 要阻塞到某個其他進程爲寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個其他進程爲讀而打開它。
2 . 若指定了O_NONBLOCK,則只讀 open 立即返回。而只寫 open 將出錯返回 -1 ;如果沒有進程已經爲讀而打開該 FIFO,其errno置ENXIO。
FIFO的通信方式類似於在進程中使用文件來傳輸數據,只不過FIFO類型文件同時具有管道的特性。在數據讀出時,FIFO管道中同時清除數據,並且“先進先出”。下面的例子演示了使用 FIFO 進行 IPC 的過程:
write_fifo.c
#include<stdio.h>
#include<stdlib.h> // exit
#include<fcntl.h> // O_WRONLY
#include<sys/stat.h>
#include<time.h> // time
int main()
{
int fd;
int n, i;
char buf[1024];
time_t tp;
printf("I am %d process.\n", getpid()); // 說明進程ID
if((fd = open("fifo1", O_WRONLY)) < 0) // 以寫打開一個FIFO
{
perror("Open FIFO Failed");
exit(1);
}
for(i=0; i<10; ++i)
{
time(&tp); // 取系統當前時間
n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
printf("Send message: %s", buf); // 打印
if(write(fd, buf, n+1) < 0) // 寫入到FIFO中
{
perror("Write FIFO Failed");
close(fd);
exit(1);
}
sleep(1); // 休眠1秒
}
close(fd); // 關閉FIFO文件
return 0;
}
read_fifo.c
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
int main()
{
int fd;
int len;
char buf[1024];
if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 創建FIFO管道,我在這!!!
perror("Create FIFO Failed");
if((fd = open("fifo1", O_RDONLY)) < 0) // 以讀打開FIFO
{
perror("Open FIFO Failed");
exit(1);
}
while((len = read(fd, buf, 1024)) > 0) // 讀取FIFO管道
printf("Read message: %s", buf);
close(fd); // 關閉FIFO文件
return 0;
}
在兩個終端裏用 gcc 分別編譯運行上面兩個文件,可以看到輸出結果。
上述例子可以擴展成:客戶進程—服務器進程通信的實例,write_fifo的作用類似於客戶端,可以打開多個客戶端向一個服務器發送請求信息,read_fifo類似於服務器,它適時監控着FIFO的讀端,當有數據時,讀出並進行處理,但是有一個關鍵的問題是,每一個客戶端必須預先知道服務器提供的FIFO接口.
3 .消息隊列:是消息的鏈接表,存放在內核中;一個消息隊列由一個標識符(即隊列ID)來標識。
特點
(1)消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。
(2)消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。
消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
#include <sys/msg.h>
// 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
int msgget(key_t key, int flag);
// 添加消息:成功返回0,失敗返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 讀取消息:成功返回消息數據的長度,失敗返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
// 控制消息隊列:成功返回0,失敗返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
在以下兩種情況下,msgget將創建一個新的消息隊列:
如果沒有與鍵值key相對應的消息隊列,並且flag中包含了IPC_CREAT標誌位。
key參數爲IPC_PRIVATE。
函數msgrcv在讀取消息隊列時,type參數有下面幾種情況:
(1)type == 0,返回隊列中的第一個消息;
(2) type > 0,返回隊列中消息類型爲 type 的第一個消息;
(3) type < 0,返回隊列中消息類型值小於或等於 type 絕對值的消息,如果有多個,則取類型值最小的消息。
例子:
下面寫了一個簡單的使用消息隊列進行IPC的例子,服務端程序一直在等待特定類型的消息,當收到該類型的消息以後,發送另一種特定類型的消息作爲反饋,客戶端讀取該反饋並打印出來。
msg_server.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
// 用於創建一個唯一的key
#define MSG_FILE "/etc/passwd"
// 消息結構
struct msg_form {
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 獲取key值
if((key = ftok(MSG_FILE,'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 打印key值
printf("Message Queue - Server key is: %d.\n", key);
// 創建消息隊列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 打印消息隊列ID及進程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());
// 循環讀取消息
for(;;)
{
msgrcv(msqid, &msg, 256, 888, 0);// 返回類型爲888的第一個消息
printf("Server: receive msg.mtext is: %s.\n", msg.mtext);
printf("Server: receive msg.mtype is: %d.\n", msg.mtype);
msg.mtype = 999; // 客戶端接收的消息類型
sprintf(msg.mtext, "hello, I'm server %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
}
return 0;
}
msg_client.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
// 用於創建一個唯一的key
#define MSG_FILE "/etc/passwd"
// 消息結構
struct msg_form {
long mtype;
char mtext[256];
};
int main()
{
int msqid;
key_t key;
struct msg_form msg;
// 獲取key值
if ((key = ftok(MSG_FILE, 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 打印key值
printf("Message Queue - Client key is: %d.\n", key);
// 打開消息隊列
if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
// 打印消息隊列ID及進程ID
printf("My msqid is: %d.\n", msqid);
printf("My pid is: %d.\n", getpid());
// 添加消息,類型爲888
msg.mtype = 888;
sprintf(msg.mtext, "hello, I'm client %d", getpid());
msgsnd(msqid, &msg, sizeof(msg.mtext), 0);
// 讀取類型爲777的消息
msgrcv(msqid, &msg, 256, 999, 0);
printf("Client: receive msg.mtext is: %s.\n", msg.mtext);
printf("Client: receive msg.mtype is: %d.\n", msg.mtype);
return 0;
}
4. 信號量
信號量(semaphore)與已經介紹過的 IPC 結構不同,它是一個計數器。信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
特點:
信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。
每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
支持信號量組。
最簡單的信號量是隻能取 0 和 1 的變量,這也是信號量最常見的一種形式,叫做二值信號量(Binary Semaphore)。而可以取多個正整數的信號量被稱爲通用信號量。
Linux 下的信號量函數都是在通用的信號量數組上進行操作,而不是在一個單一的二值信號量上進行操作。
#include <sys/sem.h>
// 創建或獲取一個信號量組:若成功返回信號量集ID,失敗返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 對信號量組進行操作,改變信號量的值:成功返回0,失敗返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信號量的相關信息
int semctl(int semid, int sem_num, int cmd, ...);
當semget創建新的信號量集合時,必須指定集合中信號量的個數(即num_sems),通常爲1; 如果是引用一個現有的集合,則將num_sems指定爲 0 。
struct sembuf
{
short sem_num; // 信號量組中對應的序號,0~sem_nums-1
short sem_op; // 信號量值在一次操作中的改變量
short sem_flg; // IPC_NOWAIT, SEM_UNDO
}
其中 sem_op 是一次操作中的信號量的改變量:
若sem_op > 0,表示進程釋放相應的資源數,將 sem_op 的值加到信號量的值上。如果有進程正在休眠等待此信號量,則換行它們。
若sem_op < 0,請求 sem_op 的絕對值的資源。
5. 共享內存
共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。
特點:
共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。
因爲多個進程可以同時操作,所以需要進行同步。
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
#include <sys/shm.h>
// 創建或獲取一個共享內存:成功返回共享內存ID,失敗返回-1
int shmget(key_t key, size_t size, int flag);
// 連接共享內存到當前進程的地址空間:成功返回指向共享內存的指針,失敗返回-1
void *shmat(int shm_id, const void *addr, int flag);
// 斷開與共享內存的連接:成功返回0,失敗返回-1
int shmdt(void *addr);
// 控制共享內存的相關信息:成功返回0,失敗返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
當用shmget函數創建一段共享內存時,必須指定其 size;而如果引用一個已存在的共享內存,則將 size 指定爲0 。
當一段共享內存被創建以後,它並不能被任何進程訪問。必須使用shmat函數連接該共享內存到當前進程的地址空間,連接成功後把共享內存區對象映射到調用進程的地址空間,隨後可像本地空間一樣訪問。
shmdt函數是用來斷開shmat建立的連接的。注意,這並不是從系統中刪除該共享內存,只是當前進程不能再訪問該共享內存而已。
shmctl函數可以對共享內存執行多種操作,根據參數 cmd 執行相應的操作。常用的是IPC_RMID(從系統中刪除該共享內存)。
小結:
(1)管道(Pipe):管道可用於具有親緣關係進程間的通信,允許一個進程和另一個與它有共同祖先的進程之間進行通信。
(2)命名管道(named pipe):命名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信。命名管道在文件系統中有對應的文件名。命名管道通過命令mkfifo或系統調用mkfifo來創建。
(3)消息(Message)隊列:消息隊列是消息的鏈接表,包括Posix消息隊列system V消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺
(4)信號量(semaphore):主要作爲進程間以及同一進程不同線程之間的同步手段。
(5)共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
(3)、(4)和(5)經常一起使用
(6)內存映射(mapped memory):內存映射允許任何多個進程間通信,每一個使用該機制的進程通過把一個共享的文件映射到自己的進程地址空間來實現它。
(7)套接口(Socket):更爲一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支持套接字。