常见的进程间通信IPC机制包括管道(pipe)、信号(signal)、消息队列(message queue)、共享内存(shared memory)和套接字(socket)。
1. 管道(Pipe)
管道是一种基于内存的、面向字节的、单向的通信方式,通常用于具有亲缘关系的进程间通信,如父子进程。管道有两种类型:匿名管道和命名管道。
通常情况下,管道设计为单向通信机制,这意味着数据只能在一个方向上流动,这种单向性是管道区别于其他IPC机制(如消息队列、共享内存等)的一个特点
在创建管道时,会生成两个文件描述符:一个用于读取(read),另一个用于写入(write)。比如,父进程会使用管道的读取端来读取数据,而子进程会使用写入端来发送数据。如果尝试在同一个管道的同一端进行读写操作,可能会导致死锁或不可预测的行为。
如果需要在两个进程之间实现双向通信,可以创建两个管道,每个管道用于一个方向的通信。例如,父进程和子进程可以分别使用一个管道进行读取和写入操作,而另一个管道则用于相反方向的操作。
读取空的管道会怎样呢?
在管道通信中,读操作和写操作是相互协调的。当一个进程(如子进程)向管道写入数据时,这些数据会被放入一个缓冲区。然后,另一个进程(如父进程)可以从管道中读取这些数据。如果父进程尝试读取数据而缓冲区为空,它将等待直到有数据可用或者写入端关闭。这种阻塞行为是管道作为同步通信手段的一个关键特性。它允许父进程和子进程在没有显式同步机制(如信号量、互斥锁等)的情况下进行协调。某个进程(如父进程)可以通过检查读操作的返回值来判断管道是否关闭,因为当管道的写入端关闭时,读操作将返回0,表示没有更多的数据可读。
关闭管道的某端会怎样呢?
管道的关闭操作是永久性的,不能被撤销或者重新打开。
如果管道的读取端被关闭,而写入端仍然尝试写入数据,写入操作将会失败,并且通常会返回一个错误。在Linux系统中,这通常会导致 write
调用返回-1,并将 errno
变量设置为 EPIPE
,表示管道的另一端已经关闭。
如果管道的写端被关闭,而读端仍然尝试读取数据,根据UNIX和Linux系统的行为,以下是可能发生的情况:
- 如果管道中还有数据 :读端可以继续读取管道中已经存在的数据,直到数据被完全读取为止。
- 如果管道中没有数据 :一旦写端关闭,管道中的数据被读取完毕,读端再尝试读取操作将会立即返回0,表示没有更多的数据可读,并且管道的读取端也被隐式地关闭了。这种行为通常称为“EOF”(End Of File)。
在编程实践中,当从管道读取到0字节时,通常意味着管道的写入端已经关闭,且管道中没有更多的数据可以读取。这是一个信号,表明管道的通信已经结束。在这种情况下,读取进程应该相应地处理这一情况,例如通过退出循环或关闭自己的读端来清理资源。
管道的大小是多少呢?
管道(pipe)的大小并不是在创建时指定的一个固定值。实际上,管道的大小是由操作系统的内核自动管理的,它会根据系统资源和当前负载动态调整。由于管道的缓冲区是内存中的,其大小也受限于系统的内存使用情况。
管道满会怎样呢?
当管道的缓冲区接近满时,尝试向管道写入更多数据的操作将会阻塞,直到管道中有一些空间被释放(即数据被读取)。如果写入端继续尝试写入数据,而读取端没有读取数据,写入操作将会一直阻塞,直到缓冲区有足够空间为止。这种行为保证了管道中的数据不会丢失,但也可能导致写入端进程长时间等待。
如何获取管道中的数据有多少?
要确定管道中有多少数据可以使用 select
系统调用或 ioctl
系统调用来查询文件描述符的状态。select
调用允许你的程序监控一组文件描述符,等待它们变得可读、可写或有错误。对于管道,你可以使用 select
来检查管道的读取端是否包含可供读取的数据,或者管道的写入端是否可以写入更多数据而不会导致阻塞。
总的来说,管道是一种可靠的通信机制,它确保了数据的顺序传输和不丢失。但是,在使用管道时,需要考虑可能出现的阻塞情况,并适当地处理这些情况,以确保程序的正确性和效率。
1.1. 匿名管道(Anonymous Pipe)
匿名管道是一种临时的、不存储在文件系统中的通信方式。它用于具有亲缘关系的进程,如父子进程之间的通信。匿名管道是单向的,数据只能在一个方向上流动,例如从父进程到子进程或从子进程到父进程。
特点:
- 单向性:数据只能在一个方向上流动。
- 临时性:匿名管道在创建它的进程结束后就会消失,不会在文件系统中创建特殊文件。
- 无需文件系统:不需要在文件系统中创建特殊文件。
- 操作函数:使用
pipe
创建匿名管道,使用read
,write
,close
等常规文件操作函数进行通信,close
后不可以再重新open
。 - 适用场景:只适用于亲缘关系的进程。
1.2. 命名管道(Named Pipe,也称为FIFO)
命名管道是一种持久的、存储在文件系统中的特殊文件,可以用于任意两个进程之间的通信,无论它们是否有亲缘关系。命名管道也是单向的,但与匿名管道不同,即使创建它的进程已经结束,命名管道仍然存在,直到被显式删除。
特点:
- 单向性:数据只能在一个方向上流动。
- 持久性:命名管道在文件系统中以文件形式存在,直到被删除,是一种特殊的文件系统对象。
- 操作函数:使用
mkfifo
和unlink
创建和删除有名管道,使用open
,read
,write
,close
等常规文件操作函数进行通信,close
后可以再重新open
。 - 适用场景: 任意进程间通信,不要求进程具有亲缘关系。
实验1:匿名管道(Anonymous Pipe)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <string.h>
#include <sys/ioctl.h>
#define PARENT_WRITE "Hello from parent"
#define CHILD_WRITE "Child response"
#define BUF_SIZE 50
void parent_communication(int read_fd, int write_fd) {
char buffer[BUF_SIZE]={'\0'};
int bytes_available = 0;
int maxfd, n;
// 向子进程发送数据
write(write_fd, PARENT_WRITE, strlen(PARENT_WRITE));
close(write_fd);
// 等待读取端准备就绪
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(read_fd, &readfds);
maxfd = read_fd + 1; // 确保maxfd是最大的文件描述符
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
// 使用select等待读取数据
while ((n=(select(maxfd, &readfds, NULL, NULL, &timeout))) > 0) {
if (FD_ISSET(read_fd, &readfds)) {
ioctl(read_fd, FIONREAD, &bytes_available);
read(read_fd, buffer, bytes_available);
printf("Parent received: %s\n", buffer);
} else if (n == 0) { // 超时
break;
}
}
close(read_fd);
}
void child_communication(int read_fd, int write_fd) {
char buffer[BUF_SIZE]={'\0'};
fd_set readfds, writefds, exceptfds;
struct timeval timeout;
int maxfd, n;
int bytes_available = 0;
// 从父进程接收数据
FD_ZERO(&readfds);
FD_SET(read_fd, &readfds);
maxfd = read_fd + 1; // 确保maxfd是最大的文件描述符
// 设置超时时间
timeout.tv_sec = 1;
timeout.tv_usec = 0;
// 使用select等待读取数据
while ((n = select(maxfd, &readfds, NULL, &exceptfds, &timeout)) > 0) {
if (FD_ISSET(read_fd, &readfds)) {
ioctl(read_fd, FIONREAD, &bytes_available);
read(read_fd, buffer, bytes_available);
printf("[%s: %d]: bytes_available=%d\n", __func__, __LINE__, bytes_available);
printf("Child received: %s\n", buffer);
} else if (n == 0) { // 超时
break;
}
}
// 向父进程发送数据
write(write_fd, CHILD_WRITE, strlen(CHILD_WRITE));
close(read_fd);
close(write_fd);
}
int main() {
int pipefd1[2]; // 父进程 -> 子进程
int pipefd2[2]; // 父进程 <- 子进程
int status;
// 创建两个管道
if (pipe(pipefd1) < 0 || pipe(pipefd2) < 0) {
perror("pipe");
exit(1);
}
pid_t pid = fork();
if (pid == 0) { // 子进程
child_communication(pipefd1[0], pipefd2[1]);
} else if (pid > 0) { // 父进程
parent_communication(pipefd2[0], pipefd1[1]);
wait(&status); // 等待子进程结束
} else {
perror("fork");
exit(1);
}
return 0;
}
实验1结果:
eon@ubuntu:~/code/test/test_process$ gcc -o test_pipe test_pipe.c
eon@ubuntu:~/code/test/test_process$ ./test_pipe
[child_communication: 68]: bytes_available=17
Child received: Hello from parent
Parent received: Child response
实验解释:
通过pipe
函数创建了两个管道 pipefd1 和 pipefd2,分别用于父进程向子进程发送消息和子进程向父进程发送消息,并且使用 select
调用监控文件描述符的状态,等待它们变得可读、可写或有错误。使用 ioctl
配合 FIONREAD
来获取管道中的数据量。
实验2:命名管道(Named Pipe,也称为FIFO)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <string.h>
#include <fcntl.h> // 包含fcntl.h以获取open函数和O_WRONLY宏的定义
#include <sys/stat.h> // 包含这个头文件以获取mkfifo函数的声明
#define FIFO_FILE "/tmp/myfifo"
void child_write_to_fifo(const char *fifo_path) {
int fd = open(fifo_path, O_WRONLY);
if (fd < 0) {
perror("open");
exit(1);
}
char message[] = "Hello, reader!\n";
write(fd, message, strlen(message));
close(fd);
}
void child_read_from_fifo(const char *fifo_path) {
int fd = open(fifo_path, O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
char buffer[20];
fd_set readfds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 5;
timeout.tv_usec = 0;
while (1) {
// 使用select等待读取数据
if (select(fd + 1, &readfds, NULL, NULL, &timeout) > 0) {
if (FD_ISSET(fd, &readfds)) {
read(fd, buffer, sizeof(buffer));
write(1, buffer, strlen(buffer));
break; // 读取到数据后退出循环
}
}
}
close(fd);
}
int main() {
pid_t pid1, pid2;
// 创建命名管道(FIFO)
if (mkfifo(FIFO_FILE, 0666) < 0) {
perror("mkfifo");
exit(1);
}
pid1 = fork();
if (pid1 == 0) { // 子进程1 - 写入命名管道
child_write_to_fifo(FIFO_FILE);
} else if (pid1 > 0) { // 父进程
pid2 = fork();
if (pid2 == 0) { // 子进程2 - 读取命名管道
child_read_from_fifo(FIFO_FILE);
} else { // 父进程等待子进程结束
wait(NULL);
}
}
// 删除命名管道文件
unlink(FIFO_FILE);
return 0;
}
实验2结果:
eon@ubuntu:~/code/test/test_process$ gcc -o test_fifo test_fifo.c
eon@ubuntu:~/code/test/test_process$ ./test_fifo
Hello, reader!
eon@ubuntu:~/code/test/test_process$
实验解释:
使用mkfifo
创建命名管道用于两个子进程间的通信,最后使用unlink
删除该命名管道。