谈谈 Linux 的进程间通信

管道

  管道可以说是 Unix 系统最古老的 IPC 方式了。管道最常见的一个用法,就是将一个进程的输出导入到另一个进程的输入。譬如说,当用户执行下面的命令,ls命令的输出将会作为wc -l的输入:

1
$ ls | wc -l

  在 Linux 系统中,管道有这些特征:

  • 数据以字节流的形式在管道中传输,也就是说,数据是没有消息边界。
  • 数据在管道中是单向流动的,管道的一端负责写数据,而另一端负责读数据。
  • 管道的缓冲区长度是 64 KB。
  • 调用write()写入数据时,如果指定写入的字节数不超过PIPE_BUF(默认值是 4KB),那么 Linux 会保证这个写入操作是原子的。

  下面的例子中,父进程向子进程发送多条消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <errno.h>
#define MAXLINE 1024
void readAndPrint(int fd)
{
char line[MAXLINE];
while (true) {
ssize_t n = read(fd, line, MAXLINE);
if (n == 0) {
break;
}
if (n == -1) {
if (errno == EINTR) {
continue;
}
perror("read");
}
write(STDOUT_FILENO, line, n);
}
}
int main()
{
// 创建管道, fd[0] 为读端,fd[1] 为写端
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid > 0) {
// 父进程不需要读数据,所以关闭读端
close(fd[0]);
write(fd[1], "first message\n", 14);
write(fd[1], "second message\n", 15);
// 父进程关闭写端,导致子进程 read() 返回 0
close(fd[1]);
} else if (pid == 0) {
// 子进程不需要写数据,所以关闭写端
close(fd[1]);
readAndPrint(fd[0]);
close(fd[0]);
} else {
perror("fork");
}
return 0;
}

eventfd() 与事件通知

  Linux 提供了eventfd(),作为一种事件通知方式,它可以和 select/poll/epoll 结合使用。调用eventfd()的时候,内核会返回一个文件描述符,并创建一个 eventfd 对象,这个对象中包含一个uint64类型的 counter。作为一种事件通知方式来说,与管道相比,eventfd()的开销更小,因为eventfd()只需要一个文件描述符。
  eventfd()的一个使用场景就是,通知 epoll 事件循环的退出,就如下面的例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <unistd.h>
#include <string.h>
#include <sys/eventfd.h>
#include <sys/epoll.h>
#include <chrono>
#include <iostream>
#include <thread>
int main()
{
constexpr int MAX_EVENTS = 64;
int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
int event_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
struct epoll_event event;
memset(&event, 0, sizeof(event));
event.events = EPOLLIN;
event.data.fd = event_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, event_fd, &event);
std::thread sender([event_fd] ()
{
std::this_thread::sleep_for(std::chrono::seconds(10));
// 往 eventfd 中写入一个非零的 uint64 类型的值
uint64_t one = 1;
write(event_fd, &one, sizeof(one));
});
sender.detach();
struct epoll_event events[MAX_EVENTS];
while (true)
{
int n = epoll_wait(epoll_fd, &events[0], MAX_EVENTS, 0);
for (int i = 0; i < n; ++i)
{
if (events[i].data.fd == event_fd)
{
// 从 eventfd 中读取一个 uint64 类型的值
uint64_t one;
read(event_fd, &one, sizeof(one));
std::cout << "Program quit!" << std::endl;
return 0;
}
// ...
}
}
return 0;
}

参考资料