06 | linux下进程通信(管道)

进程间通信:管道

在两个进程间发送消息的非常简单的方法:使用信号。我们创建通知事件,通过它引起响应,但传送的信息只限于一个信号值。

这里介绍管道,通过它进程之间可以交换更加有用的数据。

popen与pcolse

最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数

#include <stdio.h> FILE* popen(const char* command,const char* open_mode); int pclose(FILE* stream_to_close); 

1.popen 函数

允许一个程序将另一个程序作为新进程来启动。

如果open_mode为“r”,则代表本进程读取被调用程序的输出。

如果open_mode为“w”,则代表被调用程序使用本进程的输出。

注意:如果想通过管道实现双向通信,最简单的解决办法就是使用两个管道,每个管道负责一个方向的数据流。

2.pclose函数

如果被调用程序还没有结束就调用pclose函数,则pclose调用等待该程序的结束。

如果调用进程在调用pclose之前执行了一个wait语句,被调用进程的退出状态就会丢失,因为被调用进程已经 结束。此时,pclose将返回-1并设置errno为ECHILD

读取外部程序的输入

  1 #include <unistd.h>   2 #include <stdlib.h>   3 #include <stdio.h>   4 #include <string.h>   5   6 int main()   7 {   8     FILE * read_fp;   9     char buffer[BUFSIZ+1];  10     int chars_read;  11     memset(buffer,'\0',sizeof(buffer));  12     read_fp=popen(uname -a,r);  13     if(read_fp!=NULL){                                        14         chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp);  15         if(chars_read>0){  16             printf(output was:-\n%s\n,buffer);  17         }  18         pclose(read_fp);  19         exit(EXIT_SUCCESS);  20     }  21     exit(EXIT_FAILURE);  22 } 

将输出送往popen

#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h>  int main(){ 	FILE* write_fp; 	char buffer[BUFSIZ+1];  	sprintf(buffer,Once upon a time ,there was ...\n); 	write_fp=popen(od -c,w); 	if(write_fp!=NULL){ 		fwrite(buffer,sizeof(char),strlen(buffer),write_fp); 		pclose(write_fp); 		exit(EXIT_SUCCESS); 	} 	exit(EXIT_FAILURE); } 

传递更多的数据

我们目前所使用的机制都只是将所有数据通过一次fread或fwrite调用来发送或接收

为了避免定义一个非常大的缓冲区,我们可以用多个fread或fwrite调用将数据分为几部分处理。

通过管道读取大量数据

#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h>  int main(int argc, char const *argv[]) { 	FILE * read_fp; 	char buffer[BUFSIZ+1]; 	int chars_read; 	memset(buffer,'\0',sizeof(buffer)); 	read_fp=popen(ps ax,r); 	if(read_fp!=NULL){ 		chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp); 		while(chars_read>0){ 			buffer[chars_read-1]='\0'; 			printf(reading  %d:-\n %s\n,BUFSIZ,buffer); 			chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp); 		} 		pclose(read_fp); 		exit(EXIT_SUCCESS); 	} 	exit(EXIT_FAILURE); 	return 0; }     

popen函数性能

请求popen调用运行一个程序时,它首先启动一个shell,即系统中的sh命令,然后将command字符串作为一个参数传递给他,这有两个效果,一个好,一个不太好。

好:在启动程序前先启动shell来分析命令字符串,就可以使各种shell扩展(如*.c所指的是那些文件)在程序启动之前就全部完成。它允许我们通过popen启动非常复杂的shell命令。而其他一些创建进程的函数(如execl)调用起来就复杂得多,因为调用进程必须自己去完成shell扩展。

不太好:多启动一个进程,慢一点

pipe调用

popen函数底层的pipe函数。

通过这个函数在两个程序之间传递数据不需要启动一个shell来解释请求。它同时还提供了对读写数据的更多控制。

#include <unistd.h> int pipe(int file_descriptor[2]); 

这个函数就是将file_descriptor这个数组填满,里面会装有两个文件描述符。

返回值为0代表成功

两个返回的文件描述符以特殊方式连接。写到[1]的所有数据都可以从[0]中读出来,。并且是按照FIFO的原则。

特别要注意,这里使用的是文件描述符而不是 文件流 ,所以我们必须用底层的read和write来调用 访问数据,而不是 用文件流库函数 fread 和 fwrite。

#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h>  int main(int argc, char const *argv[]) { 	int data_processed; 	int file_pipes[2]; //文件描述符数组 	const char some_data[]=123; 	char buffer[BUFSIZ+1]; 	memset(buffer,'\0',sizeof(buffer)); 	if(pipe(file_pipes)==0){ 		data_processed=write(file_pipes[1],some_data,strlen(some_data)); 		printf(wrote %d bytes\n,data_processed ); 		data_processed=read(file_pipes[0],buffer,BUFSIZ); 		printf(read %d bytes: %s\n,data_processed,buffer); 		exit(EXIT_SUCCESS); 	} 	return 0; } 
  • 程序用数组中的两个文件描述符创建一个管道
  • 管道有一些内置的缓存区,它在write和read调用之间保存数据
  • 不要尝试在[1]中读,在[0]中写
  • 当fork一个子进程,原先打开的文件描述符仍保持打开,因此可通过管道在两个进程之间传递数据

跨越fork调用的管道

#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h>  int main(int argc, char const *argv[]) { 	int data_processed;//记录返回字符数 	int file_pipes[2]; //文件描述符数组 	const char some_data[]=123; 	char buffer[BUFSIZ+1]; 	pid_t fork_result;  	memset(buffer,'\0',sizeof(buffer)); 	if(pipe(file_pipes)==0){ 		fork_result=fork(); //建立子进程 		if(fork_result==-1){ 			perror(fork failure); 			exit(0); 		} 		if(fork_result==0){ 			//在子进程中读数据 			data_processed=read(file_pipes[0],buffer,BUFSIZ); 			printf(read %d bytes: %s\n,data_processed,buffer); 			exit(EXIT_SUCCESS); 		} 		else{ 			//父进程 			data_processed=write(file_pipes[1],some_data,strlen(some_data)); 			printf(wrote %d bytes\n,data_processed ); 		}			 	} 	exit(EXIT_SUCCESS); } 

wrote 3 bytes
read 3 bytes: 123

  • 如果父进程在子进程之前退出,你就会在两部分输出内容之间看到shell提示符

父进程和子进程

如何在子进程中运行一个与父进程完全不同的另外一个程序呢?

我们需要调用exec来完成这一个工作。但是exec需要知道应该访问那个文件描述符。前面的例子我们知道子进程本身有文件描述符的副本。为了exec调用后不被丢失我们可以将文件描述符作为参数传递给exec启动的程序。

演示:一个生产者,一个消费者

管道和exec函数

#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h>  int main(int argc, char const *argv[]) { 	int data_processed;//记录返回字符数 	int file_pipes[2]; //文件描述符数组 	const char some_data[]=123; 	char buffer[BUFSIZ+1];	//存放传递给子进程的 文件描述符 	pid_t fork_result;  	memset(buffer,'\0',sizeof(buffer));  	if(pipe(file_pipes)==0){ 		fork_result=fork(); //建立子进程 		if(fork_result==-1){ 			perror(fork failure); 			exit(EXIT_FAILURE); 		} 		if(fork_result==0){ 			//暂存 文件描述符 			sprintf(buffer,%d,file_pipes[0]); 			// 启动 新进程 			(void)execl(pipe4,pipe4,buffer,(char*)0); 			exit(EXIT_FAILURE); 		} 		else{ 			//父进程 			data_processed=write(file_pipes[1],some_data,strlen(some_data)); 			printf(wrote %d bytes\n,data_processed ); 		}			 	} 	exit(EXIT_SUCCESS); } 
#include <unistd.h> #include <stdio.h> #include <string.h> #include <stdlib.h>  int main(int argc, char const *argv[]) { 	int data_processed;//记录返回字符数 	char buffer[BUFSIZ+1]; 	int file_descriptor; //读到 父进程的文件描述符  	memset(buffer,'\0',sizeof(buffer)); 	//获取文件描述符 	sscanf(argv[1],%d,&file_descriptor);     //开始读取正式数据 	data_processed=read(file_descriptor,buffer,BUFSIZ);  	printf(%d - read %d bytes:%s\n,getpid(),data_processed,buffer); 	exit(EXIT_SUCCESS); } 

其中execl的参数

  • 要启动的程序
  • argv[0]:程序名
  • argv[1]:包含我们想让被调用程序去读取的文件描述符
  • (char*)0:这个参数的作用是终止被调用程序的参数列表

管道关闭后的读操作

当管道的写数据的一端进程结束的时候,读数据的一端调用read会返回0.注意这和读取一个无效的文件描述符不同,read把无效的文件描述符看作一个错误并返回-1.

如果跨越fork调用使用管道,就会有两个不同的文件描述符可以用于向管道写数据,一个在父进程中,一个在子进程中。只有把父子进程中的针对管道的写文件描述符都关闭,管道才会被认为是关闭了,对管道的read调用才会失败。我们还将深入讨论这一问题, 在学习到O_ NONBLOCK标志和FIFO时,我们将看到-一个这样的例子。

把管道用作标准输入和标准输出

为了把文件描述符 转换成 标准输入和输出 ,我们先介绍两个函数

#include <unistd . h> int dup(int file_ descriptor); int dup2(int fi1e_ descriptor one, int file_ descriptor _two); 
  • 都是来生成一个新的文件描述符。
  • dup调用的目的是打开一个新的文件描述符,这与open调用有点类似。
  • 不同之处是,dup调用创建的新文件描述符与作为它的参数的那个已有文件描述符指向同-一个文件(或管道)。对于dup函数来说,新的文件描述符总是取最小的可用值。
  • 而对于dup2函数来说,它所创建的新文件描述符或者与参数file. descriptor _two相同, 或者是第一个大于 该参数的可用值。

那么,dup是如何帮助我们在进程之间传递数据的呢?

诀窍就在于,标准输入的文件描述符总是0,而dup返回的新的文件描述符又总是使用最小可用的数字。

因此,如果我们首先关闭文件描述符0。然后调用dup,那么新的文件描述符就将是数字0。

因为新的文件描述符是复制一个已有的文件描述符,所以标准输入就会改为指向一个我们传递给dup函数的文件描述符所对应的文件或管道。我们创建了两个文件描述符,它们指向同一个文件或管道,而且其中之一是标准输入。

image-20220428223746656

int main(){ 	int data_ processed; 	int file_ pipes[2]; 	const char some_ data[] = 123 ; 	pid_ t fork_ result; 	if (pipe(file_ pipes) == 0) { 		fork_ result = fork() ; 		if (fork_ result == (pid_ t)-1) { 			fprintf (stderr, Fork failure) ; 			exit (EXIT_ FAILURE) ; 		} 		if (fork_ result == (pid_ _t)0) { 			//子进程获得 管道的 “读”的一段,“写”的一段丢掉             close(0); 			dup(file_ pipes[0]); 			close(file pipes[0]) ; 			close (file_ pipes[1]); 			execlp(oa,od,-c, (char *)0); 			exit (EXIT_ FAILURE) ;  			else { 				close(file_ pipes[0]); 				data processed = write(file_ pipes[1], some_ data, 					strlen(some_ _data) ) ; 				close(file_ pipes[1]); 				printf(ed - wrote 8d bytes\n, (int)getpid(), data_ processed) ; 			} 		} 		exit (EXIT_ SUCCESS) ; 	} 

注意:当我们fork的时候打开了4个文件描述符!

刚调用fork的情况

image-20220428224849772

经过程序调整之后

image-20220428224908031

命名管道:FIFO

至此,我们还只能在相关的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果我们想在不相关的进程之间交换数据,这还不是很方便。

我们可以用FIFO文件来完成这项工作,它通常也被称为命名管道(named pipe)。命名管道是一种特殊类型的文件(别忘了Linux中的所有事物都是文件),它在文件系统中以文件名的形式存在,但它的行为却和我们已经见过的没有名字的管道类似

在过去命令行创建

mknod filename p

更推荐

mkfifo filename

系统调用

#include <sys/types .h> #include <sys/stat.h> int mkfifo (const char * filename, mode_ t mode) ; int mknod(const char *filename, mode_ t mode| s_ IFIFO, (dev_ t) 0);  

使用较难的mknod的注意事项:与mknod命令-样,我们可以用mknod函数建立许多特殊类型的文件。要想通过这个函数创建一一个命名管道,唯- -具有可移植性的方法是使用一个dev_ t类型的值0,并将文件访问模式与s_ IFIFO按位或。我

们在下面的例子中将使用较简单的mkfifo函数。

#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> int main(){ 	int res = mkfifo(/tmp/my_fifo, 0777) ; 	if(res ==0) printf (FIFO created\n) ; 	exit (EXIT_ SUCCESS) ; } 

运行程序

./fifo1

查看管道

ls -lF /tmp/my_fifo

image-20220428225740264

注意,输出结果中的第一一个字符为p, 表示这是一个管道。最后的|符号是由ls命令的-F选项添加的,它也表示这是一个管道。

访问FIFO文件

命令行方式读取

我们在一个终端执行

cat < my_fifo

这个时候此终端会阻塞,在另一个终端执行

echo hello > my_fifo

则另一个终端会输出内容,最终同时退出

与通过pipe调用创建管道不同,FIFO是以命名文件的形式存在,而不是打开的文件描述符,所以在对它进行读操作之前必须先打开它。FIFO也用 open和close函数打开和关闭,这与我们前面看到的对文件的操作-一样,但它多了--些其他的功能。对FIFO来说,传递给open调用的是FIFO的路径名,而不是一个正常的文件。

使用open打开FIFO文件

打开FIFO的一个主要限制是,程序不能以O_ RDWR模式打开FIFO文件进行读写操作。如果确实需要在程序之间双向传递数据,最好使用- -对FIFO或管道,一个方向使用一个。

打开FIFO文件和打开普通文件的另一点区别是,对open_ flag (open函数的第二个参数)的0_ NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式。


0_ RDONLY、0_ WRONLY和0_ NONBLOCK标志共有4种合法的组合方式,我们将逐个介绍它们。

  1. open(const char *path, O_RDONLY);在这种情况下,open调用将阻塞,除非有-一个进程以写方式打开同-一个FIFO, 否则它不会返回。
  2. open(const char *path, 0 RDONLY IO_NONBLOCK) ;即使没有其他进程以写方式打开FIFO,这个open调用也将成功并立刻返回。
  3. open(const char *path, O_ WRONLY);在这种情况下,open调用将阻塞,直到有- -个进程以读方式打开同一一个FIFO为止。
  4. open(const ! char *path,O_ WRONLY I O_ NONBLOCK) ;这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件,open调用将返回-一个错误-1并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。

请注意o_ NONBLOCK分别搭配O_ RDONLY和O WRONLY在效果上的不同,如果没有进程以读方式打开管道非阻塞写方式的open调用将失败,但非阻塞读方式的open调用总是成功.close调用的行为并不受O NONBLOCK标志的影响。


#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #define FIFO_NAME ./my_fifo int main(int argc, char *argv[]){ 	int res; 	int open_mode = 0; 	int i; 	if(argc<2){ 		fprintf (stderr,Usage: %s <some combination of\ 			。RDONLY。WRONLY。NONBLOCK>\n, *argv) ; 		exit(EXIT_FAILURE) ; 	} 	for(i= 1; i <argc; i++) { 		if(strncmp(*++argv, O_RDONLY, 8)== 0) 			open_mode |= O_RDONLY; 		if (strncmp(*argv, O_WRONLY, 8) == 0) 			open_mode |= O_WRONLY ; 		if (strncmp(*argv, O_NONBLOCK, 10)== 0) 			open_mode |= O_NONBLOCK; 	}		 	if (access (FIFO_NAME,F_OK) == -1) { 		res= mkfifo(FIFO_NAME, 0777) ; 		if (res!=0) { 			fprintf (stderr, Could not create fifo %s\n,FIFO_NAME) ; 			exit (EXIT_FAILURE) ; 		} 	} 	printf (Process%d opening FIFO\n, getpid()); 	res = open(FIFO_NAME, open_mode) ; 	printf(Process %d result %d\n, getpid(),res) ; 	sleep(5) ; 	if (res != -1) (void)close(res) ; 	printf(Process %d finished\n, getpid()); 	exit (EXIT_SUCCESS) ; } 

这个程序能够在命令行上指定我们希望使用的组合方式。它会把命令行参数与程序中的常量字符串进行比较,如果匹配,就(用|=操作符)设置相应的标志。

程序用access函数来检查FIFO文件是否存在,如果不存在就创建它。

不带0_ NONBLOCK标志的0_ RDONLY和o_ WRONLY

$ ./fifo2 O_ RDONLY &
[1] 152
Process 152 opening FIFO
$./fifo2 0_ WRONLY
Process 153 opening FIFO
Process 152 result 3
Process 153 result 3
Process 152 finished
Process 153 finished

这可能是命名管道最常见的用法了。它允许先启动读进程,并在open调用中等待,当第二个程序打开FIFO文件时,两个程序继续运行。注意,读进程和写进程在open调用处取得同步。当一个Linux进程被阻塞时,它并不消耗CPU资源,所以这种进程的同步方式对CPU来说是非常有效率的。

带o_ NONBLOCK标志的o_ RDONLY和不带该标志的o_ WRONLY

这次,读进程执行open调用并立刻继续执行,即使没有写进程的存在。随后写进程开始执行,它也在执行open调用后立刻继续执行,但这次是因为FIFO已被读进程打开

$ ./fifo2 O_ RDONLY 0_NONBLOCK &
[1] 160
Process 160 opening FIFO
$ . /fifo2 O_ WRONLY
Process 161 opening FIFO
Process 160 result 3
Process 161 result 3
Process 160 fini shed
Process 161 finished
[1]+ Done
./fifo2 O_ RDONLY 0_ NONBLOCK

对FIFO进行读写操作

只使用一个FIFO并允许多个不同的程序向一个FIFO读进程发送请求的情况是很常见的。如果几个不同的程序尝试同时向FIFO写数据,能否保证来自不同程序的数据块不相互交错就非常关键了。也就是说,每个写操作都必须是“原子化”的。怎样才能做到这一点呢?

如果你能保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_ BUF字节,系统就可以确保数据决不会交错在一起。通常将每次通过FIFO传递的数据长度限制为PIPE_ BUF字节是个好方法,除非你只使用-一个写进程和一个读进程。

//生产者 #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <limits.h> // PIPE_BUF 管道容量  #define FIFO_NAME ./my_fifo #define BUFFER_SIZE PIPE_BUF //定义缓冲区的大小 #define TEN_MEG (1024 * 1024 * 10)  int main(int argc, char *argv[]){ 	int pipe_fd;  //管道描述符 	int res; 	int open_mode = O_WRONLY; //生产者 	int bytes_sent=0; 		  //总发送字节数 	char buffer[BUFFER_SIZE+1]; 	//检查管道的存在 	if (access (FIFO_NAME,F_OK) == -1) { 		res= mkfifo(FIFO_NAME, 0777) ; 		if (res!=0) { 			fprintf (stderr, Could not create fifo %s\n,FIFO_NAME) ; 			exit (EXIT_FAILURE) ; 		} 	} 	//打开管道 	printf (Process %d opening FIFO O_WRONLY\n, getpid()); 	pipe_fd=open(FIFO_NAME,open_mode);	//打开管道 	printf(Process %d result %d\n, getpid(),pipe_fd) ; 	//写入数据 	if(pipe_fd!=-1){ 		while(bytes_sent<TEN_MEG){ 			res=write(pipe_fd,buffer,BUFFER_SIZE); 			if(res==-1){ 				fprintf(stderr,write error on pipe\n); 				exit (EXIT_FAILURE) ; 			} 			bytes_sent+=res; 		} 		//关闭管道 		(void)close(pipe_fd); 	} 	else{ 		exit (EXIT_FAILURE) ; 	} 	//结束 	printf(Process %d finished \n, getpid()) ; 	exit (EXIT_SUCCESS) ; } 
#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/types.h> #include <sys/stat.h> #include <limits.h>  #define FIFO_NAME ./my_fifo #define BUFFER_SIZE PIPE_BUF //定义缓冲区的大小   int main(int argc, char *argv[]){ 	int pipe_fd;  //管道描述符 	int res; 	int open_mode = O_RDONLY; //生产者 	int bytes_read=0; 		  //总接收字节数 	char buffer[BUFFER_SIZE+1];  	memset(buffer,'\0',sizeof(buffer));  	//打开管道 	printf (Process %d opening FIFO O_RDONLY\n, getpid()); 	pipe_fd=open(FIFO_NAME,open_mode);	//打开管道 	printf(Process %d result %d\n, getpid(),pipe_fd) ; 	//读取数据 	if(pipe_fd!=-1){ 		do{ 			res=read(pipe_fd,buffer,BUFFER_SIZE); 			bytes_read+=res; 		}while(res>0); 		//关闭管道 		(void)close(pipe_fd); 	} 	else{ 		exit (EXIT_FAILURE) ; 	} 	//结束 	printf(Process %d finished, %d bytes read \n, getpid(),bytes_read) ; 	exit (EXIT_SUCCESS) ; } 

image-20220430173445091

两个程序使用的都是阻塞模式的FIFO。我们首先启动fifo3 (写进程/生产者),它将阻塞以等待读进程打开这个FIFO。fifo4 (消费者)启动以后,写进程解除阻塞并开始向管道写数据。同时,读进程也开始从管道中读取数据。

Linux会安排好这两个进程之间的调度,使它们在可以运行的时候运行,在不能运行的时候阻塞。因此,写进程将在管道满时阻塞,读进程将在管道空时阻塞。

time命令的输出显示,读进程只运行了不到0.1秒的时间,却读取了10MB的数据。这说明管道(至少在现代Linux系统中的实现)在程序之间传递数据是很有效率的。