進程間通信(IPC)介紹
進程間通信的本質就是讓兩個毫不相關的進程看到一份共同的資源,大概意思就是實現不同進程間的傳播或交換信息
進程間通信的主要方式有管道,消息隊列,共享內存,信號量,Socket,Streams等,這篇博客主要詳細講解前四種通信方式。(因爲後面兩個還沒學,嘿嘿)
一、管道
管道是UNIX中最古老的進程間通信方式
我們把從一個進程連接到另一個進程的一個數據流稱爲“管道”
管道又分爲匿名管道和命名管道兩種
匿名管道
1、特點
- 管道是半雙工的,只支持單向通信,如果需要全雙工通信,就需要建立起兩個管道
- 管道只支持有血緣關係的進程間通信,如父子進程和兄弟進程
- 管道的生命週期隨進程,進程終止管道就會被釋放
- 管道提供面向字節流的通信
2、原型
#include <unistd.h>
int pipe(int fd[2]);
管道創建成功後返回兩個文件描述符,用fd[2]數組接收,fd[0]爲讀,fd[1]爲寫
返回值:成功返回0,失敗返回錯誤代碼
3、圖解
父進程剛創建出子進程時,調用pipe函數返回的文件描述符都是打開的,父子進程通信時就要關閉對應的描述符,如父進程從管道讀數據,用的是fd[0],就要關掉它的fd[1],子進程則相反
代碼演示
int main()
{
pid_t pid;
int file[2];//用於接收文件描述符
pipe(file);//創建管道
char buf[256] = {0};
pid = fork();
if(pid < 0)
{
perror("fork");
}else if(pid == 0)
{
//子進程進行寫操作,關閉文件描述符fd[0]
close(file[0]);
write(file[1],"hello father",strlen("hello father"));
}else
{
//父進程進行讀操作,關閉文件描述符fd[1]
close(file[1]);
read(file[0],buf,sizeof(buf));
}
printf("%s\n",buf);
}
因爲在運行a.out文件時是敲了回車的,所以會自動換一次行
命名管道
也成爲FIFO,是一種文件類型
1、特點
- 支持任意兩個進程間的通信
- FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中
2、與命名管道的區別
- 匿名管道由pipe函數創建並打開。
- 命名管道由mkfifo函數創建並打開
- 唯一的區別就是打開和創建的方式不同,一但這些工作完成之後,它們具有相同的語義
3、原型
#include <sys/stat.h>
int mkfifo(const char *filename,mode_t mode);
成功返回0,出錯返回-1
其中的 mode 參數與open函數中的 mode 相同。一旦創建了一個 FIFO,就可以用一般的文件I/O函數操作它。
當 open 一個FIFO時,是否設置非阻塞標誌(O_NONBLOCK)的區別:
若沒有指定O_NONBLOCK(默認),只讀 open 要阻塞到某個其他進程爲寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個其他進程爲讀而打開它。
若指定了O_NONBLOCK,則只讀 open 立即返回。而只寫 open 將出錯返回 -1 如果沒有進程已經爲讀而打開該 FIFO,其errno置ENXIO。
4、例子:用命名管道實現文件拷貝
writer.c
int main()
{
//創建命名管道,權限是644
mkfifo("pipe",0644);
//test 文件已在當前目錄下創建,以只讀方式打開
int input = open("test",O_RDONLY);
if(input == -1)
{
perror("open");
}
//以只寫方式打開命名管道
int output = open("pipe",O_WRONLY);
if(output == -1)
{
perror("open");
}
char out[1024];
int n;
while((n = read(input,out,sizeof(out))) > 0)
{
//向管道寫數據
write(output,out,n);
}
close(input);
close(output);
return 0;
}
reader.c
int main()
{
int output;
//創建一個文件,並以只寫方式打開它
//目的是將test文件中的內容拷貝到test.bak中
output = open("test.bak",O_WRONLY|O_CREAT|O_TRUNC,0644);
if(output == -1)
{
perror("open");
}
int input;
input = open("pipe",O_RDONLY);
if(input == -1)
{
perror("open");
}
char buf[1024];
int n;
while((n = read(input,buf,sizeof(buf))) > 0)
{
write(output,buf,n);
}
close(input);
close(output);
unlink("pipe");
return 0;
}
test文件內容
test.bak 文件內容顯示拷貝成功
二、消息隊列
消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
1、特點
- 消息隊列提供了一個從一個進程向另外一個進程發送一個數據塊(有類型數據塊)的方法
消息隊裏的生命週期隨內核,進程終止後消息隊列也不會銷燬
ipcs -q 查看當前的消息隊列 ipcrm -q + 消息隊列ID 銷燬消息隊列,也可以通過在函數內調用msgctl函數清除消息隊列
- 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。
- 消息隊列也有管道一樣的不足,即每個消息隊列的最大長度是有上限的
(MSGMAX)
,每個消息隊列的總的字節數是由上線的(MSGMNB)
,系統上消息隊列的總數也有一個上限(MSGMNI)
。
2、原型
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/msg.h>
// 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
int msgget(key_t key, int flag);
// key:消息隊列的名字,下面的例子中將具體體現使用方法
// 添加消息:成功返回0,失敗返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// msqid:消息隊列的標識符,即隊列ID
// 讀取消息:成功返回消息數據的長度,失敗返回-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);
3、例子:用兩個終端實現兩個前臺進程的通信
common.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0X6667
#define SERVER_TYPE 1
#define CLIENT_TYPE 2
struct msgbuf{
long mtype;
char mtext[1024];
};
int CreateMsgQueue();
int GetMsgQueue();
int DestroyMsgQueue(int msgid);
int SendMsg(int msgid, int who, char *msg);
int RecvMsg(int msgid, int recvType, char out[]);
common.c
#include "common.h"
int CommonMsgQueue(int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);//ftok函數可以生成key
if(key < 0)
{
perror("ftok");
return -1;
}
int msgid = msgget(key,flags);
if(msgid < 0)
{
perror("msgget");
return -1;
}
return msgid;
}
int CreateMsgQueue()//創建一個消息隊列
{
//flag由9個權限標誌構成
//IPC_CREAT|IPC_EXCL一起用表示如果沒有該消息隊列,則創建一個新的,並返回隊列ID
return CommonMsgQueue(IPC_CREAT|IPC_EXCL|0666);
}
int GetMsgQueue()//獲得一個消息隊列
{
//返回已創建的消息隊列ID
return CommonMsgQueue(IPC_CREAT);
}
int DestroyMsgQueue(int msgid)//銷燬一個消息隊列
{
if (msgctl(msgid,IPC_RMID,NULL) < 0 )
{
perror("msgctl");
return -1;
}
return 0;
}
int SendMsg(int msgid, int who, char *msg)//往消息隊列中發送數據
{
struct msgbuf buf;
buf.mtype = who;
strcpy(buf.mtext,msg);
if(msgsnd(msgid,(void*)&buf,sizeof(buf.mtext),0) < 0)
{
perror("msgsnd");
return -1;
}
return 0;
}
int RecvMsg(int msgid, int recvType, char out[])//從消息隊列中讀取數據
{
struct msgbuf buf;
if((msgrcv(msgid,(void *)&buf,sizeof(buf.mtext),recvType,0)) < 0)
{
perror("msgrcv");
return -1;
}
strcpy(out,buf.mtext);
return 0;
}
server.c
#include "common.h"
int main()
{
int msgid = CreateMsgQueue();
char buf[1024];
while(1)
{
buf[0] = 0;
RecvMsg(msgid,CLIENT_TYPE,buf);
printf("Client say# %s \n",buf);
printf("Please Enter# ");
fflush(stdout);//刷新緩衝區
ssize_t s = read(0,buf,sizeof(buf));//從標準輸出讀取數據到buf中
if(s > 0)
{
buf[s-1] = 0;
SendMsg(msgid,SERVER_TYPE,buf);
printf("send done,wait for client:..\n");
}
if(strcmp(buf,"exit") == 0)
{
break;
}
}
DestroyMsgQueue(msgid);
return 0;
}
client.c
#include "common.h"
int main()
{
int msgid = GetMsgQueue();
char buf[1024];
while(1)
{
buf[0] = 0;
printf("Please Enter# ");
fflush(stdout);//刷新緩衝區
ssize_t s = read(0,buf,sizeof(buf));//從標準輸出讀取數據到buf中
if(s > 0)
{
buf[s-1] = 0;
SendMsg(msgid,CLIENT_TYPE,buf);
printf("send done, wait recv...\n");
}
RecvMsg(msgid,SERVER_TYPE,buf);
printf("Server say# %s \n",buf);
if(strcmp(buf,"exit") == 0)
{
return 0;
}
}
return 0;
}
三、共享內存
共享內存(Shared Memory),指兩個或多個進程共享一個給定的存儲區。
1、特點
- 共享內存是最快的IPC形式,因爲進程是直接對內存進行讀取的
共享內存的生命週期隨內核
ipcs -m 查看共享內存 ipcrm -m + 共享內存ID 手動銷燬共享內存或者通過調用shmctl函數來銷燬
信號量和共享內存通常放在一起使用,信號量用來同步對共享內存的訪問
2、圖解
3、原型
#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(從系統中刪除該共享內存)。
4、例子:一個進程往共享內存中寫數據,一個進程往出讀數據
commo.h
#pragma once
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define PATHNAME "."
#define PROJ_ID 0X6666
int CreateShm(int size);
int DestroyShm(int shmid);
int GetShm(int size);
common.c
#include "common.h"
//與創建消息隊列很類似,就不做介紹了
int CommonShm(int size, int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int shmid = 0;
if((shmid = shmget(key,size,flags)) < 0)
{
perror("shmget");
return -2;
}
return shmid;
}
int CreateShm(int size)
{
return CommonShm(size,IPC_CREAT|IPC_EXCL|0666);
}
int DestroyShm(int shmid)
{
if(shmctl(shmid,IPC_RMID,NULL) < 0)
{
return -1;
}
return 0;
}
int GetShm(int size)
{
return CommonShm(size,IPC_CREAT);
}
server.c
#include "common.h"
int main()
{
int shmid = CreateShm(4096);
char *addr = shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i++<26)
{
printf("client# %s\n",addr);
sleep(1);
}
//將共享內存段與當前進程脫離
shmdt(addr);
sleep(2);
DestroyShm(shmid);
return 0;
}
client.c
#include "common.h"
int main()
{
int shmid = GetShm(4096);
sleep(1);
char *addr =shmat(shmid, NULL, 0);
sleep(2);
int i = 0;
while(i<26)
{
//每隔1秒寫一個字母進去
addr[i] = 'A'+i;
i++;
addr[i] = 0;
sleep(1);
}
shmdt(addr);
sleep(2);
DestroyShm(shmid);
return 0;
}
client只負責寫數據,所以沒有輸出結果
信號量
信號量主要作用於同步和互斥,而不是存儲進程間通信數據
1、特點
信號量本質上是一個計數器,記錄了臨界資源的數目
臨界資源:系統中有些資源一次只允許一個進程訪問,被稱爲臨界資源,也可被當做互斥資源 臨界區: 在進程中涉及到互斥資源的程序段叫臨界區
- 信號量基於操作系統的P,V操作,且P,V操作是原子性的
- 每次對信號量的 PV 操作不僅限於對信號量值加 1 或減 1,而且可以加減任意正整數。
2、原型
簡單的信號量是隻能取 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 。
在semop函數中,sembuf結構的定義如下:
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 的絕對值的資源。如果相應的資源數可以滿足請求,則將該信號量的值減去sem_op的絕對值,函數成功返回。
當相應的資源數不能滿足請求時,這個操作與
sem_flg
有關。sem_flg
指定IPC_NOWAIT
,則semop函數出錯返回EAGAIN。sem_flg
沒有指定IPC_NOWAIT
,則將該信號量的semncnt值加1,然後進程掛起直到下述情況發生:
- 當相應的資源數可以滿足請求,此信號量的semncnt值減1,該信號量的值減去sem_op的絕對值。成功返回;
- 此信號量被刪除,函數smeop出錯返回EIDRM;
- 進程捕捉到信號,並從信號處理函數返回,此情況下將此信號量的semncnt值減1,函數semop出錯返回EINTR
- 若
sem_op == 0
,進程阻塞直到信號量的相應值爲0:
- 當信號量已經爲0,函數立即返回。
- 如果信號量的值不爲0,則依據
sem_flg
決定函數動作:
sem_flg
指定IPC_NOWAIT,則出錯返回EAGAIN。sem_flg
沒有指定IPC_NOWAIT,則將該信號量的semncnt值加1,然後進程掛起直到下述情況發生:
- 信號量值爲0,將信號量的semzcnt的值減1,函數semop成功返回;
- 此信號量被刪除,函數smeop出錯返回EIDRM;
- 進程捕捉到信號,並從信號處理函數返回,在此情況將此信號量的semncnt值減1,函數semop出錯返回EINTR
在semctl
函數中的命令有多種,這裏就說兩個常用的:
SETVAL
:用於初始化信號量爲一個已知的值。所需要的值作爲聯合semun的val成員來傳遞。在信號量第一次使用之前需要設置信號量。IPC_RMID
:刪除一個信號量集合。如果不刪除信號量,它將繼續在系統中存在,即使程序已經退出,它可能在你下次運行此程序時引發問題,而且信號量是一種有限的資源。3、例子:父進程與子進程之間同步打印字母,使其成對出現
commn.h
#pragma once
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define PATHNAME "."
#define PROJ_ID 0X6666
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *_buf;
};
int CreateSem(int nums);
int InitSem(int semid, int nums, int initVal);
int GetSem(int nums);
int P(int semid, int who);
int V(int semid, int who);
int DestroySem(int semid);
common.c
#include "common.h"
static int CommonSemSet(int nums, int flags)
{
key_t key = ftok(PATHNAME,PROJ_ID);
if(key < 0)
{
perror("ftok");
return -1;
}
int semid = semget(key,nums,flags);
if(semid < 0)
{
perror("semget");
return -2;
}
return semid;
}
//創建信號量集
int CreateSem(int nums)
{
return CommonSemSet(nums,IPC_CREAT|IPC_EXCL|0666);
}
//初始化信號量集
int InitSem(int semid, int nums, int initVal)
{
union semun un;
un.val = initVal;
if(semctl(semid,nums,SETVAL,un) < 0)
{
perror("semctl");
return -1;
}
return 0;
}
//獲取已創建好的信號量集
int GetSem(int nums)
{
return CommonSemSet(nums,IPC_CREAT);
}
static int CommPV(int semid, int who, int op)
{
struct sembuf sf;
sf.sem_flg = 0;
sf.sem_op = op;
sf.sem_num = who;
if(semop(semid,&sf,1)<0)
{
perror("semop");
return -1;
}
return 0;
}
//申請資源,相當於對信號量進行減操作
int P(int semid, int who)
{
return CommPV(semid,who,-1);
}
//釋放資源,相當於對信號量進行加操作
int V(int semid, int who)
{
return CommPV(semid,who,1);
}
//銷燬剛創建的信號量集
int DestroySem(int semid)
{
if(semctl(semid, 0 ,IPC_RMID) < 0)
{
perror("semctl");
return -1;
}
return 0
}
test_sem.c
#include "common.h"
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int semid = CreateSem(1);
InitSem(semid, 0 ,1);
pid_t pid = fork();
int i = 20;
if(pid < 0)
{
perror("fork");
return 0;
}else if(pid == 0)
{
//子進程
int _semid = GetSem(0);
//通過P,V操作實現兩個進程的互斥
//在屏幕上打印成對出現的AB
while(i--)
{
P(_semid, 0);
printf("A");
fflush(stdout);
usleep(123456);
printf("A ");
fflush(stdout);
usleep(321456);
V(_semid,0);
}
}
else{
//父進程
while(i--)
{
P(semid,0);
printf("B");
fflush(stdout);
usleep(223456);
printf("B ");
fflush(stdout);
usleep(121456);
V(semid,0);
}
wait(NULL);
}
DestroySem(semid);
return 0;
}
運行結果(加上PV操作),AB成對出現,父子進程實現同步,那去掉PV之後呢
去掉PV之後現象如下,可以看出父子進程沒有實現同步
五種進程間通信方式總結
- 管道:只支持有血緣關係的進程間通信,且生命週期隨進程
- FIFO:支持任意進程間通信,創建和打開方式與管道不同,其他同管道一樣
- 消息隊列:存在系統限制,而且有時候需要手動刪除,在讀一條消息的時候還要注意上一條消息沒有讀完的情況
- 共享內存:速度最快的IPC方式,但要注意保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存
- 信號量:不能用來傳遞複雜消息,只能用來互斥與同步