C语言----管道

时间:2023-03-09 09:15:17
C语言----管道

一、管道的概念

管道是一种队列类型的数据结构,它的数据从一端输入,另一端输出。管道最常见的应用是连接两个进程的输入输出,即把一个进程的输出编程另一个进程的输入。shell中存在专门的管道运算符"|",例如shell命令:

ps -ef |grep init

命令"ps -ef"分析当前运行的全部进程,并将结果打印到屏幕上。进程"grep init"从输入的字符串中查找包含字符"init"的子串,并打印结果。这两个领命通过管道符连接起来后就成了一个新的应用:查找正在应用的、命名中包含字符"init"的进程。

二、无名管道

无名管道通畅直接称之为管道,它占用两个文件描述符,不能被非血缘关系的进程共享,一般应用在父子进程中。

1.无名管道的建立

UNIX中一切皆为文件,管道也是文件的一种,成为管道文件。当系统创建一个管道时,它返回两个文件描述符:一个文件以只写打开,作为管道的输入端;另一个文件以只读打开,作为管道的输出端。

在UNIX中,采用函数pipe创建无名管道,其原型为:

#include<unistd.h>

int pipe(int fildes[2]);/*其中fildes[0]为读而开,fildes[1]为写而开,fildes[1]的输出是fildes[0]的输入*/

函数pipe在内核中创建一个管道,并分配两个文件描述符标识管道的两端,这两个文件描述符存储于fildes[0]和fildes[1]中。一般约定fildes[0]为输入端,进程向此文件描述符写入数据,fildes[1]描述管道的输出端,进程向此文件描述符中读取数据。函数pipe调用成功时返回0,否则返回-1。

2.单向管道流模型

管道的两端(输入和输出端)被一个进程控制没有太大的意义,如果管道的两端分别控制在不通的进程中,这两个进程之间就能够进行通信。拥有管道输入端的进程,可以向管道发送信息,拥有管道输出端的进程,可以从管道中接收一个进程发送来的信息。

1)从父进程流向子进程的管道

在父进程创建无名管道并产生子进程后,父子进程均拥有管道两端的访问权。此时关闭父进程的管道输出端、关闭子进程的管道输入端,就形成一个从父进程到子进程的管道流,数据由父进程写入、从子进程读出。创建从父进程流向子进程的管道过程如下:

1st:创建管道,返回无名管道的两个文件描述符fildes[0]和fildes[1]。

int fildes[2];

pipe(fildes);

2nd:创建子进程,子进程继续无名管道文件描述符。

3rd:父进程关闭管道的输出端,即关闭只读文件描述符fildes[0]。

close(fildes[0]);

4th:子进程关闭管道的输入端,即关闭只写文件描述符fildes[1]。

close(fildes[1]);

2)从子进程流行父进程的管道

在父进程创建无名管道并产生子进程后,父子进程均拥有管道两端的访问权。此时关闭父进程的管道输入端、关闭子进程的管道输出端,就形成一个从子进程到父进程的管道流,数据由子进程写入,从父进程读出。创立从子进程流向父进程的管道过程如下:

1st:创建管道,返回无名管道的两个文件描述符fildes[0]和fildes[1];

2nd:创建子进程,子进程中继续无名管道文件描述符。

3rd:父进程关闭管道的输入端,即关闭只读文件描述符fildes[1];

4th:子进程关闭管道的输出端,即关闭只写文件描述符fildes[0];

ex:一个管道的例子:父进程向管道写入一行字符,子进程读取数据并打印到屏幕上。

#include <unistd.h>
#include <stdio.h>
void main()
{
int fildes[];
pid_t pid;
int i, j;
char buf[];
if ( pipe( fildes ) < )
{
fprintf( stderr, "pipe error!/n" );
return;
}
if ( (pid = fork() ) < )
{
printf( stderr, "fork error!/n" );
return;
}
if ( pid == )
{
    close( fildes[] );
memset( buf, o, sizeof(buf) );
j = read( fildes[], buf, sizeof(buf) );
fprintf( stderr, "[child] buf =[%s] len [%d] /n", buf, j );
return;
} close( fildes[] );
write( fildes[], "Hello!", strlen( "Hello!" ) );
write( fildes[], "World!", strlen( "World!" ) );
}

程序中父进程分别向管道写入字符串"Hello!"和"World!",子进程一次性从管道中读出并打印这些数据。

【实践经验】在进程的通信中,我们无法判断每次通信中报文的字节数,即无法对数据流进行自动拆分,从而发生了上例中字进程一次性读取父进程两次通信的报文情况。为了能正常拆分发送报文,我们常常采用以下几种方法:

1)固定长度:发送进程每次写入固定字节的数据,接受进程每次读取固定字节的内容,报文中多余部分填充空格或填充0,根据填充0,根据填充的位置,本方法又可以分为左对齐和右对齐两种。

2)显式长度:每条报文由"长度域"和"数据域"组成,"长度域"大小固定,存储了"数据域"的长度,分为字符串型和整型两种,"数据域"是传输的实际报文数据。接受进程先获取"长度域"数据,转换为"数据域"的长度,再读取相应长度的信息即为"数据域"内容。

3)短连接。每当进程间需要通信时,创建一个通信线路,发送一条报文后立即废弃这条通信路线。这种方式为Socket通信中很常用。

3.双向管道模型

管道是进程之间的一种单向交流方法,要实现进程间的双向交流,就必须通过两个管道来完成。创立双向管道的过程如下:

1st:创建管道,返回两个无名管道文件描述符fildes1,fildes2。为了简化书写,我们称fildes1为管道1,fildes2为管道2。

int fildes1[2],int fildes2[2];

pipe(fildes1);

pipe(fildes2);

2nd:创建子进程,子进程中继承管道I和管道II。

3rd:父进程关闭管道I的输出端,即关闭只读文件描述符fildes[0];

close(fildes1[0]);

4th:子进程关闭管道I的输入端,即关闭只写文件描述符fildes2[1];

close(fildes[1]);

5th:父进程关闭管道II的输入端,即关闭只读文件描述符fildes2[1];

close(fildes[0]);

6th:子进程关闭管道II的输出端,即关闭只写文件描述fildes2[0];

close(fildes[1]);

ex:一个父子通信进程间双向管道通信的实例,父进程首先向子进程传送两次数据,再接受进程传送过来的两次数据。为了能够正确拆分数据流,从父进程流向子进程的管道I采用"固定长度"方法传送数据,从子进程流向父进程的管道II采用"显示长度"方法传回数据。

固定长度:管道输入时固定写入长度len个字符,管道输出时也固定读取len个字符,采用了左对齐方式,多余部分跳虫ASCII码0,“固定长度”数据的管道操作方法如下:

/*----管道固定长度操作pipe2.c----*/

void WriteG( int fd, char *str, int len )       /*写入固定长度报文*/
{
char buf[];
memset( buf, , sizeof(buf) );
sprintf( buf, "%s", str );
write( fd, buf, len ); /*管道输入*/
} char *ReadG( int fd, int len ) /*读取固定长度报文*/
{
char buf[];
memset( buf, , sizeof(buf) );
read( fd, buf, len ); /*管道输出*/
return(buf); /*返回管道输出数据*/
}

显示长度:显示长度报文的"长度域"可分为整形和字符串类型两种,以4字节“长度域”传输数据"Hello!" 为例,整型长度域为:

0x06 ,0x00, 0x00,0x00,"Hello!"

出于兼容性考虑,一般采用网络字节顺序的整型。

字符串长度域报文为:

"0006Hello!"

本例采用"4字节字符串"+"数据"的格式传送报文,输入时写入数据长度再写数据内容,如下:

/*显式长度输入操作pipe2.c*/
void WriteC( int fd, char *str )
{
char buf[];
sprintf( buf, "%04d%s", strlen( str ), str ); /*报文头增加报文长度*/
write( fd, buf, strlen( buf ) );
}

管道的输出操作可分为以下操作:

1st:读入4个字节,转化为整形长度。如将字符串"0006"转为为整型6。

2nd:读入数据,字节数为步骤1st中获取的整型。

char *ReadC( int fd )
{
char buf[];
int i, j;
memset( buf, , sizeof(buf) );
j = read( fd, buf, ); /*读入长度域*/
i = atoi( buf ); /*转化长度域为整型*/
j = read( fd, buf, i ); /*读入后续报文*/
return(buf); /*返回读入的报文*/
}

主程序:父子进程双向管道通信实例的主函数如下:

#include <unistd.h>
#include <stdio.h>
void main()
{
int fildes1[], fildes2[];
pid_t pid;
char buf[];
if ( pipe( fildes1 ) < || pipe( fildes2 ) < ) /*创建管道*/
{
fprintf( stderr, "pipe error!/n" );
return;
}
if ( (pid = fork() ) < ) /*创建子进程*/
{
fprintf( stderr, "fork error!/n" );
return;
} if ( pid == ) /*子进程*/
{
/*-------------------------------------------------*/
close( fildes1[] );
close( fildes2[] );
strcpy (buf, ReadG( fildes1[], ); /*读取管道数据*/
fprintf( stderr, "[child] buf =[%s] /n", buf );
WriteC( fildes2[], buf ); /*回传父进程*/
strcpy( buf, ReadG( fildes1[], ) );
fprintf( stderr, "child] buf =[%s]/n", buf );
WriteC( fildes2[], buf );
return;
}
/*------------------parent process---------------------*/
close( fildes1[] );
close( fildes2[] );
WriteG( fildes1[], "Hello!", );
WriteG( fildes1[], "World!", );
fpintf( stderr, "[father] buf =[%s]/n", ReadC( fildes2[] ) );
fprintf (stderr, "[father] buf =[%s]/n'ReadC(fildes2[0]));
}

4.连接标准I/O的管道模型

管道在shell中最常见的应用是连接不同进程的输入输出,比如使用A进程的输出变成B进程的输入等。考察shell命令"cat pipe3.c | more",进程"more"使用了进程"cat pipe3.c"的输出。

ex1.分别重定向标准输入、标准输出、标准错误输出到文件描述符fd1,fd2,fd3;

---复制文件描述fd1到文件描述符0即可重定向标准输入:

dup2(fd1,0);

dup2(fd2,1);

dup2(fd3,2);

当执行dup2(fd2,0)后,文件描述符0就对应到了fd1锁对应的文件中,而一些标准输出函数,如printf、puts等仍然想描述符0写入内容,从而达到了重定向的效果。

1)模型

使用管道将父进程标准输入连接到子进程标准输入的方法如下:

1st:创建管道,返回无名管道的两个文件描述符fildes[0]和fildes[1]

2nd:创建子进程,子进程中继承无名管道文件描述符

3rd:父进程关闭管道的输出端,即关闭只读文件描述符fildes[0]

4th:父进程将标准输入(stdout,文件描述符1)重定向为文件描述符fildes[1]。

5th:子进程关闭管道的输入端,即关闭只写文件描述符fildes[1].

6th:子进程将标准输入(stdin文件描述符为0)重定向为文件描述符fildes[0]。

2)实例:一个将父进程标准输入流连接到子进程标准输入流的管道,父进程向stdout输出的'Hello!'直接转移到子进程的stdin,由子进程"gets(buf)"语句所获取。

#include <unistd.h>
#include <stdio.h>
int main()
{
int fildes[];
pid_t pid;
char buf[];
if ( (pipe( fildes ) ) < )
|| ( (pid = fork() ) < )
{
frpintf( stderr, "error!/n" );
return();
}
if ( pid == )
{
/*----------child process-----------*/
close( fildes[] );
dup2( fildes[], );
close( fildes[] );
gets( buf ); /*读入输入,其实是读取父进程输出*/
fprintf( stderr, "child:[%s]/n", buf );
return();;
}
/*------------parent process-----------*/
close( fildes[] );
dup2( fildes[], );
close( fildes[] );
puts( "Hello!" );
return();
}

5.popen模型

创建连接标准I/O的管道需要多个步骤,需要使用大量的代码,型号UNIX提供了一组函数简化这个复杂的过程,其原型如下:

#include<stdio.h>

FILE *popen(const char *command,char *type);

int pclose(FILE *stream);

函数popen函数类似于函数system,它首先fork一个子进程,然后调用exec执行参数command中给定的shell命令。不同的是,函数popen自动在父进程和exec创建的子进程之间建立了一个管道,这个管道可以连接子进程的标准输入,也可以连接子进程的标准输出,参与type决定了一个管道I/O类型,其取值与含义如下:

r 创建与子进程的标准输出连接的管道(管道数据由子进程流向父进程)

w 创建于子进程的标准输入连接的管道(管道数据由父进程流向子进程)

函数popen调用成功时返回一个标准I/O的FILE文件流,它的读写属性由参数type决定,调用失败时返回NULL。

函数pclose关闭由popen打开的文件流,它调用时返回exec进程退出时的状态。否则返回-1。

ex:模拟shell命令"ps -ef |grep init"的例子,它的流程如下:

1st:调用popen创建子进程,执行命令"grep init",并创建一个写管道out连接到该子进程的标准输入,此时执行命令grep init 所分析的文本内容需要从管道out中读出。

2nd:调用popen创建子进程执行"ps -ef",并创建一个度管道in连接该子进程的标准输出,此时执行命令ps -ef 的结果将写入到管道in中。

3rd:从管道in中读取数据,并将该数据写入管道out中,即把执行命令 ps-ef打印的结果作为输入提交给命令grep init执行。

/*------------popen----------*/
#include <stdio.h>
void main()
{
FILE *out, *in;
if ( (out = popen( "grep init", "w" ) ) == NULL )
{
fprintf( stderr, "error!/n" );
return;
}
if ( (in = popen( "ps -ef", "r" ) ) == NULL )
{
fprintf( stderr, "error !/n" );
return;
}
while ( fgets( buf, sizeof(buf), in ) ) /*读取ps -ef结果*/
fputs( buf, out );
pclose( out );
pclose( in );
}

三、有名管道FIFO

管道如果无名,只能在共同血缘进程中使用;管道如果有名,就可以在整个系统中使用。FIFO管道,有名的管道,它以一种特殊的文件类型存储于文件系统中,以供血缘关系进程访问。

1.有名管道的建立:

shelle命令和C程序都可以创建有名管道,其中创建有名管道的shell命令如下:

1)命令mknod创建管道

可以创建特殊类型的文件,其实用方式如下

/etc/mknod name [b|c ] major minor/*创建块设备或字符设备文件*/

/etc/mknod name p /*创建管道文件*/

/etc/mknod name s /*创建信号量*/

/etc/mknod name m /*创建共享内存*/

参数name为创建爱你的文件名称,参数major和minor分别代表主、次设备

ex1:创建有名管道k1

$ mknod k1 p

$ls -l k1

prw-r--r-- 1 root sys 0 [date]

2)命令mkfifo创建管道

专门创建有名管道文件,它的语法如下:

mkfifo [-m Mode] File ...

其中参数Mode是管道文件创建后的访问权限,File是管道文件创建后的名称

ex1:创建一个用户本身可读写,其它任何用户都只读的管道文件k2

mkfifo -m 644 k2

3)函数mkfifo创建管道

UNIX中的C语言,也提供了创建有名管道的函数,其原型如下:

#include<sys/types.h>

#include<sys/stat.h>

int mkfifo(char *path,mode_t mode);

其中path-->管道文件的路径和名称

mode-->管道文件的权限,类似open函数的第三个参数,并且自带了O_CREAT 和O_EXCL选项,因此本函数只能创建一个不存在的管道文件,或者返回"文件已存在"错误。如果只是希望打开而不创建文件,请使用open或fopen。

成功调用mkfifo返回0,错误饭后-1。

2.有名管道的应用:

管道本身就是文件,因此对普通文件的操作也适合于管道文件,可以按照以下步骤应用管道。

1st:chuangjian guandao wenjian (mknod或mkfifo或者函数mkfifo)

2nd:读进程

1)只读打开管道文件(用open或fopen)

2)读管道(应用read或fread等)

3rd:写进程

1)只写打开管道文件(open或fopen)

2)些管道(write或fwrite)

4th:关闭管道文件(close或fclose)

低级文件编程库和标准文件编程库都可以操作管道,在打开管道文件务必请先确认该管道是否存在和是否具备访问权限。

管道在执行读写操作前,两端必须同时打开,否则执行打开管道某端操作的进程将一直阻塞知道某个进程以相反方向打开管道位置。

一个双进程读写管道的例子,写进程创建FIFO文件,再打开其写端口,然后读取键盘输入并将此输入信息发送给管道中,当键盘输入"exit"或"quit"时程序退出,如:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/errno.h>
extern int errno;
void main()
{
FILE *fp;
char buf[];
/*创建管道,如果已存在则跳过*/
if ( mkfifo( "myfifo", S_IFIFO | ) < && errno != EEXIST )
return;
while ( )
{
if ( (fp = fopen( "myfifo", "w" ) ) == NULL )
return; /*打开管道*/
printf( "please input:" );
gets( buf );
fputs( buf, fp );
fputs( "/n", fp );
if ( strncmp( buf, "quit", ) == )
|| strncmp( buf, "exit", ) == )
break;
fclose( fp );
}
}

读进程打开管道文件的读端口,然后从管道中读取信息,并将此信息打印到屏幕上,当从管道读取到"exit"或"quit"时程序退出。

#include <stdio.h>
#include <fcntl.h>
#include <stdio.h>
void main()
{
FILE *fp;
char buf[];
while ( )
{
if ( (fp = fopen( "myfifo", "r" ) ) == NULL )
return;
fgets( buf, sizeof(buf), fp );
printf( "gets:[%s]", buf );
if ( strncmp( buf, "quit", ) == || strncmp( buf, "exit", ) == )
break;
fclose( fp );
}
}

3.管道的模型:

1)1-1模型

2)n-1模型

3)n-n模型

参考文档:http://blog.csdn.net/wolfzone025/article/details/5741147