父子进程传输文件描述符,通过管道通信实现。
1. 流程
1.1. C/S 接入流程
- 父子进程通过
socketpair
创建相互通信管道。 - 客户端连接到服务主进程。
- 主进程 accept 客户端连接文件描述符 fd1。
- 主进程将 fd1 通过管道
sendmsg
传输给子进程。 - 子进程通过
recvmsg
接收主进程发送的 fd1。注意,这时候经过发送后的客户端连接文件描述符 fd1 已经发生改变,变成 fd2 了。 - 文件描述符传输完毕,客户端与子进程成功建立连接 fd2。
- 主进程关闭旧的 fd1。
1.2. 优缺点
- 优点:多进程方便调度,无锁,减少 accept 资源竞争。
- 缺点:单进程单线程接入,肯定没有多进程(多线程) accept 强大。(尽管接入操作不是很耗费资源。)
2. 源码分析
2.1. 原理
详细原理请参考 linux网络编程之socket(十六):通过UNIX域套接字传递描述符和 sendmsg/recvmsg 函数。
2.2. nginx
sendmsg 和 recvmsg 具体功能实现可以参考 nginx 源码:ngx_channel。
1
2
3
4
/* 读数据。 */
ngx_int_t ngx_read_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size, ngx_log_t *log);
/* 传数据。 */
ngx_int_t ngx_write_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size, ngx_log_t *log);
3. 源码实现
文件描述符传输逻辑,详细源码实现可以参考 kimserver。
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
56
57
58
59
/* 传输数据结构。 */
typedef struct channel_s {
int fd;
int family;
int codec;
} channel_t;
/* 文件描述符发送和接收函数 */
int write_channel(int fd, channel_t* ch, size_t size, Log* logger = nullptr);
int read_channel(int fd, channel_t* ch, size_t size, Log* logger = nullptr);
/* 父子进程创建管道进行相互通信。 */
bool Manager::create_worker(int worker_index) {
int pid, data_fds[2];
...
/* 创建管道。 */
if (socketpair(PF_UNIX, SOCK_STREAM, 0, data_fds) < 0) {
...
}
if ((pid = fork()) == 0) {
/* 将管道描述符 data_fds[1] 传给子进程。 */
} else if (pid > 0) {
/* 将管道描述符 data_fds[0] 传给父进程。 */
}
...
}
/* 发送文件描述符:主进程 accept 客户端的文件描述符,然后将其传输给子进程。 */
void Network::accept_and_transfer_fd(int fd) {
int cport, cfd, family;
char cip[NET_IP_STR_LEN] = {0};
/* 主进程 accept 客户端的接入文件描述符。*/
cfd = anet_tcp_accept(m_errstr, fd, cip, sizeof(cip), &cport, &family);
...
/* 父进程发送客户端的 cfd 到子进程。 */
int chanel_fd = m_woker_data_mgr->get_next_worker_data_fd();
if (chanel_fd > 0) {
LOG_DEBUG("send client fd: %d to worker through chanel fd %d", cfd, chanel_fd);
/* 数据填充发送结构体。 */
channel_t ch = {cfd, family, static_cast<int>(m_gate_codec)};
int err = write_channel(chanel_fd, &ch, sizeof(channel_t), m_logger);
...
}
...
}
/* 子进程接收文件描述符 */
void Network::read_transfer_fd(int fd) {
channel_t ch;
...
while (max--) {
/* 子进程接收父进程发送的客户端文件描述符。*/
err = read_channel(fd, &ch, sizeof(channel_t), m_logger);
...
}
...
}
4. 参考
- 《UNIX 网络编程卷1》 - 15.7 描述符传递。
- linux网络编程之socket(十六):通过UNIX域套接字传递描述符和 sendmsg/recvmsg 函数
- 通过UNIX域套接字传递文件描述符