操作系統之進程通信


引子 進程通信的方式

  △信號通信

  △管道通信

  △消息隊列

  △共享存儲區

一、信號通信

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):如果信箱就緒(信箱中有消息),則從當前信箱指針指向的分區讀取一條消息,否則返回狀態信息(非阻塞式)或者等待信箱就緒(阻塞式)。

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

5.消息通信的原理

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

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

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

6.消息機制的同步作用

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

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

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

7.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。

8.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,並將控制權交回調用函數的進程)。

9.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用來表示隊列的當前狀態,可以設置為空。

10.一個消息隊列通信的實例

        編寫一個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”可以看到消息隊列被銷毀了。

11.消息隊列機制用於進程同步

  改寫上述程序,要求實現以下功能:先運行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”,該參數的作用是如果消息隊列中沒有消息,則阻塞,等待消息的到來。

四、共享存儲區

        共享存儲區是指在內存中開辟一個公共存儲區,把要進行通信的進程的虛地址空間映射到共享存儲區。發送進程向共享存儲區中寫數據,接收進程從共享存儲區中讀數據。

 


免責聲明!

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



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