一、使用特殊通讯方法的必要性
进程之间的内存试隔离的,如果多个进程之间需要进行信息交换,通常的方法有以下几种:
- Unix Domain Socket IPC
- 管道(有名管道、无名管道)
- 共享内存
- 消息队列
- 信号量
此处提到的 Unix Domain Socket IPC 和信号量放在后面的章节讲解。
二、匿名管道通信
1、函数介绍
匿名管道是位于内核的一块缓冲区,用于进程间通信。创建匿名管道的系统调用为pipe。
其声明如下:
/**
*在内核空间创建管道,用于父子进程或者其他相关联的进程之间通过管道进行双向的数据传输
*
*pipefd:用于返回指向管道两端的两个文件描述符。pipefd[0]指向管道的读端。pipefd[1]指向管道的写端。*return:
*成功 0
*不成功 -1,并且pipefd 不会改变
*/
int pipe(int pipefd[2]);
一些后续常用到的宏定义:
/* We define these the same for all machines.
Changes from this to the outside world should be done in `_exit'. */
#define EXIT_FAILURE 1 /* Failing exit status. */
#define EXIT_SUCCESS 0 /* Successful exit status. */
/* Standard file descriptors. */
#define STDIN_FILENO 0 /* Standard input. */
#define STDOUT_FILENO 1 /* Standard output. */
#define STDERR_FILENO 2 /* Standard error output. */
2、测试代码
下面例子展示了父进程将argv[ 1 ]写入匿名管道,子进程读取并输出到控制台的过程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
pid_t cpid;
int pipefd[2];//存放回指向管道两端的两个文件描述符
//将程序传递进来的第一个命令行参数 通过管道传输给子进程
if (argc != 2)
{
fprintf(stderr,"%s:请填写需要传递的信息\n",argv[0]);
exit(EXIT_FAILURE);
}
//创建管道
if (pipe(pipefd) == -1)
{
perror("pipe");
exit(EXIT_FAILURE);
}
//复制父子进程
cpid = fork();
if (cpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0)
{
//子进程 读取管道的数据 打印到控制台
close(pipefd[1]); //子进程闭写管道pipefd[1]
char *str = "子进程接收信息\n";
write(STDOUT_FILENO,str,strlen(str));
char buf;
while (read(pipefd[0],&buf,1) > 0)
{
write(STDOUT_FILENO,&buf,1);
}
write(STDOUT_FILENO,"\n",1);
close(pipefd[0]);
_exit(EXIT_SUCCESS);
}else{
//父进程 写入管道数据 提供给子进程
close(pipefd[0]); //父进程闭读管道pipefd[0]
//将数据写入到管道中
printf("父进程%d向子进程传递信息\n",getpid());
write(pipefd[1],argv[1],strlen(argv[1]));//pipefd[1]为写管道
close(pipefd[1]);
waitpid(cpid,NULL,0);//等待回收子进程
exit(EXIT_SUCCESS);
}
return 0;
}
输出结果如下:
3、使用管道的限制
-
两个进程通过一个管道只能实现单向通信,比如上面的例子,父进程写子进程读,如果有时候也需要子进程写父进程读,就必须另开一个管道。
-
管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。
4、管道返回的文件描述符
管道返回的两个文件描述符分别表示读写,各自指向一个stuct fle结构体,然而,它们并不对应真正的文件。
当我们开启管道时,父进程会创建两个stuct fle结构体用于管道的读写操作,并为二者各自分配一个文件描述符。它们的private data属性指向同一个 struct pipe imode info 的结构体,由后者管理对于管道缓冲区的读写。通过forkO创建一个子进程,后者会继承文件描述符,指向相同的 stuct fle 结构体,如下。
三、有名管道(FIFO)
匿名管道只能在有父子关系的进程间使用,某些场景下并不能满足需求。与匿名管道相对的是有名管道,在Linux中称为FIFO,即FirstImFirstOut,先进先出队列。
FIFO和Pipe一样,提供了双向进程间通信渠道。但要注意的是,无论是有名管道还是匿名管道,同一条管道只应用于单向通信,否则可能出现通信混乱(进程读到自己发的数据)。
有名管道可以用于任何进程之间的通信。
1、函数介绍
#include <sys/types.h>
#include <sys/stat.h>
/**
*@brief 用于创建有名管道。该函数可以创建一个路径为 pathname 的 FIFO
*专用文件mode 指定了 FIFO 的权*限,FIF0 的权限和它绑定的文件是一致的。
*FIFO 和 pipe 唯一的区别在于创建方式的差异。一旦创建了
*FIFO专用文件,任何进程都可以像操作文件一样打开 FIFO,执行读写操作。
*@param pathname 有名管道绑定的文件路径
*@param mode 有名管道绑定文件的权限米
* @return int
*/
int mkfifo(const char *pathname, mode t mode);
2、测试代码
fifo_write.c 发送端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
char *pipe_path = "/tmp/myfifo";
char buf[1024];
ssize_t read_num;
int fd;
/**
* @brief 用于创建有名管道。该函数可以创建一个路径为pathname的FIFO专用文件,mode指定了FIFO的权限,
* FIFO的权限和它绑定的文件是一致的。FIFO和pipe唯一的区别在于创建方式的差异。一旦创建了FIFO专用文件,
* 任何进程都可以像操作文件一样打开 FIFO,执行读写操作。
*
* const char *__path:有名管道绑定的文件路径
* __mode_t __mode: 有名管道绑定文件的权限
* @return int
* int mkfifo (const char *__path, __mode_t __mode)
*/
if (mkfifo(pipe_path,0664) != 0)
{
perror("mkfifo");
if (errno != 17)//一个错误类型
{
exit(EXIT_FAILURE);
}
}
//对有名管道的特殊文件创建文件描述符fd
fd = open(pipe_path,O_WRONLY);
if (fd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
//读取控制台数据写入到管道中
while ((read_num = read(STDIN_FILENO,buf,1024)) > 0)
{
write(fd,buf,read_num);
}
if (read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("发送数据到管道完成,进程终止\n");
close(fd);
// 释放管道 清除对应的特殊文件
if (unlink(pipe_path) == -1)
{
perror("unlink");
}
return 0;
}
fifo_read.c 读取端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
char *pipe_path = "/tmp/myfifo";
char buf[1024];
ssize_t read_num;
int fd;
/**
* @brief 用于创建有名管道。该函数可以创建一个路径为pathname的FIFO专用文件,mode指定了FIFO的权限,
* FIFO的权限和它绑定的文件是一致的。FIFO和pipe唯一的区别在于创建方式的差异。一旦创建了FIFO专用文件,
* 任何进程都可以像操作文件一样打开 FIFO,执行读写操作。
*
* const char *__path:有名管道绑定的文件路径
* __mode_t __mode: 有名管道绑定文件的权限
* @return int
* int mkfifo (const char *__path, __mode_t __mode)
*/
//read文件中不需要再重复创建管道
// if (mkfifo(pipe_path,0664) != 0)
// {
// perror("mkfifo");
// exit(EXIT_FAILURE);
// }
//对有名管道的特殊文件创建文件描述符fd
fd = open(pipe_path,O_RDONLY);
if (fd == -1)
{
perror("open");
exit(EXIT_FAILURE);
}
//读取管道数据写入到控制台中
while ((read_num = read(fd,buf,1024)) > 0)
{
write(STDOUT_FILENO,buf,read_num);
}
if (read_num < 0)
{
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
printf("接收管道数据完成,进程终止\n");
close(fd);
return 0;
}
因为需要同时运行两个文件,所以只在Makefile中分别编译出两个文件的可执行文件,然后在桌面打开两个终端窗口分别运行。运行结果如下,当在运行fifo_write的终端窗口发送数据时,运行fifo_read的终端窗口就能接收到数据,按下ctrl + d结束运行,就能释放管道。
本文全部内容总结于b站up主尚硅谷