1. 程式人生 > >作業系統之程序通訊

作業系統之程序通訊

引子 程序通訊的方式

  △訊號通訊

  △管道通訊

  △訊息佇列

  △共享儲存區

一、訊號通訊

1.什麼是訊號

   

  (1)訊號是Linux程序之間一種重要的通訊機制;

  (2)訊號的作用是為了通知程序某個時間已經發生;

  (3)訊號的發出是及時的,但是訊號的響應可能會有延後,收到訊號的程序在當前執行處設定斷點,然後立即轉為執行訊號處理函式,執行結束後,會回到斷點,繼續執行之前的操作,這一點類似中斷機制;

  (4)訊號機制其實是在軟體層次上對中斷機制的一種模擬,一個程序收到訊號和收到中斷請求可以說是一樣的;

  (5)中斷和訊號的區別是,前者執行在核心態(系統),後者執行在使用者態,中斷的響應比較及時,而訊號的相應一般會有延遲;

  (6)訊號的發出者可以是程序、系統、硬體。

2.Linux下的訊號

  在終端輸入指令“kill -l”可以檢視62個訊號(沒有編號32和33)。SIGUSR1和SIGUSR2是使用者可以自定義的訊號,較為常用。

  

3.Linux下使用訊號機制

  (1)“ctrl+c”殺死一個程序:摁下“ctrl+c”會產生訊號SIGINT,程序接收到SIGINT訊號後,會結束程序。

  (2)“ctrl+z”掛起一個程序:摁下“ctrl+c”會產生訊號SIGSTP,程序接收到SIGSTP訊號後,會掛起程序。

  (3)“kill -9”殺死一個程序:在終端輸入“kill -9”後回車,會產生訊號SIGKILL,程序收到SIGKILL訊號後,會強制結束程序。

4.signal()函式

  signal()函式的作用是為指定的訊號註冊處理函式,函式格式是   

  sighandler_t signal(int signum, sighandler_t handler);

  sighandler的定義是 

    typedef void (*sighandler_t)(int);

  引數signum是指定訊號的標號,handler是處理函式的函式名。

  注意:

    ①當handler=1時,程序將忽略(遮蔽)signum所示的訊號,不會對訊號做出響應;

    ②當handler=0(預設值)時,程序在收到signum所示的訊號後會立即終止自己,類似於“ctrl+c”;

    ③當handler為大於1的正整數,即一個函式名稱時,程序在接收到signum所示的函式後會執行響應的函式。

5.kill()函式

  kill()函式的作用是向指定的程序傳送訊號,函式格式是

   int kill(int pid, int sig);

  引數pid是程序號,sig是要傳送的軟中斷訊號。

6.一個訊號通訊的例項

  編寫一段程式碼,建立一個子程序。程式開始執行時,處於阻塞等待狀態。在鍵盤上摁下“ctrl+c”後,父程序列印“Parent process:Transmitted signal to my subprocess”,然後子程序列印“Subprocess:Got the signal from my parent process”,然後退出程式。

 1 //檔名稱為test2.c
 2     #include <stdio.h>
 3     #include <stdlib.h>
 4     #include <unistd.h>
 5     #include <signal.h>
 6     
 7     int waitFlag = 0;
 8     
 9     void stopWaiting();
10     void waitForSignal();
11     
12     int main()
13     {
14       int pid;  //子程序ID號
15     
16       pid = fork(); //建立子程序
17       if(pid == -1) //程序建立失敗
18       {
19         exit(1);
20       }
21       if(pid != 0)  //父程序中執行
22       {
23         signal(SIGINT, stopWaiting);  //為SIGINT訊號重新註冊處理函式
24         waitForSignal();  //進入等待函式,將父程序阻塞,等待SIGINT訊號的到來
25         printf("Parent process:Transmitted signal to my subprocess\n");  //等待結束後,列印提示資訊
26         kill(pid, SIGUSR1); //向子程序附送使用者自定義訊號
27       }
28       else  //子程序中執行
29       {
30         signal(SIGUSR1, stopWaiting); //為SIGUSR1訊號註冊處理函式
31         waitForSignal();  //進入等待函式,將子程序阻塞,等待父程序傳送SIGUSR1訊號
32         printf("Subprocess:Got the signal from my parent process\n");  //等待結束後,列印提示資訊
33       }
34     
35       return 0;
36     }
37     
38     void stopWaiting()
39     {
40       waitFlag = 0; //將等待標誌清零
41     }
42     
43     void waitForSignal()
44     {
45       waitFlag = 1; //置數等待標誌
46       while(waitFlag == 1); //將程式阻塞在此處
47     }

執行結果如下:

   

  摁下“ctrl+c”之後,僅列印了父程序提示語句,而子程序提示語句卻沒有列印,這是為什麼呢?因為摁下“ctrl+c”後,訊號SIGINT會向所有的程序傳送,所以子程序也收到了SIGINT訊號,但是在子程序中卻沒有對SIGINT函式進行重新註冊,所以子程序仍然認為“ctrl+c”摁下後會退出程序。所以導致子程序的提示資訊沒有正常列印。我們可以在子程序中對SIGINT函式進行重新註冊,比如將它忽略,這樣就可以解決問題了。

新的程式碼如下:

 1   #include <stdio.h>
 2     #include <stdlib.h>
 3     #include <unistd.h>
 4     #include <signal.h>
 5     
 6     int waitFlag = 0;
 7     
 8     void stopWaiting();
 9     void waitForSignal();
10     
11     int main()
12     {
13       int pid;  //子程序ID號
14     
15       pid = fork(); //建立子程序
16       if(pid == -1) //程序建立失敗
17       {
18         exit(1);
19       }
20       if(pid != 0)  //父程序中執行
21       {
22         signal(SIGINT, stopWaiting);  //為SIGINT訊號重新註冊處理函式
23         waitForSignal();  //進入等待函式,將父程序阻塞,等待SIGINT訊號的到來
24         printf("Parent process:Transmitted signal to my subprocess\n");  //等待結束後,列印提示資訊
25         kill(pid, SIGUSR1); //向子程序傳送使用者自定義訊號
26       }
27       else  //子程序中執行
28       {
29         signal(SIGUSR1, stopWaiting); //為SIGUSR1訊號註冊處理函式
30         signal(SIGINT, SIG_IGN);  //SIG_IGN就是數字1,代表忽略SIGINT訊號
31         waitForSignal();  //進入等待函式,將子程序阻塞,等待父程序傳送SIGUSR1訊號
32         printf("Subprocess:Got the signal from my parent process\n");  //等待結束後,列印提示資訊
33       }
34     
35       return 0;
36     }
37     
38     void stopWaiting()
39     {
40       waitFlag = 0; //將等待標誌清零
41     }
42     
43     void waitForSignal()
44     {
45       waitFlag = 1; //置數等待標誌
46       while(waitFlag == 1); //將程式阻塞在此處
47     }

新的執行結果:

   

  可以看到,可以正確列印父程序和子程序的提示資訊了。

這段程式的執行流程是這樣的:

  (1)在父程序中對“ctrl+c”發出的訊號SIGINT進行重新註冊,讓它的處理函式變為stopWaiting(),代替了原來的“中斷程序”功能。然後進入等待函式,阻塞自己,等待SIGINT訊號的到來;

  (2)同時子程序中對使用者自定義訊號SIGUSR1進行註冊,使其也指向處理函式stopWaiting(),然後再使用signal函式忽略“ctrl+c”發出的SIGINT訊號,防止程序退出;

  (3)使用者摁下“ctrl+c”後,父程序和子程序都收到了SIGINT訊號,但是子程序遮蔽了該訊號,所以不起作用,而父程序會處理該訊號;

  (4)父程序接收到SIGINT訊號進入函式stopWaiting(),清零等待標誌位後,解除阻塞,繼續向下執行,先列印提示資訊,然後向子程序傳送訊號SIGUSR1,最後退出程序;

  (5)子程序收到訊號SIGUSR1後,進入stopWaiting(),清零等待標誌位後,解除阻塞,繼續向下執行,列印提示資訊,最後退出程序。

二、匿名管道通訊

1.管道(pipe)定義

  管道是程序之間的一種通訊機制。一個程序可以通過管道把資料傳遞給另外一個程序。前者向管道中寫入資料,後者從管道中讀出資料。

   

 

  管道的資料結構圖

   

2.管道的工作原理

  (1)管道如同檔案,可讀可寫,有讀和寫兩個控制代碼;

  (2)通過寫寫控制代碼來向管道中寫入資料;

  (3)通過讀讀控制代碼來從管道中讀取資料。

  (4)匿名管道通訊只能用於父子或兄弟程序的通訊,由父程序建立管道,並建立子程序。

3.使用管道要注意的問題

  由於管道是一塊共享的儲存區域,所以要注意互斥使用。所以程序每次在訪問管道前,都需要先檢查管道是否被上鎖,如果是,則等待。如果沒有,則給管道上鎖,然後對管道進行讀寫操作。操作結束後,對管道進行解鎖。

4.pipe()函式

  pipe()的作用是建立一個匿名管道。函式格式是

    int pipe(fd);

  fd的定義如下

    int fd[2];

  fd[0]是讀控制代碼,fd[1]是寫控制代碼。

5.read()函式

  read()函式的作用是從指定的控制代碼中讀出一定量的資料,送到指定區域。函式格式是

    ssize_t read(int fd, const void *buf, size_t byte_num);

  fd表示讀控制代碼,buf表示讀出資料要送到的區域,byte_num是要讀出的位元組數,返回值是成功讀出的位元組數。

6.write()函式

  write()函式的作用是把指定區域中一定數量的資料寫入到指定的控制代碼中。函式格式是

    ssize_t write(int fd, const void *buf, size_t byte_num);

  fd表示寫控制代碼,buf表示資料來源,byte_num表示要寫入的位元組數,返回值是成功寫入的位元組數。

7.lockf()函式

  lockf()函式的作用是給特定的檔案上鎖。函式格式是

    int lockf(int fd, int cmd, off_t len);

  fd表示要鎖定的檔案,cmd表示對檔案的操作命令(“0”表示解鎖,“1”表示互斥鎖定區域,“2”表示測試互斥鎖定區域,“3”表示測試區域),len表示要鎖定或解鎖的連續位元組數,如果為“0”,表示從檔案頭到檔案尾。

8.wait()函式

  wait()函式的作用是立即阻塞自己,直到當前程序的某個子程序執行結束。函式格式是

    pid_t wait(int *status);

  其引數用來儲存程序退出時的一些狀態,一般設定為NULL。返回值為退出的子程序的ID號。

9.一個匿名管道通訊的例項

  編寫一段程式,建立兩個子程序,這兩個子程序分別使用管道向父程序傳送資料,父程序完整接收兩個子程序傳送的資料後打印出來。

 1 #include <stdio.h>
 2     #include <signal.h>
 3     #include <unistd.h>
 4     #include <stdlib.h>
 5     
 6     int main()
 7     {
 8       int p1, p2; //兩個子程序
 9       int fd[2];  //讀寫控制代碼
10       char *s1 = "The 1st subprocess's data\n";
11       char *s2 = "The 2rd subprocess's data\n";
12       char s_read[80];
13       pipe(fd); //建立匿名管道
14       p1 = fork();
15       if(p1 == 0) //子程序一中執行
16       {
17         lockf(fd[1], 1, 0); //對管道的寫控制代碼進行鎖定
18         write(fd[1], s1, 26); //向寫控制代碼寫入26個位元組的資料,注意這裡的位元組數一定要和字串s1中的相等,否則會在寫入後增寫一個結束符,導致輸出不了理想的結果
19         lockf(fd[1], 0, 0); //解鎖寫控制代碼
20         exit(0);
21       }
22       else
23       {
24         p2 = fork();
25         if(p2 == 0) //子程序二中執行
26         {
27           lockf(fd[1], 1, 0); //鎖定寫控制代碼
28           write(fd[1],s2, 26);  //向寫控制代碼寫入24個位元組的資料
29           lockf(fd[1], 0, 0); //解鎖寫控制代碼
30           exit(0);
31         }
32         else  //父程序中執行
33         {
34           wait(NULL); //程序同步,等待一個子程序結束
35           wait(NULL); //程序同步,再等待一個子程序結束
36           //這兩個等待語句是為了確保兩個子程序都向管道中寫入了資料後,父程序才開始讀取管道中資料
37           read(fd[0], s_read, 52);  //讀讀控制代碼,將讀取的資料存入s_read中
38           printf("%s", s_read);  //列印資料
39           exit(0);
40         }
41       }
42     
43       return 0;
44     }

執行結果:

   

三、訊息佇列

1.概述

   

  (1)訊息是一個格式化的可變長的資訊單元;

  (2)小心通訊機制允許一個程序給其他任意一個程序傳送訊息;

  (3)當出現了多個訊息時,會形成訊息佇列,每個訊息佇列都有一個關鍵字key,由使用者指定,作用與檔案描述符相當。

2.為什麼引入訊息佇列機制

        訊號量和PV操作可以實現程序的同步和互斥,但是這種低階通訊方式並不方便,而且侷限性較大。當不同程序之間需要交換更大量的資訊時,甚至是不同機器之間的不同程序需要進行通訊時,就需要引入更高階的通訊方式——訊息佇列機制。

3.信箱

        訊息佇列的難點在於,傳送方不能直接將要傳送的資料複製進接收方的儲存區,這時就需要開闢一個共享儲存區域,可供雙方對這個儲存區進行讀寫操作。這個共享區域就叫做信箱。每個信箱都有一個特殊的識別符號。每個信箱都有自己特定的信箱容量、訊息格式等。信箱被分為若干個分割槽,一個分割槽存放一條訊息。

4.重要的兩條原語:

  原語具有不可分割性,執行過程不允許被中斷。

  (1)傳送訊息原語(send):如果信箱就緒(信箱還未存滿),則向當前信箱指標指向的分割槽存入一條訊息,否則返回狀態資訊(非阻塞式)或者等待信箱就緒(阻塞式)。

  (2)接收訊息原語(receive):如果信箱就緒(信箱中有訊息),則從當前信箱指標指向的分割槽讀取一條訊息,否則返回狀態資訊(非阻塞式)或者等待信箱就緒(阻塞式)。

  注:在信箱非空的情況下,每讀取一次信箱,信箱中的訊息就會少一條,直到信箱變為空狀態。

6.訊息通訊的原理

  (1)如果一個程序要和另外一個進行通訊,則這兩個程序需要開闢一個共享儲存區(信箱);

  (2)訊息通訊機制也可以用在一對多通訊上,一個server和n個client通訊時,那麼server就和這n個client各建立一個共享儲存區;

  (3)一個程序可以隨時向信箱中儲存訊息,當然一個程序也可以隨時從信箱中讀取一條訊息。

7.訊息機制的同步作用

        採用訊息佇列通訊機制,可以實現程序間的同步操作。在介紹同步功能之前,需要先介紹兩個名詞,阻塞式原語和非阻塞式原語。阻塞式原語是指某程序執行一個指令時,如果當前環境不滿足執行條件,則該程序會在此停止,等待系統環境滿足執行條件,然後繼續向下執行。非阻塞式原語是指某程序執行一個指令時,如果當前環境不滿足執行條件,則立即返回一個狀態資訊,並繼續執行接下來的指令。

        (1)非阻塞式傳送方+阻塞式接收方:兩個程序開始執行後,接收方會進入等待狀態,等待發送方給接收方傳送一條訊息,直到接收到相應的訊息後,接收方程序才會繼續向下執行。

        (2)非阻塞式傳送方+非阻塞式接收方:傳送方和接收方共享一個信箱,傳送方隨時可以向信箱中存入一條訊息,接收方可以隨時從信箱讀取一條訊息。當信箱滿時,傳送方進入阻塞狀態;當信箱空時,接收方進入阻塞狀態。

8.msgget()函式

  msgget()函式的作用是建立一個新的或開啟一個已經存在的訊息佇列,此訊息佇列與key相對應。函式格式為

    int msgget(key_t key, int msgflag);

  引數key是使用者指定的訊息佇列的名稱;引數flag是訊息佇列狀態標誌,其可能的值有:IPC_CREAT(建立新的訊息佇列)、IPC_EXCL(與IPC_CREAT一同使用,表示如果要建立的訊息佇列已經存在,則返回錯誤)、 IPC_NOWAIT(讀寫訊息佇列要求無法滿足時,不阻塞);返回值是建立的訊息佇列識別符號,如果建立失敗則則返回-1。函式呼叫方法是:

    msgget(key,IPC_CREAT|0777);

  0777是存取控制符,表示任意使用者可讀、可寫、可執行。如果執行成功,則返回訊息佇列的ID號(注意和佇列KEY值作區分,這二者不同),否則返回-1。

9.msgsnd()函式和msgrcv()函式

  msgsnd()函式的作用是將一個新的訊息寫入佇列,msgrcv()函式的作用是從訊息佇列讀取一個訊息。函式格式是

    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);

  引數msqid是訊息佇列的ID號;引數msgp是指向訊息緩衝區的指標,此位置用來暫時儲存傳送和接收的訊息,是一個使用者可定義的通用結構,形態如下

    struct msgbuf {

    long mtype; /* 訊息型別,必須 > 0 */

    char mtext[1]; /* 訊息文字 */

    };

  引數msgsz是訊息大小;引數msgtyp是訊息型別(大於0則返回其型別為msgtyp的第一個訊息,等於0則返回佇列的最早的一個訊息,小於0則返回其型別小於或等於mtype引數的絕對值的最小的一個訊息),msgflag這個引數依然是是控制函式行為的標誌(取值0,表示忽略,那麼程序將被阻塞直到函式可以從佇列中得到符合條件為止;取值IPC_NOWAIT,表示如果訊息佇列為空,則返回一個ENOMSG,並將控制權交回呼叫函式的程序)。

10.msgctl()函式

  msgctl()函式的作用是對相應訊息佇列程序控制操作。函式格式是

    int msgctl(int msqid,int cmd,struct msqid_ds *buf);

  引數msqid表示訊息佇列ID號;cmd表示對佇列的控制操作,其可能值有IPC_STAT(讀取訊息佇列的資料結構msqid_ds,並將其儲存在buf指定的地址中)、IPC_SET(設定訊息佇列的資料結構msqid_ds中的ipc_perm元素的值,這個值取自buf引數)、IPC_RMID(從系統核心中移走訊息佇列);引數*buf用來表示佇列的當前狀態,可以設定為空。

11.一個訊息佇列通訊的例項

        編寫一個receiver程式和一個sender程式。首先執行sender程式,建立一個訊息佇列,並向訊息佇列中傳送一個訊息。再執行receiver程式,從訊息佇列中接收一個訊息,將其打印出來。

 1     //檔名為sender.c
 2     #include <sys/types.h>
 3     #include <sys/msg.h>
 4     #include <sys/ipc.h>
 5     #include <stdio.h>
 6     
 7     #define KEY 60
 8     
 9     struct msgbuf
10     {
11       long mtype; //訊息型別,必須大於0
12       char mtext[50]; //訊息內容
13     };
14     
15     int main()
16     {
17       int msgqid; //訊息佇列ID號
18       struct msgbuf buf = { 1, "This is a message from sender\n"};
19       msgqid=msgget(KEY,0777|IPC_CREAT);
20       msgsnd(msgqid, &buf, 50, 0);  //傳送訊息到訊息佇列
21       return 0;
22     }
 1 //檔名為receiver.c
 2     #include <stdio.h>
 3     #include <sys/types.h>
 4     #include <sys/msg.h>
 5     #include <sys/ipc.h>
 6     
 7     #define KEY 60
 8     
 9     struct msgbuf
10     {
11       long mtype; //訊息型別,必須大於0
12       char mtext[50];  //訊息內容
13     };
14     
15     int main()
16     {
17       int msgqid = 0;
18       struct msgbuf buf;
19       msgqid = msgget(KEY, 0777);
20       msgrcv(msgqid, &buf, 50, 0, IPC_NOWAIT); //接收一條最新訊息,如果訊息佇列為空,不等待,直接返回錯誤標誌
21       printf("%s", buf.mtext);
22       msgctl(msgqid, IPC_RMID, NULL);
23     
24       return 0;
25     }

執行結果:

   

  首先使用命令“ipcs -q”檢視有無訊息佇列,開始時沒有訊息佇列。執行sender程式後,再使用命令“ipcs -q”,可以看到有了一個訊息佇列(其中的key值為“0x3c”,十進位制形式是60;“perms”項下為777,表示許可權為任何使用者可讀、可寫、可操作;“messages”項下為1,表示佇列中有一條訊息)。再執行receiver程式,讀取出訊息佇列中的訊息,將其打印出來。最後使用命令“ipcs -q”可以看到訊息佇列被銷燬了。

12.訊息佇列機制用於程序同步

  改寫上述程式,要求實現以下功能:先執行receiver程式,使其處於阻塞狀態。再執行sender程式,給receiver程式傳送一條訊息。receiver程式接收到訊息後將其打印出來,然後結束。

 1 //檔名為sender.c
 2     #include <sys/types.h>
 3     #include <sys/msg.h>
 4     #include <sys/ipc.h>
 5     #include <stdio.h>
 6     
 7     #define KEY 60
 8     
 9     struct msgbuf
10     {
11       long mtype; //訊息型別,必須大於0
12       char mtext[50]; //訊息內容
13     };
14     
15     int main()
16     {
17       int msgqid; //訊息佇列ID號
18       struct msgbuf buf = { 1, "This is a message from sender\n"};
19       msgqid=msgget(KEY,0777);  //開啟名稱為KEY的訊息佇列
20       msgsnd(msgqid, &buf, 50, 0);  //傳送訊息到訊息佇列
21       return 0;
22     }
 1     //檔名為receiver.c
 2     #include <stdio.h>
 3     #include <sys/types.h>
 4     #include <sys/msg.h>
 5     #include <sys/ipc.h>
 6     
 7     #define KEY 60
 8     
 9     struct msgbuf
10     {
11       long mtype; //訊息型別,必須大於0
12       char mtext[50];  //訊息內容
13     };
14     
15     int main()
16     {
17       int msgqid = 0;
18       struct msgbuf buf;
19       msgqid = msgget(KEY, 0777|IPC_CREAT); //建立一個訊息佇列,名稱為KEY,該佇列任何使用者可讀可寫
20       msgrcv(msgqid, &buf, 50, 0, 0); //接收一條最新訊息,如果訊息佇列為空,則阻塞,直到訊息佇列中有訊息
21       printf("%s", buf.mtext);
22       msgctl(msgqid, IPC_RMID, NULL);
23     
24       return 0;
25     }

執行結果

   

  開始時,沒有訊息佇列存在。首先執行receiver(&表示後臺執行),使用命令“ps”可以看到後臺有一個名稱為receiver的程序在執行。然後執行sender,receiver接收到sender的訊息後將其打印出來。再次使用“ps”命令,可以看到receiver程序已經銷燬。該程式實現的主要原理是receiver的接收訊息函式msgrcv使用了引數“0”,該引數的作用是如果訊息佇列中沒有訊息,則阻塞,等待訊息的到來。

四、共享儲存區

        共享儲存區是指在記憶體中開闢一個公共儲存區,把要進行通訊的程序的虛地址空間對映到共享儲存區。傳送程序向共享儲存區中寫資料,接收程序從共享儲存區中讀資料。