Nginx进程间通信–Channel篇
Nginx中全局变量ngx_processes数组,存储所有进程的信息。work进程在创建时,从master进程继承了ngx_processes数组。worker_processes=4,由于worker是顺序创建的,当work2创建时,它可以从master继承ngx_processes,从而得知worker1已经创建成功;但是在worker1创建的时候,worker2,worker3,worker4进程是没有创建的,这时就需要由master在创建一个进程时,通知所有的已经创建的worker有新的进程被创建了,以及这个进程的基本信息。
Nginx的Master和work进程通过channel进行通信,其本质上使用socketpair套接字实现的。socketpair函数创建一对无命名的、相互连接的UNIX域套接字。
UNIX域套接字
UNIX域套接字用于在同一台计算机上运行的进程之间的通信。虽然socket可用于同一目的,但UNIX域套接字的效率更高。UNIX域套接字仅仅复制数据,它们并不执行协议处理,不需要添加或删除网络报头,无需计算校验和,不产生sequence ID,无需发送ack。
UNIX域套接字提供流和数据报两种接口。UNIX域数据报服务生可靠的,既不会丢失报文也不会传递出错。UNIX域套接字像是套接字和管道的混合。
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sockfd[2]);
返回值:成功返回0;出错返回-1
一对相互连接的UNIX域套接字可以起到全双工管道的作用:两端对读写开放。我们将其称为fd管道(fd-pipe),与普通的半双工管道区分开来。
Nginx Channel实现原理
typedef struct {
//消息命令
ngx_uint_t command;
//进程ID
ngx_pid_t pid;
//发送方在ngx_processes数组中的序号
ngx_int_t slot;
//通信的socketpair句柄
ngx_fd_t fd;
} ngx_channel_t;
ngx_channel_t中command对应的指令:
//打开channel
#define NGX_CMD_OPEN_CHANNEL 1
//关闭已经打开的channel
#define NGX_CMD_CLOSE_CHANNEL 2
//要求接收方进程正常退出
#define NGX_CMD_QUIT 3
//要求接收方进程强制退出
#define NGX_CMD_TERMINATE 4
//要求接收方重新打开进程已经打开过的文件
#define NGX_CMD_REOPEN 5
Channel的产生过程
在Nginx派生子进程时,会调用ngx_spawn_process中的socketpair,产生channel。
static void
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
ngx_int_t i;
ngx_channel_t ch;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");
ngx_memzero(&ch, sizeof(ngx_channel_t));
ch.command = NGX_CMD_OPEN_CHANNEL;
//循环创建n个worker进程
for (i = 0; i < n; i++) {
ngx_spawn_process(cycle, ngx_worker_process_cycle,
(void *) (intptr_t) i, "worker process", type);
ch.pid = ngx_processes[ngx_process_slot].pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes[ngx_process_slot].channel[0];
//传递socketpair
ngx_pass_open_channel(cycle, &ch);
}
}
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
{
......
//创建UNIX域套接字socketpair,产生channel
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;
}
......
}
Channel操作
Nginx封装了4个方法对Channel进行操作。
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)
一、ngx_add_channel_event
在ngx_worker_process_init初始化worker进程时,调用ngx_add_channel_event,将worker监听的channel读事件加入到Nginx的事件处理机制里面,并设置事件处理handler=ngx_channel_handler。
对于ngx_channel_handler,我们可以看到:
static void
ngx_channel_handler(ngx_event_t *ev)
{
......
for ( ;; ) {
n = ngx_read_channel(c->fd, &ch, sizeof(ngx_channel_t), ev->log);
if (n == NGX_ERROR) {
if (ngx_event_flags & NGX_USE_EPOLL_EVENT) {
ngx_del_conn(c, 0);
}
ngx_close_connection(c);
return;
}
if (ngx_event_flags & NGX_USE_EVENTPORT_EVENT) {
if (ngx_add_event(ev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return;
}
}
if (n == NGX_AGAIN) {
return;
}
switch (ch.command) {
//要求接收方进程正常退出
case NGX_CMD_QUIT:
ngx_quit = 1;
break;
//要求接收方进程强制退出
case NGX_CMD_TERMINATE:
ngx_terminate = 1;
break;
//要求接收方重新打开进程已经打开过的文件
case NGX_CMD_REOPEN:
ngx_reopen = 1;
break;
//打开channel
case NGX_CMD_OPEN_CHANNEL:
ngx_processes[ch.slot].pid = ch.pid;
ngx_processes[ch.slot].channel[0] = ch.fd;
break;
//关闭已经打开的channel
case NGX_CMD_CLOSE_CHANNEL:
if (close(ngx_processes[ch.slot].channel[0]) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno, "close() channel failed");
}
ngx_processes[ch.slot].channel[0] = -1;
break;
}
}
......
}
二、ngx_write_channel
ngx_write_channel在三个地方被调用,分别是:
1、ngx_pass_open_channel:向已创建的worker传递在其后创建的兄弟worker进程信息时;
2、ngx_signal_worker_processes:worker的信号处理函数接收到信号时;
3、ngx_reap_children:master回收worker进程,向其他的worker发送该进程的NGX_CMD_CLOSE_CHANNEL信号。
三、ngx_read_channel
ngx_read_channel只有在ngx_channel_handler回调处理函数中被调用。用来接收channel中的数据。
四、ngx_close_channel
ngx_reap_children:master回收worker进程,并关闭该进程对应的channel。
Channel应用于worker进程间通信
目前Nginx仅通过channel来实现master进程管理worker进程的状态,但完全可以将其进行改造,来满足自定义的worker间的通信需求。
例如在nginx-rtmp的control模块中,当我们配置多个worker并且需要drop直播流时,可以在接收到drop请求时,向其他的worker广播drop指令。并且在ngx_channel_handler中添加NGX_CMD_DROP_STREAM指令处理。
void
ngx_process_broadcast(ngx_str_t info)
{
ngx_channel_t ch;
ngx_int_t i;
size_t strlength;
//NGX_CMD_DROP_STREAM 自定义drop直播流指令
ch.command = NGX_CMD_DROP_STREAM;
ch.pid = ngx_getpid();
ch.slot = ngx_process_slot;
ch.fd = -1;
ngx_memzero(ch.data, 256);
strlength = info.len < 255 ? info.len : 255;
ngx_memcpy(ch.data, info.data, strlength);
for (i = 0; i < NGX_MAX_PROCESSES; i++) {
if (i == ngx_process_slot
|| ngx_processes[i].pid == 0
|| ngx_processes[i].pid == NGX_INVALID_PID
|| ngx_processes[i].channel[0] == -1)
{
continue;
}
ch.pid = ngx_processes[i].pid;
ch.slot = i;
ngx_write_channel(ngx_processes[i].channel[0], &ch,
sizeof(ngx_channel_t), ngx_cycle->log);
}
}
static void
ngx_channel_handler(ngx_event_t *ev)
{
......
switch (ch.command) {
case NGX_CMD_DROP_STREAM:
volatile ngx_array_t *evhs;
ngx_handler_pt *evh;
ngx_uint_t n;
evhs = &ngx_cycle->events[NGX_DROP_STREAM];
evh = evhs->elts;
for(n = 0; n < evhs->nelts; ++n, ++evh) {
if (!evh) {
continue;
}
//回调处理函数
switch ((*evh)(ch.data)) {
case NGX_ERROR:
ngx_log_debug1(NGX_LOG_DEBUG_CORE, ngx_cycle->log, 0,
"handler %d failed", n);
break;
case NGX_DONE:
break;
}
}
break;
}
}
......
}