命名管道的特点
命名管道也叫有名管道(FIFO),他有以下特点:
-
能够用于非亲缘进程之间的通信,当然亲缘进程肯定也可以;
-
命名管道和无名管道原理类似,都是通过在进程之间开辟一点中间介质,不同的进程通过往这个介质中读写内容,即完成了信息的收发;
-
顾名思义,命名管道同无名管道相比就是因为他有名,那就是文件名,文件件都可以使用open() 函数打开并返回一个文件描述符,所以,我们就不用指定必须要亲缘进程才能通信,非亲缘进程之间只要使用open() 函数打开同一个文件描述符,就可完成不同进程通信了;
-
同样,在命名管道中,如果一个管道的读端全部关闭,但是还有进程尝试往里面写内容,该进程就会收到内核发来的一个终止进程的信号(SIGPIPE),我们同样可以使用signal() 函数来捕捉该信号;可参考无名管道这篇博客;
命名管道的使用
函数原型
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
成功返回 0 ;失败返回 -1,且错误 errno 的值会被设置为以下值:
EACCES:路径名中的一个目录不允许搜索(执行)权限。
EDQUOT:用户在文件系统上的磁盘块或信息节点配额已用完。
EEXIST:路径名已经存在。这包括路径名是符号链接的情况,不管是否悬空。
参数
- pathname:被创建管道文件的文件路径名;
- mode:指定被创建时的原始权限,一般为0664,要包含读写权限;
命名管道单向通信
程序要点:当我们使用mkfifo() 创建文件时,如果失败或者该管道文件已存在都会返回 -1;但是如果存在这一条件,我们是可以继续执行读写的,所以在判断函数是否失败时,要除开已存在这一条件,通过上面所说的,errno的值,判断
errno != EEXITS即可;单向通信发送端代码:
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#define FIFO_NAME "./fifo1"
#define SEND_MSG "Hello World"
void err_quit(char *estr)
{
perror(estr);
exit(-1);
}
void signal_func1(int signo)
{
remove(FIFO_NAME);
exit(-1);
}
void signal_func2(int signo)
{
printf("因为向没有读端的管道中发写数据,所以接收到进程终止信号!\n");
exit(-1);
}
int main(int argc,char *argv[])
{
char buf[1024];
int rv = -1;
int fd = -1;
signal(SIGINT,signal_func1); /*若接收到退出的信号,则先删除创建的管道文件,再退出 */
signal(SIGPIPE,signal_func2);
rv = mkfifo(FIFO_NAME,0664);
if((rv < 0) && (errno != EEXIST))
/* 创建fifo,如果错误或者该管道文件已存在,则rv 会返回-1,但他存在的这种情况我们是可以继续操作的,所以不能退出,要将他忽略 */
{
err_quit("mkfifo failure");
}
if((fd = open(FIFO_NAME,O_WRONLY)) < 0)
{
err_quit("open faliure");
}
int i = 1;
while(1)
{
write(fd,SEND_MSG,12);
printf("第 %d 次写入成功\n",i);
i++;
sleep(1);
}
return 0;
}
要注意使用open() 打开的权限,为了避免一些不当的操作,例如在只有一个管道的的时候,发送方尝试自己读管道中的信息造成抢读(接收方无法获取数据),在使用open() 函数打开时就设置 只写 的权限,接收方使用 只读 的权限;
接收端代码:
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#define FIFO_NAME "./fifo1"
void err_quit(char *estr)
{
perror(estr);
exit(-1);
}
void signal_func(int signo)
{
remove(FIFO_NAME);
exit(-1);
}
int main(int argc,char *argv[])
{
char buf[1024];
int rv = -1;
int fd = -1;
signal(SIGINT,signal_func); /*若接收到退出的信号,则先删除创建的管道文件,再退出 */
rv = mkfifo(FIFO_NAME,0664);
if((rv < 0) && (errno != EEXIST))
/* 创建fifo,如果错误或者该管道文件已存在,则rv 会返回-1,但他存在的这种情况我们是可以继续操作的,所以不能退出,要将他忽略 */
{
err_quit("mkfifo failure");
}
if((fd = open(FIFO_NAME,O_RDONLY)) < 0)
{
err_quit("open faliure");
}
int i = 1;
while(1)
{
bzero(buf,sizeof(buf));
read(fd,buf,sizeof(buf));
printf("第 %d 次接收成功:%s\n",i,buf);
i++;
}
return 0;
}
需要注意的是,当管道的读端还没有被打开过时,尝试往里面写会被阻塞,若打开过,但又被关闭了,尝试往里面写的程序就会被发送终止进程的信号;
下面验证:
代码演示与验证
编译发送方代码,编译接收方代码:
首先,运行发送方代码:
会发现,当首先运行发送方时,因为管道的读端还没有被打开过,所以写端将被阻塞于此,并没有像管道内写内容;此时在其他终端使用 ls 命令可以看到,在当前路径下,创建了一个 fifo1 的文件:
该文件在程序结束后,就会被删除,因为程序中使用了remove()命令;
运行接收端:
这时接收端可以接收到数据了,那此时此刻,发送端又是什么情况呢:
我们发现,发送端也成功执行了;
按照我之前所说的,如果这时断开接收方,则管道的读端在打开后均被关闭,
那么,发送方在接下来的第一次发送数据时,就会收内核发送的SIGPIPE信号,于是我们捕捉该信号,并加入打印信息,验证:
记住,一定是先结束接收方才满足条件哦:
运行程序,先断开接收方,就可以看到:
程序退出,且退出原因是向没有读端的管道中写;
命名管道双向通信
命名管道的双向通信与无名管道一样,那就是使用两次mkfifo() 函数,创建两个管道文件,一个是A进程 —> B进程,另一个是B进程 —> A进程;
代码:两个进程几乎一样;不要同时都先读就可以了;
进程1:
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#define FIFO_NAME1 "./fifo1"
#define FIFO_NAME2 "./fifo2"
#define SEND_MSG "Hello World"
void err_quit(char *estr)
{
perror(estr);
exit(-1);
}
void signal_func(int signo)
{
remove(FIFO_NAME1);
remove(FIFO_NAME2);
exit(-1);
}
void signal_func2(int signo)
{
printf("因为向没有读端的管道中发写数据,所以接收到进程终止信号!\n");
exit(-1);
}
int main(int argc,char *argv[])
{
char buf[1024];
int rv = -1;
int fd1 = -1;
int fd2 = -1;
signal(SIGINT,signal_func); /*若接收到退出的信号,则先删除创建的管道文件,再退出 */
signal(SIGPIPE,signal_func2);
rv = mkfifo(FIFO_NAME1,0664);
if((rv < 0) && (errno != EEXIST))
/* 创建fifo,如果错误或者该管道文件已存在,则rv 会返回-1,但他存在的这种情况我们是可以继续操作的,所以不能退出,要将他忽略 */
{
err_quit("mkfifo failure");
}
rv = mkfifo(FIFO_NAME2,0664);
if((rv < 0) && (errno != EEXIST))
{
err_quit("mkfifo failure");
}
if((fd1 = open(FIFO_NAME1,O_WRONLY)) < 0)
{
err_quit("open faliure");
}
if((fd2 = open(FIFO_NAME2,O_RDONLY)) < 0)
{
err_quit("open faliure");
}
while(1)
{
bzero(buf,sizeof(buf));
write(fd1,SEND_MSG,12);
read(fd2,buf,sizeof(buf));
printf("recv:%s\n",buf);
sleep(1);
}
return 0;
}
进程2:
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#define FIFO_NAME1 "./fifo1"
#define FIFO_NAME2 "./fifo2"
#define SEND_MSG "Hello World"
void err_quit(char *estr)
{
perror(estr);
exit(-1);
}
void signal_func(int signo)
{
remove(FIFO_NAME1);
remove(FIFO_NAME2);
exit(-1);
}
void signal_func2(int signo)
{
printf("因为向没有读端的管道中发写数据,所以接收到进程终止信号!\n");
exit(-1);
}
int main(int argc,char *argv[])
{
char buf[1024];
int rv = -1;
int fd1 = -1;
int fd2 = -1;
signal(SIGINT,signal_func); /*若接收到退出的信号,则先删除创建的管道文件,再退出 */
signal(SIGPIPE,signal_func2);
rv = mkfifo(FIFO_NAME1,0664);
if((rv < 0) && (errno != EEXIST))
/* 创建fifo,如果错误或者该管道文件已存在,则rv 会返回-1,但他存在的这种情况我们是可以继续操作的,所以不能退出,要将他忽略 */
{
err_quit("mkfifo failure");
}
rv = mkfifo(FIFO_NAME2,0664);
if((rv < 0) && (errno != EEXIST))
{
err_quit("mkfifo failure");
}
if((fd1 = open(FIFO_NAME1,O_RDONLY)) < 0)
{
err_quit("open faliure");
}
if((fd2 = open(FIFO_NAME2,O_WRONLY)) < 0)
{
err_quit("open faliure");
}
while(1)
{
bzero(buf,sizeof(buf));
write(fd2,SEND_MSG,12);
read(fd1,buf,sizeof(buf));
printf("recv:%s\n",buf);
sleep(1);
}
return 0;
}
这样,两个进程都可以在发送消息给另一个进程的同时,接收另一个进程的消息了
:
进程1:
进程2:
命名管道的缺点:
虽然命名管道解决了无名管道只能亲缘进程间通信的局限,但是,还是只能两个进程之间通信,无法形成网状通信结构,所以后来便引出了:消息队列,信号量与共享内存!