程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> (十二) 一起學 Unix 環境高級編程 (APUE) 之 進程間通信(IPC),apueipc

(十二) 一起學 Unix 環境高級編程 (APUE) 之 進程間通信(IPC),apueipc

編輯:關於C語言

(十二) 一起學 Unix 環境高級編程 (APUE) 之 進程間通信(IPC),apueipc


.

.

.

.

.

目錄

(一) 一起學 Unix 環境高級編程 (APUE) 之 標准IO

(二) 一起學 Unix 環境高級編程 (APUE) 之 文件 IO

(三) 一起學 Unix 環境高級編程 (APUE) 之 文件和目錄

(四) 一起學 Unix 環境高級編程 (APUE) 之 系統數據文件和信息

(五) 一起學 Unix 環境高級編程 (APUE) 之 進程環境

(六) 一起學 Unix 環境高級編程 (APUE) 之 進程控制

(七) 一起學 Unix 環境高級編程 (APUE) 之 進程關系 和 守護進程

(八) 一起學 Unix 環境高級編程 (APUE) 之 信號

(九) 一起學 Unix 環境高級編程 (APUE) 之 線程

(十) 一起學 Unix 環境高級編程 (APUE) 之 線程控制

(十一) 一起學 Unix 環境高級編程 (APUE) 之 高級 IO

(十二) 一起學 Unix 環境高級編程 (APUE) 之 進程間通信(IPC)

 

 

在第一篇博文中我們介紹過,進程間通信(IPC) 分為 PIPE(管道)、Socket(套接字) 和 XSI(System V)。XSI 又分為 msg(消息隊列)、sem(信號量數組) 和 shm(共享內存)。

這些手段都是用於進程間通訊的,只有進程間通訊才需要借助第三方機制,線程之間的通訊是不需要借助第三方機制的,因為線程之間的地址空間是共享的。

管道分為命名管道(FIFO)和匿名管道(PIPE),無論是哪種管道,都是由內核幫你創建和維護的。

管道的特點:

1.管道是半雙工的,即同一時間數據只能從一端流向另一端。試想,如果一個管道從兩邊同時輸入和輸出數據,那麼管道裡的數據就亂了。

2.管道的兩端一端作為讀端,一端作為寫端。

3.管道具有自適應的特點,默認會適應速度比較慢的一方,管道被寫滿或讀空時速度快的一方會自動阻塞。

 

pipe(2) 函數

1 pipe - create pipe
2 
3 #include <unistd.h>
4 
5 int pipe(int pipefd[2]);

 

pipe(2) 用於創建管道,pipefd 是一個數組,表示管道的兩端文件描述符,pipefd[0] 端作為讀端,pipefd[1] 端作為寫端

pipe(2) 產生的是匿名管道,在磁盤的任何位置上找不到這個管道文件,而且匿名管道只能用於具有親緣關系的進程之間通信

一般情況有親緣關系的進程之間使用管道進行通信時,會把自己不用的一端文件描述符關閉。

下面是創建匿名管道在父子進程之間傳送了一個字符串“Hello”的小栗子。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 
 5 #include <sys/types.h>
 6 #include <sys/wait.h>
 7 
 8 #define BUFSIZE        1024
 9 
10 int main()
11 {
12     int pd[2];
13     char buf[BUFSIZE];
14     pid_t pid;
15     int len;
16 
17     // 創建匿名管道
18     if(pipe(pd) < 0)
19     {
20         perror("pipe()");
21         exit(1);
22     }
23 
24     // 創建子進程
25     pid = fork();
26     if(pid < 0)
27     {
28         perror("fork()");
29         exit(1);
30     }
31     if(pid == 0) { // 子進程 讀取管道數據
32         // 關閉寫端
33         close(pd[1]);
34         // 從管道中讀取數據,如果子進程比父進程先被調度會阻塞等待數據寫入
35         len = read(pd[0],buf,BUFSIZE);
36         puts(buf);
37         /* 管道是 fork(2) 之前創建的,
38          * 父子進程裡都有一份,
39          * 所以退出之前要確保管道兩端都關閉
40          */
41         close(pd[0]);
42         exit(0);
43     } else { // 父進程 向管道寫入數據
44         close(pd[0]);
45         write(pd[1],"Hello!",6);
46         close(pd[1]);
47         wait(NULL);
48         exit(0);
49     }
50 }

在上面的栗子中,父進程創建了一個匿名管道,在 pd[2] 數組中湊齊了讀寫雙方,子進程同樣繼承了具有讀寫雙方的數組 pd[2]。

父進程先關閉管道的讀端然後向管道中寫入數據,然後將用完的寫端也關閉,等待子進程消亡並為其收屍。

子進程先關閉管道的寫端然後讀取父進程寫入管道的字符串,把它打印到控制台之後再關閉用完的讀端,然後退出。

這個程序在 fork(2) 之後如果是子進程先運行,那麼會阻塞在 read(2) 階段,等待父進程被調度並向管道中寫入數據。

如果在 fork(2) 之後是父進程先運行,那麼父進程會阻塞在 wait(2) 階段等待子進程運行結束。

所以無論是誰先運行,只要沒有出現異常運行的結果都是我們預期之內的。

 

mkfifo(3) 函數

1 mkfifo - make a FIFO special file (a named pipe)
2 
3 #include <sys/types.h>
4 #include <sys/stat.h>
5 
6 int mkfifo(const char *pathname, mode_t mode);

mkfifo(3) 函數用於創建命名管道,作用與匿名管道相同,不過可以在不同的進程之間使用,相當於對一個普通文件進行讀寫操作就可以了。

這個管道文件是任何有權限的進程都可以使用的,兩端都像操作一個普通文件一樣對它進行打開、讀寫、關閉動作就可以了,只要一端寫入數據另一端就可以都出來。

但是最好一端只讀一端只寫,否則在實際項目中你很難保證拿出的不是髒數據(自己寫進去數據的混合著另一端寫進去的數據被其中一端讀了出來),除非像下面那個栗子那樣結構簡單清晰。

參數列表:

  pathname:管道文件的路徑和文件名。

  mode:創建管道文件的權限。還是老規矩,傳入的 mode 值要與系統的 umask 值做運算:(mode & ~umask)

返回值:成功返回 0,失敗返回 -1 並設置 errno。

我們看下面的栗子是如何使用命名管道通訊的:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <unistd.h>
 4 #include <fcntl.h>
 5 
 6 #include <sys/types.h>
 7 #include <sys/stat.h>
 8 
 9 #define PATHNAME    "/tmp/myfifo"
10 
11 int main (void)
12 {
13     pid_t pid;
14     int fd = -1;
15     char buf[BUFSIZ] = "";
16 
17     // 創建一個命名管道,大家可以用 ls -l 命令查看這個管道文件的屬性
18     if (mkfifo(PATHNAME, 0644) < 0) {
19         perror("mkfifo()");
20         exit(1);
21     }
22 
23     fflush(NULL);
24     pid = fork();
25     if (pid < 0) {
26         perror("fork()");
27         exit(1);
28     }
29     if (!pid) { // parent
30         pid = fork();
31         if (pid < 0) {
32             perror("fork()");
33             exit(1);
34         }
35         if (!pid) { // parent
36             // 兩個子進程都創建完之後父進程直接退出,使兩個子進程不具有親緣關系。
37             exit(0);
38         }
39         /* child 2 */
40         /* 像操作普通文件一樣對這個管道進行 open(2)、read(2)、write(2)、close(2) */
41         fd = open(PATHNAME, O_RDWR);
42         if (fd < 0) {
43             perror("open()");
44             exit(1);
45         }
46         read(fd, buf, BUFSIZ);
47         printf("%s", buf);
48         write(fd, " World!", 8);
49         close(fd);
50         exit(0);
51     } else { // child 1
52         fd = open(PATHNAME, O_RDWR);
53         if (fd < 0) {
54             perror("open()");
55             exit(1);
56         }
57         write(fd, "Hello", 6);
58         sleep(1); // 剛寫完管道不要馬上讀,等第二個進程讀取完並且寫入新數據之後再讀。
59         read(fd, buf, BUFSIZ);
60         close(fd);
61         puts(buf);
62         // 肯定是這個進程最後退出,所以把管道文件刪除,不然下次再創建的時候會報文件已存在的錯誤
63         remove(PATHNAME);
64         exit(0);
65     }
66 
67     return 0;
68 }

這段代碼很簡單,父進程首先在磁盤上創建一個命名管道文件,然後創建兩個子進程後退出。每個子進程都對管道文件進行一次讀和一次寫的動作,然後子進程退出,整個過程就結束了。

第一個子進程在寫完管道之後要先休眠,等待第二個子進程從管道的另一端把數據讀入並寫入新的數據,第一個子進程再醒來讀出管道的內容。如果第一個子進程不休眠而是在寫完之後馬上讀管道,很可能在它寫完管道之後第二個子進程還沒來得及調度,它自己就又把管道裡的數據讀出來了,這樣不僅讀出來的不是第二個子進程寫入的數據,還會導致第二個子進程永遠阻塞在 read(2) 階段,因為管道中不會再有數據寫入。

管道大家都會用了嗎?看上去不是很難是吧,趕快自己動手寫寫試試吧。

 

協同進程

這一小節主要是說管道是半雙工的,兩個進程一個只能對它讀,另一個只能對它寫,否則會出現髒數據,也就是無法區分出讀出來的數據是來自於自己的還是來自於另一個進程的。

如果想要實現雙工,必須采用兩個管道,一個進程對一個管道只讀,對另一個管道只寫。

明白了這個原理,相信大家可以利用上面的 pipe(2) 或 mkfifo(3) 函數利用兩個半雙工管道實現進程之間的全雙工通訊。

栗子 LZ 就不寫了,大家自己多動手練練吧,遇到問題可以貼到評論裡與 LZ 討論。

 

XSI IPC

XSI IPC 是 System V 規范裡面的進程間通信手段,而不是 POSIX 標准的。關於 System V、POSIX 等等的前世今生大家可以自行 Google 一下,網上 balabalabala...... 一大堆什麼都有,LZ 就不把那些東西放在這裡了。

在學習 XSI IPC 之前,我們先來認識兩條命令:

ipcs(1) 命令可以查看 XSI IPC 的使用情況。

ipcrm(1) 命令可以刪除指定的 XSI IPC。

為什麼要先介紹這兩個命令呢?如果實驗的過程中需要查看 IPC 是否建立成功,可以通過 ipcs(1) 命令,如果實驗出問題了,則可以使用 ipcrm(1) 命令刪除錯誤的 IPC。

>$ ipcs

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      
0x00000000 32769      usera      600        393216     2          dest         
0x00000000 65538      usera      600        393216     2          dest         
0x00000000 98307      usera      600        393216     2          dest         
0x00000000 131076     usera      600        393216     2          dest         
0x00000000 163845     usera      600        393216     2          dest         
0x00000000 196614     usera      600        393216     2          dest         
0x00000000 229383     usera      600        393216     2          dest         
0x00000000 262152     usera      600        393216     2          dest         
0x00000000 294921     usera      600        393216     2          dest         
0x00000000 327690     usera      600        393216     2          dest         
0x00000000 360459     usera      600        393216     2          dest         
0x00000000 393228     usera      600        393216     2          dest         
0x00000000 425997     usera      600        393216     2          dest         
0x00000000 458766     usera      600        393216     2          dest         
0x00000000 491535     usera      600        393216     2          dest         
0x00000000 524304     usera      600        393216     2          dest         
0x00000000 557073     usera      600        393216     2          dest         
0x00000000 589842     usera      600        393216     2          dest         
0x00000000 655380     usera      600        393216     2          dest         

------ Semaphore Arrays --------
key        semid      owner      perms      nsems     

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages  

>$

 

通過 ipcs(1) 命令可以看出來,命令的輸出結果分為三個部分,第一部分是系統中當前開辟的共享內存(shm),第二部分是信號量數組(sem),第三部分是消息隊列(msg)

可以看到,不論是哪一部分,都有一列叫做“key”,沒錯,使用 XSI IPC 通信的進程就是通過同一個 key 值操作同一個共享資源的。這個 key 是一個正整數,與文件描述符不同的是,生成一個新 key 值時它不采用當前可用數值中的最小值,而是類似生成進程 ID 的方式,key 值連續的加 1,直至達到一個整數的最大正值,然後再回轉到 0 從頭開始累加。

 

 

XSI 消息隊列(msg)

消息隊列可以讓通信雙方傳送結構體數據,這樣也提高了傳送數據的靈活性。

既然是通訊,那麼在通信之前就要先在通信雙方約定通信協議,協議就是通信雙方約定的數據交換格式。

從消息隊列開始一直到 Socket,我們都會看到比較類似的程序架構,因為無論是消息隊列還是 Socket,它們都需要約定通信協議,而且都是按照一定的步驟才能實現通訊。

消息隊列在約定協議的時候,在我們自己定義的結構體裡要強制添加一個 long mtype; 成員。這個成員的作用是用於區分多種消息類型中的不同類型的數據包,當只有一種類型的包時這個成員沒什麼用,但是也必須得帶上。

 

既然是通訊,那麼就要區分主動端(先發包的一方)和被動端(先收包的一方,先運行),它們運行的時機不同,作用不同,甚至調用的函數也不同,所以我們的後面的每個栗子幾乎都要編譯出 2 個不同的可執行程序來測試。

前面說了,學到這裡操作基本上都是按部就班的了,所以 msg、sem 和 shm 都有一系列函數遵循下面的命名規則:

xxxget() // 創建

xxxop() // 相關操作

xxxctl() // 其它的控制或銷毀

下面我們看看消息隊列的創建函數:msgget(2)

1 msgget - get a System V message queue identifier
2 
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/msg.h>
6 
7 int msgget(key_t key, int msgflg);

msgget(2) 函數的作用是創建一個消息隊列,消息讀列是雙工的,兩邊都可以讀寫。

參數列表:

  key 相當於通信雙方的街頭暗號,擁有相同 key 的雙方才可以通信。

  key 值必須是唯一的,系統中有個 ftok(3) 函數可以用於獲取 key,通過文件 inode 和 salt 進行 hash 運算來生成唯一的 key,只要兩個進程使用相同的文件和 salt 就可以生成一樣的 key 值了。

  msgflg:特殊要求。無論有多少特殊要求,只要使用了 IPC_CREAT,就必須按位或一個權限,權限也不是你想指定多大就能多大的,還是老規矩,要用它 &= ~umask,這個我們前面提到過。

同一個消息隊列只需要創建一次,所以誰先運行起來誰有責任創建消息隊列,後運行起來的就不需要創建了。

同理,對於後啟動的進程來說,消息隊列不是它創建的,那麼它也沒有必要銷毀了。

 

msgrcv(2) 函數和 msgsnd(2) 函數

 1 msgrcv, msgsnd - message operations
 2 
 3 #include <sys/types.h>
 4 #include <sys/ipc.h>
 5 #include <sys/msg.h>
 6 
 7 int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
 8 
 9 ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
10                int msgflg);
11 
12 /* msgp 成員的定義要類似 msgbuf 這個結構體,第一個成員必須是 long 類型的 mtype,並且必須是 > 0 的值 */
13 struct msgbuf {
14     long mtype;       /* 消息類型,必須 > 0 */
15     char mtext[1];    /* 消息數據字段 */
16 };

msgrcv(2) 函數從 msgid 這個消息隊列中接收數據,並將接收到的數據放到 msgp 結構體中,這段空間有 msgz 個字節大小,msgz 的值要減掉強制的成員 mtype 的大小(sizeof(long))。

msgtyp 是 msgp 結構體中的 mtype 成員,表示要接收哪種類型的消息。雖然 msg 是消息隊列,但是它並不完全遵循隊列的形式,可以讓接收者挑消息接收。如果不挑消息可以填寫 0,這樣就按照隊列中的消息順序返回。

msfglg 是特殊要求位圖,沒有寫0。

msgsnd(2) 函數向 msgid 這個消息隊列發送 msgp 結構體數據,msgp 的大小是 msgsz,msgflg 是特殊要求,沒有特殊要求可以寫 0。 

 

msgctl(2) 函數

1 msgctl - message control operations
2 
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/msg.h>
6 
7 int msgctl(int msqid, int cmd, struct msqid_ds *buf);

msgctl(2) 函數是不是有點眼熟呢?沒錯,跟 iocrtl(2)、fcntl(2) 這種函數的用法很類似。通過 cmd 指定具體的命令,然後通過 buf 為 cmd 命令設定參數,當然有些命令是需要參數的,有些命令則不需要參數。

最長用的 cmd 就是 IPC_RMID,表示刪除(結束)某個 IPC 通信,並且這個命令不需要 buf 參數,直接傳入 NULL 即可。

buf 結構體裡面的成員很多,由於 cmd 一般只有 IPC_RMID 比較常用,所以 LZ 就不把 buf 結構體拿出來挨個成員解釋了,需要用到的童鞋自行查閱一下 man 手冊吧,遇到了什麼問題可以在評論中討論。

 

看下面的小栗子,我們把上面講到的幾個消息隊列的函數使用一遍,在兩個進程之間實現簡單的通訊。

  1 /* proto.h 定義雙方都需要使用的數據或對象 */
  2 #ifndef PROTO_H__
  3 #define PROTO_H__
  4 
  5 #define NAMESIZE        32
  6 
  7 /* 通訊雙方生成 key 值共同使用的文件 */
  8 #define KEYPATH            "/tmp/out"            
  9 /* 通訊雙方生成 key 值共同使用的 salt 值 */
 10 #define KEYPROJ            'a'
 11 
 12 /* 消息類型,只要是大於 0 的合法整數即可 */
 13 #define MSGTYPE            10
 14 
 15 /* 通訊雙方約定的協議 */
 16 struct msg_st
 17 {
 18     long mtype;
 19     char name[NAMESIZE];
 20     int math;
 21     int chinese;
 22 };
 23 
 24 
 25 #endif
 26 
 27 
 28 /******************** rcver.c 接收方 ********************/
 29 #include <stdio.h>
 30 #include <stdlib.h>
 31 #include <sys/types.h>
 32 #include <sys/ipc.h>
 33 #include <sys/msg.h>
 34 
 35 #include "proto.h"
 36 
 37 int main()
 38 {
 39     key_t key;
 40     int msgid;
 41     struct msg_st rbuf;
 42 
 43     // 通過 /tmp/out 文件和字符 'a' 生成唯一的 key,注意文件必須是真實存在的
 44     key = ftok(KEYPATH,KEYPROJ);
 45     if(key < 0)
 46     {
 47         perror("ftok()");
 48         exit(1);
 49     }
 50 
 51     // 接收端應該先啟動,所以消息隊列由接收端創建
 52     msgid = msgget(key,IPC_CREAT|0600);
 53     if(msgid < 0)
 54     {
 55         perror("msgget()");
 56         exit(1);
 57     }
 58 
 59     // 不停的接收消息
 60     while(1)
 61     {
 62         // 沒有消息的時候會阻塞等待
 63         if(msgrcv(msgid,&rbuf,sizeof(rbuf)-sizeof(long),0,0) < 0)
 64         {
 65             perror("msgrcv");
 66             exit(1);
 67         }
 68 
 69         /* 用結構體中強制添加的成員判斷消息類型,
 70          * 當然在這個例子中只有一種消息類型,所以不判斷也可以。
 71          * 如果包含多種消息類型這裡可以寫一組 switch...case 結構
 72          */
 73         if(rbuf.mtype == MSGTYPE)
 74         {
 75             printf("Name = %s\n",rbuf.name);
 76             printf("Math = %d\n",rbuf.math);
 77             printf("Chinese = %d\n",rbuf.chinese);
 78         }
 79     }
 80 
 81     /* 誰創建誰銷毀。
 82      * 當然這個程序是無法正常結束的,只能通過信號殺死。
 83      * 使用信號殺死之後大家可以用 ipcs(1) 命令查看一下,消息隊列應該是沒有被銷毀的,
 84      * 大家可以使用上面我們提到的 ipcrm(1) 命令把它刪掉。
 85      */
 86     msgctl(msgid,IPC_RMID,NULL);
 87 
 88     exit(0);
 89 }
 90 
 91 
 92 
 93 
 94 /******************** snder.c 發送方 ********************/
 95 #include <stdio.h>
 96 #include <stdlib.h>
 97 #include <sys/types.h>
 98 #include <sys/ipc.h>
 99 #include <sys/msg.h>
100 #include <string.h>
101 #include <unistd.h>
102 #include <time.h>
103 
104 #include "proto.h"
105 
106 
107 int main()
108 {
109     key_t key;
110     int msgid;
111     struct msg_st sbuf;
112 
113     // 設置隨機數種子
114     srand(time(NULL));
115     // 用與接收方相同的文件和 salt 生成一樣的key,這樣才可以通訊
116     key = ftok(KEYPATH,KEYPROJ);
117     if(key < 0)
118     {
119         perror("ftok()");
120         exit(1);
121     }
122 
123     // 取得消息隊列
124     msgid = msgget(key,0);
125     if(msgid < 0)
126     {
127         perror("msgget()");
128         exit(1);
129     }
130 
131     // 為要發送的結構體賦值
132     sbuf.mtype = MSGTYPE;    
133     strcpy(sbuf.name,"Alan");
134     sbuf.math = rand()%100;
135     sbuf.chinese = rand()%100;
136 
137     // 發送結構體
138     if(msgsnd(msgid,&sbuf,sizeof(sbuf)-sizeof(long),0) < 0)
139     {
140         perror("msgsnd()");
141         exit(1);
142     }
143 
144     puts("ok!");
145 
146     // 消息隊列不是發送方創建的,所以發送方不用負責銷毀
147 
148     exit(0);
149 }

 

這段程序的源文件有三個:proto.h、rcver.c 和 snder.c。

proto.h 中的 KEYPROJ (salt 值)用一個字符來替代整形數,為什麼不直接寫數字呢?因為宏定義是沒有數據類型的,沒有單位的數字是不靠譜的,而字符的 ASCII 碼一定是一個 0-255 之間的整形數。

接收方要先運行,所以又接收方創建消息隊列。發送方要使用相同的文件和 salt 生成於接收方相同的 key 值,這樣它們才能使用同一個消息隊列。

發送方生成一個結構體,用隨機數為結構體中的兩個成員賦值,分別模擬學生的數學和語文成績,接收方在接收到數據之後把每個成員解析出來,並打印到控制台上。

可以看出來,發送方和接收方必須使用相同的結構體才能保證數據能夠正常被解析,所以這個結構體就是我們上面所說的“協議”。既然是協議,我們就要把它定義在一個共用的結構體(proto.h)中,讓發送方和接收方都引用這個頭文件,這樣就能保證它們可以使用相同的結構體通訊了。

 

信號量

1 semget - get a semaphore set identifier
2 
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/sem.h>
6 
7 int semget(key_t key, int nsems, int semflg);

 

semget(2) 函數用於創建信號量。

成功返回 sem  ID,失敗返回 -1 並設置 errno。

參數列表:

  key:具有親緣關系的進程之間可以使用一個匿名的 key 值,key 使用宏 IPC_PRIVATE 即可。

  nsems:表示你到底有多少個 sem。信號量實際上是一個計數器,所以如果設置為 1 可以用來模擬互斥量。

  semflg:IPC_CREAT 表示創建 sem,同時需要按位或一個權限,如果是匿名 IPC 則無需指定這個宏,直接給權限就行了。

 

semctl(2)

1 semctl - semaphore control operations
2 
3 #include <sys/types.h>
4 #include <sys/ipc.h>
5 #include <sys/sem.h>
6 
7 int semctl(int semid, int semnum, int cmd, ...);

 

semctl(2) 函數用來控制或銷毀信號量。

參數列表:

  semnum:信號量素組下標;

  cmd:可選的宏參見《APUE》第三版 P457。常用的有 IPC_RMID,表示從系統中刪除該信號量集合。SETVAL 可以為第幾個成員設置值。關於這兩個宏的用法,我們在下面的栗子中會見到。

  ...:根據不同的命令設置不同的參數,所以後面的參數是變長的。

 

semop(2)

 1 semop - semaphore operations
 2 
 3 #include <sys/types.h>
 4 #include <sys/ipc.h>
 5 #include <sys/sem.h>
 6 
 7 int semop(int semid, struct sembuf *sops, unsigned nsops);
 8 
 9 
10 struct sembuf {
11     unsigned short sem_num; /* 對第幾個資源(數組下標)操作 */
12     short sem_op; /* 取幾個資源寫負數幾(不要寫減等於),歸還幾個資源就寫正數幾 */
13     short sem_flg; /* 特殊要求 */
14 };

 

這個函數就是讓我們操作信號量的。由於多個信號量可以組成數組,所以我們又看到了熟悉的函數定義方式,一個參數作為數組的起始位置,另一個參數指定數組的長度。

參數列表:

  sops:結構體數組起始位置;

  nsops:結構體數組長度;

返回值:成功返回0,失敗返回-1並設置 errno。

前面說過了,信號量實際上就是計數器,所以每次在使用資源之前,我們需要扣減信號量,當信號量被減到 0 時會阻塞等待。每次使用完成資源之後,需要歸還信號量,也就是增加信號量的數值。

下面我們使用上面操作信號量的函數實現一個通過信號量實現互斥量的栗子。

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 #include <sys/wait.h>
  6 #include <sys/types.h>
  7 #include <sys/ipc.h>
  8 #include <sys/sem.h>
  9 #include <errno.h>
 10 
 11 #define PROCNUM        20    
 12 #define FNAME        "/tmp/out"
 13 #define BUFSIZE        1024
 14 
 15 // 多個函數都要使用這個信號量 ID,所以定義為全局變量
 16 static int semid;
 17 
 18 static void P(void)
 19 {
 20     struct sembuf op;
 21 
 22     op.sem_num = 0; // 只有一個資源,所以數組下標是 0
 23     op.sem_op = -1; // 取一個資源就減1
 24     op.sem_flg = 0; // 沒有特殊要求
 25     while(semop(semid,&op,1) < 0)
 26     {
 27         // 出現假錯就重試
 28         if(errno != EINTR && errno != EAGAIN)
 29         {
 30             perror("semop()");
 31             exit(1);
 32         }
 33     }
 34 
 35 }
 36 
 37 static void V(void)
 38 {
 39     struct sembuf op;
 40 
 41     op.sem_num = 0;
 42     op.sem_op = 1; // 歸還一個資源
 43     op.sem_flg = 0;
 44     while(semop(semid,&op,1) < 0)
 45     {
 46         if(errno != EINTR && errno != EAGAIN)
 47         {
 48             perror("semop()");
 49             exit(1);
 50         }
 51     }
 52 }
 53 
 54 static void func_add()
 55 {
 56     FILE *fp;
 57     char buf[BUFSIZE];
 58 
 59     fp = fopen(FNAME,"r+");    
 60     if(fp == NULL)
 61     {
 62         perror("fopen()");
 63         exit(1);
 64     }
 65 
 66     // 先取得信號量在操作文件,取不到就阻塞等待,避免發生競爭
 67     P();
 68     fgets(buf,BUFSIZE,fp);
 69     rewind(fp);    
 70     sleep(1); // 調試用,為了放大競爭,更容易看出來互斥量發揮了作用
 71     fprintf(fp,"%d\n",atoi(buf)+1);
 72     fflush(fp);
 73     // 操作結束之後歸還信號量,讓其它進程可以取得信號量
 74     V();
 75     fclose(fp);
 76 
 77     return ;
 78 }
 79 
 80 int main()
 81 {
 82     int i;
 83     pid_t pid;
 84 
 85     // 在具有親緣關系的進程之間使用,所以設置為 IPC_PRIVATE 即可。
 86     // 另外想要實現互斥量的效果,所以信號量數量設置為 1 個即可。
 87     semid = semget(IPC_PRIVATE,1,0600);
 88     if(semid < 0)
 89     {
 90         perror("semget()");
 91         exit(1);
 92     }
 93 
 94     // 將 union semun.val 的值設置為 1
 95     if(semctl(semid,0,SETVAL,1) < 0)
 96     {
 97         perror("semctl()");
 98         exit(1);
 99     }
100 
101 
102     // 創建 20 個子進程
103     for(i = 0 ; i < PROCNUM ; i++)
104     {
105         pid = fork();        
106         if(pid < 0)
107         {
108             perror("fork()");
109             exit(1);
110         }
111         if(pid == 0)    // child
112         {
113             func_add();
114             exit(0);
115         }
116     }
117 
118     for(i = 0 ; i < PROCNUM ; i++)
119         wait(NULL);
120 
121     // 銷毀信號量
122     semctl(semid,0,IPC_RMID);
123 
124     exit(0);
125 }

 

大家還記得以前我們寫的用 20 個進程同時向一個文件中寫入累加的數值嗎?還是這個程序,這次我們使用信號量來實現它們之間的互斥效果。

程序的結構跟以前的實現方式差不多,只不過鎖的形式不一樣了而已。代碼中注釋已經寫得比較明白了,LZ 這就不做太多解釋了。

 

 

共享存儲

還記得之前在《高級 IO》部分的博文中我們利用 mmap(2) 函數實現過一個共享內存嗎?

這次我們使用的是 XSI 的共享內存,比使用 mmap(2) 實現的共享內存稍微麻煩一點。不過不用擔心,也一樣很簡單,不就是遵循上面說的那個命名規則的三個函數嘛,有了消息隊列和信號量的鋪墊,相信不用 LZ 講大家也差不多能才出來 shm 是個什麼套路了。

沒錯,第一個函數就是 shmget(2),我們來看一下它的函數原型:

1 shmget - allocates a shared memory segment
2 
3 #include <sys/ipc.h>
4 #include <sys/shm.h>
5 
6 int shmget(key_t key, size_t size, int shmflg);

 

參數列表:

  key:共享內存的唯一標識,具有親緣關系的進程之間使用共享內存可以使用 IPC_PRIVATE 宏代替。

  size:是共享內存的大小。

  shmflg:IPC_CREAT 表示創建 shm,同時需要按位或一個權限,如果是匿名 IPC 則無需指定這個宏,直接給權限就行了。

返回值:成功返回 shm ID;失敗返回 -1,man 手冊裡沒說是否設置 errno,這個需要大家根據自己的環境測試一下,或者查閱自己環境下的 man 手冊。

 

shmat(2) 函數和 shmdt(2) 函數

1 shmat - shared memory operations
2 
3 #include <sys/types.h>
4 #include <sys/shm.h>
5 
6 void *shmat(int shmid, const void *shmaddr, int shmflg);
7
8 int shmdt(const void *shmaddr);

 

雖然函數名叫做 shmat,根據上面說過的約定,用 man 手冊查 shmop 也是可以查到這個命令的。

shmat(2) 函數使進程與共享內存關聯起來。

shmat(2)函數中的 shmaddr 參數是共享內存的起始地址,傳入 NULL 由內核幫我們尋找合適的地址,一般情況我們都是傳入 NULL 值。

shmdt(2) 函數用於使進程分離共享內存,共享內存使用完畢之後需要用這個函數分離。分離不帶表釋放了這塊空間,使用共享內存的雙方依然要遵守“誰申請,誰釋放”的原則,所以沒有申請的一方是不需要釋放的,但是雙方都需要分離。

 

shmctl(2)

1 shmctl - shared memory control
2 
3 #include <sys/ipc.h>
4 #include <sys/shm.h>
5 
6 int shmctl(int shmid, int cmd, struct shmid_ds *buf);

 

與消息隊列和信號量一樣,這個函數用於控制或刪除共享內存。

參數 LZ 就不做介紹了,只說一下如何刪除共享內存吧。cmd 參數設置為 IPC_RMID 並且 buf 參數設置為 NULL 就可以刪除共享內存了。

下面我們來看一個共享內存實現進程間通訊的栗子。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <sys/mman.h>
 4 #include <sys/types.h>
 5 #include <sys/stat.h>
 6 #include <unistd.h>
 7 #include <fcntl.h>
 8 #include <string.h>
 9 #include <sys/ipc.h>
10 #include <sys/shm.h>
11 #include <sys/wait.h>
12 
13 // 申請的共享內存大小,單位是字節
14 #define MEMSIZE        1024
15 
16 int main()
17 {
18     char *str;
19     pid_t pid;
20     int shmid;
21 
22     // 有親緣關系的進程 key 參數可以使用 IPC_PRIVATE 宏,並且創建共享內存 shmflg 參數不需要使用 IPC_CREAT 宏
23     shmid = shmget(IPC_PRIVATE,MEMSIZE,0600);
24     if(shmid < 0)
25     {
26         perror("shmget()");
27         exit(1);
28     }
29 
30     pid = fork();
31     if(pid < 0)
32     {
33         perror("fork()");
34         exit(1);
35     }
36     if(pid == 0) // 子進程
37     {
38         // 關聯共享內存
39         str = shmat(shmid,NULL,0);
40         if(str == (void *)-1)
41         {
42             perror("shmat()");
43             exit(1);
44         }
45         // 向共享內存寫入數據
46         strcpy(str,"Hello!");
47         // 分離共享內存
48         shmdt(str);
49         // 無需釋放共享內存
50         exit(0);
51     }
52     else // 父進程
53     {
54         // 等待子進程結束再運行,因為需要讀取子進程寫入共享內存的數據
55         wait(NULL);
56         // 關聯共享內存
57         str = shmat(shmid,NULL,0);
58         if(str == (void *)-1)
59         {
60             perror("shmat()");
61             exit(1);
62         }
63         // 直接把共享內存中的數據打印出來
64         puts(str);
65         // 分離共享內存
66         shmdt(str);
67         // 釋放共享內存
68         shmctl(shmid,IPC_RMID,NULL);
69         exit(0);
70     }
71 
72     exit(0);
73 }

 

最後父進程要在父子進程用完共享內存之後調用 shmctl(2) 使用 IPC_RMID 宏來回收資源,參數為 NULL。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved