1、定义
管道是一种在Unix和类Unix系统中用于进程间通信的机制。管道可以分为匿名管道和命名管道两种类型。
1.1 匿名管道(Anonymous Pipe)
匿名管道是一种单向通信机制,只能在具有共同祖先的进程之间使用。它通过pipe系统调用创建,其中一个进程作为读端,另一个进程作为写端。
优点:
- 简单易用,不需要额外的系统调用来创建和使用。
- 适用于需要在具有共同祖先的两个进程之间进行单向通信的场景,比如父子进程之间的通信。
缺点:
- 单向通信,无法实现双向通信。
- 有限的缓冲区大小,可能会导致阻塞。
- 仅适用于具有共同祖先的进程之间的通信。
1.2 命名管道(FIFO)
命名管道是一种特殊类型的文件,允许无关的进程之间进行通信。它通过mkfifo系统调用创建,可以在文件系统中看到。
优点:
- 允许无关的进程进行通信,不需要具有共同祖先。
- 可以实现双向通信。
- 适用于需要在无关的进程之间进行通信的场景。
缺点:
- 需要额外的系统调用来创建和使用。
- 通信双方需要事先知道管道的路径。
- 无法传递复杂的数据结构,只能传递字节流。
1.3 小结
匿名管道适用于具有共同祖先的两个进程之间的单向通信,比如父子进程之间的通信。命名管道适用于无关的进程之间的通信,可以实现双向通信,适用于需要在不同进程之间进行数据交换的场景,比如进程间的协作或数据传输。
管道通信是基于字节流的,不支持传递复杂的数据结构,因此在需要传递结构化数据或大量数据的情况下,可能需要考虑其他进程间通信机制,比如消息队列、共享内存或套接字等。
2. 编程测试
2.1 匿名管道编程
匿名管道只能进行单向通信,因此创建两个管道实现父子间进程的双向通信:
创建完描述符后关闭自己不需要的功能,让管道1负责父进程写子进程读,管道2负责子进程写父进程读,实现效果如下:
编写如下测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
// 打印时分秒的宏
#define PRINT_MIN_SEC() do { \
time_t t = time(NULL); \
struct tm *tm_ptr = localtime(&t); \
printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
} while (0)
int main()
{
int pipefd1[2]; // 管道1 父进程写,子进程读 [0]为读端 [1]为写端
int pipefd2[2]; // 管道2 子进程写,父进程读
char buffer1[100] = {0}; // 父进程接收缓存
char buffer2[100] = {0}; // 子进程接收缓存
pid_t pid;
if (pipe(pipefd1) == -1 || pipe(pipefd2) == -1)
{
perror("pipe");
return 0;
}
pid = fork();
if (pid == -1)
{
perror("fork");
return 0;
}
if (pid > 0) // 父进程
{
close(pipefd1[0]); // 关闭管道1父进程读取端
close(pipefd2[1]); // 关闭管道2父进程写入端
// 向管道1写入数据
write(pipefd1[1], "I am parent send msg", sizeof("I am parent send msg"));
while(1)
{
// 从管道2读取数据
bzero(buffer1, sizeof(buffer1));
read(pipefd2[0], buffer1, sizeof(buffer1));
PRINT_MIN_SEC();
printf("Read from child: %s\n", buffer1);
sleep(3);
write(pipefd1[1], "I am parent send msg", sizeof("I am parent send msg"));
}
}
else // 子进程
{
close(pipefd1[1]); // 关闭管道1子进程写入端
close(pipefd2[0]); // 关闭管道1子进程读取端
// 向管道2写入数据
write(pipefd2[1], "I am child send msg", sizeof("I am child send msg"));
while(1)
{
bzero(buffer2, sizeof(buffer2));
read(pipefd1[0], buffer2, sizeof(buffer2));
PRINT_MIN_SEC();
printf("Read from parent: %s\n", buffer2);
sleep(3);
write(pipefd2[1], "I am child send msg", sizeof("I am child send msg"));
}
}
return 0;
}
每隔3秒,父进程和子进程都会读取对方发送的消息,并重新发送给对方一次消息,测试结果如下:
对于创建的读写描述符有以下属性:
- O_CLOEXEC:在文件描述符上设置 close-on-exec(FD_CLOEXEC)标志。当一个进程调用 exec 函数时,如果该标志被设置,那么该进程将关闭所有设置了 FD_CLOEXEC 标志的文件描述符。这对于在执行新程序时自动关闭文件描述符非常有用,可以避免在新程序中不必要地继承文件描述符。
- O_DIRECT:创建一个以“数据包”模式进行 I/O 的管道。对于管道的每次 write 操作都被视为一个单独的数据包,而对管道的 read 操作将一次读取一个数据包。这对于特定类型的数据传输非常有用,例如需要进行原子操作的数据传输。
- O_NONBLOCK:在文件描述符上设置 O_NONBLOCK 文件状态标志。这将使得对该文件描述符的读写操作变成非阻塞的。当对非阻塞文件描述符进行读写操作时,如果没有数据可用或者无法立即完成写操作,操作将立即返回而不是阻塞等待。
可以通过fcntl来设置相关属性值,例如设置管道1读取非阻塞:
fcntl(pipefd1[0], F_SETFL, O_NONBLOCK)
2.2 有名管道编程
使用两个FIFO可以实现无血缘关系的两个进程之间的通信,编写测试用例,进程1代码如下,实现向fifo1写入数据,并且堵塞读取fifo2中的数据:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#define PROCESS_1_SEND "/home/fifo1"
#define PROCESS_2_SEND "/home/fifo2"
// 打印时分秒的宏
#define PRINT_MIN_SEC() do { \
time_t t = time(NULL); \
struct tm *tm_ptr = localtime(&t); \
printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
} while (0)
// 进程1
int main()
{
int fd1, fd2;
int ret = 0;
char buffer[100];
// 创建FIFO
if (-1 == access(PROCESS_1_SEND, F_OK))
{
if (0 != mkfifo(PROCESS_1_SEND, 0666))
{
perror("mkfifo PROCESS_1_SEND err");
return 0;
}
}
if (-1 == access(PROCESS_2_SEND, F_OK))
{
if (0 != mkfifo(PROCESS_2_SEND, 0666))
{
perror("mkfifo PROCESS_2_SEND err");
return 0;
}
}
fd1 = open(PROCESS_1_SEND, O_RDWR);
fd2 = open(PROCESS_2_SEND, O_RDWR);
write(fd1, "Process 1 Start", sizeof("Process 1 Start"));
while(1)
{
read(fd2, buffer, sizeof(buffer));
PRINT_MIN_SEC();
printf("Read from process 2: %s\n", buffer);
sleep(5);
write(fd1, "Process 1 Msg", sizeof("Process 1 Msg"));
}
close(fd1);
close(fd2);
return 0;
}
进程2代码如下,实现向fifo2写入数据,并且堵塞读取fifo1中的数据:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#define PROCESS_1_SEND "/home/fifo1"
#define PROCESS_2_SEND "/home/fifo2"
// 打印时分秒的宏
#define PRINT_MIN_SEC() do { \
time_t t = time(NULL); \
struct tm *tm_ptr = localtime(&t); \
printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
} while (0)
// 进程2
int main()
{
int fd1, fd2;
int ret = 0;
char buffer[100];
fd1 = open(PROCESS_1_SEND, O_RDWR);
fd2 = open(PROCESS_2_SEND, O_RDWR);
write(fd2, "Process 2 Start", sizeof("Process 2 Start"));
while(1)
{
read(fd1, buffer, sizeof(buffer));
PRINT_MIN_SEC();
printf("Read from process 1: %s\n", buffer);
sleep(5);
write(fd2, "Process 2 Msg", sizeof("Process 2 Msg"));
}
close(fd1);
close(fd2);
return 0;
}
每隔5秒,进程1和进程2都会读取对方发送的消息,并重新发送给对方一次消息,测试结果如下:
2.3 注意问题
管道默认是有最大限值的,如果一直写入没有读取的话会造成堵塞,Linux下通过以下命令可以查看:
sysctl fs.pipe-max-size
或者
cat /proc/sys/fs/pipe-max-size
3、总结
本文阐述了管道通信的特点和优缺点,通过测试用例示例了管道在编程中的具体使用。