進程間通信的各種模式
- Pipe
- Message Queue
- Shared Memory
管道
還記得咱們最初學 Linux 命令的時候,有下面這樣一行命令:
ps -ef | grep 關(guān)鍵字 | awk '{print $2}' | xargs kill -9
這里面的豎線“|”就是一個管道溺职。它會將前一個命令的輸出隆嗅,作為后一個命令的輸入霍弹。從管道的這個名稱可以看出來谒撼,管道是一種單向傳輸數(shù)據(jù)的機制,它其實是一段緩存熄捍,里面的數(shù)據(jù)只能從一端寫入烛恤,從另一端讀出。如果想互相通信余耽,我們需要創(chuàng)建兩個管道才行棒动。管道分為兩種類型,“|” 表示的管道稱為匿名管道宾添,意思就是這個類型的管道沒有名字船惨,用完了就銷毀了。就像上面那個命令里面的一樣缕陕,豎線代表的管道隨著命令的執(zhí)行自動創(chuàng)建粱锐、自動銷毀。用戶甚至都不知道自己在用管道這種技術(shù)扛邑,就已經(jīng)解決了問題怜浅。所以這也是面試題里面經(jīng)常會問的,到時候千萬別說這是豎線蔬崩,而要回答背后的機制恶座,管道。
另外一種類型是命名管道沥阳。這個類型的管道需要通過 mkfifo
命令顯式地創(chuàng)建跨琳。
mkfifo hellohello
就是這個管道的名稱。管道以文件的形式存在桐罕,這也符合 Linux 里面一切皆文件的原則脉让。這個時候桂敛,我們 ls 一下,可以看到溅潜,這個文件的類型是 p术唬,就是 pipe 的意思。
ls -lprw-r--r-- 1 root root 0 May 21 23:29 hello
接下來滚澜,我們可以往管道里面寫入東西粗仓。例如,寫入一個字符串设捐。
echo "hello world" > hello
這個時候借浊,管道里面的內(nèi)容沒有被讀出,這個命令就是停在這里的挡育,這說明當一個項目組要把它的輸出交接給另一個項目組做輸入巴碗,當沒有交接完畢的時候,前一個項目組是不能撒手不管的即寒。這個時候橡淆,我們就需要重新連接一個終端。在終端中母赵,用下面的命令讀取管道里面的內(nèi)容:
cat < hello hello world
一方面逸爵,我們能夠看到,管道里面的內(nèi)容被讀取出來凹嘲,打印到了終端上师倔;另一方面,echo 那個命令正常退出了周蹭,也即交接完畢趋艘,前一個項目組就完成了使命,可以解散了凶朗。
消息隊列
和管道將信息一股腦兒地從一個進程瓷胧,倒給另一個進程不同,消息隊列有點兒像郵件棚愤,發(fā)送數(shù)據(jù)時搓萧,會分成一個一個獨立的數(shù)據(jù)單元,也就是消息體宛畦,每個消息體都是固定大小的存儲塊瘸洛,在字節(jié)流上不連續(xù)。
消息結(jié)構(gòu)的定義
struct msg_buffer { long mtype; char mtext[1024];};
接下來次和,我們需要創(chuàng)建一個消息隊列反肋,使用 msgget 函數(shù)。這個函數(shù)需要有一個參數(shù) key斯够,這是消息隊列的唯一標識囚玫,應該是唯一的喧锦。如何保持唯一性呢读规?這個還是和文件關(guān)聯(lián)抓督。我們可以指定一個文件,ftok 會根據(jù)這個文件的 inode束亏,生成一個近乎唯一的 key铃在。只要在這個消息隊列的生命周期內(nèi),這個文件不要被刪除就可以了碍遍。只要不刪除定铜,無論什么時刻,再調(diào)用 ftok怕敬,也會得到同樣的 key揣炕。這種 key 的使用方式在這一章會經(jīng)常遇到,這是因為它們都屬于 System V IPC 進程間通信機制體系中东跪。
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
int main() {
int messagequeueid;
key_t key;
if((key = ftok("/root/messagequeue/messagequeuekey", 1024)) < 0)
{
perror("ftok error");
exit(1);
}
printf("Message Queue key: %d.\n", key);
if ((messagequeueid = msgget(key, IPC_CREAT|0777)) == -1)
{
perror("msgget error");
exit(1);
}
printf("Message queue id: %d.\n", messagequeueid);
}
ipcs -q
就能看到上面我們創(chuàng)建的消息隊列對象畸陡。
------ Message Queues --------
key msqid owner perms used-bytes messages
0x00288297 0 root 777 0 0
發(fā)送消息主要調(diào)用 msgsnd 函數(shù)。第一個參數(shù)是 message queue 的 id虽填,第二個參數(shù)是消息的結(jié)構(gòu)體丁恭,第三個參數(shù)是消息的長度,最后一個參數(shù)是 flag斋日。這里 IPC_NOWAIT 表示發(fā)送的時候不阻塞牲览,直接返回。
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <getopt.h>
#include <string.h>
struct msg_buffer {
long mtype;
char mtext[1024];
};
int main(int argc, char *argv[]) {
int next_option;
const char* const short_options = "i:t:m:";
const struct option long_options[] = {
{ "id", 1, NULL, 'i'},
{ "type", 1, NULL, 't'},
{ "message", 1, NULL, 'm'},
{ NULL, 0, NULL, 0 }
};
int messagequeueid = -1;
struct msg_buffer buffer;
buffer.mtype = -1;
int len = -1;
char * message = NULL;
do {
next_option = getopt_long (argc, argv, short_options, long_options, NULL);
switch (next_option)
{
case 'i':
messagequeueid = atoi(optarg);
break;
case 't':
buffer.mtype = atol(optarg);
break;
case 'm':
message = optarg;
len = strlen(message) + 1;
if (len > 1024) {
perror("message too long.");
exit(1);
}
memcpy(buffer.mtext, message, len);
break;
default:
break;
}
}while(next_option != -1);
if(messagequeueid != -1 && buffer.mtype != -1 && len != -1 && message != NULL){
if(msgsnd(messagequeueid, &buffer, len, IPC_NOWAIT) == -1){
perror("fail to send message.");
exit(1);
}
} else {
perror("arguments error");
}
return 0;
}
收消息主要調(diào)用 msgrcv 函數(shù)恶守,第一個參數(shù)是 message queue 的 id第献,第二個參數(shù)是消息的結(jié)構(gòu)體,第三個參數(shù)是可接受的最大長度兔港,第四個參數(shù)是消息類型, 最后一個參數(shù)是 flag庸毫,這里 IPC_NOWAIT 表示接收的時候不阻塞,直接返回押框。
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <getopt.h>
#include <string.h>
struct msg_buffer {
long mtype;
char mtext[1024];
};
int main(int argc, char *argv[]) {
int next_option;
const char* const short_options = "i:t:";
const struct option long_options[] = {
{ "id", 1, NULL, 'i'},
{ "type", 1, NULL, 't'},
{ NULL, 0, NULL, 0 }
};
int messagequeueid = -1;
struct msg_buffer buffer;
long type = -1;
do {
next_option = getopt_long (argc, argv, short_options, long_options, NULL);
switch (next_option)
{
case 'i':
messagequeueid = atoi(optarg);
break;
case 't':
type = atol(optarg);
break;
default:
break;
}
}while(next_option != -1);
if(messagequeueid != -1 && type != -1){
if(msgrcv(messagequeueid, &buffer, 1024, type, IPC_NOWAIT) == -1){
perror("fail to recv message.");
exit(1);
}
printf("received message type : %d, text: %s.", buffer.mtype, buffer.mtext);
} else {
perror("arguments error");
}
return 0;
}
接下來岔绸,我們可以編譯并運行這個發(fā)送程序∠鹕。可以看到盒揉,如果有消息,可以正確地讀到消息兑徘;如果沒有刚盈,則返回沒有消息。
# ./recv -i 0 -t 123
received message type : 123, text: hello world.
# ./recv -i 0 -t 123
fail to recv message.: No message of desired type
共享內(nèi)存
有時候挂脑,項目組之間的溝通需要特別緊密藕漱,而且要分享一些比較大的數(shù)據(jù)欲侮。如果使用郵件,就發(fā)現(xiàn)肋联,一方面郵件的來去不及時威蕉;另外一方面,附件大小也有限制橄仍,所以韧涨,這個時候,我們經(jīng)常采取的方式就是侮繁,把兩個項目組在需要合作的期間虑粥,拉到一個會議室進行合作開發(fā),這樣大家可以直接交流文檔呀宪哩,架構(gòu)圖呀娩贷,直接在白板上畫或者直接扔給對方,就可以直接看到锁孟”蜃妫可以看出來,共享會議室這種模型罗岖,類似進程間通信的共享內(nèi)存模型涧至。前面咱們講內(nèi)存管理的時候,知道每個進程都有自己獨立的虛擬內(nèi)存空間桑包,不同的進程的虛擬內(nèi)存空間映射到不同的物理內(nèi)存中去南蓬。這個進程訪問 A 地址和另一個進程訪問 A 地址,其實訪問的是不同的物理內(nèi)存地址哑了,對于數(shù)據(jù)的增刪查改互不影響赘方。但是,咱們是不是可以變通一下弱左,拿出一塊虛擬地址空間來窄陡,映射到相同的物理內(nèi)存中。這樣這個進程寫入的東西拆火,另外一個進程馬上就能看到了跳夭,都不需要拷貝來拷貝去,傳來傳去们镜。共享內(nèi)存也是 System V IPC 進程間通信機制體系中的币叹,所以從它使用流程可以看到熟悉的面孔。
int shmget(key_t key, size_t size, int flag);
創(chuàng)建完畢之后模狭,我們可以通過 ipcs 命令查看這個共享內(nèi)存颈抚。
#ipcs ----shmems
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x0000162e 65594 root 666 27 0
接下來,如果一個進程想要訪問這一段共享內(nèi)存嚼鹉,需要將這個內(nèi)存加載到自己的虛擬地址空間的某個位置贩汉,通過 shmat 函數(shù)驱富,就是 attach 的意思。其中 addr 就是要指定 attach 到這個地方匹舞。但是這個地址的設(shè)定難度比較大褐鸥,除非對于內(nèi)存布局非常熟悉,否則可能會 attach 到一個非法地址策菜。所以晶疼,通常的做法是將 addr 設(shè)為 NULL酒贬,讓內(nèi)核選一個合適的地址又憨。返回值就是真正被 attach 的地方。
void *shmat(int shm_id, const void *addr, int flag);
如果共享內(nèi)存使用完畢锭吨,可以通過 shmdt 解除綁定蠢莺,然后通過 shmctl,將 cmd 設(shè)置為 IPC_RMID零如,從而刪除這個共享內(nèi)存對象躏将。
int shmdt(void *addr); int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
shm_server.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#define SHMSZ 27
main()
{
char c;
int shmid;
key_t key;
char *shm, *s;
/*
* We'll name our shared memory segment
* "5678".
*/
key = 5678;
/*
* Create the segment.
*/
if ((shmid = shmget(key, SHMSZ, IPC_CREAT | 0666)) < 0) {
perror("shmget");
exit(1);
}
/*
* Now we attach the segment to our data space.
*/
if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {
perror("shmat");
exit(1);
}
/*
* Now put some things into the memory for the
* other process to read.
*/
s = shm;
for (c = 'a'; c <= 'z'; c++)
*s++ = c;
*s = NULL;
/*
* Finally, we wait until the other process
* changes the first character of our memory
* to '*', indicating that it has read what
* we put there.
*/
while (*shm != '*')
sleep(1);
exit(0);
}
shm_client.c
/*
* shm-client - client program to demonstrate shared memory.
*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#define SHMSZ 27
main()
{
int shmid;
key_t key;
char *shm, *s;
/*
* We need to get the segment named
* "5678", created by the server.
*/
key = 5678;
/*
* Locate the segment.
*/
if ((shmid = shmget(key, SHMSZ, 0666)) < 0) {
perror("shmget");
exit(1);
}
/*
* Now we attach the segment to our data space.
*/
if ((shm = shmat(shmid, NULL, 0)) == (char *) -1) {
perror("shmat");
exit(1);
}
/*
* Now read what the server put in the memory.
*/
for (s = shm; *s != NULL; s++)
putchar(*s);
putchar('\n');
/*
* Finally, change the first character of the
* segment to '*', indicating we have read
* the segment.
*/
*shm = '*';
exit(0);
}
test:
./shm_server
./shm_client
abcdefghijklmnopqrstuvwxyz