Linux C編程--進程間通信(IPC)4--管道詳解

linux管道

管道相關內容的簡介
管道是單向的字節流,它將某個進程的標準輸出連接到另一個進程的標準輸入。管道和有名管道是最早的進程間通信機制之一,管道可用於具有親緣關係進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係進程間的通信。管道和有名管道的讀寫規則是在程序中應用它們的關鍵。

管道相關的概念

在linux中管道是通過指向同一個臨時的VFS inode的兩個file數據結構來實現的,此VFS inode指向內存中的同一個物理頁面。這就隱藏了讀寫管道和讀寫普通文件的差別。管道是半雙工的,數據只能向一個方向流動;需要雙方通信時,需要建立起兩個管道;只能用於父子進程或者兄弟進程之間(具有親緣關係的進程);
管道對於管道兩端的進程而言,就是一個文件,但它不是普通的文件,它不屬於某種文件系統,單獨構成一種文件系統,並且只存在與內存中。數據的讀出和寫入:一個進程向管道中寫的內容被管道另一端的進程讀出。寫入的內容每次都添加在管道緩衝區的末尾,並且每次都是從緩衝區的頭部讀出數據。

管道的創建:
#include
int pipe(int fd[2])
該函數創建的管道的兩端處於一個進程中間,在實際應用中沒有太大意義,因此,一個進程在由pipe()創建管道後,一般再fork一個子進程,然後通過管道實現父子進程間的通信(因此也不難推出,只要兩個進程中存在親緣關係,這裏的親緣關係指的是具有共同的祖先,都可以採用管道方式來進行通信)。

管道的讀規則:

管道兩端可分別用描述字fd[0]以及fd[1]來描述,需要注意的是,管道的兩端是固定了任務的。即一端只能用於讀,由描述字fd[0]表示,稱其爲管道讀端;另一端則只能用於寫,由描述字fd[1]來表示,稱其爲管道寫端。如果試圖從管道寫端讀取數據,或者向管道讀端寫入數據都將導致錯誤發生。一般文件的I/O函數都可以用於管道,如close、read、write等等。
從管道中讀取數據:如果管道的寫端不存在,則認爲已經讀到了數據的末尾,讀函數返回的讀出字節數爲0;當管道的寫端存在時,如果請求的字節數目大於PIPE_BUF,則返回管道中現有的數據字節數,如果請求的字節數目不大於PIPE_BUF,則返回管道中現有數據字節數(此時,管道中數據量小於請求的數據量);或者返回請求的字節數(此時,管道中數據量不小於請求的數據量)。管道寫端關閉後,寫入的數據將一直存在,直到讀出爲止。

管道的寫規則:

向管道中寫入數據:向管道中寫入數據時,Linux將不保證寫入的原子性,管道緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據。如果讀進程不讀走管道緩衝區中的數據,那麼寫操作將一直阻塞。

注:只有在管道的讀端存在時,向管道中寫入數據纔有意義。否則,向管道中寫入數據的進程將收到內核傳來的SIFPIPE信號,應用程序可以處理該信號,也可以忽略(默認動作則是應用程序終止)。對管道的寫規則的驗證1:寫端對讀端存在的依賴性
在向管道寫入數據時,至少應該存在某一個進程,其中管道讀端沒有被關閉,否則就會出現錯誤(管道斷裂,進程收到了SIGPIPE信號,默認動作是進程終止)對管道的寫規則的驗證2:Linux不保證寫管道的原子性驗證
寫入管道的數據量大於4096字節時,緩衝區的空閒空間將被寫入數據(補齊),直到寫完所有數據爲止,如果沒有進程讀數據,則一直阻塞。

管道的侷限性
只支持單向數據流;
只能用於具有親緣關係的進程之間;
沒有名字;
管道的緩衝區是有限的(管道制存在於內存中,在管道創建時,爲緩衝區分配一個頁面大小);
管道所傳送的是無格式字節流,這就要求管道的讀出方和寫入方必須事先約定好數據的格式,比如多少字節算作一個消息(或命令、或記錄)等等。


管道的創建

管道由函數pipe創建,只能提供單向的數據傳遞的數據傳送。

格式:

#include <unistd.h>

int pipe(int fd[2]);

fd爲兩個文件描述符:fd[0]用來讀,fd[1]用來寫。

1.父子進程的單向通信方式如下圖:

image

一個進程創建一個管道——>派生一個自身的拷貝——>父進程關閉管道的讀出端,子進程的寫入端關閉(上圖中的虛線)——>父子進程就建立了單向通信了。

2.父子進程的雙向通信方式如下圖:

image

創建管道1(fd1[0],fd1[1])和管道2(fd2[0],fd2[1])——>派生出一個子進程——>

父進程關閉管道1的讀出端(fd1[0])和管道2的寫入端(fd2[1]);

子進程關閉管道1的寫入端(fd1[1])和管道2的讀入端(fd2[0])。

下面給出兩個實例說明創建管道的過程:

1.

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>

int main()
{
	int n, fd[2];
	pid_t pid;
	char buffer[BUFSIZ+1];
	if(pipe(fd)<0)
	{
		printf("pipe failed!\n ");
		exit(1);
	}
	if((pid=fork())<0)
	{
		printf("fork failed!\n ");
		exit(1);
	}
	else if (pid>0)
	{
		close(fd[0]);
		write(fd[1],"How are you?\n",12);
	}
	else
	{
		close(fd[1]);
		n=read(fd[0],buffer,BUFSIZ);
		write(STDOUT_FILENO,buffer,n);
	}
	exit(0);
}


2.假設有一個用戶可執行程序upcase,可以從標準輸入設備讀入字母,將其從小寫轉化爲大寫輸出。這個程序用管道實現將某一文本文件中的字母轉換爲大寫輸出的程序,其中,文本文件名作爲參數傳進來。

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int n,fd[2];
	pid_t pid;
	char buffer[BUFSIZ+1];
	FILE *fp;

	if(argc<=1)
	{
		printf("usage: %s <pathname>\n",argv[0]);
		exit(1);
	}
	/*打開文本文件*/
	if((fp=fopen(argv[1],"r"))==NULL)
	{
		printf("Can't open %s \n", argv[1]);
		exit(1);
	}
	/*創建管道*/
	if(pipe(fd)<0)
	{
		printf("pipe failed!\n ");
		exit(1);
	}
	/*創建子進程*/
	if((pid=fork())<0)
	{
		printf("fork failed!\n ");
		exit(1);
	}
	else if (pid>0)   /*父進程*/
	{
		close(fd[0]);
		while(fgets(buffer,BUFSIZ,fp)!=NULL)
		{
			n=strlen(buffer);
			/*向管道中寫入數據*/
			if(write(fd[1],buffer,n)!=n)
			{
				printf("write error to pipe.\n");
				exit(1);
			}
		}
		if(ferror(fp))
		{
			printf("fgets error. \n");
			exit(1);
		}
		close(fd[1]);
		if(waitpid(pid, NULL, 0)<0)
		{
			printf("waitpid error!\n");
			exit(1);
		}
		exit(0);
	}
	else  /*子進程*/
	{
		close(fd[1]);
		if(fd[0]!=STDIN_FILENO)
		{
		/*將管道複製到標準輸入*/
			if(dup2(fd[0],STDIN_FILENO)!=STDIN_FILENO)
			{
				printf("dup2 error to stdin! \n");
				exit(1);
			}
			close(fd[0]);
		}
		/*運行用戶程序*/
		if(execl("upcase","upcase",(char *)0)<0)
		{
			printf("execl error for upcase.\n");
			exit(1);
		}
		exit(0);
	}
}


創建管道的簡單方法

利用popen和pclose函數可以創建和關閉管道

函數原型:

  #include “stdio.h”

  FILE *popen( const char* command, const char* mode )

  參數說明:

  command: 是一個指向以 NULL 結束的 shell 命令字符串的指針。這行命令將被傳到 bin/sh 並使用 -c 標誌,shell 將執行這個命令。

  mode: 只能是讀或者寫中的一種,得到的返回值(標準 I/O 流)也具有和 type 相應的只讀或只寫類型。如果 type 是 “r” 則文件指針連接到 command 的標準輸出;如果 type 是 “w” 則文件指針連接到 command 的標準輸入。

  返回值:

  如果調用成功,則返回一個讀或者打開文件的指針,如果失敗,返回NULL,具體錯誤要根據errno判斷

  int pclose (FILE* stream)

  參數說明:

  stream:popen返回的文件指針

  返回值:

  如果調用失敗,返回 -1

  作用:

  popen() 函數用於創建一個管道:其內部實現爲調用 fork 產生一個子進程,執行一個 shell 以運行命令來開啓一個進程這個進程必須由 pclose() 函數關閉。


下面給出一個實例實現上一個實例所實現的功能

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	char buffer[BUFSIZ+1];
	FILE *fpin, *fpout;
	if(argc<=1)
	{
		printf("usage: %s <pathname>\n",argv[0]);
		exit(1);
	}
	if((fpin=fopen(argv[1],"r"))==NULL)
	{
		printf("Can't open %s \n", argv[1]);
		exit(1);
	}
	if((fpout=popen("./upcase","w"))==NULL)
	{
		printf("popen error \n");
		exit(1);
	}
	while(fgets(buffer,BUFSIZ,fpin)!=NULL)
	{
		if(fputs(buffer,fpout)==EOF)
		{
			printf("fputs error to pipe. \n");
			exit(1);
		}
	}
	if(ferror(fpin))
	{
		printf("fgets error. \n");
		exit(1);
	}
	if(pclose(fpout)==-1)
	{
		printf("pclose error.\n");
		exit(1);
	}
	exit(0);
}

有名管道詳解

有名管道相概念  FIFO

從名字就可以看出來它是支持先進先出的原則的。管道應用的一個重大限制是它沒有名字,因此,只能用於具有親緣關係的進程間通信,在有名管道(named pipe或FIFO)提出後,該限制得到了克服。FIFO不同於管道之處在於它提供一個路徑名與之關聯,以FIFO的文件形式存在於文件系統中。這樣,即使與FIFO的創建進程不存在親緣關係的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信(能夠訪問該路徑的進程以及FIFO的創建進程之間),因此,通過FIFO不相關的進程也能交換數據。值得注意的是,FIFO嚴格遵循先進先出(first in first out),對管道及FIFO的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。它們不支持諸如lseek()等文件定位操作。

有名管道的創建

#include
#include
int mkfifo(const char * pathname, mode_t mode)

int mknod

(const char * pathname, mode_t mode,dev_t dev)

該函數的第一個參數是一個普通的路徑名,也就是創建後FIFO的名字。第二個參數與打開普通文件的open()函數中的mode 參數相同。如果mkfifo的第一個參數是一個已經存在的路徑名時,會返回EEXIST錯誤,所以一般典型的調用代碼首先會檢查是否返回該錯誤,如果確實返回該錯誤,那麼只要調用打開FIFO的函數就可以了。一般文件的I/O函數都可以用於FIFO,如close、read、write等等。

FIFO的打開規則:

有名管道比管道多了一個打開操作:open。如果當前打開操作是爲讀而打開FIFO時,若已經有相應進程爲寫而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲寫而打開該FIFO(當前打開操作設置了阻塞標誌);或者,成功返回(當前打開操作沒有設置阻塞標誌)。
如果當前打開操作是爲寫而打開FIFO時,如果已經有相應進程爲讀而打開該FIFO,則當前打開操作將成功返回;否則,可能阻塞直到有相應進程爲讀而打開該FIFO(當前打開操作設置了阻塞標誌);或者,返回ENXIO錯誤(當前打開操作沒有設置阻塞標誌)。對打開規則的驗證參見附2。

有名管道的讀規則
約定:如果一個進程爲了從FIFO中讀取數據而阻塞打開FIFO,那麼稱該進程內的讀操作爲設置了阻塞標誌的讀操作。
如果有進程寫打開FIFO,且當前FIFO內沒有數據,則對於設置了阻塞標誌的讀操作來說,將一直阻塞。對於沒有設置阻塞標誌讀操作來說則返回-1,當前errno值爲EAGAIN,提醒以後再試。
對於設置了阻塞標誌的讀操作說,造成阻塞的原因有兩種:當前FIFO內有數據,但有其它進程在讀這些數據;另外就是FIFO內沒有數據。解阻塞的原因則是FIFO中有新的數據寫入,不論信寫入數據量的大小,也不論讀操作請求多少數據量。
讀打開的阻塞標誌只對本進程第一個讀操作施加作用,如果本進程內有多個讀操作序列,則在第一個讀操作被喚醒並完成讀操作後,其它將要執行的讀操作將不再阻塞,即使在執行讀操作時,FIFO中沒有數據也一樣(此時,讀操作返回0)。如果沒有進程寫打開FIFO,則設置了阻塞標誌的讀操作會阻塞。
注:如果FIFO中有數據,則設置了阻塞標誌的讀操作不會因爲FIFO中的字節數小於請求讀的字節數而阻塞,此時,讀操作會返回FIFO中現有的數據量。

約定:如果一個進程爲了向FIFO中寫入數據而阻塞打開FIFO,那麼稱該進程內的寫操作爲設置了阻塞標誌的寫操作。對於設置了阻塞標誌的寫操作:當要寫入的數據量不大於PIPE_BUF時,Linux將保證寫入的原子性。如果此時管道空閒緩衝區不足以容納要寫入的字節數,則進入睡眠,直到當緩衝區中能夠容納要寫入的字節數時,纔開始進行一次性寫操作。
當要寫入的數據量大於PIPE_BUF時,Linux將不再保證寫入的原子性。FIFO緩衝區一有空閒區域,寫進程就會試圖向管道寫入數據,寫操作在寫完所有請求寫的數據後返回。
對於沒有設置阻塞標誌的寫操作:

當要寫入的數據量不大於PIPE_BUF時,Linux將保證寫入的原子性。如果當前FIFO空閒緩衝區能夠容納請求寫入的字節數,寫完後成功返回;如果當前FIFO空閒緩衝區不能夠容納請求寫入的字節數,則返回EAGAIN錯誤,提醒以後再寫。


命名管道的應用

(1)shell命令行使用命名管道將數據從一條命令傳送到另一條命令,而不需要創建中間的臨時文件

(2)在客戶--服務器結構中,使用命名管道在客戶和服務器之間交換數據


下面給出一個實例

編寫一個多客戶--單一服務器模式的程序,用命名管道實現客戶到服務器之間傳遞數據的操作

服務器端

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/stat.h>

#define FIFO_FILE "MYFIFO"

int main()
{
	FILE *fp;
	char readbuf[80];
	if((fp=fopen(FIFO_FILE,"r"))==NULL)
	{
		umask(0);
		mknod(FIFO_FILE,S_IFIFO|0666,0);
	}
	else
		fclose(fp);
	while(1)
	{
		if((fp=fopen(FIFO_FILE,"r"))==NULL)
		{
			printf("open fifo failed. \n");
			exit(1);
		}
		if(fgets(readbuf,80,fp)!=NULL)
		{
			printf("Received string :%s \n", readbuf);
			fclose(fp);
		}
		else
		{
			if(ferror(fp))
			{
				printf("read fifo failed.\n");
				exit(1);
			}
		}
	}
	return 0;
}

客戶端

#include <stdio.h>
#include <stdlib.h>

#define FIFO_FILE "MYFIFO"

int main(int argc, char *argv[])
{
	FILE *fp;
	int i;
	if(argc<=1)
	{
		printf("usage: %s <pathname>\n",argv[0]);
		exit(1);
	}
	if((fp=fopen(FIFO_FILE,"w"))==NULL)
	{
		printf("open fifo failed. \n");
		exit(1);
	}
	for(i=1;i<argc;i++)
	{
		if(fputs(argv[i],fp)==EOF)
		{
			printf("write fifo error. \n");
			exit(1);
		}
		if(fputs(" ",fp)==EOF)
		{
			printf("write fifo error. \n");
			exit(1);
		}
	}
	fclose(fp);
	return 0;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章