在進程控制開發中,讀者已經學會了如何建立進程以及如何對進程進行基本的控制,這些都只是停留在父子進程之間的控制,本章將樣學習不一樣的進程間進行通訊的方法。經過本章學習,讀者將會掌握如下內容:程序員
在上一章中,讀者已經知道了進程是一個程序的依稀執行的過程。這裏所說的進程通常是指運行在用戶態的進程,而因爲處於用戶態的不一樣進程之間是彼此隔離的,就像處於不一樣城市的人們,它們必須經過某種方式來提供通訊,例如人們如今普遍使用的手機等方式。本章就是講述如何創建這些不一樣的通話方式,就像人們有不少種通訊方式同樣。shell
Linux下的進程通訊手段基本上是從UNIX平臺上的進程通訊手段繼承而來的。而對UNIX發展作出重要貢獻的兩大主力AT&T的貝爾實驗室即BSD在進程間的通訊方面的側重點有所不一樣。前者是對UNIX早期的進程間通訊手段進行了系統的改進和擴充,造成了「systemV PIC」,其通訊進程主要侷限在單個計算機內;後者則跳過了該限制,造成了基於套接字(socket)的進程間通訊機制。而Linux則把二者的優點都繼承了下來。如圖:數組
如今在Linux中使用較多的進程間通訊方式主要有一下幾種。 app
(1) 管道(pipe)及有名管道(named pipe):管道可用於具備親緣關係進程間的通訊;有名管道,除具備管道所具備的的功能外,它還容許無親緣關係進程間的通訊。ssh
(2) 信號(signal):信號是在軟件層次上對中斷機制的一種模擬,它是比較複雜的通訊方式,用於通知接受進程有某個事件發生,一個進程收到一個信號與處理器收到一箇中斷請求的效果上能夠說是同樣的。異步
(3) 消息隊列:消息隊列是消息的連接表,包括Posix消息隊列、SystemV消息隊列。它克服了前兩種通訊方式中信息量有限的缺點,具備寫權限的進程能夠向消息隊列中按照必定的規則添加新消息:對消息隊列有讀權限的進程則能夠從消息隊列中讀取消息。socket
(4) 共享內存:能夠說這是最有用的進程間通訊方式。它使得多個進程能夠訪問同一塊內存空間,不一樣進程能夠及時看到對方進程中對共享內存中數據的更新。這種通訊方式須要依靠某種同步機制,如互斥鎖和信號量等。ide
(5) 信號量:主要做爲進程間以及同一進程不一樣線程之間的同步手段。函數
(6) 套接字(Socket):這是一種更爲通常的進程間通訊機制,它可用於不一樣機器之間的進程間通訊,應用很是普遍。post
8.2.1 管道概述
細心的讀者可能會注意到本書在第2張中介紹「ps」的命令時提到過管道,當時指出了管道是Linux中很重要的一種通訊方式,它是把一個程序的輸出直接鏈接到另外一個程序的輸入,這裏以第2章中的 ps -ef|grep ntp 爲例,描述管道的通訊過程,如圖:
管道是Linux中進程間通訊的一種方式。這裏所說的管道主要指無名管道,它具備以下特色:
8.2.2 管道建立與關閉
1. 管道建立與關閉說明
管道是基於文件描述符的通訊方式,當一個管道創建時,它會建立兩個文件描述符fds[0]和fds[1],其中fds[0]固定於讀管道,而fds[1]固定於寫管道,這就構成了一個半雙工的通道。
管道關閉時只須要將兩個文件描述符關閉便可,可以使用普通的close函數逐個關閉各個文件描述符。
注意:一個管道共享了多對文件描述符時,若將其中一對讀寫文件描述符都刪除,則該管道就時效。
2. 管道建立函數
建立管道能夠經過調用pipe來實現
所需頭文件 | #include<unistd.h> |
函數原型 | int pipe(int fd[2]) |
函數傳入值 | fd[2]:管道的兩個文件描述符,以後就能夠直接操做這兩個文件描述符 |
函數返回值 | 成功:0 |
出錯:-1 |
3. 管道建立實例
建立管道很是簡單,只須要調用函數pipe便可:
#include <stdlib.h> #include <errno.h> #include <stdio.h> #include <unistd.h> int main() { int pipe_fd[2]; /* 建立一個無名管道 */ if(pipe(pipe_fd)<0){ printf("pipe create error\n"); return -1; }else printf("pipe create success\n"); /* 關閉管道描述符 */ close(pipe_fd[0]); close(pipe_fd[1]); }
程序運行後先成功建立一個無名管道,以後再將其關閉。
8.2.3 管道讀寫
1. 管道讀寫說明
用pipe函數建立的管道兩端處於一個進程中,因爲管道是主要用在不一樣進程間通訊的,所以這在實際應用中沒有太大意義。實際上,一般先是建立一個管道,在經過fork()函數建立一子進程,該子進程就會繼承父進程鎖建立的管道,這時,父子進程管道的文件描述符對應關係如圖:
這時的關係看似很是複雜,實際上卻已經給不一樣進程之間的讀寫創造了很好的條件。這時,父子進程分別擁有本身的讀寫的通道,爲了實現父子進程之間的讀寫,只須要把無關的讀端或寫端的文件描述符關閉便可。例如把父進程的寫端fd[1]和子進程的讀端fd[0]關閉。這時,父子進程之間就創建起了一條「子進程寫入父進程讀」的通道。
一樣,也能夠關閉父進程fd[0]和子進程fd[1],這樣就能夠創建一條「父進程寫子進程讀」的通道。另外,父進程還能夠建立多個子進程,各個子進程都繼承了相應的fd[0]和fd[1],這時,只須要關閉相應端口就能夠創建起各個子進程之間的通訊。
想想:爲何無名管道只能創建具備親緣關係的進程之間?
2.管道讀寫實例
在本例中,首先建立管道,以後父進程使用fork函數建立子進程,以後經過關閉父進程的讀描述符和子進程的寫描述符,創建起它們之間的管道通訊。
#include <unistd.h> #include <sys/types.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <string.h> int main() { int pipe_fd[2]; pid_t pid; char buf_r[100]; char *p_wbuf; int r_num; memset(buf_r,0,sizeof(buf_r)); /* 建立管道 */ if((pipe(pipe_fd))<0){ printf("pipe create error\n"); return -1; } /* 建立一個子進程 */ if((pid=fork())==0){ printf("\n"); /* 關閉子進程寫描述符,並經過使父進程暫停2秒確保父進程以關閉相應的讀描述符 */ close(pipe_fd[1]); sleep(2); /* 子進程讀取管道內容 */ if((r_num=read(pipe_fd[0],buf_r,100))>0){ printf("%d numbers read from the pipe is %s\n",r_num,buf_r); } /* 關閉子進程讀描述符 */ close(pipe_fd[0]); exit(0); }else if(pid>0){ /* 關閉父進程讀描述符,並分兩次向管道中寫入 Hello Pipe */ close(pipe_fd[0]); if(write(pipe_fd[1],"Hello",5)!=-1) printf("parent write1 success!\n"); if(write(pipe_fd[1]," Pipe",5)!=-1) printf("parent write2 success!\n"); /* 關閉符進程寫描述符 */ close(pipe_fd[1]); sleep(3); /* 收集子進程退出信息 */ waitpid(pid,NULL,0); exit(0); } }
運行結果:
parent write1 success! parent write2 success! 10 numbers read from the pipe is Hello Pipe
3. 管道讀寫注意點
8.2.4 標準流管道
1. 標準流管道函數說明
與Linux中文件操做有基於文件流的標準I/O操做同樣,管道的操做也支持基於文件流的模式。這種基於文件流的管道主要用來建立一個鏈接到另外一個進程的管道,這裏的「另外一個進程」也就是一個能夠執行必定操做的可執行文件,例如,用戶執行「cat popen.c」或者本身編寫的程序"hello"等。因爲這一類操做很常見,所以標準流管道就將一系列的建立過程合併到一個函數popen中完成。它所完成的工做有一下幾步:
這個函數的使用能夠大大減小代碼的編寫量,但同時也有一些不利之處,例如,它沒有前面管道建立的函數靈活多樣,而且用popen建立的管道必須使用標準I/O函數進行操做,但不能使用前面的read、write一類不帶緩衝的I/O函數。
與之相對應,關閉用popen建立的流管道必須使用函數pclose來關閉該管道流。該函數關閉標準I/O流,並等待命令執行結束。
2.函數格式
popen函數格式:
所需頭文件 | #include<stdio.h> | |
函數原型 | FILE *popen(const char *command,const char *type) | |
函數傳入值 | command:指向的是一個以null結束符結尾的字符串,這個字符串包含一個shell命令,並被送到/bin/sh以-c參數執行,即由shell來執行 | |
type | "r":文件指針鏈接到command的標準輸出,即該命令的結果產生輸出 "w":文件指針鏈接到command的標準輸入,即該命令的結果產生的輸入 |
|
函數返回值 | 成功:文件流指針 | |
出錯:-1 |
pclose函數格式
所需頭文件 | #include<stdio.h> |
函數原型 | int pclose(FILE *stream) |
函數傳入值 | stream:要關閉的文件流 |
函數返回值 | 成功:返回popen中執行命令的終止狀態 |
出錯:-1 |
3. 函數使用實例
在該實例中,使用popen來執行「ps -ef」命令。能夠看出,popen函數的使用可以使程序變得短小精悍。
#include <stdio.h> #include <stdlib.h> #define BUFSIZE 1000 int main() { FILE *fp; char *cmd="ps -ef"; char buf[BUFSIZE]; /* 調用popen函數執行相應的命令 */ if((fp=popen(cmd,"r"))==NULL) perror("popen"); while((fgets(buf,BUFSIZE,fp))!=NULL) printf("%s",buf); pclose(fp); exit(0); }
運行結果:
UID PID PPID C STIME TTY TIME CMD root 1 0 0 10:23 ? 00:00:02 /sbin/init splash root 2 0 0 10:23 ? 00:00:00 [kthreadd] root 4 2 0 10:23 ? 00:00:00 [kworker/0:0H] root 6 2 0 10:23 ? 00:00:00 [mm_percpu_wq] root 7 2 0 10:23 ? 00:00:00 [ksoftirqd/0] root 8 2 0 10:23 ? 00:00:01 [rcu_sched] root 9 2 0 10:23 ? 00:00:00 [rcu_bh] root 10 2 0 10:23 ? 00:00:00 [migration/0] root 11 2 0 10:23 ? 00:00:00 [watchdog/0] root 12 2 0 10:23 ? 00:00:00 [cpuhp/0] root 13 2 0 10:23 ? 00:00:00 [kdevtmpfs] ...... root 5036 2 0 16:11 ? 00:00:00 [kworker/0:0] root 5075 2 0 16:22 ? 00:00:00 [kworker/0:2] root 5190 898 0 16:25 ? 00:00:00 sshd: abc [priv] abc 5216 5190 0 16:25 ? 00:00:00 sshd: abc@notty abc 5217 5216 0 16:25 ? 00:00:00 /usr/lib/openssh/sftp-server abc 5230 3621 0 16:26 pts/4 00:00:00 ./popen abc 5231 5230 0 16:26 pts/4 00:00:00 sh -c ps -ef abc 5232 5231 0 16:26 pts/4 00:00:00 ps -ef
8.2.5 FIFO
1. 有名管道說明
前面介紹的管道是無名管道,它只能用於具備親緣關係的進程之間,這就大大地限制了管道的使用。有名管道的出現突破了這種限制,它可使互不相關的兩個進程實現彼此通訊。該管道能夠經過路徑名來指出,而且在文件系統中是可見的。在創建了管道以後,兩個進程就能夠把它當作普通文件同樣進行讀寫操做,使用很是方便。不過值得注意的事,FIFO是嚴格遵循先進先出規則的,對管道及FIFO的讀老是從開始處返回數據,對它們的寫則把數據添加到末尾,它們不支持如lseek()等文件定位操做。
有名管道的建立可使用函數mkfifo(),該函數相似文件中的open()操做,能夠指定管道的路徑和打開的模式。
小知識:用戶還能夠在命令行使用 「mknod 管道名 p」 來建立有名管道。
在建立管道成功以後,就可使用open、read、write這些函數了。與普通文件的開發設置同樣,對於爲讀而打開的管道可在open中設置O_RDONLY,對於爲寫而打開的管道可在open中設置O_WRONLY,在這裏與普通文件不一樣的是阻塞問題。因爲普通文件的讀寫時不會出現阻塞問題,而在管道的讀寫中卻有阻塞的可能,這裏的非阻塞標誌能夠在open函數中設定爲O_NONBLOCK。下面分別對阻塞打開和非阻塞打開的讀寫進行必定的討論。
對於讀進程
對於寫進程
2. mkfifo函數格式
mkfifo函數的語法要點
所需頭文件 | #include<sys/types.h> | |
#include<sys/state.h> | ||
函數原型 | int mkfifo(const char *filename,mode_t mode) | |
函數傳入值 | filename:要建立的管道 | |
mode | O_RDONLY:讀管道 | |
O_ERONLY:寫管道 | ||
O_REWR:讀寫管道 | ||
O_NONBLOCK:非阻塞 | ||
O_CREAT:若是該文件不存在,那麼就建立一個新的文件,並用第三個參數爲其設置權限。 | ||
O_EXCL:若是使用O_CREAT時文件存在,那麼可返回錯誤消息。這一參數可測試文件是否存在 | ||
函數返回值 | 成功:0 | |
出錯:-1 |
FIFO相關的出錯信息
EACCESS | 參數filename所指定的目錄路徑無可執行的權限 |
EEXIST | 參數filename所指定的文件已存在 |
ENAMETOOLONG | 參數filename的路徑名太長 |
ENOENT | 參數filename包含的目錄不存在 |
ENOSPC | 文件系統的剩餘空間不足 |
ENOTDIR | 參數filename路徑中的目錄存在到卻非真正的目錄 |
EROFS | 參數filename指定的文件存在於只讀文件系統內 |
3.使用實例
下面的實例包含了兩個程序,一個用於讀管道,另外一個用於寫管道。其中在寫管道的程序裏建立管道,而且做爲main函數裏的參數由用戶輸入要寫入的內容。讀管道讀出了用戶寫入管道的內容,這兩個函數用的是非阻塞讀寫管道。
#include <string.h> #include <unistd.h> #define FIFO_SERVER "/tmp/myfifo" int main(int argc,char *argv[]) { int fd; char w_buf[100]; int nwrite; if(fd==-1) if(errno==ENXIO) printf("open error;no reading process\n"); /* 打開FIFO管道,並設置非阻塞標誌 */ fd = open(FIFO_SERVER,O_WRONLY|O_NONBLOCK,0); if(argc==1) printf("Please send something\n"); strcpy(w_buf,argv[1]); /* 向管道中寫入字符串 */ if((nwrite=write(fd,w_buf,100))==-1){ if(errno==EAGAIN) printf("The FIFO has not been read yet.Please try later\n"); }else printf("write %s to the FIFO\n",w_buf); }
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <string.h> #define FIFO "/tmp/myfifo" int main(int argc,char *argv[]) { char buf_r[100]; int fd; int nread; /* 建立有名管道,並設置相應的權限 */ if((mkfifo(FIFO,O_CREAT|O_EXCL)<0)&&(errno!=EEXIST)) printf("cannot create fifoserver\n"); printf("Preparing for reading bytes...\n"); memset(buf_r,0,sizeof(buf_r)); /* 打開有名管道,並設置非阻塞標誌 */ fd = open(FIFO,O_RDONLY|O_NONBLOCK,0); if(fd==-1){ perror("open"); exit(1); } while(1){ memset(buf_r,0,sizeof(buf_r)); if((nread=read(fd,buf_r,100))==-1){ if(errno==EAGAIN) printf("no data yet\n"); } printf("read %s from FIFO\n",buf_r); sleep(1); } pause(); unlink(FIFO); }
爲了能更好地觀察運行結果,須要把這兩個程序分別在兩個終端裏運行,在這裏首先啓動讀管道程序。因爲這是非阻塞管道,所以在創建管道以後程序就開始循環從管道里讀出內容。在啓動了寫管道程序後,讀進程可以從讀管道里讀出用戶的輸入內容,程序運行結果以下:
abc@pc:~/c/app$ sudo ./fifo_read [sudo] password for abc: Preparing for reading bytes... read from FIFO read from FIFO read from FIFO read from FIFO read 123456 from FIFO read from FIFO read from FIFO read from FIFO read from FIFO read from FIFO read Hello from FIFO read from FIFO read from FIFO read from FIFO read from FIFO
abc@pc:~/c/app$ ./fifo_write 123456 write 123456 to the FIFO abc@pc:~/c/app$ ./fifo_write Hello write Hello to the FIFO
8.3.1 信號概述
信號是UNIX中所使用的進程間通訊的一種最古老的方法。它是在軟件層次上對中斷機制的一種模擬,是一種異步通訊方式。信號能夠直接進行用戶空間進程和內核進程之間的交互,內核進程也能夠利用它來通知用戶空間進程發生了哪些系統時間。它能夠在任什麼時候候發給某一進程,而無需知道該進程的狀態。若是該進程當前並未處於執行態,則該信號就由內核保存起來,直到該進程恢復執行再傳遞給它爲止;若是一個信號被進程設置爲阻塞,則該信號的傳遞被延遲,直到其阻塞被取消時才被傳遞給進程。
細心的讀者是否還記得,在第2章kill命令中曾講解到「-l」選項,這個選項能夠列出該系統所支持的全部信號列表。在筆者的系統中,信號值在32以前的則有不一樣的名稱,而信號值在32以後的都是用「SIGRTMIN」或「SIGRTMAX」開頭的,這就是兩種典型的信號。前者是從UNIX系統中繼承下來的信號,爲不可靠信號(也成爲非實時信號);後者是爲了解決前面"不可靠信號"的問題而進行了更改和擴充的信號,成爲「可靠信號」(也稱爲實時信號)。那麼爲何以前的信號不可靠呢?這裏首先要介紹一下信號的聲明週期。
一個完整的信號聲明週期能夠分爲3個重要階段,這3個階段由4個重要事件來刻畫的:信號產生、信號在進程中註冊、信號在進程中註銷、執行信號處理函數,如線圖所示:
相鄰兩個事件的時間間隔構成信號聲明週期的一個階段。要注意這裏的信號處理有多重方式,通常是由內核完成的,固然也能夠由用戶進程來完成,故在此沒有明確畫出。
一個不可靠信號的處理過程是這樣的:若是發現該信號已經在進程中註冊,那麼久忽略該信號。所以,前一個信號還未註銷又產生了相同的信號就會產生信號丟失。而當可靠信號發送給一個進程時,無論該信號是否已經在進程中註冊,都會被再註冊一次,所以信號就不會丟失。全部可靠信號都支持排隊,而不可靠信號則都不支持排隊。
注意:這裏信號的產生、註冊、註銷等是指信號的內部實現機制,而不是信號的函數實現。所以,信號註冊與否,與本節後面降到的發送信號函數(如kill()等)以及信號安裝函數(如signal()等)無關,只與信號值有關。
用戶進程對信號的響應能夠由3中方式。
Linux中大多數信號是提供給內核的,下表列出Linux中最爲常見信號的函數及其默認操做。
信號名 | 含義 | 默認操做 |
SIGHUP | 該信號在用戶端連接(正常或非正常)結束時發出,一般是在終端的控制進程結束時,通知同一會話內的各個做業與控制終端再也不關聯。 | 終止 |
SIGINT | 該信號在用戶鍵入INTR字符時發出,終端驅動程序發送此信號並送到前臺進程中的每個進程。 | 終止 |
SIGQUIT | 該信號與SIGINT相似,但由QUIT字符(一般是Ctrl-\)來控制。 | 終止 |
SIGILL | 該信號在一個進程企圖執行一條非法指令時(可執行文件自己出現錯誤,或者試圖直線數據段、堆棧溢出時)發出。 | 終止 |
SIGFPE | 該信號在發生致命的算術運算錯誤是發出。這裏不只包括浮點運算錯誤,還包括溢出及除數爲0等其餘全部的算術的錯誤。 | 終止 |
SIGKILL | 該信號用來當即結束程序的運行,而且不能被阻塞、處理和忽略。 | 終止 |
SIGALRM | 該信號當一個定時器到時的時候發出 | 終止 |
SIGSTOP | 該信號用於暫停一個進程,且不能被阻塞、處理貨忽略 | 暫停進程 |
SIGTSTP | 該信號用於交互中止進程,用戶可鍵入SUSP字符時(一般是Ctrl+Z)發出這個信號。 | 中止進程 |
SIGCHLD | 子進程改變狀態是,父進程會受到這個信號 | 忽略 |
SIGABORT |
8.3.2 信號發送與捕捉
發送信號的函數主要有kill()、raise()、alarm()以及pause(),下面就一次對其進行介紹。
1. kill()和raise()
(1) 函數說明
kill函數同讀者熟知的kill系統命令同樣,能夠發送信號給進程或進程組(實際上,kill系統命令只是kill函數的一個用戶接口)。這裏要注意的事,它不只能夠停止進程(實際上發送SIGKILL信號),也能夠向進程發送其餘信號。
與kill函數所不一樣的是,raise函數容許進程間向自身發送信號。
(2) 函數格式
kill
所需頭文件 | #include<signal.h> | |
#include<sys/types.h> | ||
函數原型 | int kill(pid_t pid,int sig) | |
函數傳入值 | pid | 正數:要發送信號的進程號 |
0:信號被髮送到全部和pid進程在同一個進程組的進程 | ||
-1:信號發送給全部的進程表中的進程(除了進程號最大的進程外) | ||
sig | 信號 | |
函數返回值 | 成功 | 0 |
出錯 | -1 |
raise
所需頭文件 | #include<signal.h> | |
#include<sys/types.h> | ||
函數原型 | int raise(int sig) | |
函數傳入值 | sig:信號 | |
函數返回值 | 成功:0 | |
出錯:-1 |
(3) 函數實例
下面這個示例首先使用fork建立了一個子進程,接着爲了保證子進程不在父進程調用kill以前推出,在子進程中使用raise函數向子進程發送SIGSTOP信號,使子進程暫停。接下來再在父進程中調用kill向子進程發送信號,在該實例中使用的是SIGKILL,讀者可使用其餘信號進行練習。
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { pid_t pid; int ret; /* 建立一子進程 */ if((pid=fork())<0){ perror("fork"); exit(1); } if(pid==0){ /* 在子進程中使用raise函數發出SIGSTOP信號 */ raise(SIGSTOP); exit(0); }else{ /* 在父進程中收集子進程發出的信號,並調用kill函數進行相應的操做 */ printf("pid=%d\n",pid); if((waitpid(pid,NULL,WNOHANG))==0){ if((ret=kill(pid,SIGKILL))==0){ printf("kill %d\n",pid); }else{ perror("kill"); } } } }
運行結果:
abc@pc:~/c/app$ ./kill pid=20460 kill 20460 abc@pc:~/c/app$ ./kill pid=20462 kill 20462
2. alarm()和pause()
(1) 函數說明
alarm也稱爲鬧鐘函數,它能夠在進程中設置一個定時器,當定時器指定的時間到時,它就向進程飯掃那個SIGALARM信號。要注意的是,一個進程只能有一個鬧鐘時間,若是在調用alarm以前已經設置過鬧鐘時間,則任何之前的鬧鐘時間都被新值所代替。
pause函數是用於將調用進程掛起直至捕捉到信號位置。這個函數很經常使用,一般能夠用於判斷信號時候已到。
(2) 函數格式
alarm
所需頭文件 | #inlcude<unistd.h> |
函數原型 | unsigned int alarm(unsigned int seconds) |
函數傳入值 | seconds:指定秒數 |
函數返回值 | 成功:若是調用此alarm()前,進程中已經設置了鬧鐘時間,則返回上一個鬧鐘時間的剩餘時間,不然返回0 |
出錯:-1 |
pause
所需頭文件 | #include<unistd.h> |
函數原型 | int pause(void) |
函數返回值 | -1: 而且報error值設爲EINTR |
(3) 函數實例
該實例實際上已完成了一個簡單的sleep函數的功能,因爲SIGALARM默認的系統動做爲終止該進程,所以在程序調用pause以後,程序就終止了。
#include <unistd.h> #include <stdio.h> #include <stdlib.h> int main() { int ret; /* 調用alarm定時器函數 */ ret = alarm(5); pause(); printf("I have waken up. %d\n",ret); }
運行結果:
abc@pc:~/c/app$ ./alarm
Alarm clock
8.3.3 信號的處理
在瞭解了信號的產生和捕獲以後,接線來就要對信號進行具體的操做了。從前面的信號概述中讀者也能夠看到,特定的信號是與必定的進程相聯繫的。也就是說,一個進程能夠決定在該進程中須要對那些信號進行什麼樣的處理。例如,一個進程能夠選擇忽略某些信號而只處理其餘一些信號,另外,一個進程還能夠選擇如何處理信號。總之,這些都是與特定的進程相聯繫的。所以,首先就要創建其信號與進程之間的對應關係,這就是信號的處理。
注意:請讀者區分信號的註冊於信號的處理之間的差異,前者信號是主動方,然後者進程是主動方。信號的註冊是在進程選擇了特定信號處理以後特定信號的主動行爲。
信號處理的主要方法有兩種,一種是使用簡單的signal函數,另外一種是使用信號集函數組。下面分別介紹這兩種處理方式。
1. signal()
(1) 函數說明
使用signal函數處理時,只需把要處理的信號和處理函數列出便可。它主要是用於前32中非實時信號的處理,不支持信號傳遞信息,可是因爲使用簡單、易於理解,所以也收到不少程序員的歡迎。
(2)函數格式
signal
所需頭文件 | #include<signal.h> | |
函數原型 | void (*signal(int signum,void(*handler)(int)))(int) | |
函數傳入值 | signum:指定信號 | |
handler | SIG_ING:忽略該信號 | |
SIG_DFL:採用系統默認方式處理信號 | ||
自定義的信號處理函數指針 | ||
函數返回值 | 成功:之前的信號處理配置 | |
出錯:-1 |
這裏須要對函數原型進行說明。這個函數原型很是複雜。可先用以下typedef進行替換說明:
typedef void sign(int); sign *signal(int ,handler *);
可見,首先該函數原型總體指向一個無返回值帶一個整形參數的函數指針,也就是信號的原始配置函數。接着該原型有帶有兩個參數,其中的第二個參數能夠是用於自定義的信號處理函數的函數指針。
(3) 使用實例
該示例代表瞭如何使用signal函數捕捉相應信號,並做出給定的處理。這裏,my_func就是信號處理的函數指針。讀者還能夠將其改成SIG_ING或SIG_DFL查看運行結果。
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> /* 自定義信號處理函數 */ void my_func(int sign_no) { if(sign_no==SIGINT) printf("I have get SIGINT\n"); else if(sign_no==SIGQUIT) printf("I have get SIGQUIT\n"); } int main() { printf("Waiting for signal SIGINT or SIGQUIT\n"); /* 發出相應的信號,並跳轉到信號處理函數 */ signal(SIGINT,my_func); signal(SIGQUIT,my_func); pause(); exit(0); }
運行結果:
abc@pc:~/c/app$ ./my_signal Waiting for signal SIGINT or SIGQUIT ^CI have get SIGINT abc@pc:~/c/app$ ./my_signal Waiting for signal SIGINT or SIGQUIT ^\I have get SIGQUIT
2. 信號集函數組
(1) 函數說明
使用信號集函數組處理信號時涉及一系列的函數,這些函數按照調用的前後次序可分爲如下幾大功能模塊:建立信號集合、登記信號處理器以及檢測信號。
其中,建立信號集合主要用於建立用戶感興趣的信號,其函數包括如下幾個:
登記信號處理器主要用戶決定進程如何處理信號。這裏要注意的事,信號集裏的信號並非真真能夠處理的信號,只要當信號的狀態處於非阻塞狀態是才真正其做用。所以,首先就要判斷出當前阻塞能不能傳遞給該信號的信號集。這裏首先使用sigprocmask函數判斷檢測或更改信號屏蔽字,而後使用sigaction函數用於改變進程接收到特定信號以後的行爲。
檢測信號是信號處理的後續步驟,但不是必須的。因爲內核能夠在任什麼時候刻向某一進程發出信號,所以,若該進程必須保持非中斷狀態且但願將這些信號阻塞,這些信號就處於「未決」狀態(也就是進程不清楚它的存在)。因此,在但願保持非中斷進程完成相應的任務以後,就應該將這些信號解除阻塞。sigpending函數就容許進程檢測「未決」信號,並進一步決定對它們做何處理。
(2) 函數格式
首先介紹建立信號集合的函數格式
所需頭文件 | #inlcude<signal.h> |
函數原型 | int sigemptyset(sigset_t *set) |
int sigfillset(sigset_t *set) | |
int sigaddset(sigset_t *set,int signum) | |
int sigdelset(sigset_t *set,int signum) | |
int sigismember(sigset_t *set,int signum) | |
函數傳入值 | set :信號集 |
signnum:指定信號值 | |
函數返回值 | 成功:0 (sigismember成功返回1,失敗返回0) |
出錯:-1 |
sigprocmask
所需頭文件 | #include<signal.h> | |
函數原型 | int sigprocmask(int how,const sigset_t *set,sigset_t *oset) | |
函數傳入值 | how:決定函數的操做方式 | SIG_BLOCK:增長一個信號集合到當前進程的阻塞集合中 |
SIG_UNBLOCK:從當前的阻塞集合之中刪除一個信號集合 | ||
SIG_SETMASK:將當前的信號集合設置爲信號阻塞集合 | ||
set:指定信號集 | ||
oset:信號屏蔽字 | ||
函數返回值 | 成功:0(sigismember成功返回1,失敗返回0) | |
出錯:-1 |
此處,若set是一個非空指針,則參數how表示函數的操做方式;若how爲空,則表示忽略此操做。
sigaction
所需頭文件 | #include<signal.h> |
函數原型 | int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact) |
函數傳入值 | signum:信號的值,能夠爲SIGKILL及SIGSTOP外的任何一個特定有效的信號 |
act:指向結構sigaction的一個實例的指針,指定對特定信號的處理 | |
oldact:保存原來對相對信號的處理 | |
函數返回值 | 成功:0 |
出錯:-1 |
這裏要說明的是sigaction函數中第2個和第3個參數用到的sigaction結構。這是一個看似很是複雜的結構,但願讀者可以慢慢閱讀此段內容。
首先給出了sigaction的定義
struct sigaction { void (*sa_handler)(int signo); sigset_t sa_mask; int sa_flags; void (*sa_restore)(void); }
sa_handler 是一個函數指針,指定信號關聯函數,這裏出能夠是用戶自定義的處理函數外,還能夠爲SIG_DFL(採用缺省的處理方式)或SIG_IGN(忽略信號)。它的處理函數只有一個參數,即信號值。
sa_mask 是一個信號集,它能夠指定在信號處理程序執行過程當中哪些信號應當被阻塞,在調用信號捕獲函數以前,該信號集要加入到信號的信號屏蔽字中。
sa_flags 中包含了不少標誌位,是對信號進行處理的各個選擇項。它的常見可選值以下:
選項 | 含義 |
SA_NODEFER\SA_NOMASK | 當捕捉到此信號時,在執行其信號捕捉函數時,系統不會自動阻塞此信號。 |
SA_NOCLDSTOP | 進程忽略子進程產生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU信號。 |
SA_RESTART | 可以讓重啓的系統調用從新起做用。 |
SA_ONESHOT\SA_RESETHAND | 自定義信號只執行一次,在執行完畢後恢復信號的系統默認動做。 |
sigpending語法
所需頭文件 | #include<signal.h> |
函數原型 | int sigpending(sigset_t *set) |
函數傳入值 | set:要檢測的信號集 |
函數返回值 | 成功:0 |
出錯:-1 |
總之,在處理信號時,通常遵循下圖操做流程:
(3) 使用實例
該實例首先把SIGQUIT、SIGINT兩個信號加入信號集,而後將該信號集設爲阻塞狀態,並在該狀態下使程序暫停5秒。接下來再講信號集設爲非阻塞狀態,再對這兩個信號分別操做,其中SGIQUIT執行默認操做,而SIGINT執行用戶自定義函數的操做。
#include <sys/types.h> #include <unistd.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> /* 自定義的信號處理函數 */ void my_func(int signum) { printf("If you want to quit,please try SIQUIT\n"); } int main() { sigset_t set,pendset; struct sigaction action1,action2; /* 初始化信號集爲空 */ if(sigemptyset(&set)<0) perror("sigemptyset"); /* 將相應的信號加入信號集 */ if(sigaddset(&set,SIGQUIT)<0) perror("sigaddset"); if(sigaddset(&set,SIGINT)<0) perror("sigaddset"); /* 設置信號集屏蔽字 */ if(sigprocmask(SIG_BLOCK,&set,NULL)<0) perror("sigprocmask"); else{ printf("blocked\n"); sleep(5); } if(sigprocmask(SIG_UNBLOCK,&set,NULL)<0) perror("sigprocmask"); else printf("unblock\n"); /* 對應的信號進行循環處理 */ while(1) { if(sigismember(&set,SIGINT)){ sigemptyset(&action1.sa_mask); action1.sa_handler = my_func; sigaction(SIGINT,&action1,NULL); }else if(sigismember(&set,SIGQUIT)){ sigemptyset(&action2.sa_mask); action2.sa_handler = SIG_DFL; sigaction(SIGTERM,&action2,NULL); } } }
運行結果:
abc@pc:~/c/app$ ./sigaction blocked unblock ^CIf you want to quit,please try SIQUIT ^CIf you want to quit,please try SIQUIT ^CIf you want to quit,please try SIQUIT ^CIf you want to quit,please try SIQUIT ^CIf you want to quit,please try SIQUIT ^\Quit (core dumped) abc@pc:~/c/app$
可見,在信號處於阻塞狀態是,所發出的信號對進程不起作。讀者等待5秒,在信號解除阻塞狀態以後,用戶發出的信號才能正常運行。這裏的SIGINT已安裝用戶定義的函數運行。
8.4.1 共享內存概述
能夠說,共享內存是一種最爲高效的進程間通訊方式。由於進程能夠直接讀寫內存,不須要任何數據的拷貝。爲了在多個進程間交換信息,內核專門留出了一塊內存區。這段內存區能夠由須要訪問的進程將其映射到在即的私有地址空間。所以,進程就能夠直接讀寫這一內存區而不須要進行數據的拷貝,從而大大提升了效率。固然,因爲多個進程共享一段內存,所以也須要依靠某種同步機制,如互斥鎖和信號量等。其原理示意圖:
8.4.2 共享內存實現
1. 函數說明
共享內存的實現分爲兩個步驟,第一步是建立共享內存,這裏用到的函數是shmget,也就是從內存中得到一段共享內存區域。第二步映射共享內存,也就是把這段建立的共享內存映射到具體的進程空間去,這裏使用的函數是shmat。到這裏,就可使用這段共享內存了,也就是可使用不帶緩衝的I/O讀寫命令對其進行操做。除此以外,固然還有撤銷映射的操做,其函數爲shmdt。這裏就主要介紹這3個函數。
2. 函數格式
shmget
所需頭文件 | #include<sys/types.h> |
#include<sys/ipc.h> | |
#include<sys/shm.h> | |
函數原型 | int shmget(key_t key,int size,int shmflg) |
函數傳入值 | key:IPC_PRIVATE |
size: 共享內存區大小 | |
shmflg:同open函數的權位,也能夠用八進制表示法 | |
函數返回值 | 成功:共享內存段標誌符 |
出錯:-1 |
shmat
所需頭文件 | #inlcude<sys/types.h> | |
#include<sys/ipc.h> | ||
#include<sys/shm.h> | ||
函數原型 | char *shmat(int shmid,const *shmaddr,int shmflg) | |
函數傳入值 | shmid:要映射的共享內存區標誌符 | |
shmaddr:將共享內存映射到指定位置(若爲0則表示把該段共享內存映射到調用進程的地址空間) | ||
shmflg | SHM_RDONLY:共享內存只讀 | |
默認0:共享內存可讀寫 | ||
函數返回值 | 成功:被映射的段地址 | |
出錯:-1 |
shmdt
所需頭文件 | #include<sys/types.h> |
#include<sys/ipc.h> | |
#inlcude<sys/shm.h> | |
函數原型 | int shmdt(const void*shmaddr) |
函數傳入值 | shmaddr:被映射的共享內存段地址 |
函數返回值 | 成功:0 |
出錯:-1 |
3. 使用實例
該實例說明了如何使用基本的共享內存函數,首先是建立一個共享內存區,以後將其映射到被進程中,最後再解除這種映射關係。這裏要介紹的一個命令是ipcs,這是用於報告進程間通訊機制狀態的命令。它能夠查看共享內存、消息隊列等各類進程間通訊機制的狀況,這裏使用了system函數用於調用shell命令「ipcs」。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <stdlib.h> #define BUFSZ 2048 int main() { int shmid; char *shmadd; /* 建立共享內存 */ if((shmid=shmget(IPC_PRIVATE,BUFSZ,0666))<0){ perror("shmget"); exit(1); }else printf("create share-memofy:%d\n",shmid); system("ipcs -m"); /* 映射共享內存 */ if((shmadd=shmat(shmid,0,0))<(char *)0){ perror("shmat"); exit(1); }else printf("attached shared_memory\n"); /* 顯示系統內存狀況 */ system("ipcs -m"); /* 刪除共享內存 */ if((shmdt(shmadd))<0){ perror("shmdt"); exit(1); }else printf("deleted shared-memory\n"); system("ipcs -m"); exit(0); }
運行結果:
abc@pc:~/c/app$ ./shmadd create share-memofy:1474571 ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 65536 abc 600 524288 2 dest ...... 0x00000000 1441802 abc 600 524288 2 dest 0x00000000 1474571 abc 666 2048 0 attached shared_memory ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 65536 abc 600 524288 2 dest ...... 0x00000000 1441802 abc 600 524288 2 dest 0x00000000 1474571 abc 666 2048 1 deleted shared-memory ------ Shared Memory Segments -------- key shmid owner perms bytes nattch status 0x00000000 65536 abc 600 524288 2 dest ...... 0x00000000 1441802 abc 600 524288 2 dest 0x00000000 1474571 abc 666 2048 0
8.5.1 消息隊列概述
顧名思義,消息隊列就是一個消息的列表。用戶能夠從消息隊列中添加消息、讀取消息等。從這點上看,消息隊列具備必定的FIFO的特性,可是它能夠實現消息的隨機查詢,比FIFO具備更大的優點。同時,這些消息優點存在於內核中的,有「隊列ID」來標識。
8.5.2 消息隊列實現
1. 函數說明
消息隊列的實現包括建立或打開消息隊列、添加消息、讀取消息和控制消息隊列這四種操做。其中建立或打開消息隊列使用的函數是msgget,這裏建立的消息對隊列的數量會受到系統消息隊列數量的限制;添加消息使用的函數是msgsnd函數,它把消息添加到已打開的消息隊列末尾;讀取消息使用的函數是msgrcv,它把消息從消息隊列中取出,與FIFO不一樣的是,這裏能夠指定取走某一條消息;最後控制消息隊列使用的函數是msgctl,它能夠完成多項功能。
2. 函數格式
msgget
所需頭文件 | #include<sys/types.h> |
#include<sys/ipc.h> | |
#include<sys/shm.h> | |
函數原型 | int msgget(key_t key,int flag) |
函數傳入值 | key:返回新的或已有隊列ID,IPC_PRIVATE |
flag | |
函數返回值 | 成功:消息隊列ID |
出錯:-1 |
msgsnd
所需頭文件 | #include<sys/types.h> | |
#include<sys/ipc.h> | ||
#inlcude<sys/shm.h> | ||
函數原型 | int msgsnd(int msqid,const void *prt,size_t size,int falg) | |
函數傳入值 | msqid:消息隊列的隊列ID | |
prt:指向消息結構的指針。該消息的結果msgbuf爲: struct msgbuf{ long mtype;//消息類型 char mtext[1];//消息正文 } |
||
size:消息的字節數,不要以null結尾 | ||
flag | IPC_NOWAIT 若消息並無當即發送而調用進程會當即返回 | |
0:msgsnd調用阻塞直到條件知足爲止 | ||
函數返回值 | 成功:0 | |
出錯:-1 |
msgrcv
所需頭文件 | #inlcude<sys/types.h> | |
#include<sys/ipc.h> | ||
#include<sys/shm.h> | ||
函數原型 | int msgrcv(int msgid,msbuf* msgp,int size,long msgtype,int flag) | |
函數傳入值 | msgid:消息隊列的隊列ID | |
msgp:消息緩衝區 | ||
size:消息的字節數,不要以null結尾 | ||
msgtype | 0:接收消息隊列中第一個消息 | |
大於0:接收消息隊列中第一個類型爲msgtyp的消息 | ||
小於0:接收消息隊列中第一個類型值不小於msgtyp絕對值且類型值又最小的消息 | ||
flag | MSG_NOERROR:若返回的消息比size字節多,則消息就會截斷奧size字節,且不通知消息發送進程 | |
IPC_NOWAIT:若消息並無當即發送而調用進程會當即返回 | ||
0:msgsnd調用阻塞直到條件知足爲止 | ||
函數返回值 | 成功:0 | |
出錯:-1 |
msgctl
所需頭文件 | #include<sys/types.h> | |
#include<sys/ipc.h> | ||
#include<sys/shm.h> | ||
函數原型 | int msgctl(int msgqid,int cmd,struct msqid_ds *buf) | |
函數傳入值 | msqid:消息隊列的隊列ID | |
cmd | IPC_STAT:讀取消息隊列的數據結果msqid_ds,並將其存儲在buf指定的地址中 | |
IPC_SET:設置消息隊列的數據結果msqid_ds中的ipc_perm元素的值。這個值取自buf參數。 | ||
IPC_RMID:從系統內核中移走消息隊列 | ||
buf:消息隊列緩衝區 | ||
函數返回值 | 成功:0 | |
出錯:-1 |
3.使用實例
這個實例體現瞭如何使用消息隊列進行進程間通訊,包括消息隊列的建立、消息發送與讀取、消息隊列的撤銷等多種操做。注意這裏使用了函數ftok,它能夠根據不一樣的路徑和關鍵表示產生標準的key。
#include<sys/ipc.h> #include<sys/msg.h> #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #define BUFSZ 512 struct message { long msg_type; char msg_text[BUFSZ]; }; int main() { int qid; key_t key; int len; struct message msg; /* 根據不一樣的路徑和關鍵表示產生標準的key */ if((key=ftok(".",'a'))==-1){ perror("ftok"); exit(1); } /* 建立消息隊列 */ if((qid=msgget(key,IPC_CREAT|0666))==-1){ perror("msgget"); exit(1); } printf("opened queue %d\n",qid); puts("Please enter the message to queue:"); if((fgets((&msg)->msg_text,BUFSZ,stdin))==NULL){ puts("no message"); exit(1); } msg.msg_type = getpid(); len = strlen(msg.msg_text); /* 添加消息隊列 */ if((msgsnd(qid,&msg,len,0))<0){ perror("message posted"); exit(1); } /* 讀取消息隊列 */ if(msgrcv(qid,&msg,BUFSZ,0,0)<0){ perror("msgrcv"); exit(1); } printf("message is:%s\n",(&msg)->msg_text); /* 從系統內核中移走消息隊列 */ if((msgctl(qid,IPC_RMID,NULL))<0){ perror("msgctl"); exit(1); } exit(0); }
運行:
abc@pc:~/c/app$ ./msg opened queue 0 Please enter the message to queue: hello message is:hello abc@pc:~/c/app$ ./msg opened queue 32768 Please enter the message to queue: world message is:world