进程间通信

在这之前先介绍一下过滤程序和协同进程

  • 过滤程序:从标准输入和标准输出中读写数据

  • 协同进程:当两个过滤进程互为数据输入、输出时,则两者为协同进程

管道

管道要求使用的进程之间具有拥有同一祖先,一般是半双工的。管道有两套 API 用于访问

int pipe(int fd[2]);
int pipe2(int pipefd[2], int flags);

fd 为输入参数

当 flags == 0 时,pipe2 和 pipe 相同,flags 的取值为:

含义

O_CLOEXEC

在 pipefd 中的两个文件描述符上设置 FD_CLOEXEC 标志

O_DIRECT 创建的文件描述符以

packet 模式形成

O_NONBLOCK

在文件描述符上追加 O_NONBLOCK 标志

成功时返回 0,失败时返回 -1

经过 pipe 后,fd 包含两个文件描述符,第一个用于读,第二个用于写。在经由 fork 后,fd 被复制到子进程中,然后双方关闭不需要的一个文件描述符就可以用于通信。

  • 管道支持多进程读写,但是读写都会加锁,因此同一时刻只有一个进程访问管道

  • 管道被读取后,如果管道内还有残留数据,则直接丢弃

  • 如果写一个读端关闭的管道,则产生信号 SIGPIPE。如果忽略此信号,则 write 返回 -1,errno 设为 EPIPE

  • 写管道时,要求写入的字节数小于 PIPE_BUF。这样的一个操作为原子操作

例如子进程向父进程发送消息:

#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
    int fd[2];
    pipe(fd);
    if(fork() != 0) {
        // 子进程
        close(fd[0]);
        FILE* w = fdopen(fd[1], "w");
        fputs("hello, world", w);
        fclose(w);
        close(fd[1]);
        return 0;
    }
    close(fd[1]);
    FILE* r = fdopen(fd[0], "r");
    char  buf[255];
    while(fgets(buf, 255, r) != NULL) { puts(buf); }
    fclose(r);
    close(fd[0]);
    return 0;
}

由于需要调用 fdopen 将文件描述符转为 FILE 指针,因此略显繁琐。另外子进程别忘了退出。

另一种 API 则会自动创建进程并连接到一个端口:

FILE* popen(const char* cmdstring, const char* type);
int pclose(FILE* fp);

type 的值为 r/w,意为此进程可读/写

popen 首先 fork 一个子进程,然后根据 type 关闭对应的管道,然后调用 exec 执行 cmdstring。执行 cmdstring 的过程相当于 sh -c cmdstring 。因此不要将 cmdstring 暴露给用户以防止被提权

例如开子线程获取 cpu 信息:

#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
    FILE* rd = popen("cat /proc/cpuinfo", "r");
    if(!rd) {
        fputs("error", stderr);
        return -1;
    }
    char buf[255];
    while(fgets(buf, 255, rd) != NULL) {
        fputs(buf, stdout);
        fflush(stdout);
    }
    pclose(rd);
    return 0;
}

在 cmdstring 中是允许包含通道符的

eventfd

当 pipe 只是简单地作为一种信号机制时,eventfd 提供了性能更好,使用更方便的机制。eventfd 底层使用了一个计数器。在计数器等于零时读或者超过 uint64_t 的最大值时写会导致阻塞

无论是读还是写,eventfd 可以操纵的数据类型只有 uint64_t,其余数据类型是不可用的。对 eventfd 的连续写会导致计数器累加,而当EFD_SEMAPHORE 标志没有设置时,每次读会导致将计数器中的计数全部读出,否者每次读减一。

例如:

#include <sys/eventfd.h>
#include <iostream>
#include <thread>

int main(int argc, char* argv[]) {
    int      efd = eventfd(0, 0);
    uint64_t a   = 1;
    // write(efd, &a, sizeof(int));
    if(!fork()) {
        sleep(2);
        for(; a < 100; ++a)
            ;
        write(efd, &a, sizeof(uint64_t));
    } else {
        ssize_t ret = read(efd, &a, sizeof(uint64_t));
        if(ret != sizeof(uint64_t)) fprintf(stderr, "errno: %d, ret: %l", errno, ret);
        std::cout << a;
    }
    return 0;
}

eventfd 提供了三个标志:

标志

作用

EFD_CLOEXEC

fork 时子进程不继承

EFD_NONBLOCK

非阻塞

EFD_SEMAPHORE

更改读操作的行为

另外,由于写操作只会在超过最大值时阻塞(基本上不可能),因此在与 epoll 配合时只需要监控可读事件。

Netlink是linux提供的用于内核和用户态进程之间的通信方式。

Last moify: 2022-12-04 15:11:33
Build time:2025-07-18 09:41:42
Powered By asphinx