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;
        }
    }
......
}