進程間的5種通信方式詳細介紹
進程間通信(IPC,InterProcess Communication)是指在不同進程之間傳播或交換信息。
IPC的方式通常有管道(包括無名管道和命名管道)、消息隊列、信號量、共享存儲、Socket、Streams等。其中 Socket和Streams支持不同主機上的兩個進程IPC。
一、管道
無名管道
特點:
- 半雙工通信,即數據流只能在一個方向上流動
- 親緣進程之前的通信
- 特殊的一種文件,不屬於任何文件系統,只存在於在內存中
包含頭文件:#include <unistd.h>
int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1
當一個管道建立時,它會創建兩個文件描述符:fd[0]爲讀而打開,fd[1]爲寫而打開。
函數名: ctime
功 能: 把日期和時間轉換爲字符串
用 法: char *ctime(const time_t *time);
(1)使用fork()之後的半雙工通信管道
(2)從父進程到子進程的通信管道
我們做個實例如下:
#include<stdio.h>
#include<unistd.h>
int main()
{
int f[2]; // 兩個文件描述符
pid_t pid;
char buff[20];
if(pipe(f) < 0) // 創建管道
printf("Create Pipe Error!\n");
if((pid = fork()) < 0) // 創建子進程
printf("Fork Error!\n");
else if(pid > 0) // 父進程
{
close(f[0]); // 關閉讀端
write(f[1], "hello world\n", 12);
}
else
{
close(f[1]); // 關閉寫端
read(f[0], buff, 20);
printf("%s", buff);
}
return 0;
}
二、FIFO命名管道
1、FIFO也稱命名管道,是一種特殊文件類型
2、命名管道的打開規則
(1)只讀且阻塞方式
open(const char *pathname, O_RDONLY);
(2)只讀且非阻塞方式
open(const char *pathname, O_RDONLY | O_NONBLOCK);
(3)只寫且阻塞方式
open(const char *pathname, O_WRONLY);
(4)只寫且非阻塞方式
open(const char *pathname, O_WRONLY | O_NONBLOCK);
3、特點
(1)可在無關進程間通信
(2)關聯路徑名,是一種存在於文件系統中的設備文件
4、實例如下:
(1)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;
pid_t pid;
char buf[1024];
printf("my pid is %d\n",getpid());
if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 創建FIFO管道
perror("Create FIFO Failed");
printf("xixi\n");
if((fd = open("fifo1", O_RDONLY)) < 0) // 以讀打開FIFO
{
perror("Open FIFO Failed");
exit(1);
}
printf("zixiniloveyou\n");
len = read(fd,buf,1024);
printf("len = %d\n",len);
while((len = read(fd, buf, 1024)) > 0) // 讀取FIFO管道
printf("Read message: %s", buf);
close(fd); // 關閉FIFO文件
return 0;
}
(2)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;
}
首先,爲了看到結果,我們要打開兩個終端,在兩個終端上對這兩個C文件進行編譯和執行,結果如下:
首先執行read_fifo.c,你會發現,進程一直處於阻塞狀態,這和上文中第2點命名管道的打開規則有關,請看這行代碼fd = open(“fifo1”, O_RDONLY),此行代碼規定了命名管道的打開規則是隻讀阻塞方式,就是說該命名管道是隻讀的,一旦打開了該管道,如果管道沒有內容的話就會就會處於阻塞狀態。
如果我們將規則設置爲fd = open(“fifo1”, O_RDONLY|O_NONBLOCK),即使命名管道沒有內容,該進程也會正常結束。
我們在另一個終端上執行write_fifo.c,結果如下:
你會發現,與此同時的read_fifo.c文件所在的終端也運行起來了:
而上述這種實例我們可以擴展爲服務器進程與客戶進程通信,read_fifo是服務器端,write_fifo是客戶端,服務器端實時監控着用戶端,當有數據時,讀出並處理。
三、消息隊列
1 、消息隊列提供了一種從一個進程向另一個進程發送一個數據塊的方法。 每個數據塊都被認爲含有一個類型,接收進程可以獨立地接收含有不同類型的數據結構。我們可以通過發送消息來避免命名管道的同步和阻塞問題。但是消息隊列與命名管道一樣,每個數據塊都有一個最大長度的限制
2、消息隊列跟命名管道有不少的相同之處,通過與命名管道一樣,消息隊列進行通信的進程可以是不相關的進程,同時它們都是通過發送和接收的方式來傳遞數據的。在命名管道中,發送數據用write,接收數據用read,則在消息隊列中,發送數據用msgsnd,接收數據用msgrcv。而且它們對每個數據都有一個最大長度的限制。
3、與命名管道相比,消息隊列的優勢在於,1、消息隊列也可以獨立於發送和接收進程而存在,從而消除了在同步命名管道的打開和關閉時可能產生的困難。2、同時通過發送消息還可以避免命名管道的同步和阻塞問題,不需要由進程自己來提供同步方法。3、接收程序可以通過消息類型有選擇地接收數據,而不是像命名管道中那樣,只能默認地接收。
#include <sys/msg.h>
2 // 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
3 int msgget(key_t key, int flag);
4 // 添加消息:成功返回0,失敗返回-1
5 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
6 // 讀取消息:成功返回消息數據的長度,失敗返回-1
7 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
8 // 控制消息隊列:成功返回0,失敗返回-1
9 int msgctl(int msqid, int cmd, struct msqid_ds *buf);
4、各個函數詳解:
(1)創建或打開消息隊列函數
int msgget(key_t, key, int msgflg);
與其他的IPC機制一樣,程序必須提供一個鍵來命名某個特定的消息隊列。msgflg是一個權限標誌,表示消息隊列的訪問權限,它與文件的訪問權限一樣。msgflg可以與IPC_CREAT做或操作,表示當key所命名的消息隊列不存在時創建一個消息隊列,如果key所命名的消息隊列存在時,IPC_CREAT標誌會被忽略,而只返回一個標識符。
在程序中若要使用消息隊列,必須要能知道消息隊列key,因爲應用進程無法直接訪問內核消息隊列中的數據結構,因此需要一個消息隊列的標識,讓應用進程知道當前操作的是哪個消息隊列,同時也要保證每個消息隊列key值的唯一性。
申請一塊內存,創建一個新的消息隊列(數據結構msqid_ds),將其初始化後加入到msgque向量表中的某個空位置處,返回標示符。或者在msgque向量表中找鍵值爲key的消息隊列。
(2)添加消息函數
int msgsnd(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msgid是由msgget函數返回的消息隊列標識符。
msg_ptr是一個指向準備發送消息的指針,但是消息的數據結構卻有一定的要求,指針msg_ptr所指向的消息結構一定要是以一個長整型成員變量開始的結構體,接收函數將用這個成員來確定消息的類型。所以消息結構要定義成這樣:
struct my_message{ long int message_type; /* The data you wish to transfer*/ };
msg_sz是msg_ptr指向的消息的長度,注意是消息的長度,而不是整個結構體的長度,也就是說msg_sz是不包括長整型消息類型成員變量的長度。
msgflg用於控制當前消息隊列滿或隊列消息到達系統範圍的限制時將要發生的事情。
如果調用成功,消息數據的一分副本將被放到消息隊列中,並返回0,失敗時返回-1.
(3)從一個消息隊列中獲取消息:
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgid, msg_ptr, msg_st的作用也函數msgsnd函數的一樣。
msgtype可以實現一種簡單的接收優先級。如果msgtype爲0,就獲取隊列中的第一個消息。如果它的值大於零,將獲取具有相同消息類型的第一個信息。如果它小於零,就獲取類型等於或小於msgtype的絕對值的第一個消息。
msgflg用於控制當隊列中沒有相應類型的消息可以接收時將發生的事情。
調用成功時,該函數返回放到接收緩存區中的字節數,消息被複制到由msg_ptr指向的用戶分配的緩存區中,然後刪除消息隊列中的對應消息。失敗時返回-1.
(4)消息隊列控制函數:
int msgctl(int msgid, int command, struct msgid_ds *buf);
command是將要採取的動作,它可以取3個值,
IPC_STAT:把msgid_ds結構中的數據設置爲消息隊列的當前關聯值,即用消息隊列的當前關聯值覆蓋msgid_ds的值。
IPC_SET:如果進程有足夠的權限,就把消息列隊的當前關聯值設置爲msgid_ds結構中給出的值
IPC_RMID:刪除消息隊列
buf是指向msgid_ds結構的指針,它指向消息隊列模式和訪問權限的結構。msgid_ds結構至少包括以下成員:
struct msgid_ds { uid_t shm_perm.uid; uid_t shm_perm.gid; mode_t shm_perm.mode; };
成功時返回0,失敗時返回-1.
5、廢話不多說,直接上實例如下:
消息發送端:send.c文件內容如下
#include <stdio.h>
#include <stdlib.h>//exit()
#include <string.h>//strcpy()
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#define MSGKEY 1024
struct msgstru {
long msgtype;
char msgtext[2048];
};
main()
{
struct msgstru msgs;
int msg_type;
char str[256];
int ret_value;
int msqid;
msqid=msgget(MSGKEY,IPC_EXCL); /*檢查消息隊列是否存在*/
if(msqid < 0)
{
msqid = msgget(MSGKEY,IPC_CREAT|0666);/*創建消息隊列*/
if(msqid <0)
{
printf("failed to create msq | errno=%d [%s]\n",errno,strerror(errno));
exit(-1);
}
}
while (1)
{
printf("input message type(end:0):");
scanf("%d",&msg_type);
if (msg_type == 0)
break;
printf("input message to be sent:");
scanf ("%s",str);
msgs.msgtype = msg_type;
strcpy(msgs.msgtext, str);
/* 發送消息隊列 */
ret_value = msgsnd(msqid,&msgs,sizeof(struct msgstru),IPC_NOWAIT);
if ( ret_value < 0 )
{
printf("msgsnd() write msg failed,errno=%d[%s]\n",errno,strerror(errno));
exit(-1);
}
}
msgctl(msqid,IPC_RMID,0); //刪除消息隊列
}
消息接收端:receive.c文件如下:
/*receive.c */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#define MSGKEY 1024
struct msgstru
{
long msgtype;
char msgtext[2048];
};
/*子進程,監聽消息隊列*/
void childproc()
{
struct msgstru msgs;
int msgid,ret_value;
char str[512];
while(1)
{
msgid = msgget(MSGKEY,IPC_EXCL );
/*檢查消息隊列是否存在 */
if(msgid < 0){
printf("msq not existed! errno=%d [%s]\n",errno,strerror(errno));
sleep(2);
continue;
}
/*接收消息隊列*/
ret_value = msgrcv(msgid,&msgs,sizeof(struct msgstru),0,0);
printf("text=[%s] pid=[%d]\n",msgs.msgtext,getpid());
}
return;
}
void main()
{
int i,cpid;
/* create 5 child process */
for (i=0;i<5;i++)
{
cpid = fork();
if (cpid < 0)
printf("fork failed\n");
else if (cpid ==0) /*child process*/
childproc();
}
}
四、信號量
1、信號量本質上是一個計數器(不設置全局變量是因爲進程間是相互獨立的,而這不一定能看到,看到也不能保證++引用計數爲原子操作),用於多進程對共享數據對象的讀取,它和管道有所不同,它不以傳送數據爲主要目的,它主要是用來保護共享資源(信號量也屬於臨界資源),使得資源在一個時刻只有一個進程獨享。
2、特點
-
信號量用於進程間同步,若要在進程間傳遞數據需要結合共享內存。
-
信號量基於操作系統的 PV 操作,程序對信號量的操作都是原子操作。
-
每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
-
支持信號量組。
3、信號量工作原理
由於信號量只能進行兩種操作等待和發送信號,即P(sv)和V(sv),他們的行爲是這樣的:
(1)P(sv):如果sv的值大於零,就給它減1;如果它的值爲零,就掛起該進程的執行
(2)V(sv):如果有其他進程因等待sv而被掛起,就讓它恢復運行,如果沒有進程因等待sv而掛起,就給它加1.
在信號量進行PV操作時都爲原子操作(因爲它需要保護臨界資源)
注:原子操作:單指令的操作稱爲原子的,單條指令的執行是不會被打斷的
實驗代碼如下:
#include<stdio.h>
#include<stdlib.h>
#include<sys/sem.h>
// 聯合體,用於semctl初始化
union semun
{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
};
// 初始化信號量
int init_sem(int sem_id, int value)
{
union semun tmp;
tmp.val = value;
if(semctl(sem_id, 0, SETVAL, tmp) == -1)
{
perror("Init Semaphore Error");
return -1;
}
return 0;
}
// P操作:
// 若信號量值爲1,獲取資源並將信號量值-1
// 若信號量值爲0,進程掛起等待
int sem_p(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = -1; /*P操作*/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("P operation Error");
return -1;
}
return 0;
}
// V操作:
// 釋放資源並將信號量值+1
// 如果有進程正在掛起等待,則喚醒它們
int sem_v(int sem_id)
{
struct sembuf sbuf;
sbuf.sem_num = 0; /*序號*/
sbuf.sem_op = 1; /*V操作*/
sbuf.sem_flg = SEM_UNDO;
if(semop(sem_id, &sbuf, 1) == -1)
{
perror("V operation Error");
return -1;
}
return 0;
}
// 刪除信號量集
int del_sem(int sem_id)
{
union semun tmp;
if(semctl(sem_id, 0, IPC_RMID, tmp) == -1)
{
perror("Delete Semaphore Error");
return -1;
}
return 0;
}
int main()
{
int sem_id; // 信號量集ID
key_t key;
pid_t pid;
// 獲取key值
if((key = ftok(".", 'z')) < 0)
{
perror("ftok error");
exit(1);
}
// 創建信號量集,其中只有一個信號量
if((sem_id = semget(key, 1, IPC_CREAT|0666)) == -1)
{
perror("semget error");
exit(1);
}
// 初始化:初值設爲0資源被佔用
init_sem(sem_id, 0);
if((pid = fork()) == -1)
perror("Fork Error");
else if(pid == 0) /*子進程*/
{
sleep(2);
printf("Process child: pid=%d\n", getpid());
sem_v(sem_id); /*釋放資源*/
}
else /*父進程*/
{
sem_p(sem_id); /*等待資源*/
printf("Process father: pid=%d\n", getpid());
sem_v(sem_id); /*釋放資源*/
del_sem(sem_id); /*刪除信號量集*/
}
return 0;
}
實驗結果如下:
從結果我們可以看出,父進程等待子進程運行結束後在運行。分析代碼,我們知道,子進程釋放了父進程需要的資源,子進程執行V原語操作,父進程執行P原語操作。
五、共享內存
共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。
1、特點
共享內存是最快的一種 IPC,因爲進程是直接對內存進行存取。
因爲多個進程可以同時操作,所以需要進行同步。
信號量+共享內存通常結合在一起使用,信號量用來同步對共享內存的訪問。
2、原型
#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);
實驗代碼:
在這裏插入代碼片
實驗結果:
客戶端進程執行情況如下:
服務器端進程處理情況如下:
注意:當scanf()輸入字符或字符串時,緩衝區中遺留下了\n,所以每次輸入操作後都需要清空標準輸入的緩衝區。但是由於 gcc 編譯器不支持fflush(stdin)(它只是標準C的擴展),所以我們使用了替代方案:
作者: ZH奶酪——張賀
Q Q: 1203456195
郵箱: [email protected]
出處: http://www.cnblogs.com/CheeseZH/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
注:本文大部分內容皆來源於此,文章也有自己的總結,與原文有一部分不一樣。