我们的主要目标:
将不同进程的标准输入输出来进行通信
核心函数和方法
dup2
dup2 将文件描述符 oldfd 复制到 newfd 上,复制后,oldfd 和 newfd 指向同一个内核文件表项,可以共享缓冲和状态,但引用计数独立。
常用于将 pipe 或 socket fd 重定向到标准输入(STDIN_FILENO)或标准输出(STDOUT_FILENO),以便通过 stdio 函数进行 IPC。
函数签名:
int dup2(int oldfd, int newfd);
注:如果 newfd 已经被占用,会先关闭它。
pipe 方案
效果
进程A 执行 cout << “message” << endl ,进程B执行 cin >> msg ,结果是 msg = “message”;
特点
匿名管道,仅限亲缘进程间通信,随进程结束而销毁,无需手动清理
pipe函数
调用 pipe(fd) 会在内核创建一个单向缓冲区,返回两个 fd,其中 fd[0] 用于读取,fd[1] 用于写入。函数签名如下:
int pipe(int pipefd[2]);
缓冲区
对于缓冲区而言,本身只有一端用于写、一端用于读,我们可以将fd(文件描述符) 绑定到端上,一个端可以绑定多个fd。
一个写端对应多个 fd 时,所有 fd 共享同一缓冲区,写入数据均进入该缓冲区。
关闭任一 fd 仅减少引用计数,只有当最后一个写端 fd 关闭时,读端才会收到 EOF。因此,绑定读写端后应关闭多余的 fd。
实例
这里例子的效果是两个进程互相用stdio通信,所以需要两个管道,为了演示,将一个进程的输出改为了标准输出。
使用 pipe 实现父子进程通信的步骤如下:
- 管道创建:首先创建两条管道
father2son和son2father,然后fork子进程。 - 绑定管道(子进程):子进程通过
dup2将father2son的读端绑定到stdin,将son2father的写端绑定到stdout,随后关闭所有管道 fd(已重定向,不再需要)。 - 绑定管道(父进程):父进程关闭不需要的端:
father2son[0](不读)和son2father[1](不写)。 - 数据发送:父进程通过
father2son[1]写入数据,写完后关闭写端,触发 EOF。 - 数据接受:子进程使用
getline从stdin读取数据,并通过stdout将回复写回父进程。父进程从son2father[0]读取子进程的输出,最后等待子进程结束。
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main() {
int father2son[2]; // 父进程 -> 子进程
int son2father[2]; // 子进程 -> 父进程
pipe(father2son);
pipe(son2father);
pid_t pid = fork();
if (pid == 0) { // 子进程
// 重定向标准输入和输出
dup2(father2son[0], STDIN_FILENO); // 从 father2son 读
dup2(son2father[1], STDOUT_FILENO); // 向 son2father 写
// 关闭所有管道 fd(已重定向,不再需要)
close(father2son[0]);
close(father2son[1]);
close(son2father[0]);
close(son2father[1]);
std::string msg;
std::getline(std::cin, msg);
std::cout << "Child received: " << msg << std::endl;
std::cout.flush();
return 0;
} else { // 父进程
// 关闭不需要的端
close(father2son[0]); // 不读 father2son
close(son2father[1]); // 不写 son2father
// 向子进程发送消息
std::string msg = "Hello from parent via pipe";
write(father2son[1], msg.c_str(), msg.size());
write(father2son[1], "\\n", 1);
close(father2son[1]); // 发送完毕,关闭写端
// 读取子进程回复
char buf[256];
int n = read(son2father[0], buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\\0';
std::cout << "Parent received: " << buf;
}
close(son2father[0]);
wait(nullptr); // 等待子进程结束
}
return 0;
}
运行结果:
Parent received: Child received: Hello from parent via pipe
FIFO 方案
效果:
进程 A 执行 cout << "message" << endl,进程 B 执行 cin >> msg,结果为 msg = "message"
特点:
- 命名管道,以文件形式存在于文件系统
- 生命周期独立于进程,创建后持续存在,可供任意进程多次打开使用
- 使用结束后需手动
unlink删除文件
简介
FIFO,也称为命名管道,是一种可以在文件系统中存在的管道。它和 pipe 类似,也是字节流通信,但可以被不同进程通过文件路径访问,实现跨进程通信。FIFO 需要使用 mkfifo(path, mode) 创建,之后通过 open、read、write 或 stdio 函数操作。
mkfifo
int mkfifo(const char *pathname, mode_t mode);
mkfifo 用于在文件系统中创建一个命名管道(FIFO)。pathname 指定 FIFO 文件路径,mode 指定权限。成功返回 0,失败返回 -1 并设置 errno。FIFO 允许多个进程通过文件路径打开并进行读写,实现跨进程通信。
实例
在使用 FIFO 进行父子或多进程通信时,需要注意打开方式和阻塞行为。fifo实际上和pipe其他都类似,不同于 FIFO 的生命周期不依赖于进程,只要文件存在就可以被新进程打开,适合长期或跨进程通信。
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
int main() {
const char* fifo_path = "/tmp/myfifo";
mkfifo(fifo_path, 0666); // 创建命名管道(FIFO)
pid_t pid = fork();
if (pid == 0) { // 子进程:只读
// 以只读方式打开 FIFO
int fd = open(fifo_path, O_RDONLY);
dup2(fd, STDIN_FILENO); // 将 FIFO 读端重定向到标准输入
close(fd); // 关闭原 fd(已重定向,不再需要)
std::string msg;
std::getline(std::cin, msg); // 从标准输入(即 FIFO)读取
std::cout << "Child received: " << msg << std::endl;
std::cout.flush();
return 0;
} else { // 父进程:只写
// 以只写方式打开 FIFO
int fd = open(fifo_path, O_WRONLY);
dup2(fd, STDOUT_FILENO); // 将 FIFO 写端重定向到标准输出
close(fd); // 关闭原 fd
std::string msg = "Hello via FIFO";
write(STDOUT_FILENO, msg.c_str(), msg.size()); // 写入标准输出(即 FIFO)
write(STDOUT_FILENO, "\\n", 1);
close(STDOUT_FILENO); // 关闭标准输出,触发 EOF
wait(nullptr); // 等待子进程结束
}
unlink(fifo_path); // 删除 FIFO 文件
return 0;
}
输出结果:
Child received: Hello via FIFO
socket-pair 方案
效果:
进程 A 执行 cout << "message" << endl,进程 B 执行 cin >> msg,结果为 msg = "message"
特点:
- 双向通信,单条通道即可实现数据收发
- 仅限亲缘进程间通信(如父子进程)
- 随进程结束而销毁,无需手动清理
- 支持流式数据传输(
SOCK_STREAM)和数据报模式(SOCK_DGRAM)
简介
socketpair 是一种基于 UNIX domain 的套接字通信方式,可以在父子或任意两进程之间实现全双工通信。与 pipe 不同,socketpair 本身就是双向的,因此不需要创建两条通道即可完成双向 stdio 通信。通过 dup2 可以将 socket fd 绑定到标准输入输出,从而使用 cin/cout 或 scanf/printf 进行通信。
socketpair 函数
int socketpair(int domain, int type, int protocol, int sv[2]);
socketpair 用于创建一对互相连接的套接字,domain 通常使用 AF_UNIX 表示本地进程通信,type 一般为 SOCK_STREAM 表示流式套接字,protocol 通常填 0,sv 是返回的两个文件描述符数组。成功返回 0,失败返回 -1 并设置 errno。这两个 fd 可以通过 dup2 重定向到 stdio,实现父子进程的双向通信。
实例
父子进程通过 socketpair 进行通信时,需要注意关闭多余 fd 并处理 stdio 缓冲问题。
不同点在于不用考虑缓冲区的读写端,两边都可读写。
#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main() {
int sv[2];
// 创建双向通信的 socketpair(AF_UNIX 域,SOCK_STREAM 类型)
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
pid_t pid = fork();
if (pid == 0) { // 子进程
// 将 sv[0] 重定向到标准输入和输出,实现双向通信
dup2(sv[0], STDIN_FILENO);
dup2(sv[0], STDOUT_FILENO);
// 关闭原套接字 fd(已重定向,不再需要)
close(sv[0]);
close(sv[1]);
// 从标准输入(即 sv[0])读取父进程消息
std::string msg;
std::getline(std::cin, msg);
// 通过标准输出(即 sv[0])将回复写回父进程
std::cout << "Child received: " << msg << std::endl;
std::cout.flush();
return 0;
} else { // 父进程
// 关闭 sv[0],只保留 sv[1] 与子进程通信
close(sv[0]);
// 向子进程发送消息(通过 sv[1] 写入,子进程从 sv[0] 读取)
std::string msg = "Hello from parent via socketpair";
write(sv[1], msg.c_str(), msg.size());
write(sv[1], "\\n", 1);
// 尝试从 sv[1] 读取子进程回复
// 注意:这里会读到子进程通过 stdout 写入的数据(因为子进程将 stdout 重定向到 sv[0])
char buf[256];
int n = read(sv[1], buf, sizeof(buf)-1);
if (n > 0) {
buf[n] = '\\0';
std::cout << "Parent received: " << buf;
}
close(sv[1]);
wait(nullptr); // 等待子进程结束
}
return 0;
}
socketpair 的特点是全双工、无需双管道,适合父子或任意两进程之间双向通信。
Unix Domain Socket 方案
效果
进程 A 执行 cout << "message" << endl,进程 B 执行 cin >> msg,结果为 msg = "message"
特点
- 支持全双工通信(可双向收发数据)
- 可以用于任意两个进程之间通信,不必是亲缘进程
- 通过文件系统创建 socket 文件,实现命名访问
- 生命周期独立于进程,创建后可由多个进程打开,使用完成后需手动
unlink删除 - 支持流式通信(
SOCK_STREAM)和数据报通信(SOCK_DGRAM)
简介
Unix Domain Socket(本地套接字)是一种在同一台机器上的进程间通信方式。它可以通过文件系统路径命名,允许任意两个进程打开同一个 socket 文件进行通信。
使用步骤:
- 创建 socket
- 绑定到文件路径(
bind) - 监听连接(
listen,适用于SOCK_STREAM) - 接受连接(
accept,客户端用connect) - 读写数据(
read/write或cin/cout重定向) - 关闭 socket,必要时删除文件
Unix Domain Socket 函数
int socket(int domain, int type, int protocol);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
int shutdown(int sockfd, int how);
domain通常使用AF_UNIX表示本地通信type可以为SOCK_STREAM(面向流)或SOCK_DGRAM(数据报)bind将 socket 文件与进程关联listen/accept用于服务器端处理连接connect用于客户端连接服务器shutdown用户关闭连接
实例
父子进程通过 Unix Domain Socket 进行通信:
- 父进程创建 listen_fd,开始监听
- fork() 创建子进程(此时连接还未建立)
- 子进程:创建 client_fd,connect() 到 addr
- 父进程:accept() 接受连接,得到 conn_fd
- 连接建立完成,父子进程通过这个连接通信
#include <iostream>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main() {
const char* socket_path = "/tmp/my_unix_socket";
int listen_fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr{};
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, socket_path);
unlink(socket_path);
bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(listen_fd, 1);
pid_t pid = fork();
if (pid == 0) { // 子进程:接收端
int client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
connect(client_fd, (struct sockaddr*)&addr, sizeof(addr));
// 关闭写端,只能读取
shutdown(client_fd, SHUT_WR);
dup2(client_fd, STDIN_FILENO);
close(client_fd);
std::string msg;
std::getline(std::cin, msg);
std::cout << "Child received: " << msg << std::endl;
return 0;
} else { // 父进程:发送端
int conn_fd = accept(listen_fd, nullptr, nullptr);
// 关闭读端,只能写入
shutdown(conn_fd, SHUT_RD);
dup2(conn_fd, STDOUT_FILENO);
close(conn_fd);
close(listen_fd);
std::string msg = "Hello from parent (one-way communication)";
std::cout << msg << std::endl;
std::cout.flush();
wait(nullptr);
unlink(socket_path);
}
return 0;
}
运行结果:
Child received: Hello from parent (one-way communication)
安卓 Binder 机制
属于题外话,因为binder机制不使用stdio而是一套独立的框架,可以做到跨进程互相调用各自的方法。
定义接口
- 编写
.aidl文件,定义可跨进程调用的方法。
服务端实现
- 编译 AIDL 生成
Stub类。 - 服务端继承 Stub,实现接口方法。
- 将 Stub 注册到系统(ServiceManager 或 bindService)。
客户端获取代理
- 客户端通过 ServiceManager 或 bindService 获取 Binder 对象。
- 调用
asInterface()得到 Proxy 对象(如果跨进程),否则直接得到 Stub。
客户端调用方法
- 调用 Proxy 方法 → 内部把方法参数写入 Parcel。
- Proxy 调用
transact(),通过 Binder 内核驱动发送 Parcel。
Binder 内核转发
- 内核根据 Binder 对象句柄找到服务端进程。
- 调用服务端 Stub 的
onTransact(),把 Parcel 数据交给它。
服务端处理请求
- Stub
onTransact()从 Parcel 读取参数。 - 调用服务端实际方法。
- 把返回值写入返回 Parcel。
返回客户端
- Binder 内核把返回 Parcel 发回客户端 Proxy。
- Proxy 解析 Parcel,返回调用结果给客户端代码。