您的位置:首页 > 运维架构 > Linux

linux进程间通信(一)--------管道

2013-12-13 10:44 471 查看
一、管道的概念
管道是一种两个进程间进行单向通信的机制。因为管道传递数据的单向性,管道又称之半双工管道。管道的这一特点决定了其使用的局限性。

数据只能由一个进程流向另一个进程(其中一个写管道,另一个读管道);如果要进行全双工通信,需要建立两个管道。

管道只能用于父子进程或兄弟进程间的通信,也就是说管道只能用于具有亲缘关系的进程间的通信,无亲缘关系的进程不能使用管道。

除了以上局限性,管道还有其他一些不足,如管道没有名字,管道的缓冲区大小是受限制的,管道所传送的是无格式的字节流。这就要求管道的输入方和输出方事先约定好数据的格式。虽然有这么多不足,但对于一些简单的进程间的通信,管道还是完全可以胜任的。

使用管道进行通信时,两端的进程向管道读写数据是通过创建管道时,系统设置的文件描述符进行的。因此对于管道两端的进程来说,管道就是一个特殊的文件,这个文件只存在于内存中。在创建管道时,系统为管道分配一个页面作为数据缓冲区,进行管道通信的两个进程通过读写这个缓冲区来进行通信。

通过管道通信的两个进程,一个进程向管道写数据,另一个进程从管道的另一端读数据。写入的数据每次都添加在管道缓冲区的末尾,读数据的时候都是从缓冲区的头部读出数据。

二、管道的创建与读写

1、管道的创建

Linux下创建管道可以通过函数pipe来完成。该函数如果调用成功返回0,并且数组中将包含两个新的文件描述符:如果有错误发生,返回-1。该函数原型如下:

#include <unistd.h>

int pipe(int fd[2]);

管道两端可分别用描述符fd[0]以及fd[1]来描述。需要注意的是,管道两端的任务是固定的,一端只能用于读,由描述符fd[0]表示,称其为管道读端;另一端只能用于写,由描述符fd[1]来表示,称其为管道写端。如果试图从管道写端读数据,或者向管道读端写数据都将导致出错。

管道是一种文件,因此对文件操作的I/O函数都可以用于管道,如read(),write()等。

注意:管道是一旦创建成功,就可以作为一般的文件来使用,对一般文件进行操作的I/O函数也适用于管道,如:

管道的一般用法是,进程在使用fork函数创建子进程前先创建一个管道,该管道用于在父子进程间通信,然后创建子进程,之后你进程关闭管道的读端,子进程关闭管道的写端。父进程负责向管道写数据而子进程负责读数据。当然父进程可以关闭管道的写端而子进程关闭管道的读端。这样管道就可以用于父子进程间的通信,也可用于兄弟进程间的通信。下面介绍进程是如何通过管道来读写数据的。

2、从管道中读数据

如果某进程要读取管道中的数据,那么该进程应当关闭fd1,同时向管道写数据的进程应当关闭fd0。因为管道只能用于具有亲缘关系的进程间的通信,在各进程进行通信时,它们共享文件描述符。在使用前,应及时地关闭不需要的管道的另一端,以避免意外错误的发生。

进程在管道的读端读数据时,如果管道的写端不存在,则读进程认为已经读到了数据的末尾,该函数返回读出的字节数为0;管道的写端不存在,则读取的字节数大于PIPE_BUF,则返回管道中现有的所有数据;如果请求的字节数不大于PIPE_BUF,则返回管道中现有的所有数据(此时,管道中数据量小于请求的数据量),或者返回请求的字节数(此时,管道中数据量大于等于请求的数据量)。

注意:PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。

3、向管道中写数据

如果某进程希望向管道中写入数据,那么该进程应该关闭fd0文件描述符,同时管道另一端的进程关闭fd1。向管道中写入数据时,Linux不保证写入的原子性(原子性是指操作在任何时候都不能被任何原因所打断,操作要么不做要么就一定完成)。管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直被阻塞等待。

在写管道时,如果要求写的字节数小于等于PIPE_BUF,则多个进程对同一管道的写操作不会交错进行。但是,如果有多个进程同时写一个管道,而且某些进程要求写的字节数超过PIPE_BUF所能容纳时,则多个写操作的数据可能会交错。

注意:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIGPIPE信号。应用程序可以处理也可以忽略该信号,如果忽略该信号或者捕捉该信号并从其处理程序返回,则write出错,错误码为EPIPE。

下面通过一个例10-1,说明管道的创建,以及对管道的读写是如何进行的。

例10-1

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

/*读管道*/

void read_from_pipe(int fd)

{

char message[100];

read(fd,message,100);

printf("read from pipe:%s",message);

}

/*写管道*/

void write_to_pipe(int fd)

{

char *message = "Hello,pipe!\n";

write(fd,message,strlen(message)+1);

}

int main(void)

{

int fd[2];

pid_t pid;

int stat_val;

if (pipe(fd))

{

printf("create pipe failed!\n");

exit(1);

}

pid = fork();

switch(pid)

{

case -1:

printf("fork error!\n");

exit(1);

case 0:

/*子进程关闭fd1*/

close(fd[1]);

read_from_pipe(fd[0]);

exit(1);

default:

/*你进程关闭fd0*/

close(fd[0]);

write_to_pipe(fd[1]);

wait(&stat_val);

exit(1);

}

return 0;

}

程序说明:

该程序中,父进程向管道中写数据,子进程从管道中获取数据。可以看到,对管道的读写和对一般文件的读写没有什么区别。

程序运行结果如下:

$ ./10-1

read from pipe:Hello,pipe!

注意:必须在系统调用fork()之前调用pipe(),否则子进程将不会继承管道的文件描述符。

管道是半双工的(一端只写不能读,另一端只读不能写),但是可以通过创建两个管道来实现一个全双工(两端都可以读和写)通信。下面通过例10-2来说明如何实现全双工通信。

例10-2

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <unistd.h>

/*子进程读写管道的函数*/

void child_rw_pipe(int readfd,int writefd)

{

char *message1 = "from child process!\n";

write(writefd,message1,strlen(message1) + 1);

char message2[100];

read(readfd,message2,100);

printf("child process read from pipe:%s",message2);

}

/*父进程读写管道的函数*/

void parent_rw_pipe(int readfd,int writefd)

{

char *message1 = "from parent process!\n";

write(writefd,message1,strlen(message1) + 1);

char message2[100];

read(readfd,message2,100);

printf("parent process read from pipe:%s",message2);

}

int main(void)

{

int pipe1[2],pipe2[2];

pid_t pid;

int stat_val;

printf("realize full-duplex communication:\n\n");

if (pipe(pipe1))

{

printf("pipe1 failed!\n");

exit(1);

}

if (pipe(pipe2))

{

printf("pipe2 failed!\n");

exit(1);

}

pid = fork();

switch(pid)

{

case -1:

printf("fork error!\n");

exit(1);

case 0:

/*子进程关闭pipe1的读端,关闭pipe2的写端*/

close(pipe1[1]);

close(pipe2[0]);

child_rw_pipe(pipe1[0],pipe2[1]);

exit(0);

default:

close(pipe1[0]);

close(pipe2[1]);

parent_rw_pipe(pipe2[0],pipe1[1]);

wait(&stat_val);

exit(0);

}

}

程序说明:

该程序父子进程之间相互发送信息。程序的运行结果如下:

$ ./10-2

realize full-duplex communication:

child process read from pipe:from parent process!

parent process read from pipe:from child process!

4、dup()和dup2()

前面的例子中,子进程可以直接共享你进程的文件描述符。但是如果子进程调用exec函数执行另外一个应用程序时,就不能再共享了。这种情况下可以将子进程中的文件描述符重定向到标准输入,当新执行的程序从标准输入获取数据时实际上是从你进程中获取输入数据。dup和dup2函数提供了复制文件描述符的功能。两个函数的声明均在头文件unistd.h中:

#include <unistd.h>

int dup(int oldfd);

int dup2(int oldfd,int newfd);

dup和dup2函数调用成功时均返回一个oldfd文件描述符的副本,失败则返回-1。所不同的是由dup函数返回的文件描述符是当前可用文件描述符中的最小数值,而dup2函数则可以利用参数newfd指定欲返回的文件描述符。如果参数newfd指定的文件描述符已经打开,系统先将其关闭,然后将oldfd指定的文件描述符赋值到该参数。如果newfd等于oldfd,则dup2返回newfd,而不关闭它。

下面是dup和dup2使用的对比:

/*dup程序片断*/

pid = fork();

if (pid == 0)

{

/*关闭子进程的标准输出*/

close(1);

//复制管道输入端到标准输出

dup(fd[1]);

execve("exam",argv,environ);

...

}

/*dup2程序片断*/

pid = fork();

if (pid == 0)

{

/*关闭标准输出并复制管道输出端到标准输出*/

dup(1,fd[1]);

execvc("exam",argv,environ);

...

}

可见dup2系统调用将close操作和文件描述符拷贝操作集成在同一个函数里,而且它保证操作具有原子性。

三、管道的应用实例

管道的一种常见用法是:在父进程创建子进程后向子进程传递参数。例如,一个应用软件有一个主进程和很多个不同的子进程。主进程创建子进程后,在子进程调用exec函数执行一个新程序前,通过管道给即将执行的程序传递命令行参数,子进程根据传来的参数进行初始化或其他操作。下面通过例10-3和例10-4来演示这种用法,这个程序包括一个主进程程序和一个子进程中要执行的新程序。

例10-3

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

int main(int arg,char *argv[],char **environ)

{

int fd[2];

pid_t pid;

int stat_val;

if (arg < 2)

{

printf("wrong parameters \n");

exit(0);

}

if (pipe(fd))

{

perror("pipe failed");

exit(1);

}

pid = fork();

switch (pid)

{

case -1:

perror("fork failed!\n");

exit(1);

case 0:

close(0);

dup(fd[0]);

execve("10-4",(void *)argv,environ);

exit(0);

default:

close(fd[0]);

/*将命令行第一个参数写进管道*/

write(fd[1],argv[1],strlen(argv[1]));

break;

}

wait(&stat_val);

exit(0);

}

例10-4

#include <stdio.h>

#include <unistd.h>

int main(int arg,char *argv[])

{

int n;

char buf[1024];

while (1)

{

if ((n = read(0,buf,1024))>0)

{

buf
= '\0';

printf("10-4 receive: %s\n",buf);

if (!strcmp(buf,"exit"))

exit(0);

if (!strcmp(buf,"getpid"))

{

printf("My pid:%d\n",getpid());

sleep(3);

exit(0);

}

}

}

}

程序说明:

主进程向管道中写一个命令行参数,子进程从标准输入里面读出参数,进行相应的操作。首先编译10-4.c

$ gcc -o 10-4 10-4.c

再编译10-3.c

$ gcc -o 10-3 10-3.c

分别运行./10-3和./10-4,结果分别如下:

$ ./10-3 exit

10-4 receive: exit

$ ./10-3 getpid

10-4 receive: getpid

My pid:24786

可见,被监控子进程受监控主进程的命令,执行不同的操作。这在实际项目中是经常见到的,监控进程启动加载多个具有不同功能的子进程,并通过管道的形式向子进程传送参数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: