linux基礎(chǔ)復(fù)習(xí)(8)進(jìn)程通信
數(shù)據(jù)傳輸:一個進(jìn)程需要將它的數(shù)據(jù)發(fā)送給另一個進(jìn)程,發(fā)送的數(shù)據(jù)量在一個字節(jié)到幾兆字節(jié)之間。
共享數(shù)據(jù):多個進(jìn)程想要操作共享數(shù)據(jù),一個進(jìn)程對共享數(shù)據(jù)的修改,別的進(jìn)程應(yīng)該立刻看到。
通知事件:一個進(jìn)程需要向另一個或一組進(jìn)程發(fā)送消息,通知它(它們)發(fā)生了某種事件(如進(jìn)程終止時要通知父進(jìn)程)。
資源共享:多個進(jìn)程之間共享同樣的資源。為了作到這一點,需要內(nèi)核提供鎖和同步機(jī)制。
進(jìn)程控制:有些進(jìn)程希望完全控制另一個進(jìn)程的執(zhí)行(如Debug進(jìn)程),此時控制進(jìn)程希望能夠攔截另一個進(jìn)程的所有陷入和異常,并能夠及時知道它的狀態(tài)改變。
UNIX 進(jìn)程間通信(IPC)方式包括管道、FIFO、信號。
Linux 中使用較多的進(jìn)程間通信方式主要有以下幾種。
(1)管道(Pipe)及有名管道(named pipe):管道可用于具有親緣關(guān)系進(jìn)程間的通信,有名管道,除具有管道所具有的功能外,它還允許無親緣關(guān)系進(jìn)程間的通信。
(2)信號(Signal):信號是在軟件層次上對中斷機(jī)制的一種模擬,它是比較復(fù)雜的通信方式,用于通知接受進(jìn)程有某事件發(fā)生,一個進(jìn)程收到一個信號與處理器收到一個中斷請求效果上可以說是一樣的。
(3)消息隊列:消息隊列是消息的鏈接表,包括Posix 消息隊列systemV 消息隊列。它克服了前兩種通信方式中信息量有限的缺點,具有寫權(quán)限的進(jìn)程可以向消息隊列中按照一定的規(guī)則添加新消息;對消息隊列有讀權(quán)限的進(jìn)程則可以從消息隊列中讀取消息。
(4)共享內(nèi)存:可以說這是最有用的進(jìn)程間通信方式。它使得多個進(jìn)程可以訪問同一塊內(nèi)存空間,不同進(jìn)程可以及時看到對方進(jìn)程中對共享內(nèi)存中數(shù)據(jù)的更新。這種通信方式需要依靠某種同步機(jī)制,如互斥鎖和信號量等。
(5)信號量:主要作為進(jìn)程間以及同一進(jìn)程不同線程之間的同步手段。
(6)套接字(Socket):這是一種更為一般的進(jìn)程間通信機(jī)制,它可用于不同機(jī)器之間的進(jìn)程間通信,應(yīng)用非常廣泛。
[b]管道通信[/b]
普通的Linux shell都允許重定向,而重定向使用的就是管道。例如:
ps | grep vsftpd
管道是單向的、先進(jìn)先出的、無結(jié)構(gòu)的、固定大小的字節(jié)流,它把一個進(jìn)程的標(biāo)準(zhǔn)輸出和另一個進(jìn)程的標(biāo)準(zhǔn)輸入連接在一起。寫進(jìn)程在管道的尾端寫入數(shù)據(jù),讀進(jìn)程在管道的首端讀出數(shù)據(jù)。數(shù)據(jù)讀出后將從管道中移走,其它讀進(jìn)程都不能再讀到這些數(shù)據(jù)。管道提供了簡單的流控制機(jī)制。進(jìn)程試圖讀空管道時,在有數(shù)據(jù)寫入管道前,進(jìn)程將一直阻塞。同樣,管道已經(jīng)滿時,進(jìn)程再試圖寫管道,在其它進(jìn)程從管道中移走數(shù)據(jù)之前,寫進(jìn)程將一直阻塞。
管道主要用于不同進(jìn)程間通信。
創(chuàng)建一個簡單的管道,可以使用系統(tǒng)調(diào)用pipe( )。它接受一個參數(shù),也就是一個包括兩個整數(shù)的數(shù)組。如果系統(tǒng)調(diào)用成功,此數(shù)組將包括管道使用的兩個文件描述符。創(chuàng)建一個管道之后,一般情況下進(jìn)程將產(chǎn)生一個新的進(jìn)程。
系統(tǒng)調(diào)用:pipe( );
原型:int pipe( int fd[2] );
返回值:如果系統(tǒng)調(diào)用成功,返回0。如果系統(tǒng)調(diào)用失敗返回- 1:
errno = EMFILE (沒有空閑的文件描述符)
EMFILE (系統(tǒng)文件表已滿)
EFAULT (fd數(shù)組無效)
注意:fd[0] 用于讀取管道,fd[1] 用于寫入管道。
#i nclude
#i nclude
#i nclude
#i nclude
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]);
}
管道主要用于不同進(jìn)程間通信。實際上,通常先創(chuàng)建一個管道,再通過fork函數(shù)創(chuàng)建一個子進(jìn)程。
可以通過打開兩個管道來創(chuàng)建一個雙向的管道。但需要在子進(jìn)程中正確地設(shè)置文件描述符。
必須在系統(tǒng)調(diào)用fork( )中調(diào)用pipe( ),否則子進(jìn)程將不會繼承文件描述符。
當(dāng)使用半雙工管道時,任何關(guān)聯(lián)的進(jìn)程都必須共享一個相關(guān)的祖先進(jìn)程。因為管道存在于系統(tǒng)內(nèi)核之中,所以任何不在創(chuàng)建管道的進(jìn)程的祖先進(jìn)程之中的進(jìn)程都將無法尋址它。而在命名管道中卻不是這樣。
與linux中文件操作有文件流的標(biāo)準(zhǔn)I/O一樣,管道的操作也支持基于文件流的模式。接口函數(shù)如下
庫函數(shù):popen();
原型: FILE *popen ( char *command, char *type);
返回值:如果成功,返回一個新的文件流。如果無法創(chuàng)建進(jìn)程或者管道,返回NULL。
管道中數(shù)據(jù)流的方向是由第二個參數(shù)type控制的。此參數(shù)可以是r或者w,分別代表讀或?qū)?。但不能同時為讀和寫。在Linux系統(tǒng)下,管道將會以參數(shù)type中第一個字符代表的方式打開。所以,如果你在參數(shù)type中寫入rw,管道將會以讀的方式打開。
使用popen()創(chuàng)建的管道必須使用pclose( )關(guān)閉。其實,popen/pclose和標(biāo)準(zhǔn)文件輸入/輸出流中的fopen() / fclose()十分相似。
庫函數(shù): pclose();
原型: int pclose( FILE *stream );
返回值: 返回系統(tǒng)調(diào)用wait4( )的狀態(tài)。
如果stream無效,或者系統(tǒng)調(diào)用wait4( )失敗,則返回 -1。
注意此庫函數(shù)等待管道進(jìn)程運(yùn)行結(jié)束,然后關(guān)閉文件流。
庫函數(shù)pclose( )在使用popen( )創(chuàng)建的進(jìn)程上執(zhí)行wait4( )函數(shù)。當(dāng)它返回時,它將破壞管道和文件系統(tǒng)。
#i nclude
#i nclude
#i nclude
#i nclude
#define BUFSIZE 1024
int main()
{
FILE *fp;
char *cmd = "ps -ef";
char buf[BUFSIZE];
buf[BUFSIZE] = '\0';
if((fp=popen(cmd,"r"))==NULL)
perror("popen");
while((fgets(buf,BUFSIZE,fp))!=NULL)
printf("%s",buf);[!--empirenews.page--]
pclose(fp);
exit(0);
}
[b]命名管道([/b][b]FIFO[/b][b])[/b]
命名管道和一般的管道基本相同,但也有一些顯著的不同:
n 命名管道是在文件系統(tǒng)中作為一個特殊的設(shè)備文件而存在的。
n 不同祖先的進(jìn)程之間可以通過管道共享數(shù)據(jù)。
n 當(dāng)共享管道的進(jìn)程執(zhí)行完所有的I / O操作以后,命名管道將繼續(xù)保存在文件系統(tǒng)中以便以后使用。
管道只能由相關(guān)進(jìn)程使用,它們共同的祖先進(jìn)程創(chuàng)建了管道。但是,通過FIFO,不相關(guān)的進(jìn)程也能交換數(shù)據(jù)。
命名管道創(chuàng)建
#i nclude
#i nclude
int mkfifo(const char * pathname,
mode_t mode) ;
返回:若成功則為0,若出錯則為- 1
一旦已經(jīng)用mkfifo創(chuàng)建了一個FIFO,就可用open打開它。確實,一般的文件I / O函數(shù)(close、read、write、unlink等)都可用于FIFO。
當(dāng)打開一個FIFO時,非阻塞標(biāo)志(O_NONBLOCK)產(chǎn)生下列影響:
(1) 在一般情況中(沒有說明O_NONBLOCK),只讀打開要阻塞到某個其他進(jìn)程為寫打開此FIFO。類似,為寫而打開一個FIFO要阻塞到某個其他進(jìn)程為讀而打開它。
(2) 如果指定了O_NONBLOCK,則只讀打開立即返回。但是,如果沒有進(jìn)程已經(jīng)為讀而打開一個FIFO,那么只寫打開將出錯返回,其errno是ENXIO。
類似于管道,若寫一個尚無進(jìn)程為讀而打開的FIFO,則產(chǎn)生信號SIGPIPE。若某個FIFO的最后一個寫進(jìn)程關(guān)閉了該FIFO,則將為該FIFO的讀進(jìn)程產(chǎn)生一個文件結(jié)束標(biāo)志。
FIFO相關(guān)出錯信息:
n EACCES (無存取權(quán)限)
n EEXIST (指定文件不存在)
n ENAMETOOLONG (路徑名太長)
n ENOENT (包含的目錄不存在)
n ENOSPC (文件系統(tǒng)剩余空間不足)
n ENOTDIR (文件路徑無效)
n EROFS (指定的文件存在于只讀文件系統(tǒng)中)
[b]信號通信[/b]
信號是軟件中斷。信號(signal)機(jī)制是Unix系統(tǒng)中最為古老的進(jìn)程之間的通信機(jī)制。它用于在一個或多個進(jìn)程之間傳遞異步信號。
很多條件可以產(chǎn)生一個信號。
n 當(dāng)用戶按某些終端鍵時,產(chǎn)生信號。在終端上按DELETE鍵通常產(chǎn)生中斷信號(SIGINT)。這是停止一個已失去控制程序的方法。(第11章將說明此信號可被映射為終端上的任一字符。)
n 硬件異常產(chǎn)生信號:除數(shù)為0、無效的存儲訪問等等。這些條件通常由硬件檢測到,并將其通知內(nèi)核。然后內(nèi)核為該條件發(fā)生時正在運(yùn)行的進(jìn)程產(chǎn)生適當(dāng)?shù)男盘?。例如,對?zhí)行一個無效存儲訪問的進(jìn)程產(chǎn)生一個SIGSEGV。
n 進(jìn)程用kill( 2 )函數(shù)可將信號發(fā)送給另一個進(jìn)程或進(jìn)程組。自然,有些限制:接收信號進(jìn)程和發(fā)送信號進(jìn)程的所有者必須相同,或發(fā)送信號進(jìn)程的所有者必須是超級用戶。
n 用戶可用kill( 1 )命令將信號發(fā)送給其他進(jìn)程。此程序是kill函數(shù)的界面。常用此命令終止一個失控的后臺進(jìn)程。
n 當(dāng)檢測到某種軟件條件已經(jīng)發(fā)生,并將其通知有關(guān)進(jìn)程時也產(chǎn)生信號。這里并不是指硬件產(chǎn)生條件(如被0除),而是軟件條件。例如SIGURG (在網(wǎng)絡(luò)連接上傳來非規(guī)定波特率的數(shù)據(jù))、SIGPIPE (在管道的讀進(jìn)程已終止后一個進(jìn)程寫此管道),以及SIGALRM(進(jìn)程所設(shè)置的鬧鐘時間已經(jīng)超時)。
內(nèi)核為進(jìn)程生產(chǎn)信號,來響應(yīng)不同的事件,這些事件就是信號源。主要的信號源如下:
n 異常:進(jìn)程運(yùn)行過程中出現(xiàn)異常;
n 其它進(jìn)程:一個進(jìn)程可以向另一個或一組進(jìn)程發(fā)送信號;
n 終端中斷:Ctrl-C,Ctrl-\等;
n 作業(yè)控制:前臺、后臺進(jìn)程的管理;
n 分配額:CPU超時或文件大小突破限制;
n 通知:通知進(jìn)程某事件發(fā)生,如I/O就緒等;
n 報警:計時器到期。
下面是幾個常見的信號。
n SIGHUP: 從終端上發(fā)出的結(jié)束信號;
n SIGINT: 來自鍵盤的中斷信號(Ctrl-C);
n SIGQUIT:來自鍵盤的退出信號(Ctrl-\);
n SIGFPE: 浮點異常信號(例如浮點運(yùn)算溢出);
n SIGKILL:該信號結(jié)束接收信號的進(jìn)程;
n SIGALRM:進(jìn)程的定時器到期時,發(fā)送該信號;
n SIGTERM:kill 命令發(fā)出的信號;
n SIGCHLD:標(biāo)識子進(jìn)程停止或結(jié)束的信號;
n SIGSTOP:來自鍵盤(Ctrl-Z)或調(diào)試程序的停止執(zhí)行信號
可以要求系統(tǒng)在某個信號出現(xiàn)時按照下列三種方式中的一種進(jìn)行操作。
(1) 忽略此信號。大多數(shù)信號都可使用這種方式進(jìn)行處理,但有兩種信號卻決不能被忽略。它們是:SIGKILL和SIGSTOP。這兩種信號不能被忽略的原因是:它們向超級用戶提供一種使進(jìn)程終止或停止的可靠方法。另外,如果忽略某些由硬件異常產(chǎn)生的信號(例如非法存儲訪問或除以0),則進(jìn)程的行為是未定義的。
(2) 捕捉信號。為了做到這一點要通知內(nèi)核在某種信號發(fā)生時,調(diào)用一個用戶函數(shù)。在用戶函數(shù)中,可執(zhí)行用戶希望對這種事件進(jìn)行的處理。如果捕捉到SIGCHLD信號,則表示子進(jìn)程已經(jīng)終止,所以此信號的捕捉函數(shù)可以調(diào)用waitpid以取得該子進(jìn)程的進(jìn)程ID以及它的終止?fàn)顟B(tài)。
(3) 執(zhí)行系統(tǒng)默認(rèn)動作。對大多數(shù)信號的系統(tǒng)默認(rèn)動作是終止該進(jìn)程。
每一個信號都有一個缺省動作,它是當(dāng)進(jìn)程沒有給這個信號指定處理程序時,內(nèi)核對信號的處理。有5種缺省的動作:
n 異常終止(abort):在進(jìn)程的當(dāng)前目錄下,把進(jìn)程的地址空間內(nèi)容、寄存器內(nèi)容保存到一個叫做core的文件中,而后終止進(jìn)程。
n 退出(exit):不產(chǎn)生core文件,直接終止進(jìn)程。
n 忽略(ignore):忽略該信號。
n 停止(stop):掛起該進(jìn)程。
n 繼續(xù)(continue):如果進(jìn)程被掛起,則恢復(fù)進(jìn)程的運(yùn)行。否則,忽略信號。
[b]信號發(fā)送與捕捉[/b]
kill()和raise()
kill()不僅可以中止進(jìn)程,也可以向進(jìn)程發(fā)送其他信號。
與kill函數(shù)不同的是,raise()函數(shù)運(yùn)行向進(jìn)程自身發(fā)送信號。
#i nclude
#i nclude
int kill(pid_t pid, int signo) ;
int raise(int signo) ;
兩個函數(shù)返回:若成功則為0,若出錯則為-1。
kill的pid參數(shù)有四種不同的情況:
[!--empirenews.page--]n pid>0 將信號發(fā)送給進(jìn)程ID為pid的進(jìn)程。
n pid == 0 將信號發(fā)送給其進(jìn)程組I D等于發(fā)送進(jìn)程的進(jìn)程組ID,而且發(fā)送進(jìn)程有許可權(quán)向其發(fā)送信號的所有進(jìn)程。
n pid 將信號發(fā)送給其進(jìn)程組ID等于pid絕對值,而且發(fā)送進(jìn)程有許可權(quán)向其發(fā)送信號的所有進(jìn)程。如上所述一樣,“所有進(jìn)程”并不包括系統(tǒng)進(jìn)程集中的進(jìn)程。
n pid ==-1 POSIX.1未定義此種情況。
#i nclude
#i nclude
#i nclude
#i nclude
#i nclude
int main()
{
pid_t pid;
int ret;
if((pid=fork())0){
perror("fork");
exit(1);
}
if(pid == 0){
raise(SIGSTOP);
exit(0);
}
else{
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");
}
}
}
}
使用alarm函數(shù)可以設(shè)置一個時間值(鬧鐘時間),在將來的某個時刻該時間值會被超過。當(dāng)所設(shè)置的時間值被超過后,產(chǎn)生SIGALRM信號。如果不忽略或不捕捉此信號,則其默認(rèn)動作是終止該進(jìn)程。
#i nclude
unsigned int alarm(unsigned int seconds) ;
返回:0或以前設(shè)置的鬧鐘時間的余留秒數(shù)
參數(shù)seconds的值是秒數(shù),經(jīng)過了指定的seconds秒后會產(chǎn)生信號SIGALRM。
每個進(jìn)程只能有一個鬧鐘時間。如果在調(diào)用alarm時,以前已為該進(jìn)程設(shè)置過鬧鐘時間,而且它還沒有超時,則該鬧鐘時間的余留值作為本次alarm函數(shù)調(diào)用的值返回。以前登記的鬧鐘時間則被新值代換。
如果有以前登記的尚未超過的鬧鐘時間,而且seconds值是0,則取消以前的鬧鐘時間,其余留值仍作為函數(shù)的返回值。
pause函數(shù)使調(diào)用進(jìn)程掛起直至捕捉到一個信號。
#i nclude
int pause(void);
返回:-1,errno設(shè)置為EINTR
只有執(zhí)行了一個信號處理程序并從其返回時,pause才返回。
#i nclude
#i nclude
#i nclude
int main()
{
int ret;
ret=alarm(5);
pause();
printf("I have been waken up.\n",ret);
}