第十五章:進程間通信


 

Unix系統早期一般使用多進程解決問題。有時多個進程之間需要交互數據,而進程和進程之間不能直接交互數據,因此引入了進程間通信(IPC)。

 

IPC主要包括以下方式:

1. 文件I/O

2. 信號

3. 管道

4. 共享內存

5. 消息隊列

6. 信號量集

7. 網絡socket

其中,共享內存、消息隊列和信號量集都遵循XSI IPC,因此三者編程中有很多共性。管道目前很少使用。

 

 

一、管道

管道(FIFO或PIPE)之所以目前很少使用,是由於他有兩點局限性:

1. 一般為半雙工(即同一時刻,數據只能在一個方向流動,也就是不能同時傳輸)。

2. 管道只能在兩個有共同祖先進程的兩個進程之間使用。

 

管道分為有名管道(有文件名)和無名管道(沒有名字)。有名管道就是自己創建管道文件,然后進行交互。無名管道就是系統幫我們創建管道文件,利用系統的管道文件進行交互。

也就是有名管道適用於所有的進程的通信,無名管道只適用於fork()創建的父子進程之間的通信。

 

管道也是一個文件,后綴名為.pipe。我們可以使用mkfifo命令或mkfifio()函數創建一個有名管道文件:

$ mkfifo a.pipe

mkfifo函數定義如下:

#include<sys/types.h>
#include<sys/stat.h>

int mkfifo(const char * pathname,mode_t mode);

/* 示例 */
mkfifo("a.pipe", 0777);    /* 成功返回0,失敗返回-1,錯誤代碼存在errno中 */

 

讀管道示例代碼如下: 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 
 6 int main()
 7 {
 8     int fd = open("a.pipe", O_RDONLY);
 9     if (fd == -1)
10         perror("open"), exit(-1);
11 
12     int x = 0;
13     while (1) {
14         int res = read(fd, &x, sizeof(x));
15         if (res == -1) {
16             perror("read");
17             close(fd);
18             exit(-1);
19         }
20         if (!res) break;
21         printf("x=%d\n", x);
22     }
23     close(fd);
24 
25     return 0;
26 }
View Code

寫管道示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 
 6 int main()
 7 {
 8 //    int fd = open("a.pipe", O_RDWR);    // 讀寫管道
 9     int fd = open("a.pipe", O_WRONLY);
10     if (fd == -1)
11         perror("open"), exit(-1);
12     
13     int x;
14     for (x = 0; x < 100; x++) {
15         int res = write(fd, &x, 4);
16         if (res == -1) {
17             perror("write");
18             close(fd);
19             exit(-1);
20         }
21     }
22     close(fd);
23 
24     return 0;
25 }
View Code

 

在創建完成管道文件a.pipe后,先后執行兩代碼,讀管道程序會打印x=1到x=100。

 

 

二、XSI IPC

XSI IPC的固定步驟:

1. 創建或獲取IPC結構之前,必須提供一個外部提供的key

2. 每個IPC結構都有一個唯一的ID與之對應,使用key可以獲取ID

3. 外部key的類型是key_t,獲得key的方式有以下三種:

a. 使用宏IPC_PRIVATE做key。這種方式一般不使用,因為這個key只能創建,不能獲取

b. 使用ftok()函數創建key

c. 在公共的頭文件中定義每個IPC結構使用的key,key本身是一個整數

4. xxxget()函數可以使用key創建或獲得ID,比如:

shmget()/msgget()

5. 使用xxxget()函數新建IPC結構,參數flag一般為IPC_CREAT | 權限

6. 每種IPC結構都提供了一個xxxctl()函數,此函數可以修改、刪除和查詢IPC結構

7. xxxctl()函數中,cmd支持以下宏:

IPC_STAT:用於查詢

IPC_SET:用於修改權限

IPC_RMID:用於刪除

 

由上分析,ftok()是XSI IPC通用代碼,其函數定義如下:

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);

 


函數參數以及返回值:

pathname:路徑名,真實存在即可

proj_id:項目ID,就是0到255的任一整數

返回值:成功返回key;出錯返回(key_t)-1。


 

 

三、共享內存

共享內存允許兩個或多個進程共享一個給定的存儲區。因為數據不需要在客戶進程和服務器進程之間復制,所以這是最快的一種IPC。

其類函數定義如下:

#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>

/* 1. 創建共享內存ID */
int shmget(key_t key, size_t size, int flag);  /* size表示新建的共享內存大小,只獲取共享內存時指定為0 */

/* 2. 映射共享內存 */
void *shmat(int shmid, const void *shmaddr, int shmflg);  
             /* shmaddr,一般設為NULL */
              /* shmflg定義為SHM_RDONLY為只讀模式,其它為讀寫模式
*/ /* 3. 數據交互 */ /* 4. 解除映射 */ int shmdt(const void *shmaddr); /* 5. 刪除共享內存,buf用於存儲共享內存信息 */ int shmctl(int shmid, int cmd, struct shmid_ds *buf);

 

讀共享內存示例代碼如下: 

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/shm.h>
 5 
 6 int main()
 7 {
 8     key_t key = ftok(".", 100);
 9     if (key == -1)
10         perror("ftok"), exit(-1);
11     
12     /* 獲取共享內存,size和flag指定為0 */
13     int shmid = shmget(key, 0, 0);
14     if (shmid == -1)
15         perror("shmget"), exit(-1);
16     
17     /* 讀取共享內存數據 */
18     int* p = shmat(shmid, 0, 0);
19     printf("*p=%d\n", *p);
20     
21     shmdt(p);
22 
23     return 0;
24 }
View Code

寫共享內存示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <sys/ipc.h>
 5 #include <sys/shm.h>
 6 
 7 int main()
 8 {
 9     key_t key = ftok(".", 100);
10     if (key == -1)
11         perror("ftok"), exit(-1);
12     
13     int shmid = shmget(key, 4, IPC_CREAT | 0666);
14     if (shmid == -1)
15         perror("shmget"), exit(-1);
16 
17     void* p = shmat(shmid, 0, 0);            // 讀寫模式
18     int* pi = p;
19     *pi = 10;
20     shmdt(p);
21     
22     sleep(10);
23     
24     struct shmid_ds ds;
25     shmctl(shmid, IPC_STAT, &ds);            // 查詢
26     printf("shmid=%d\n", shmid);            // id
27     printf("size=%d\n", ds.shm_segsz);        // 大小 
28     printf("nattch=%d\n", ds.shm_nattch);    // 權限
29     printf("mode=%d\n", ds.shm_perm.mode);    // 掛接數
30 
31     ds.shm_segsz = 400;                        // 不能改
32     ds.shm_perm.mode = 0644;                // 能改
33     shmctl(shmid, IPC_SET, &ds);            // 修改,只能修改權限
34     shmctl(shmid, IPC_RMID, 0);                // 刪除
35 
36     return 0;
37 }
View Code

執行兩示例需要注意的是,寫示例需要先執行。如果讀示例先執行,會導致shmget()函數出錯。

 

共享內存的缺點是若多個進程同時寫,會導致讀出的數據完全混亂。

 

 

四、消息隊列

相比共享內存,消息隊列設計更加的合理。消息隊列也是采用內存做交互媒介,它先把數據傳入消息中,再把消息存入隊列。也就是從隊列中取出或放入數據。

 

為了克服共享內存的缺點,消息隊列引入了無類型消息和有類型消息。有類型消息需要定義為結構體,示例如下:

struct mymsg               // 結構名稱可以自定義
{
    long mtype;             // 消息類型,結構體的第一個成員必須是它
    char mtext[512];    // 數據區,支持任意類型的數據
};

 

消息隊列類函數定義如下: 

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

/* 1. 創建消息隊列ID */
int msgget(key_t key, int flag);

/* 2. 數據交互 */
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

/* 3. 刪除消息隊列 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

 


msgsnd()和msgrcv()函數參數以及返回值:

msqid:消息隊列ID

msgp:消息結構體指針

msgsz:消息結構體的數據部分大小

msgtyp:指定消息類型

> 0,接受特定類型的消息

= 0,接受任意類型的消息

< 0,接受類型小於等於msgtyp絕對值的消息,接收順序按消息結構體中mtype的數值從小到大

msgflg:0表示阻塞,IPC_NOWAIT非阻塞

返回值:msgsnd()成功返回0;出錯返回-1。msgrcv()成功返回消息結構體的數據部分大小;出錯返回-1。


 

讀消息隊列示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/msg.h>
 5 #include <string.h>
 6 
 7 struct Msg
 8 {
 9     long mtype;
10     char buf[256];
11 };
12 
13 int main()
14 {
15     key_t key = ftok(".", 100);
16     
17     int msgid = msgget(key, 0);
18     if (msgid == -1)
19         perror("msgget"), exit(-1);
20 
21     struct Msg msg;
22 
23     int res = msgrcv(msgid, &msg, sizeof(msg.buf), -2, 0);
24     
25     if (res == -1)
26         perror("msgrcv"), exit(-1);
27     
28     else
29         printf("%s\n", msg.buf);
30 
31     return 0;
32 }

寫消息隊列示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/msg.h>
 5 #include <string.h>
 6 
 7 struct Msg
 8 {
 9     long mtype;
10     char buf[256];
11 };
12 
13 int main()
14 {
15     key_t key = ftok(".", 100);
16     
17     int msgid = msgget(key, IPC_CREAT | 0666);
18     if (msgid == -1)
19         perror("msgget"), exit(-1);
20     
21     struct Msg msg1, msg2;
22     msg1.mtype = 1;
23     strcpy(msg1.buf, "Hello ");
24     
25     msg2.mtype = 2;
26     strcpy(msg2.buf, "World");
27 
28     msgsnd(msgid, &msg1, sizeof(msg1.buf), 0);
29     msgsnd(msgid, &msg2, sizeof(msg2.buf), 0);
30 
31     return 0;
32 }

 

 

五、信號量

信號量是一個計數器,負責控制訪問共享資源的最大並行進程總數。

信號量初始時設為最大值,每進入一個進程計數器-1,每離開一個進程計數器+1,當計數器到0時進程阻塞,直到計數器重新大於0則解除阻塞。

 

如果有多個共享資源需要控制最大並行進程數,則需要多個信號量。信號量集用於存儲多個信號量。IPC拿到的是信號量集,而不是單一的信號量。

 

信號量集類函數定義如下:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 1. 創建信號量集ID */
int semget(key_t key, int nsems, int flag);

/* 2. 為每個信號量設置初始的最大值 */
int semctl(int semid, int semnum, int cmd, ...);

/* 3. 計數 +1 或 -1 */
int semop(int semid, struct sembuf *sops, size_t nsops);

/* 4. 刪除信號量集 */
int semctl(int semid, int semnum, int cmd, ...);

 

semop()函數的sops定義如下:

struct sembuf
{
    unsigned short sem_num;    // 操作信號量的下標
    short sem_op;    // -1代表計數減1,1代表加1
    short sem_flg;    // 0代表阻塞,IPC_NOWAIT非阻塞
};

 

信號量集一般用於控制某類事務的數量。在此以控制子進程並發個數為例。

信號量控制示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/sem.h>
 5 #include <signal.h>
 6 
 7 int semid;
 8 
 9 void sig_exit(int signo)
10 {
11     printf("准備刪除信號量集\n");
12     semctl(semid, 0, IPC_RMID, NULL);
13     exit(0);
14 }
15 
16 int main()
17 {
18     printf("用Ctrl + C退出\n");
19     key_t key = ftok(".", 100);
20     
21     semid = semget(key, 1, IPC_CREAT | 0666);    // 1個元素
22     if (semid == -1)
23         perror("semget"),exit(-1);
24 
25     int res = semctl(semid, 0, SETVAL, 5);        // 最多支持5個並行進程
26     if (!res)
27         printf("信號量集成功創建\n");
28 
29     signal(SIGINT, sig_exit);
30 
31     while(1);
32 
33     return 0;
34 }
View Code

進程並行示例代碼如下:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/ipc.h>
 4 #include <sys/sem.h>
 5 #include <unistd.h>
 6 
 7 int main()
 8 {
 9     key_t key = ftok(".", 100);
10     
11     int semid = semget(key, 0, 0);    // 獲取不新建
12     if (semid == -1)
13         perror("semget"),exit(-1);
14 
15     int i;
16     for (i = 0; i < 10; i++) {
17         pid_t pid = fork();
18         if (!pid) {
19             printf("申請一個計數\n");
20             
21             struct sembuf buf;
22             buf.sem_num = 0;
23             buf.sem_op = -1;        // 計數 -1
24             buf.sem_flg = 0;        // 阻塞
25             
26             semop(semid, &buf, 1);    // 數組就是首元素的地址
27             printf("申請成功\n");
28             
29             sleep(5);
30             
31             buf.sem_op = 1;            // 計數 +1
32             printf("釋放一個計數\n");
33             
34             semop(semid, &buf, 1);    // 數組就是首元素的地址
35             exit(0);
36         }
37     }
38 }
View Code

 

首先執行信號量控制程序,再執行進程並發程序。可以發現首先會有5個進程執行,另外5個進程等待前5個進程結束后執行。

 

 

下一章  第十六章:網絡IPC 套接字

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM