Linux 進程間的通信(一)—管道通信(有名管道和無名管道)
Linux 下進程間通信概述
進程間通信就是在不同進程之間傳播或交換信息,那麼不同進程之間存在着什麼雙方都可以訪問的介質呢?每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開闢一塊緩衝區,進程1把數據從用戶空間拷到內核緩衝區,進程2再從內核緩衝區把數據讀走,內核提供的這種機制稱爲進程間通信(IPC,InterProcess Communication)
進程間的通信主要包括:
1.管道及有名管道:管道可用於具有親緣關係進程間的通信,有名管道:name_pipe, 除了管道的功能外,還可以在許多並不相關的進程之間進行通訊。
2.信號(Signal):信號是比較複雜的通信方式,用於通知接收進程有某種事件發生,除了用於進程間通信外,進程還可以發送信號給進程本身;Linux除了支持Unix早期信號語義函數sigal外,還支持語義符合Posix標準的信號函數sigaction。
3.報文(Message)隊列(消息隊列):消息隊列是消息的鏈接表,包括Posix消息隊列systemV消息隊列。有足夠權限的進程可以向隊列中添加消息,被賦予讀權限的進程則可以讀走隊列中的消息。消息隊列克服了信號承載信息量少,管道只能承載無格式字節流以及緩衝區大小受限等缺點。
4.共享內存:使得多個進程可以訪問同一塊內存空間,是最快的可用IPC形式。是針對其他通信機制運行效率較低而設計的。往往與其它通信機制,如信號量結合使用,來達到進程間的同步及互斥。
5.信號量:主要作爲進程間以及同一進程不同線程之間的同步手段。
6.套接口(Socket):更爲一般的進程間通信機制,可用於不同機器之間的進程間通信。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到Linux上。
管道通信
管道就是一種連接一個進程的標準輸出到另一個進程的標準輸入的方法。管道是最古老
的IPC工具,從UNIX系統一開始就存在。它提供了一種進程之間單向的通信方法。管道在系統中的應用很廣泛,即使在shell環境中也要經常使用管道技術。管道通信分爲管道和有名管道
,管道可用於具有親緣關係進程間
的通信,有名管道
, 除了管道的功能外,還可以在許多並不相關的進程
之間進行通訊。
管道 (匿名管道)
管道是一種“無名”、“無形”的文件
當進程創建一個管道時,系統內核設置了兩個管道可以使用的文件描述符。一個用於向管道中輸入信息(write),另一個用於管道中獲取信息(read)。
管道的特點:
·管道時半雙工的
,數據只能向一個方向流動。雙方通信時,需要建立起兩個管道。
·只能用於父子進程或則兄弟進程之間(具有親緣關係的進程
)
·單獨構成一種獨立的文件系統
:管道對於管道兩端的進程而言,就是一個文件,對於它的讀寫也可以使用普通的read、write等函數。但它不是普通的文件,它不屬於某種文件系統,而是自立門戶,單獨構成一種文件系統,並且只存在於內存中
。
·數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出數據。
管道的創建
管道是基於文件描述符
的通信方式,當一個管道建立時,它會創建兩個文件描述符fd[0]和fd[1],其中fd[0]固定用於讀管道
,而fd[1]固定用於寫管道
,無名管道的建立比較簡單,可以使用pipe()
函數來實現。其函數原型如下:
#include <unistd.h>
int pipe(int fd[2])
說明:參數fd[2]表示管道的兩個文件描述符,之後就可以直接操作這兩個文件描述符;函數調用成功則返回0,失敗返回−1。
管道的關閉
使用pipe()函數創建了一個管道,那麼就相當於給文件描述符fd[0]和fd[1]賦值,之後我們對管道的控制就像對文件的操作一樣,那麼我們就可以使用close()
函數來關閉文件,關閉了fd[0]和fd[1]就關閉了管道。
管道的讀/寫操作
fd[0] 稱爲管道讀端,fd[1] 稱爲管道寫端。管道的兩端是固定了任務的,即一端只能用於讀,另一端只能用於寫。
pipe.c
/* ************************************************************************
* Filename: pipe.c
* Description:
* Version: 1.0
* Created: 05/05/2020 10:38:37 PM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT)
* Company:
* ************************************************************************/
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER 80
int main(){
int fd[2], nbytes;
pid_t childpid;
char string[] = "Hello, PIC!\n";
char readbuffer[BUFFER];
if( pipe(fd) < 0 ){ // 創建管道
printf("創建失敗!\n");
return -1;
}
if( ( childpid = fork() ) == -1 ){ // 創建一個子進程
perror("fork\n");
exit(1);
}
if( childpid == 0 ){ // 子進程
close(fd[0]); // 子進程關閉讀取端
sleep(3); // 暫停確保父進程已關閉相應的寫描述符
write( fd[1], string, strlen(string)); // 通過寫端發送字符串
close(fd[1]); // 關閉子進程寫描述符
exit(0);
}
else{
close(fd[1]); // 父進程關閉寫端
memset(readbuffer, 0, sizeof(readbuffer)); // 清空緩衝區防止亂碼
nbytes = read( fd[0], readbuffer, sizeof(readbuffer)); // 從管道中讀取字符串
printf("Received string : %s\n", readbuffer);
close(fd[0]); // 關閉父進程讀描述符
}
return 0;
}
編譯運行
出現亂碼的情況是字符串沒有結束標誌,可以在末尾加’\0’,也可以接受前將緩衝區清零(memset(readbuffer, 0, sizeof(readbuffer));
)。
標準流管道
如果你認爲上面創建和使用管道的方法過於繁瑣的話,你也可以使用下面的簡單的方法:
庫函數:popen();
原型: FILE *popen ( char *command, char *type);
返回值:如果成功,返回一個新的文件流。如果無法創建進程或者管道,返回 NULL。管道中數據流的方向是由第二個參數type控制的。此參數可以使r(讀) 或者 w(寫),不能同時讀寫。Linux 下,管道將會以type 中的第一個字符代表的方式打開。
使用popen()創建的管道必須使用pclose()關閉。其實, popen/pclose和標準文件輸入/輸出流中的fopen()/fclose()十分相似。
庫函數: pclose();
原型: int pclose( FILE *stream );
返回值: 返回popen中執行命令的終止狀態 。如果stream無效,或者系統調用失敗,則返回-1。
popen函數其實是對管道操作的一些包裝,所完成的工作有以下幾步:
創建一個管道
fork 一個子進程
在父子進程中關閉不需要的文件描述符
執行 exec 函數族調用
執行函數中所指定的命令
popen.c
/* ************************************************************************
* Filename: popen.c
* Description:
* Version: 1.0
* Created: 05/05/2020 11:33:18 PM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define MAXSTRS 5
int main(void){
int cntr;
FILE *pipe_fp;
char *strings[MAXSTRS] = { "echo", "bravo", "alpha","charlie", "delta"};
if (( pipe_fp = popen("sort", "w")) == NULL) /*調用popen創建管道 */
{
perror("popen");
exit(1);
}
for(cntr=0; cntr<MAXSTRS; cntr++) /* 循環處理 */
{
fputs(strings[cntr], pipe_fp);
fputc('\n', pipe_fp);
}
pclose(pipe_fp); /* 關閉管道 */
return(0);
}
編譯運行
popen2.c
/* ************************************************************************
* Filename: popen2.c
* Description:
* Version: 1.0
* Created: 05/05/2020 11:33:18 PM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define BUFSIZE 200
int main(void){
FILE *pipe_fp;
char buf[BUFSIZE] = { 0 };
if (( pipe_fp = popen("cat 1.txt", "r")) == NULL) /*調用popen創建管道 */
{
perror("popen");
exit(1);
}
while( fgets( buf, BUFSIZE, pipe_fp) != NULL) /* 循環處理 */
{
fputs(buf, stdout);
memset(buf, 0, BUFSIZE);
}
pclose(pipe_fp); /* 關閉管道 */
return(0);
}
編譯運行結果
有名管道
有名管道是一種“有名”、“有形”的文件
有名管道,即FIFO管道和一般的管道基本相同,但也有一些顯著的不同:
FIFO管道不是臨時對象,而是在文件系統中作爲一個特殊的設備文件而存在的實體。並且可以通過mkfifo命令來創建。進程只要擁有適當的權限就可以自由的使用FIFO管道
不同祖先的進程之間可以通過管道共享數據
當共享管道的進程執行完所有的 I/O操作以後,命名管道將繼續保存在文件系統中以便以後使用
FIFO嚴格地遵循先進先出規則,對管道及FIFO的讀總是從開始處返回數據,對它們的寫則是把數據添加到末尾,它們不支持如 lseek()等文件定位操作。
有名管道可以用於任何兩個程序間通信,因爲有名字可引用。注意管道都是單向的,因此雙方通信需要兩個管道。
FIFO 的創建
mknod MYFIFO p // 只能通過chmod 修改權限
mkfifo -m a=rw MYFIFO1 // 提供FIFO文件存取的途徑
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo( const char *pathname, mode_t mode );
mkfifo函數需要兩個參數,第一個參數(pathname)是將要在文件系統中創建的一個專用文件。第二個參數(mode)用來規定FIFO的讀寫權限。Mkfifo函數如果調用成功的話,返回值爲0;如果調用失敗返回值爲-1。
實例:
fifowrite.c
/* ************************************************************************
* Filename: fifowrite.c
* Description:
* Version: 1.0
* Created: 05/06/2020 03:41:26 AM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include<sys/types.h>
#include<sys/stat.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#define BUFSIZE 256
int main()
{
char write_fifo_name[] = "lucy";
char read_fifo_name[] = "peter";
int write_fd, read_fd;
char buf[BUFSIZE];
// 創建管道 可讀可寫
int ret = mkfifo(write_fifo_name, S_IRUSR | S_IWUSR);
printf("%s create!\n", write_fifo_name);
if( ret == -1){
printf("Fail to create FIFO %s: %s",write_fifo_name,strerror(errno)); exit(-1);
return -1;
}
printf("open write %s\n", write_fifo_name);
write_fd = open(write_fifo_name, O_WRONLY); // 寫的方式打開
printf("open write %s success!\n", write_fifo_name);
if(write_fd == -1) {
printf("Fail to open FIFO %s: %s",write_fifo_name,strerror(errno));
exit(-1);
}
printf("wait open read %s...\n", read_fifo_name);
while((read_fd = open(read_fifo_name,O_RDONLY)) == -1){
sleep(1); // 等待創建完成
}
printf("wait open read %s success!\n", read_fifo_name);
while(1) {
printf("Lucy: ");
memset(buf, 0, BUFSIZE); // 緩衝區清零
fgets(buf, BUFSIZE, stdin);
buf[strlen(buf)-1] = '\0';
if(strncmp(buf,"quit", 4) == 0)
{
close(write_fd); // 關閉寫端
unlink(write_fifo_name); // 刪除管道
close(read_fd); // 關閉讀端
exit(0);
}
write(write_fd, buf, strlen(buf));
memset(buf, 0, BUFSIZE); // 緩衝區清零
printf("wait %s message!\n", read_fifo_name);
if( read(read_fd, buf, BUFSIZE) > 0 ){
// buf[len] = '\0';
printf("Peter: %s\n", buf);
}
}
}
fiforead.c
/* ************************************************************************
* Filename: fiforead.c
* Description:
* Version: 1.0
* Created: 05/06/2020 04:43:51 AM
* Revision: none
* Compiler: gcc
* Author: YOUR NAME (WCT),
* Company:
* ************************************************************************/
#include<sys/types.h>
#include<sys/stat.h>
#include<string.h>
#include<stdio.h>
#include<errno.h>
#include<fcntl.h>
#include<stdlib.h>
#define BUFSIZE 256
int main(void)
{
char write_fifo_name[] = "peter";
char read_fifo_name[] = "lucy";
int write_fd, read_fd;
char buf[BUFSIZE];
// 創建管道 可讀可寫
int ret = mkfifo(write_fifo_name, S_IRUSR | S_IWUSR);
printf("%s create!\n", write_fifo_name);
//sleep(3); // 用於觀察管道操作流程
if( ret == -1) // 如果創建失敗
{
printf("Fail to create FIFO %s: %s",write_fifo_name,strerror(errno));
exit(-1);
}
// 等待 read_fifo_name 創建完成
printf("wait open read %s...\n", read_fifo_name);
while((read_fd = open(read_fifo_name, O_RDONLY)) == -1)
{
sleep(1);
}
printf("wait open read %s success!\n", read_fifo_name);
// 寫的方式打開 write_fifo_name
sleep(3); // 用於觀察管道操作流程
printf("open write %s\n", write_fifo_name);
write_fd = open(write_fifo_name, O_WRONLY);
printf("open write %s success!\n", write_fifo_name);
if(write_fd == -1)
{
printf("Fail to open FIFO %s: %s", write_fifo_name, strerror(errno));
exit(-1);
}
while(1)
{
memset(buf, 0, BUFSIZE); // buf 清零
printf("wait %s message!\n", read_fifo_name);
if( read(read_fd, buf, BUFSIZE) > 0)
{
printf("Lucy: %s\n",buf);
}
printf("Peter: ");
fgets(buf, BUFSIZE, stdin);
buf[strlen(buf)-1] = '\0';
if(strncmp(buf,"quit", 4) == 0)
{
close(write_fd);
unlink(write_fifo_name); // 刪除管道
close(read_fd);
exit(0);
}
write(write_fd, buf, strlen(buf));
}
}
測試結果
測試結果表明其中的 open(xxx_fifo_name, O_RDONLY) 函數會阻塞,直到確定了 xxx_fifo_name 的兩個方向,即讀方向和寫方向。只有正確的指定了 xxx_fifo_name 管道的方向後 open 函數方可正常執行退出(當然瞭如果 xxx_fifo_name 兩個方向指定後,其他進程的操作同一個 xxx_fifo_name 時open 函數就不會阻塞了
)。對管道的讀寫操作,當讀管道時他會將此時管道中的所有數據讀出。當管道中沒有數據時 read 會阻塞,直到管道寫入數據。
如果將上述 fiforead.c 中的
while((read_fd = open(read_fifo_name, O_RDONLY)) == -1)
改爲:
while((read_fd = open(read_fifo_name, O_WRONLY)) == -1)
會出現open函數永久阻塞的局面,出現這個局面主要是隻對管道指定一個方向,沒有正確指明兩個方向。結果如下圖: