14.4 Nginx频道

ngx_channel_t频道是Nginx master进程与worker进程之间通信的常用工具,它是使用本机套接字实现的。下面先来看看socketpair方法,它用于创建父子进程间使用的套接字。


int socketpair(int d,int type,int protocol,int sv[2]);


这个方法可以创建一对关联的套接字sv[2]。下面依次介绍它的4个参数:参数d表示域,在Linux下通常取值为AF_UNIX;type取值为SOCK_STREAM或者SOCK_DGRAM,它表示在套接字上使用的是TCP还是UDP;protocol必须传递0;sv[2]是一个含有两个元素的整型数组,实际上就是两个套接字。当socketpair返回0时,sv[2]这两个套接字创建成功,否则socketpair返回-1表示失败。

当socketpair执行成功时,sv[2]这两个套接字具备下列关系:向sv[0]套接字写入数据,将可以从sv[1]套接字中读取到刚写入的数据;同样,向sv[1]套接字写入数据,也可以从sv[0]中读取到写入的数据。通常,在父、子进程通信前,会先调用socketpair方法创建这样一组套接字,在调用fork方法创建出子进程后,将会在父进程中关闭sv[1]套接字,仅使用sv[0]套接字用于向子进程发送数据以及接收子进程发送来的数据;而在子进程中则关闭sv[0]套接字,仅使用sv[1]套接字既可以接收父进程发来的数据,也可以向父进程发送数据。

再来介绍一下ngx_channel_t频道。ngx_channel_t结构体是Nginx定义的master父进程与worker子进程间的消息格式,如下所示。


typedef struct{

//传递的TCP消息中的命令

ngx_uint_t command;

//进程ID,一般是发送命令方的进程ID

ngx_pid_t pid;

//表示发送命令方在ngx_processes进程数组间的序号

ngx_int_t slot;

//通信的套接字句柄

ngx_fd_t fd;

}ngx_channel_t;


这个消息的格式似乎过于简单了,没错,因为Nginx仅用这个频道同步master进程与worker进程间的状态,这点从针对command成员已经定义的命令就可以看出来,如下所示。


//打开频道,使用频道这种方式通信前必须发送的命令

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


在8.6节我们介绍过master进程是如何监控、管理worker子进程的,那图8-8中的master又是如何启动、停止worker子进程的呢?正是通过socketpair产生的套接字发送命令的,即每次要派生一个子进程之前,都会先调用socketpair方法。在Nginx派生子进程的ngx_spawn_process方法中,会首先派生基于TCP的套接字,如下所示。


ngx_pid_t ngx_spawn_process(ngx_cycle_tcycle,ngx_spawn_proc_pt proc,voiddata,char*name,ngx_int_t respawn)

{

……

//ngx_processes[s].channel数组正是将要用于父、子进程间通信的套接字对

if(socketpair(AF_UNIX,SOCK_STREAM,0,ngx_processes[s].channel)==-1)

{

return NGX_INVALID_PID;

}

//接下来会把channel套接字对都设置为非阻塞模式

……

}


上段代码提到的ngx_processes数组定义了Nginx服务中所有的进程,包括master进程和worker进程,如下所示。


define NGX_MAX_PROCESSES 1024

//虽然定义了NGX_MAX_PROCESSES个成员,但已经使用的元素仅与启动的进程个数有关

ngx_process_t ngx_processes[NGX_MAX_PROCESSES];


它的类型是ngx_process_t,对于频道来说,这个结构体只关心它的channel成员。


typedef struct{

……

//socketpair创建的套接字对

ngx_socket_t channel[2];

}ngx_process_t;


如何使用频道发送ngx_channel_t消息呢?Nginx封装了4个方法,首先来看看用于发送消息的ngx_write_channel方法。


ngx_int_t ngx_write_channel(ngx_socket_t s,ngx_channel_tch,size_t size,ngx_log_tlog);


这里的s参数是要使用的TCP套接字,ch参数是ngx_channel_t类型的消息,size参数是ngx_channel_t结构体的大小,log参数是日志对象。

再来看看读取消息的方法ngx_read_channel。


ngx_int_t ngx_read_channel(ngx_socket_t s,ngx_channel_tch,size_t size,ngx_log_tlog);


这里的参数意义与ngx_write_channel方法完全相同,只是要注意s套接字,它与发送方使用的s套接字是配对的。例如,在Nginx中,目前仅存在master进程向worker进程发送消息的场景,这时对于socketpair方法创建的channel[2]套接字对来说,master进程会使用channel[0]套接字来发送消息,而worker进程则会使用channel[1]套接字来接收消息。

worker进程是怎样调度ngx_read_channel方法接收频道消息呢?毕竟Nginx是单线程程序,这唯一的线程还在同时处理大量的用户请求呢!这时就需要使用ngx_add_channel_event方法把接收频道消息的套接字添加到epoll中了,当接收到父进程消息时子进程会通过epoll的事件回调相应的handler方法来处理这个频道消息,如下所示。


ngx_int_t ngx_add_channel_event(ngx_cycle_t*cycle,ngx_fd_t fd,

ngx_int_t event,ngx_event_handler_pt handler);


cycle参数自然是每个Nginx进程必须具备的ngx_cycle_t核心结构体;fd参数就是上面说过的需要接收消息的套接字,对于worker子进程来说,就是对应的channel[1]套接字;event参数是需要检测的事件类型,在上述场景下必然是EPOLLIN;handler参数指向的方法就是用于读取频道消息的方法,Nginx定义了一个ngx_channel_handler方法用于处理频道消息。

当进程希望关闭这个频道通信方式时,可以调用ngx_close_channel方法,它会关闭这对套接字,如下所示。


void ngx_close_channel(ngx_fd_tfd,ngx_log_tlog);


参数fd就是上面说过的channel[2]套接字数组。

实际上,基于本机TCP的套接字可以进行复杂的双工通信,虽然目前Nginx仅用于帮助master进程管理worker进程的状态,但完全可以轻易地进行改造,使之满足复杂的进程间通信需求。