在上一篇中,我们看到了nginx共享内存方式的进程间通信。这次我们看下nginx使用套接字的进程间通信方式。
同样的几个问题:
1.什么时候需要使用套接字方式的进程间通信机制呢?
举个栗子:我们知道nginx有master进程和worker进程,那么master进程是如何向worker进程发送消息的呢?worker进程又是如何接收master发送过来的消息呢?答案就是使用套接字。注意的是虽然套接字是双工的,但目前套接字仅用于master进程管理worker进程,而没用于worker发消息给master,或者worker进程间通信(参考:《深入理解Nginx》).
2.nginx的套接字是如何实现的呢?
nginx的套接字是本机套接字,即通过socketpair创建的,不是网络套接字(通过socket创建)。
socketpair:
int socketpair(int family, int type, int protocol, int sockfd[2]);
返回值:成功:0;出错:-1
socketpair仅适用于unix域套接字,因此,family必须为AF_LOCAL,protocol必须为0。type可以为SOCK_STREAM或SOCK_DGRAM,表示使用TCP还是UDP。sockfd[2]是套接字对,套接字是双工的。因此,通常都是父进程fork子进程之前调用socketpair创建套接字对,接着父进程fork子进程,子进程继承套接字对。父进程关闭sockfd[1],子进程关闭sockfd[0]。父进程通过sockfd[0]发送接收消息,子进程通过sockfd[1]发送接收消息。
下面看一个关键函数: ngx_spawn_process
.../os/unix/ngx_process.c:
/* 创建套接字 派生子进程 */
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
{
......
if (respawn != NGX_PROCESS_DETACHED) {
/* Solaris 9 still has no AF_LOCAL */
/* 创建套接字用于master与worker进程间通信 */
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"socketpair() failed while spawning \"%s\"", name);
return NGX_INVALID_PID;
}
ngx_log_debug2(NGX_LOG_DEBUG_CORE, cycle->log, 0,
"channel %d:%d",
ngx_processes[s].channel[0],
ngx_processes[s].channel[1]);
/* 套接字设置为非阻塞 */
if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_nonblocking_n " failed while spawning \"%s\"",
name);
ngx_close_channel(ngx_processes[s].channel, cycle->log);
return NGX_INVALID_PID;
}
......
/* master 创建worker进程 */
pid = fork();
......
我们看到,在ngx_spawn_process函数内,先通过socketpair创建套接字,创建的套接字放在ngx_processes[s].channel内。而ngx_processes是个结构体数组,元素类型是ngx_process_t
.../os/unix/ngx_process.h:
typedef struct {
ngx_pid_t pid;
int status;
ngx_socket_t channel[2]; /* socketpair创建的套接字对,用于master与worker间通信*/
ngx_spawn_proc_pt proc;
void *data;
char *name;
unsigned respawn:1;
unsigned just_spawn:1;
unsigned detached:1;
unsigned exiting:1;
unsigned exited:1;
} ngx_process_t;
socketpair创建的套接字正是放在ngx_process_t结构体的channel成员这。因为nginx一个进程是单线程的,每个进程要处理数以万,以几十万计的连接,因此都非常忙,所以关键一点就是nginx的进程一定不能阻塞!!!一阻塞,nginx效率就会大幅度下降。因此,可以看到在ngx_spawn_process函数内部,创建完套接字后,接着就是通过ngx_nonblocking将套接字设置为非阻塞,然后才可以通过fork()创建worker进程。
3.master如何通过套接字向worker发送消息呢?
上面一节介绍了如何创建套接字,下面讲如何通过套接字来发送消息。介绍之前得先知道nginx目前只是将套接字用于master向worker发送命令,而没用于worker间通信。
有了套接字就相当于有了发送命令的窗口,接下来就是要构造包装命令的工具,以及发送接收命令等相配套的方法了。nginx是通过ngx_channel_t(频道)来包装命令的。master通过频道就可以向worker发送消息。
下面看ngx_channel_t的源码:
.../os/unix/ngx_channel.h
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#ifndef _NGX_CHANNEL_H_INCLUDED_
#define _NGX_CHANNEL_H_INCLUDED_
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_event.h>
/* 频道定义 */
typedef struct {
ngx_uint_t command; /* 传递的命令 */
ngx_pid_t pid; /* 发送命令方的进程ID */
ngx_int_t slot; /* 发送命令方在进程数组ngx_processex中的序号 */
ngx_fd_t fd; /* 套接字句柄 */
} ngx_channel_t;
/* 频道相关的方法 */
ngx_int_t ngx_write_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size,
ngx_log_t *log);
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_add_channel_event(ngx_cycle_t *cycle, ngx_fd_t fd,
ngx_int_t event, ngx_event_handler_pt handler);
void ngx_close_channel(ngx_fd_t *fd, ngx_log_t *log);
#endif /* _NGX_CHANNEL_H_INCLUDED_ */
ngx_channel_t的command就是要发送的命令,类型有:
.../os/unix/ngx_process_cycle.h:
/* 打开频道,使用频道前必须先发送此命令 */
#define NGX_CMD_OPEN_CHANNEL 1
/* 关闭已经打开的频道 */
#define NGX_CMD_CLOSE_CHANNEL 2
/* 要求接收方正常退出 */
#define NGX_CMD_QUIT 3
/* 要求接收方强制结束进程 */
#define NGX_CMD_TERMINATE 4
/* 要求接收方重新打开已经打开的文件 */
#define NGX_CMD_REOPEN 5
通过这些命令,master进程就能控制worker进程了。之后是4个频道配套的方法。
发送频道:
ngx_write_channel:s是创建的套接字,ch是频道,size是频道大小,log是日志对象。可以看到函数通过套接字s发送了频道ch。
接收频道:
ngx_read_channel。参数都一样,注意的是master是用s[0]发送,worker是用s[1]接收。
添加频道事件:
ngx_add_channel_event。nginx的worker进程都是非常繁忙的,如何让worker知道有频道消息过来了呢?方法就是将套接字加入到epoll中,使worker进程既能处理用户请求,又能接收master进程的消息。当woker从epoll读取到相应的套接字,就可以调用ngx_read_channel读取频道消息了。
cycle:进程的结构体;fd:接收套接字,即s[1];event:epoll事件,EPOLLIN;handler:回调函数
关闭频道:
ngx_close_channel。fd:套接字数组,即s[2]
参考:《深入理解Nginx》