进程间通信
在这之前先介绍一下过滤程序和协同进程
过滤程序:从标准输入和标准输出中读写数据
协同进程:当两个过滤进程互为数据输入、输出时,则两者为协同进程
管道
管道要求使用的进程之间具有拥有同一祖先,一般是半双工的。管道有两套 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 被复制到子进程中,然后双方关闭不需要的一个文件描述符就可以用于通信。
|
例如子进程向父进程发送消息:
#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
Netlink是linux提供的用于内核和用户态进程之间的通信方式。