######################################## 进程间通信 ######################################## 在这之前先介绍一下过滤程序和协同进程 - 过滤程序:从标准输入和标准输出中读写数据 - 协同进程:当两个过滤进程互为数据输入、输出时,则两者为协同进程 管道 **************************************** 管道要求使用的进程之间具有拥有同一祖先,一般是半双工的。管道有两套 API 用于访问 .. code-block:: c 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。这样的一个操作为原子操作 例如子进程向父进程发送消息: .. code-block:: c #include #include #include #include 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 则会自动创建进程并连接到一个端口: .. code-block:: c 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 信息: .. code-block:: c #include #include #include 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; } .. note:: 在 cmdstring 中是允许包含通道符的 eventfd **************************************** 当 pipe 只是简单地作为一种信号机制时,eventfd 提供了性能更好,使用更方便的机制。eventfd 底层使用了一个计数器。在计数器等于零时读或者超过 uint64_t 的最大值时写会导致阻塞 无论是读还是写,eventfd 可以操纵的数据类型只有 uint64_t,其余数据类型是不可用的。对 eventfd 的连续写会导致计数器累加,而当EFD_SEMAPHORE 标志没有设置时,每次读会导致将计数器中的计数全部读出,否者每次读减一。 例如: .. code-block:: cpp #include #include #include 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 **************************************** Netlink是linux提供的用于内核和用户态进程之间的通信方式。