【管道 】
【管道】
- 目录:
- 进程间通信(IPC)概览
- 匿名管道(PIPE)
- 基本逻辑
- 函数接口
- 管道的读写特性
- 管道的阻塞特性
- 注意事项
- 代码示例
- 运行示例
- 相关知识点
- 管道的父进程 子进程
- 管道的创建
- 父进程和子进程的使用模式
- 单向通信
- 双向通信
- 资源管理
目录:
进程间通信(IPC)概览
进程间通信(Inter - Process Communication,IPC)是指在不同进程之间传播或交换信息的机制。在 Linux 系统中,常见的 IPC 方式有匿名管道(PIPE)、命名管道(FIFO)、消息队列、共享内存、信号量等。不同的 IPC 方式适用于不同的场景,例如,匿名管道适用于具有亲缘关系的进程间通信,而命名管道可以用于无亲缘关系的进程间通信。
匿名管道(PIPE)
基本逻辑
匿名管道是一种半双工的通信方式,数据只能在一个方向上流动,即一端用于写入数据,另一端用于读取数据。匿名管道只能用于具有亲缘关系的进程(如父子进程)之间的通信。在创建管道时,系统会为管道分配两个文件描述符,一个用于读操作(fd[0]
),另一个用于写操作(fd[1]
)。
函数接口
在 Linux 中,使用 pipe
函数来创建匿名管道,其原型如下:
#include <unistd.h>
int pipe(int fd[2]);
- 参数:
fd
是一个包含两个整数的数组,fd[0]
用于读取管道中的数据,fd[1]
用于向管道中写入数据。 - 返回值:成功时返回 0,失败时返回 -1,并设置相应的错误码。
管道的读写特性
- 写入操作:向管道中写入数据时,数据会被追加到管道的末尾。如果管道已满,写入操作会被阻塞,直到管道中有足够的空间。
- 读取操作:从管道中读取数据时,数据会从管道的头部开始被读取。如果管道为空,读取操作会被阻塞,直到有数据被写入管道。
管道的阻塞特性
- 写入阻塞:当管道已满(默认情况下管道的缓冲区大小为 65536 字节),写入进程会被阻塞,直到有其他进程从管道中读取数据,释放出足够的空间。
- 读取阻塞:当管道为空时,读取进程会被阻塞,直到有其他进程向管道中写入数据。如果所有的写端文件描述符都已关闭,读取操作将不会阻塞,而是返回 0,表示已经读取到文件末尾。
注意事项
- 半双工通信:匿名管道是半双工的,数据只能在一个方向上流动。如果需要双向通信,需要创建两个管道。
- 亲缘关系:匿名管道只能用于具有亲缘关系的进程之间的通信,通常是父子进程。
- 文件描述符关闭:在使用完管道后,需要及时关闭不再使用的文件描述符,避免资源泄漏。
代码示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd[2];
pid_t pid;
char buffer[BUFFER_SIZE];
// 创建管道
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
// 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
close(fd[0]); // 关闭读端
const char *message = "Hello, parent process!";
// 向管道中写入数据
if (write(fd[1], message, strlen(message)) == -1) {
perror("write");
return 1;
}
close(fd[1]); // 关闭写端
exit(0);
} else {
// 父进程
close(fd[1]); // 关闭写端
// 从管道中读取数据
ssize_t bytes_read = read(fd[0], buffer, BUFFER_SIZE);
if (bytes_read == -1) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("父进程收到消息: %s\n", buffer);
close(fd[0]); // 关闭读端
}
return 0;
}
运行示例
shaseng@ubuntu:~$ gcc -o pipe_example pipe_example.c
shaseng@ubuntu:~$ ./pipe_example
父进程收到消息: Hello, parent process!
相关知识点
- 文件描述符:在 Linux 系统中,文件描述符是一个非负整数,用于标识打开的文件、管道、套接字等。
pipe
函数返回的两个文件描述符分别用于管道的读和写操作。- fork 函数:
fork
函数用于创建一个新的进程,新进程是原进程的子进程。在使用匿名管道进行通信时,通常先创建管道,然后使用fork
函数创建子进程,父子进程通过管道进行通信。- 读写操作:
write
函数用于向文件描述符中写入数据,read
函数用于从文件描述符中读取数据。在管道通信中,通过write
函数向管道的写端写入数据,通过read
函数从管道的读端读取数据。
管道的父进程 子进程
在使用匿名管道进行进程间通信时,父进程和子进程扮演着不同的角色,下面从创建、使用和资源管理等方面详细介绍管道中父进程和子进程的相关内容。
管道的创建
在使用匿名管道进行进程间通信时,通常是由父进程先创建管道,再创建子进程。这是因为匿名管道只能在具有亲缘关系的进程(如父子进程)间使用,且管道的文件描述符是通过 pipe
函数创建的。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int fd[2];
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
// 后续可创建子进程
return 0;
}
在上述代码中,父进程调用 pipe
函数创建了一个管道,fd[0]
用于读,fd[1]
用于写。
父进程和子进程的使用模式
单向通信
- 父进程写,子进程读
父进程关闭读端(fd[0]
),向写端(fd[1]
)写入数据;子进程关闭写端(fd[1]
),从读端(fd[0]
)读取数据。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd[2];
pid_t pid;
char buffer[BUFFER_SIZE];
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
close(fd[1]); // 关闭写端
ssize_t bytes_read = read(fd[0], buffer, BUFFER_SIZE);
if (bytes_read == -1) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("子进程收到消息: %s\n", buffer);
close(fd[0]);
} else {
// 父进程
close(fd[0]); // 关闭读端
const char *message = "Hello, child process!";
if (write(fd[1], message, strlen(message)) == -1) {
perror("write");
return 1;
}
close(fd[1]);
}
return 0;
}
- 子进程写,父进程读
子进程关闭读端(fd[0]
),向写端(fd[1]
)写入数据;父进程关闭写端(fd[1]
),从读端(fd[0]
)读取数据。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd[2];
pid_t pid;
char buffer[BUFFER_SIZE];
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
close(fd[0]); // 关闭读端
const char *message = "Hello, parent process!";
if (write(fd[1], message, strlen(message)) == -1) {
perror("write");
return 1;
}
close(fd[1]);
} else {
// 父进程
close(fd[1]); // 关闭写端
ssize_t bytes_read = read(fd[0], buffer, BUFFER_SIZE);
if (bytes_read == -1) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("父进程收到消息: %s\n", buffer);
close(fd[0]);
}
return 0;
}
双向通信
若要实现父子进程的双向通信,需要创建两个管道,一个用于父进程向子进程发送数据,另一个用于子进程向父进程发送数据。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd1[2], fd2[2];
pid_t pid;
char buffer[BUFFER_SIZE];
if (pipe(fd1) == -1 || pipe(fd2) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程
close(fd1[1]); // 关闭父->子管道的写端
close(fd2[0]); // 关闭子->父管道的读端
// 从父进程读取数据
ssize_t bytes_read = read(fd1[0], buffer, BUFFER_SIZE);
if (bytes_read == -1) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("子进程收到父进程消息: %s\n", buffer);
// 向父进程发送数据
const char *message = "Hello back, parent!";
if (write(fd2[1], message, strlen(message)) == -1) {
perror("write");
return 1;
}
close(fd1[0]);
close(fd2[1]);
} else {
// 父进程
close(fd1[0]); // 关闭父->子管道的读端
close(fd2[1]); // 关闭子->父管道的写端
// 向子进程发送数据
const char *message = "Hello, child!";
if (write(fd1[1], message, strlen(message)) == -1) {
perror("write");
return 1;
}
// 从子进程读取数据
ssize_t bytes_read = read(fd2[0], buffer, BUFFER_SIZE);
if (bytes_read == -1) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("父进程收到子进程消息: %s\n", buffer);
close(fd1[1]);
close(fd2[0]);
}
return 0;
}
资源管理
在使用完管道后,父子进程都需要及时关闭不再使用的文件描述符,以避免资源泄漏。例如,在上述代码中,子进程和父进程在完成读写操作后,都会调用 close
函数关闭相应的文件描述符。
此外,父进程还需要负责回收子进程的资源,避免产生僵尸进程。可以通过调用 wait
或 waitpid
函数来实现。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main() {
int fd[2];
pid_t pid;
if (pipe(fd) == -1) {
perror("pipe");
return 1;
}
pid = fork();
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
// 子进程操作
// ...
exit(0);
} else {
// 父进程操作
// ...
int status;
wait(&status);
}
return 0;
}
综上所述,在管道通信中,父进程和子进程需要根据通信需求合理使用管道的读写端,并做好资源管理工作。