目录
一、简介
二、管道(Pipe)
1、管道的基本概念
2、管道的局限性
3、管道的创建
4、管道的读写规则
5、实战演练
三、命名管道(fifo)
1、命名管道的基本概念
2、命名管道的创建
3、实战演练
4、运行结果
四、补充
1、wait()函数
2、acess()函数
3、Linux下文件系统权限
本篇文章主要讲解Linux环境编程中进程间通信的两种常用方法:管道(Pipe)和命名管道(FIFO)。
管道:一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(通常是指父子进程关系)的进程间使用;
命名管道:也是一种半双工通信,但是它允许无亲缘关系进程间的通信;
以上只是向大家简单介绍了管道与命名管道,具体的讲解请继续往下看:
管道是UNIX系统IPC的最古老的形式,它的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据。
管道由两个文件描述符引用,一个表示读端,一个表示写端。管道一端的进程顺序地将进程数据写入缓冲区,另一端的进程则顺利地读取数据。
该缓冲区可以看成一个循环队列,读和写的位置都是自动增加的,一个数据只能被读一次,数据被读出后,就不会再存在于管道之中了。当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程是否进入等待队列,当空的缓冲区有新数据写入或者原来的缓冲区有数据读出时,就会唤醒等待队列中的进程继续读写。
(1)半双工通信方式,数据只能在一个方向流动。
(2)管道只能在具有公共祖先之间的两个进程之间使用。
(3)数据一旦被读走,便不在管道中存在,不可反复读取。
(4)管道的缓冲区是有限的(管道存在于内存中,在管道创建时,为缓冲区分配一个页面大小)。
(5) 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须实现约定好数据的格式,比如多少字节算一个消息(或命令、或记录)等待;
相信大家在接触Linux的时候都听过一句话——Linux下一切皆是文件!我们也可以把管道看做是一种特殊的文件,对于它的读写我们也可以调用write、read等函数。但是管道不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
管道是通过调用pipe函数创建的,让我们先看一下函数原型吧:
#include
int pipe(int pipefd[2]); //返回值:成功返回0,出错返回-1
经由参数fd返回的两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。
那创建管道,我们又如何能够实现进程之间通信呢?
通常,我们的进程先调用pipe,接着调用fork,从而创建了父进程与子进程之间的IPC通道。调用fork之后,如图所示:
fork()之后做什么取决于我们想要的数据流的反向,共两种情况,让我们分别来看看:
(1)从父进程到子进程方向的管道
即父写子读,关闭父进程管道的读端fd[0],再关闭子进程管道的写端fd[1],如图:
(2)从子进程到父进程方向的管道
即子写父读,关闭父进程管道的写端fd[1],再关闭子进程管道的读端fd[0],如图:
当没有数据可读时
(1)O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
(2)O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道数据满的时候
(1)O_NONBLOCK disable: write调用阻塞,直到有进程读走数据。
(2)O_NONBLOCK enable:调用返回-1,errno值为EAGAIN。
关闭管道的一端
(1)当读一个写端被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束。
(2)当写一个读端被关闭的管道时,则产生信号SIGPIPE,如果忽略该信号或者捕捉该信号并从其处理程序返回,则write返回-1 。
题目:
编写一个程序,实现父进程给子进程方向发送数据。
思考过程:
首先父进程调用pipe创建管道之后fork(),这时子进程会继承父进程所有打开的文件描述符(包括管道),这时对于一个管道就有四个读写端(父子进程各有一对管道读写端),如果需要父进程往子进程里写数据,则需要在父进程中关闭读端,在子进程中关闭写端;而如果需要子进程往父进程中写数据,则可以在父进程关闭写端,然后子进程中关闭读端。
代码如下:
#include
#include
#include
#include
#include
#include
#define MSG_STR "This message is from parent:Hello,child process!"
int main(int argc,char *agv[])
{
int pipe_fd[2];
int rv;
int pid;
char buf[512];
int status;
if(pipe(pipe_fd) < 0)
{
printf("Create pipe failure:%s\n",strerror(errno));
return -1;
}
if((pid = fork()) < 0)
{
printf("Create child prcess failure:%s\n",strerror(errno));
return -2;
}
else if(pid == 0)
{
/*child process close write ,then read data from parent process*/
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;
}
/*parent process close read,then write data to child process*/
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 -4;
}
printf("Parent start wait child process exit...\n");
wait(&status);
return 0;
}
运行结果:
命名管道通信也属于进程间通信IPC的一种常见方式,他与管道的不同是:命名管道可以实现两个毫不相干的独立进程间通信。
FIFO不同于管道之处在于它提供一个路径与之相关联,以FIFO的文件形式存在于系统中。它在磁盘上有对应的节点,但是没有数据块。换句话说就是,命名管道只是拥有一个名字和相应的访问权限,并且命名管道的大小为0 ,命名管道就是一个内存文件。
大家不用把命名管道想的很复杂,你就可以把它理解为在文件系统中挂了个名字,但也仅仅是个挂个名而已,没有实际的大小。目的就是能叫进程看到这个文件,因为文件系统中的文件都可以被进程看见。
我们可以通过调用mkfifo()来创建命名管道。一旦建立,任何进程都可以通过文件名将其打开和进行书写,而不局限于父子进程,当然前提是进程对FIFO有适当的访问权。当不再被进程使用时,FIFO在内存中释放,但磁盘节点仍在。
让我们先来看一下mkfifo()函数的原型吧:
#include
#includeint mkfifo(const char *pathname, mode_t mode);
参数说明:
(1)第一个参数pathname: 在这里既可以传绝对路径,也可以传相对路径(关于绝对路径与相对路径)。当然,绝对路径更灵活,但是也更长。
(2)第二个参数mode:其实就是创建命名管道时的初始权限,实际权限需要经过umask掩码进行计算。(关于文件权限我在文章最后给大家讲解,就不在这里赘述了)
题目---实现进程间聊天:
在程序中创建两个掩藏的命名管道文件(.fifo_chat1和.fifo_chat2)在不同的进程间进行通信。该程序需要运行两次(即两个进程),其中进程0(mode=0)从标准输入里读入数据后通过命名管道2(.fifo_chat2)写入数据给进程1(mode=1);而进程1(mode=1)则从标准输入里读入数据后通过命名管道1(.fifo_chat1)写给进程0。
请利用命名管道编写一个程序来实现上述要求。
代码如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define FIFO_FILE1 ".fifo_chat1"
#define FIFO_FILE2 ".fifo_chat2"
int g_stop = 0;
void sig_pipe(int signum)
{
if(SIGPIPE == signum)
{
printf("get pipe broken signal and let programe exit\n");
g_stop = 1;
}
}
int main(int argc,char *argv[])
{
int fdr_fifo;
int fdw_fifo;
int rv;
fd_set rdset;
char buf[1024];
int mode = -1;
if(argc != 2)
{
printf("Usage:%s [0/1]\n",basename(argv[0]));
printf("This chat program need run twice,1st time run with [0] and 2nd time with [1]\n");
return -1;
}
mode = atoi(argv[1]);
/*管道是一种半双工的通信方式,如果要实现两个进程间的双向通信则需要两个管道,即两个管道分别作为两个进程的读端和写端*/
/*测试文件是否存在,不存在则建立*/
if(access(FIFO_FILE1,F_OK))
{
printf("FIFO file \"%s\" not exist and create it now\n",FIFO_FILE1);
mkfifo(FIFO_FILE1,0666);
}
if(access(FIFO_FILE2,F_OK))
{
printf("FIFO file \"%s\" not exist and create it now\n",FIFO_FILE2);
mkfifo(FIFO_FILE2,0666);
}
signal(SIGPIPE,sig_pipe);
if(0 == mode)
{
/*这里以只读模式打开命名管道FIFO_FILE1的读端,默认是阻塞模式;如果命名管道的写端打不开,则open()将会一直阻塞,所以另外一个进程必须首先以>
写模式打开该文件FIFO_FILE1,否则会出现死锁*/
printf("start open '%s' for read and it will blocked untill write endpoint opened...\n",FIFO_FILE1);
if((fdr_fifo = open(FIFO_FILE1,O_RDONLY)) < 0)
{
printf("Open fifo[%s] for chat read endpoint failure:%s\n",FIFO_FILE1,strerror(errno));
return -1;
}
printf("start open '%s' for write...\n",FIFO_FILE2);
if((fdw_fifo = open(FIFO_FILE2,O_WRONLY)) < 0)
{
printf("Open fifo[%s] for chat write endpoint failure:%s\n",FIFO_FILE2,strerror(errno));
return -1;
}
}
else
{
/*这里以只写模式打开命名管道FIFO_FILE1的写端,默认是阻塞模式;如果命名管道的读端不打开,则open()将会一直阻塞,因为前一个进程是先以读模式>
打开该管道文件的读端,所以这里必须先以写模式打开该文件的写端,否则将会出现死锁*/
printf("start open '%s' for write and it will blocked untill read endpoint opened..\n",FIFO_FILE1);
if((fdw_fifo = open(FIFO_FILE1,O_WRONLY)) < 0)
{
printf("Open fifo[%s] for chat write endpoint failure:%s\n",FIFO_FILE1,strerror(errno));
return -1;
}
printf("start open '%s' for read...\n",FIFO_FILE2);
if((fdr_fifo = open(FIFO_FILE2,O_RDONLY)) < 0)
{
printf("Open fifo[%s] for chat read endpoint failure:%s\n",FIFO_FILE2,strerror(errno));
return -1;
}
}
printf("start chating with another program now,please input message now:\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 get timeout or error:%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("read data from FIFO get error:%s\n",strerror(errno));
break;
}
else if(0 == rv)
{
printf("Another side of FIFO get closed and program will exit now\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));
}
}
}
./fifo_chat
由图我们可以看图,我们仅仅输入一个参数是不行的。
./fifo_chat 0
./fifo_chat 1
通过命名管道,我们成功实现了进程0和进程1聊天的程序。并且,我们也可以在文件系统中看到创建的两个掩藏的命名管道文件(.fifo_chat1和.fifo_chat2),并且大小为0 。
最后,就上文提到的一些知识点做一些简单的补充。
函数原型如下:
#include
#includeint wait(int * status)
(1)函数功能:
父进程一旦调用wait函数就立即开始阻塞,然后wait会分析当前进程的某个子进程是否已经退出,如果让它找到了这样一个退出的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回,如果没有找到,就一直阻塞,直至找到一个结束的子进程或接收到了一个指定的信号为止。
【注意: 当父进程忘记调用wait()等待已终止的子进程,子进程就会进入一种没有父进程的状态,此时子进程就是zombie(僵尸)进程。】
(2)参数status:
用来保存被收集进程退出时的状态,它是一个指向int类型的指针,如果我们对这个子进程如何死掉的不在意,只想这把这个被僵尸进程消灭掉,就把这个参数置为NULL。如果status的值不是NULL,wait把子进程的退出状态取出并存入其中,这是一个整数值(int)。
函数原型如下:
#include
int access(const char * pathname, int mode)
参数说明:
(1)第一个参数pathname:需要检测的文件路径名。
(2)第二个参数mode:需要测试的操作模式。
mode说明如下:
R_OK 测试读许可权
W_OK 测试写许可权
X_OK 测试执行许可权
F_OK 测试文件是否存在
对于一个文件来说能访问它的用户大致分为三种: 拥有者(owner)、所属组(grouper)、其他人(other)。
输入ls -al,出现如图所示的情况,第一列即为文件权限。
接下来给大家简单讲解一下mkfifo()里mode值的计算:
第一个字母代表文件的类型,如“d”代表目录、“p”代表管道、“-”代表文件等等。
接下来共有九位,三位为一组,分别对应owner权限、grouper权限和other权限。r代表可读、w代表可写、x代表可执行(文件代表可执行、目录代表可进入),“-”则代表该成员没有相应的权限。
接下来讲一下“421”法,“r”用4来表示,“w”用2来表示,“x”用1来表示。那举个例子,计算drwxrwxrx-就是(4+2+1)(4+2+1)(4+2+0),也就是该目录的权限为“0777”(第一位0表示特殊权限,暂时不用理会)。
文章到这里就结束了,如果哪里有问题,欢迎大家在评论区指出!