进程间通信的概念及目的在上一篇文章: 进程间通信机制(IPC)–信号。
本文介绍的是用来实现进程间相互发送非常短小的,频率很高的的消息方法:管道(pipe)和命名管道(fifo), 主要适用于两个进程间的通信。还是要提一下为什么要有进程间通信,因为每个进程各自有不同的地址空间,其数据段、代码段、堆栈段相互独立,因此进程间要交换数据需要借助内核,在内核中开辟一段内核缓冲区,在缓冲区读写交换数据。 而管道方式属于半双工模式,数据只能往一个方向流动。
无名管道(pipe)
无名管道是一种特殊的文件,在内核中有一段特殊的内存空间,对其读写可使用普通的read、write等函数,在其对应的存储空间内以循环队列的方式存储数据,通信双方结束通讯时,内核资源会自动释放。其特点是只能在具有公共祖先(父子进程、兄弟进程)的两个进程通信。
管道创建
#include <unistd.h> //头文件
int pipe(int pipedes[2]) //函数原型
//调用成功返回0,出错返回-1
其参数为存放文件描述符的数组,其中Pipedes[1]
为写打开,Pipedes[0]
为读打开。
通常进程会调用pipe然后调用fork,从而创建了父进程与子进程的IPC通道。然后我们需要确定数据流的方向,上面说到的管道数据流是半双工的工作方式,即数据只能单向流动,如果从父进程到子进程则需要关闭父进程的读端Pipedes[0],关闭子进程的写端Pipedes[1]。如下图所示:
管道注意事项:
- 若读端不读(Pipedes[0]未关闭),写端一直写:这样持续会导致管道写满数据,从而导致再次write时导致堵塞,直到管道中有空位置时才能写入数据并返回。
- 写端不写(Pipedes[1]未关闭),但是读端一直读 :管道中剩余的数据被读取完之后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
- 读端一直读,且Pipedes[0]保持打开,而写端写了一部分数据不写了,并且关闭Pipedes[1]。:当管道中剩余数据都被读取后,再次read会返回0,就像读到文件末尾一样。
- 读端读了一部分数据,不读了且关闭Pipedes[0],写端一直在写且Pipedes[1]还保持打开状态。:一旦读端的Pipedes[0]被关闭,子进程继续向管道的写端write,那么子进程会收到信号SIGPIPE,通常会导致进程异常终止。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MSG_STR "hello,Simon"
int main(int argc, char **argv)
{
int pipe_fd[2];
int rv;
int pid;
char buf[512];
int wstatus;
if( pipe(pipe_fd) < 0) //判定管道创建是否成功
{
printf("Create pipe failure: %s\n",strerror(errno));
return -1;
}
+ pipe.c
if( (pid=fork()) < 0 ) //判定创建子进程是否成功
{
printf("Create child process failure: %s\n", strerror(errno));
return -2;
}
else if(pid == 0) //子进程执行程序
{
close(pipe_fd[1]); //子进程关闭写端
memset(buf, 0, sizeof(buf));
rv = read(pipe_fd[0], buf, sizeof(buf));
if(rv < 0)
{
printf("Child process read from pipe failure: %s\n", strerror(errno));
return -3;
}
printf("Child process read %d bytes data from pipe: \"%s\"\n", rv, buf);
return 0;
}
close(pipe_fd[0]); //父进程执行程序、父进程关闭读端
if( write(pipe_fd[1], MSG_STR, strlen(MSG_STR)) < 0)
{
printf("Parent process write data to pipe failure: %s\n", strerror(errno));
return -3;
}
printf("Parent start wait child process exit...\n");
wait(&wstatus); //避免出现僵尸进程
return 0;
}
程序运行结果如下:
panghu@Ubuntu-14:~$ gcc pipe.c -o pipe
panghu@Ubuntu-14:~$ ./pipe
Parent start wait child process exit...
Child process read 11 bytes data from pipe: "hello,Simon"
命名管道(fifo)
在实际应用中往往两个需要通信的进程可能不一定为同一个祖先的进程,因此为了解决不相关的进程也能交换数据我们引入了命名管道(Named Pipe)FIFO,它不同于管道只处在于它提供一个路径与之关联,而是以FIFO的文件形式存在于系统中。他在磁盘中有相对应的节点,但没有数据块——只拥有一个名字的和相应的访问权限,通过mknode()
和mkfifo()
系统调用来建立,建立后任何进程都可通文件名打开而进行读写,不局限于两个拥有共同祖先的进程,前提要拥有该管道文件流的访问权限,当退出时FIFO在内存中被释放但其磁盘节点将保留。
#include <sys/stat.h> //头文件
int mknod(const char* path, mode_t mod, dev_t dev); //函数原型
int mkfifo(const char* path, mode_t mod);
//返回值:这两个函数都是成功返回 0 ,失败返回 -1
创建完成后与管道使用方法一样,但命名管道使用前需要用open()
打开,因为命名管道为设备文件,他存储在硬盘上,而管道存储在内存中。命名管道用open()
打开可能会阻塞:如果用读写的方式打开一定不会阻塞,如果用只读(O_RDONLY)方式打开,用open函数时会被阻塞直到有数据到来,如果用只写(O_WRONLY)方式打开也会被阻塞直到有用读方式打开该管道。
示例代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <libgen.h>
#include <stdlib.h>
#define FIFO_FILE1 ".fifo_chat1"
#define FIFO_FILE2 ".fifo_chat2"
int g_stop = 0;
void sig_pipe(int signum)
{
if(SIGPIPE == signum)
{
printf("得到信号管道破裂,程序将要退出\n");
g_stop = 1;
}
}
int main(int argc, char **argv)
{
int fdr_fifo;
int fdw_fifo;
int rv;
ifd_set rdset;
char buf[1024];
int mode = 0;
if( argc != 2 )
{
printf("Usage: %s [0/1]\n", basename(argv[0]));
printf("这个程序需要运行两次,且其中一个参数为1写端,另一个参数为0读端\n");
return -1;
}
mode = atoi(argv[1]); //选择该用户的模式
//管道为半双工通信,如果双向通信则需要两个管道
if( access(FIFO_FILE1 , F_OK) )
{
printf("管道文件: \"%s\" 不存在将要被创建\n", FIFO_FILE1);
mkfifo(FIFO_FILE1, 0666);
}
if( access(FIFO_FILE2 , F_OK) )
{
printf("管道文件: \"%s\" 不存在将要被创建\n", FIFO_FILE2);
mkfifo(FIFO_FILE2, 0666);
}
signal(SIGPIPE, sig_pipe); //判断管道是否破裂信号
if( 0 == mode )
{
// 以只读方式打开管道1的读端,默认阻塞,如果该管道写端不被打开则会一直阻塞
printf("开始打开 '%s' 为了读取数据,他将会阻塞直到写端被打开...\n", FIFO_FILE1);
if( (fdr_fifo=open(FIFO_FILE1, O_RDONLY)) < 0 )
{
printf("打开管道:[%s] 的读端失败: %s\n", FIFO_FILE1, strerror(errno));
return -1;
}
printf("开始打开: '%s' 为了写入数据...\n", FIFO_FILE2);
if( (fdw_fifo=open(FIFO_FILE2, O_WRONLY)) < 0 )
{
printf("打开管道:[%s] 的写端失败: %s\n", FIFO_FILE2, strerror(errno));
return -1;
}
}
else
{
//以只写方式打开管道2的写端,默认阻塞,如果该管道读端不被打开则会一直阻塞
printf("开始打开 '%s' 为了写入数据,他将会阻塞直到读端被打开...\n", FIFO_FILE1);
if( (fdw_fifo=open(FIFO_FILE1, O_WRONLY)) < 0 )
{
printf("开始打开:[%s] 的写端失败: %s\n", FIFO_FILE1, strerror(errno));
return-1;
}
printf("开始打开: '%s' 为了读入数据...\n", FIFO_FILE2);
if( (fdr_fifo=open(FIFO_FILE2, O_RDONLY)) < 0 )
{
printf("打开管道:[%s] 的读端失败: %s\n", FIFO_FILE2, strerror(errno));
return-1;
}
}
printf("聊天程序开启,请输入发送数据: \n");
while( !g_stop )
{
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &rdset);
FD_SET(fdr_fifo, &rdset);
//select 多路复用监听标准输入和作为输入的命名管道读端
rv = select(fdr_fifo+1, &rdset, NULL, NULL, NULL);
if( rv <= 0 )
{
printf("Select 没读到数据或超时: %s\n", strerror(errno));
continue;
}
//输入的命名管道上有数据到来则从管道上读入数据并打印到标准输出
if( FD_ISSET(fdr_fifo, &rdset) )
{
memset(buf, 0, sizeof(buf));
rv=read(fdr_fifo, buf, sizeof(buf));
if( rv < 0 )
{
printf("读取管道数据失败: %s\n", strerror(errno));
break;
}
else if( 0 == rv ) //写端关闭
{
printf("管道的另一端关闭程序将要退出\n");
break;
}
printf("<-- %s", buf);
}
//标准输入有数据到来,从标准输入上读完后写入到作为输出的管道的另一个进程
if( FD_ISSET(STDIN_FILENO, &rdset) ) //接收键盘输入
{
memset(buf, 0, sizeof(buf));
fgets(buf, sizeof(buf), stdin);
write(fdw_fifo, buf, strlen(buf));
}
}
}
该程序实现了两个无关进程间的相互传送数据的功能:
打开管道的写端程序:
panghu@Ubuntu-14:~$ ./fifo 1
开始打开 '.fifo_chat1' 为了写入数据,他将会阻塞直到读端被打开...
开始打开: '.fifo_chat2' 为了读入数据...
聊天程序开启,请输入发送数据:
打开程序的读端:
panghu@Ubuntu-14:~$ ./fifo 0
开始打开 '.fifo_chat1' 为了读取数据,他将会阻塞直到写端被打开...
发送数据
panghu@Ubuntu-14:~$ ./fifo 0
开始打开 '.fifo_chat1' 为了读取数据,他将会阻塞直到写端被打开...
开始打开: '.fifo_chat2' 为了写入数据...
聊天程序开启,请输入发送数据:
<-- hello,simon
完成数据的传输,当我们退出任一程序时,另一端也会随之退出。