引子 進程通信的方式
△信號通信
△管道通信
△消息隊列
△共享存儲區
一、信號通信
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”,該參數的作用是如果消息隊列中沒有消息,則阻塞,等待消息的到來。
四、共享存儲區
共享存儲區是指在內存中開辟一個公共存儲區,把要進行通信的進程的虛地址空間映射到共享存儲區。發送進程向共享存儲區中寫數據,接收進程從共享存儲區中讀數據。