我们的主要目标:

将不同进程的标准输入输出来进行通信

核心函数和方法

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 实现父子进程通信的步骤如下:

  • 管道创建:首先创建两条管道 father2sonson2father,然后 fork 子进程。
  • 绑定管道(子进程):子进程通过 dup2father2son 的读端绑定到 stdin,将 son2father 的写端绑定到 stdout,随后关闭所有管道 fd(已重定向,不再需要)。
  • 绑定管道(父进程):父进程关闭不需要的端:father2son[0](不读)和 son2father[1](不写)。
  • 数据发送:父进程通过 father2son[1] 写入数据,写完后关闭写端,触发 EOF。
  • 数据接受:子进程使用 getlinestdin 读取数据,并通过 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/coutscanf/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 文件进行通信。

使用步骤:

  1. 创建 socket
  2. 绑定到文件路径(bind
  3. 监听连接(listen,适用于 SOCK_STREAM
  4. 接受连接(accept,客户端用 connect
  5. 读写数据(read/writecin/cout 重定向)
  6. 关闭 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,返回调用结果给客户端代码。