面試必備 進(jìn)程間的五種通信方式
進(jìn)程間通信(IPC,Inter-Process Communication)是指在不同進(jìn)程間傳播或交換信息。
IPC的方式通常有管道(包括無名管道和命名管道)、消息隊(duì)列、信號(hào)量、共享存儲(chǔ)、Socket、Streams等。其中 Socket和Streams支持不同主機(jī)上的兩個(gè)進(jìn)程IPC。
一、管道
管道,通常指無名管道,是 UNIX 系統(tǒng)IPC最古老的形式。
1. 特點(diǎn)
- 半雙工(數(shù)據(jù)流向僅有一個(gè)方向),具有固定的讀端和寫端
- 只能用于父進(jìn)程或兄弟線程之間通信(具有血緣關(guān)系的線程之間)
- 一種特殊文件,可以用普通的read、write函數(shù)進(jìn)行讀寫,但又不是普通文件,不屬于任何其它文件系統(tǒng),僅存在于內(nèi)存之中
2. 原型
- #include <unistd.h>
- int pipe(int fd[2]); // 返回值:若成功返回0,失敗返回-1
當(dāng)一個(gè)管道建立時(shí),它會(huì)創(chuàng)建兩個(gè)文件描述符:fd[0]為讀而打開,fd[1]為寫而打開。要關(guān)閉管道只需要關(guān)閉這兩個(gè)文件描述符即可。如下圖:
3. 例子
單個(gè)進(jìn)程中的管道幾乎沒有任何用處。所以,通常調(diào)用 pipe 的進(jìn)程接著調(diào)用 fork,這樣就創(chuàng)建了父進(jìn)程與子進(jìn)程之間的 IPC 通道。如下圖所示:
fork之后的半雙工管道
從父進(jìn)程到子進(jìn)程之間的管道
若要數(shù)據(jù)流從父進(jìn)程流向子進(jìn)程,則關(guān)閉父進(jìn)程的讀端(fd[0])與子進(jìn)程的寫端(fd[1]);反之,則可以使數(shù)據(jù)流從子進(jìn)程流向父進(jìn)程。
- #include<stdio.h>
- #include<unistd.h>
- int main()
- {
- int fd[2]; // 兩個(gè)文件描述符
- pid_t pid;
- char buff[20];
- if(pipe(fd) < 0) // 創(chuàng)建管道
- printf("Create Pipe Error!\n");
- if((pid = fork()) < 0) // 創(chuàng)建子進(jìn)程
- printf("Fork Error!\n");
- else if(pid > 0) // 父進(jìn)程
- {
- close(fd[0]); // 關(guān)閉讀端
- write(fd[1], "hello world\n", 12);
- }
- else
- {
- close(fd[1]); // 關(guān)閉寫端
- read(fd[0], buff, 20);
- printf("%s", buff);
- }
- return 0;
- }
二、命名管道(FIFO)
FIFO,也稱為命名管道,它是一種文件類型。
1. 特點(diǎn)
- 與無名管道不同,命名管道可以在無關(guān)進(jìn)程間通信
- FIFO以一種特殊設(shè)備文件形式存在于文件系統(tǒng)中,有路徑名與之關(guān)聯(lián)
2. 原型
- #include <sys/stat.h>
- int mkfifo(const char *pathname, mode_t mode); // 返回值:成功返回0,出錯(cuò)返回-1
其中的 mode 參數(shù)與下文中open函數(shù)中的 mode 相同
3. 例子
wirte:
- #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()); // 說明進(jìn)程ID
- //當(dāng) open 一個(gè)FIFO時(shí),是否設(shè)置非阻塞標(biāo)志(O_NONBLOCK)的區(qū)別:
- //若沒有指定O_NONBLOCK(默認(rèn)),只讀 open 要阻塞到某個(gè)其他進(jìn)程為寫而打開此 FIFO。類似的,只寫 open 要阻塞到某個(gè)其他進(jìn)程為讀而打開它。
- 若指定了O_NONBLOCK,則只讀 open 立即返回。而只寫 open 將出錯(cuò)返回 -1 如果沒有進(jìn)程已經(jīng)為讀而打開該 FIFO,其errno置ENXIO。
- if((fd = open("fifo1", O_WRONLY)) < 0) // 以寫打開一個(gè)FIFO
- {
- perror("Open FIFO Failed");
- exit(1);
- }
- for(i=0; i<10; ++i)
- {
- time(&tp); // 取系統(tǒng)當(dāng)前時(shí)間
- 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); // 關(guān)閉FIFO文件
- return 0;
- }
read:
- #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) // 創(chuàng)建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); // 關(guān)閉FIFO文件
- return 0;
- }
三、消息隊(duì)列
消息隊(duì)列,是消息的鏈接表,存放在內(nèi)核中。一個(gè)消息隊(duì)列由一個(gè)標(biāo)識(shí)符(即隊(duì)列ID)來標(biāo)識(shí)。
1. 特點(diǎn)
- 消息隊(duì)列是面向記錄的,其中的消息具有特定的格式以及特定的優(yōu)先級(jí)
- 消息隊(duì)列獨(dú)立于發(fā)送與接收進(jìn)程。進(jìn)程終止時(shí),消息隊(duì)列及其內(nèi)容并不會(huì)被刪除
- 消息隊(duì)列可以實(shí)現(xiàn)消息的隨機(jī)查詢, 消息不一定要以先進(jìn)先出的次序讀取,也可以按消息的類型讀取
2. 原型
- #include <sys/msg.h>
- // 創(chuàng)建或打開消息隊(duì)列:成功返回隊(duì)列ID,失敗返回-1
- int msgget(key_t key, int flag);
- // 添加消息:成功返回0,失敗返回-1
- int msgsnd(int msqid, const void *ptr, size_t size, int flag);
- // 讀取消息:成功返回消息數(shù)據(jù)的長度,失敗返回-1
- int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
- // 控制消息隊(duì)列:成功返回0,失敗返回-1
- int msgctl(int msqid, int cmd, struct msqid_ds *buf);
在以下兩種情況下,msgget將創(chuàng)建一個(gè)新的消息隊(duì)列:
- 如果沒有與鍵值key相對(duì)應(yīng)的消息隊(duì)列,并且flag中包含了IPC_CREAT標(biāo)志位。
- key參數(shù)為IPC_PRIVATE。
函數(shù)msgrcv在讀取消息隊(duì)列時(shí),type參數(shù)有下面幾種情況:
- type == 0,返回隊(duì)列中的第一個(gè)消息;
- type > 0,返回隊(duì)列中消息類型為 type 的第一個(gè)消息;
- type < 0,返回隊(duì)列中消息類型值小于或等于 type 絕對(duì)值的消息,如果有多個(gè),則取類型值最小的消息。
可以看出,type值非 0 時(shí)用于以非先進(jìn)先出次序讀消息。也可以把 type 看做優(yōu)先級(jí)的權(quán)值。
3. 例子
msg_server:
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/msg.h>
- // 用于創(chuàng)建一個(gè)唯一的key
- #define MSG_FILE "/etc/passwd"
- // 消息結(jié)構(gòu)
- 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);
- // 創(chuàng)建消息隊(duì)列
- if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
- {
- perror("msgget error");
- exit(1);
- }
- // 打印消息隊(duì)列ID及進(jìn)程ID
- printf("My msqid is: %d.\n", msqid);
- printf("My pid is: %d.\n", getpid());
- // 循環(huán)讀取消息
- for(;;)
- {
- msgrcv(msqid, &msg, 256, 888, 0);// 返回類型為888的第一個(gè)消息
- 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:
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/msg.h>
- // 用于創(chuàng)建一個(gè)唯一的key
- #define MSG_FILE "/etc/passwd"
- // 消息結(jié)構(gòu)
- 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);
- // 打開消息隊(duì)列
- if ((msqid = msgget(key, IPC_CREAT|0777)) == -1)
- {
- perror("msgget error");
- exit(1);
- }
- // 打印消息隊(duì)列ID及進(jìn)程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;
- }
四、信號(hào)量
信號(hào)量(semaphore)與已經(jīng)介紹過的 IPC 結(jié)構(gòu)不同,它是一個(gè)計(jì)數(shù)器。信號(hào)量用于實(shí)現(xiàn)進(jìn)程間的互斥與同步,而不是用于存儲(chǔ)進(jìn)程間通信數(shù)據(jù)。
1. 特點(diǎn)
- 信號(hào)量用于進(jìn)程間同步,若要在進(jìn)程間傳遞數(shù)據(jù)需要結(jié)合共享內(nèi)存
- 信號(hào)量基于操作系統(tǒng)的 PV 操作,程序?qū)π盘?hào)量的操作都是原子操作
- 每次對(duì)信號(hào)量的 PV 操作不僅限于對(duì)信號(hào)量值加 1 或減 1,而且可以加減任意正整數(shù)
- 支持信號(hào)量組
2. 原型
最簡單的信號(hào)量是只能取 0 和 1 的變量,這也是信號(hào)量最常見的一種形式,叫做二值信號(hào)量(Binary Semaphore)。而可以取多個(gè)正整數(shù)的信號(hào)量被稱為通用信號(hào)量。
Linux 下的信號(hào)量函數(shù)都是在通用的信號(hào)量數(shù)組上進(jìn)行操作,而不是在一個(gè)單一的二值信號(hào)量上進(jìn)行操作。
- #include <sys/sem.h>
- // 創(chuàng)建或獲取一個(gè)信號(hào)量組:若成功返回信號(hào)量集ID,失敗返回-1
- int semget(key_t key, int num_sems, int sem_flags);
- // 對(duì)信號(hào)量組進(jìn)行操作,改變信號(hào)量的值:成功返回0,失敗返回-1
- int semop(int semid, struct sembuf semoparray[], size_t numops);
- // 控制信號(hào)量的相關(guān)信息
- int semctl(int semid, int sem_num, int cmd, ...);
當(dāng)semget創(chuàng)建新的信號(hào)量集合時(shí),必須指定集合中信號(hào)量的個(gè)數(shù)(即num_sems),通常為1; 如果是引用一個(gè)現(xiàn)有的集合,則將num_sems指定為 0 。
在semop函數(shù)中,sembuf結(jié)構(gòu)的定義如下:
- struct sembuf
- {
- short sem_num; // 信號(hào)量組中對(duì)應(yīng)的序號(hào),0~sem_nums-1
- short sem_op; // 信號(hào)量值在一次操作中的改變量
- short sem_flg; // IPC_NOWAIT, SEM_UNDO
- }
五、共享內(nèi)存
1. 特點(diǎn)
- 共享內(nèi)存是最快的一種 IPC,因?yàn)檫M(jìn)程是直接對(duì)內(nèi)存進(jìn)行存取
- 因?yàn)槎鄠€(gè)進(jìn)程可以同時(shí)操作,所以需要進(jìn)行同步
- 信號(hào)量+共享內(nèi)存通常結(jié)合在一起使用,信號(hào)量用來同步對(duì)共享內(nèi)存的訪問
2. 原型
- #include <sys/shm.h>
- // 創(chuàng)建或獲取一個(gè)共享內(nèi)存:成功返回共享內(nèi)存ID,失敗返回-1
- int shmget(key_t key, size_t size, int flag);
- // 連接共享內(nèi)存到當(dāng)前進(jìn)程的地址空間:成功返回指向共享內(nèi)存的指針,失敗返回-1
- void *shmat(int shm_id, const void *addr, int flag);
- // 斷開與共享內(nèi)存的連接:成功返回0,失敗返回-1
- int shmdt(void *addr);
- // 控制共享內(nèi)存的相關(guān)信息:成功返回0,失敗返回-1
- int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
當(dāng)用shmget函數(shù)創(chuàng)建一段共享內(nèi)存時(shí),必須指定其 size;而如果引用一個(gè)已存在的共享內(nèi)存,則將 size 指定為0 。
當(dāng)一段共享內(nèi)存被創(chuàng)建以后,它并不能被任何進(jìn)程訪問。必須使用shmat函數(shù)連接該共享內(nèi)存到當(dāng)前進(jìn)程的地址空間,連接成功后把共享內(nèi)存區(qū)對(duì)象映射到調(diào)用進(jìn)程的地址空間,隨后可像本地空間一樣訪問。
shmdt函數(shù)是用來斷開shmat建立的連接的。注意,這并不是從系統(tǒng)中刪除該共享內(nèi)存,只是當(dāng)前進(jìn)程不能再訪問該共享內(nèi)存而已。
shmctl函數(shù)可以對(duì)共享內(nèi)存執(zhí)行多種操作,根據(jù)參數(shù) cmd 執(zhí)行相應(yīng)的操作。常用的是IPC_RMID(從系統(tǒng)中刪除該共享內(nèi)存)。