进程篇——进程间通信:管道

说明
  本文章旨在总结备份、方便以后查询,由于是个人总结,如有不对,欢迎指正;另外,内容大部分来自网络、书籍、和各类手册,如若侵权请告知,马上删帖致歉。
  QQ 群 号:513683159 【相互学习】
内容来源
  《Linux系统编程》、《Linux网络编程》、《Unix环境高级编程》
  linux中管道的概念,浅谈Linux管道【Linux】Linux的管道

管道(pipe)

零、概念:

  管道是UNIX族中进程通信的最古老的方式,本质也是一种文件,但与一般文件又有所不同,本身并不占用磁盘或其他外部存储空间,实现上占用内存空间,故管道实际上是一种:操作方式为文件的内存缓冲区
  利用内核在两个进程之间建立通道,将两进程之间的标准输入与标准输出连接起来的机制,利用读写的方式在进程之间传递数据。

一、匿名管道:

  又可称作:无名管道、半双工管道,在shell中使用“|”表示管道。
  是具有公共祖先进程之间通信的一种方式
    父进程在产生子进程前创建管道文件,这样子进程通过拷贝父进程地址空间可获取到该文件描述符,该文件描述符的文件在父子进程双方所共享并设置只有一方可写,另一方可读,则该文件被称为管道,父子进程通过该文件(管道)以达到通信的目的。
  总结:由于该文件无文件名,故非亲进程不能打开,只可用于亲属进程通信,又由于无文件名故被称作匿名管道

(1)pipe()——创建一个管道

  函数功能
    创建一个管道,一个单向的数据通道,可用于进程间通信。
    数组pipefd用于返回指向管道末端的两个文件描述符。pipefd[0]表示管道的读取端。pipefd[1]表示管道的写端。写入管道的写端数据被内核缓冲,直到从管道的读端读取数据为止。

项目 说明
函数原型 int pipe(int pipefd[2]);
头文件 unistd.h
参数说明 pipefd[2]:文件描述符
返回值 若成功则返回0
若失败则返回-1并设置errno,并保持pipefd不变。
注意 ①fildes[0]是一个具有“只读”属性的文件描述符
②fildes[1]是一个具有“只写”属性的文件描述符

pipe()的示例实践:

  <1>源代码pipe.c

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

int main (int argc,char *argv[])
{
	/* Step 1 初始化 */
	pid_t pid;						// PID值
	int result = 1;					//管道创建结果
	int fd[2];						//文件描述符
	int nbytes;						//字符个数
	char string[]="你好,管道";
	char readbuffer[80];		
	/*文件描述符0用于读,文件描述符1用于写*/
	int *read_fd=&fd[0];			//读文件描述符
	int *write_fd= &fd[1];			//写文件描述符
	
	/* Step 2 建立管道 */
	result = pipe(fd);			
	if(result == -1)				//管道创建失败
	{
		printf("建立管道失败\n"); 
		return -1;						
	}	

	/* Step 3 新建进程 */
	pid = fork();			
	if( pid == -1)					//新建进程失败
	{
		printf("fork 进程失败\n");
		return -1;			
	}

	/*Step 4 父子进程间的通讯*/
	if(pid == 0)					/* 子进程 */	
	{	
		close(*read_fd);			//关闭读端
		/*向管道端写入字符*/
		result = write(*write_fd,string,strlen(string));
		return 0;
	}
	else							/* 父进程 */	
	{
		close(*write_fd);			//关闭写端
		/*从管道读取数值*/
		nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
		printf("接收到%d个数据,内容为:%s\n",nbytes,readbuffer);
				
	}
	return 0;
}

    <2>编译运行及输出结果
      ①编译:gcc pipe.c -o pipe
      ②运行:./pipe
      ③输出结果:接收到13个数据,内容为:你好,管道

管道阻塞和管道的原子性的描述与示例实践

  1️⃣当管道的写端没有关闭时
  ①若写请求的字节数 大于 阈值PIPE_BUF时,
    写操作返回值是 管道中 目前的 数据字节数
  ②若写请求的字节数 不大于 阈值PIPE_BUF时,
    写操作返回值是 管道中现有的 数据字节数(管道中数据量 小于 请求的数据量)
    写操作返回值是 请求的字节数 数据字节数(管道中数据量 不小于 请求的数据量)
  2️⃣当管道进行写入操作时
  ①若写入数据的数目 小于128K 时,写入是非原子的。
  ②若父进程中两次写入字节数都改为128K(写入管道数据量 大于128K 时),缓冲区的数据将被连续的写入管道,知道数据全部写完为止,若没有进程读数据,则一直阻塞
  PS:
    PIPE_BUFinclude/Linux/limits.h中定义,不同内核版本可能有区别。
  3️⃣示例实践
    ①源文件:test.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#define K 1024
#define WRITELEN (128*K)
int main (int argc,char *argv[])
{
	/* Step 1 初始化 */
	pid_t pid;							// PID值
	int result = 1;						//管道创建结果
	int fd[2];							//文件描述符
	int nbytes;							//字符个数
	char string[WRITELEN]="你好,管道";
	char readbuffer[10*K]={0};		   		//读缓冲区
	/*文件描述符0用于读,文件描述符1用于写*/
	int *read_fd=&fd[0];				//读文件描述符
	int *write_fd= &fd[1];				//写文件描述符
	


	/* Step 2 建立管道 */
	result = pipe(fd);			
	if(result == -1)					//管道创建失败
	{
		printf("建立管道失败\n"); 
		return -1;						
	}	

	/* Step 3 新建进程 */
	pid = fork();				
	if( pid == -1)						//新建进程失败
	{
		printf("fork 进程失败\n");
		return -1;			
	}

	/*Step 4 父子进程间的通讯*/
	if(pid == 0)						/* 子进程 */	
	{	
		int write_size = WRITELEN;		//写入的长度
		result = 0;						//结果
		close(*read_fd);				//关闭读端
		/*向管道端写入字符*/
		while (write_size >= 0)
		{
			result = write(*write_fd,string,write_size);
			if (result > 0)				/* 写入成功 */
			{
				write_size -= result;	//写入的长度	
				printf("写入%d个数据,剩余%d个数据\n",result,write_size);
			}
			else						/* 写入失败 */
			{
				sleep(10);				//等待10s,读端将数据读出
			}
		}
		return 0;
	}
	else							/* 父进程 */	
	{
		close(*write_fd);			//关闭写端
		/*从管道读取数值*/
		while (1)					//一直读取数据
		{
			nbytes = read(*read_fd,readbuffer,sizeof(readbuffer));
			if (nbytes <= 0)		//读取数据失败
			{
				printf("没有数据写入了\n");
				break;				//退出循环
			}	
			printf("接收到%d个数据,内容为:%s\n",nbytes,readbuffer);
		}		
	}
	return 0;
}

    ②编译运行及结果
      <1>编译:gcc test.c -o test
      <2>运行:./test
      <3>结果:

接收到10240个数据,内容为:你好,管道
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到10240个数据,内容为:
接收到8192个数据,内容为:
写入131072个数据,剩余0个数据

    ③分析
      可发现,父进程每次读取10K字节数据,读13次将全部数据读完,最后一次读数据由于缓冲区只剩下8K数据,故仅读取8K。
      字进程一次性写入128K字节数据,父进程将全部数据读取完毕时,子进程的write()函数才打印出对应信息。

(2)popen()——创建一个shell进程的管道流

  1.函数功能
    通过创建管道、fork()和调用shell来打开进程。由于管道的定义是单向的,type参数可以只指定读或写,而不能同时指定读或写;相应的,生成的流是只读或只写的。

项目 说明
函数原型 FILE *popen(const char *command, const char *type);
头文件 stdio.h
参数说明 command:命令字符串
指向一个包含shell命令行的以空结束的字符串
type:类型字符串
指向以空字符结束的字符串的指针,该字符串必须包含用于读取的字母'r'或用于写入的字母'w'
返回值 成功返回一个指向打开流的指针,该指针可用于读取或写入管道;
失败返回NULL并将errno设置为适当的值
注意 ①必须用pclose()而不是fclose(3)关闭,②默认情况下是块缓冲的。

(3)pclose()——关闭一个shell进程的管道流

  1.函数功能
    等待关联的进程终止,并返回wait4(2)返回的命令退出状态。

项目 说明
函数原型 int pclose(FILE *stream);
头文件 stdio.h
参数说明 stream:文件流
返回值 成功返回命令的退出状态;
失败返回-1并将errno设置为适当的值
注意

popen()和pclose()示例实践

1.项目描述:

  调用myuclc执行程序,实现终端输入大写字符过滤为小写字符。

2.具体实现:

  STEP 1: 先编译生成“将大写字符转化为小写字符的过滤程序”myuclc并测试功能。
源文件:myuclc.c

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

/* 将大写字符转化为小写字符的过滤程序 */
int main(int argc, char *argv[])
{
    int c;

    while( (c = getchar()) != EOF )
    {
        if(isupper(c))
            c = tolower(c);
        if(putchar(c) == EOF)
            fprintf(stderr,"output error");
        if( c == '\n')
            fflush(stdout);
    }

    return 0;
}

bash过程:

xsndz@computer:$ gcc myuclc.c -o myuclc
xsndz@computer:$ ./myuclc 
HELLO WORLD
hello world
^C

  STEP 2: 编译popen.c文件
源文件:popen.c

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

#define MAXLINE 1024

int main(int argc, char *argv[])
{
    char line[MAXLINE];
    FILE *fpin;
    if ((fpin = popen("./myuclc", "r")) == NULL)
        fprintf(stderr, "popen error");
    for (;;)
    {
        fputs("prompt> ", stdout);
        fflush(stdout);
        if (fgets(line, MAXLINE, fpin) == NULL)
            break;
        if (fputs(line, stdout) == EOF)
            fprintf(stderr, "fputs error to pipe");
    }
    if (pclose(fpin) == -1)
        fprintf(stderr, "pclose error");
    putchar('\n');
    return 0;
}

bash过程:

xsndz@computer:$ gcc popen.c -o popen
xsndz@computer:$ ./popen 
prompt> HELLO WORLD
hello world
prompt> ^C

二、命名管道(FIFO):

  由于匿名管道没有文件名,无法实现不同进程的通信,故便产生 命名管道
  由于命名管道不支持诸如lseek()等文件定位操作,严格遵守先进先出的原则进行传输数据,即对管道的读总是从开始处返回数据,对它的写总是把数据添加到末尾,所以这种管道也叫做 FIFO文件
  命名管道提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中,在文件系统中产生一个物理文件,其他进程只要访问该文件路径,就能彼此通过管道通信。在读数据端以只读方式打开管道文件,在写数据端以只写方式打开管道文件.
  命名管道与普通的管道区别
    ①在文件系统中命名管道是以设备特殊文件的形式存在的。
    ②不同的进程可以通过命名管道共享数据。
  FIFO文件(命名管道)与普通文件的区别
    ①普通文件无法实现字节流方式管理,而且多进程之间访问共享资源会造成意想不到的问题;
    ②FIFO文件采用字节流方式管理,遵循先入先出原则,不涉及共享资源访问。

(4)mkfifo()——创建一个FIFO文件

  ① shell创建,指令:mkfifo
目录/ipc下建立一个名为namedfifo的命名管道

mkfifo /ipc/namedfifo
ls -l /ipc/namedfifo

  ② C语言创建
  函数介绍:创建一个命名管道。【详细可查看:man 3 mkpipe

项目 说明
函数原型 int mkfifo(const char* pathname,mode_t mode);
头文件 sys/types.h、sys/stat.h
参数说明 pathname:命名管道文件名
mode:操作权限(与open相同)
返回值 若成功则返回0
若失败则返回-1
注意

FIFO操作

  对命名管道FIFO来说,IO操作与普通的管道IO操作基本上是一样的,二者之间存在着一个主要的区别。
    在 FIFO中,必须使用一个open()函数来显式地建立联接到管道的通道。
    一般来说FIFO总是处于阻塞状态。也就是说,如果命名管道FIFO打开时设置了读权限,则读进程将一直“阻塞”,一直到其他进程打开该FIFO并且向管道中写入数据。这个阻塞动作反过来也是成立的,如果一个进程打开一个管道写入数据,当没有进程冲管道中读取数据的时候,写管道的操作也是阻塞的,直到已经写入的数据被读出后,才能进行写入操作。如果不希望在进行命名管道操作的时候发生阻塞,可以在 open()调用中使用O_NONBLOCK标志,以关闭默认的阻塞动作。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值