worker进程启动后,其首先会初始化自身运行所需要的环境,然后会进入一个循环,在该循环中不断检查是否有需要执行的事件,然后处理事件。在这个过程中,worker进程也是需要与master进程交互的,更有甚者,worker进程作为一个子进程,也是可以接收命令行指令(比如kill等)以进行相应逻辑的处理的。那么worker进程是如何与master或者命令行指令进行交互的呢?本文首先会对worker进程与master进程交互方式,以及worker进程如何处理命令行指令的流程进行讲解,然后会从源码上对worker进程交互的整个工作流程进行介绍。
1. worker与master进程交互方式
这里首先需要说明的是,无论是master还是外部命令的方式,nginx都是通过标志位的方式来处理相应的指令的,也即在接收到一个指令(无论是master还是外部命令)的时候,worker会在其回调方法中设置与该指令相对应的标志位,然后在worker进程在其自身的循环中处理完事件之后会依次检查这些标志位是否为真,是则根据该标志位的作用执行相应的逻辑。
对于worker进程与master进程的交互,其是通过socket管道的方式进行的。在ngx_process.h文件中声明了一个ngx_process_t结构体,这里我们主要关注其channel属性:
这里的ngx_process_t结构体的作用是存储某个进程相关的信息的,比如pid、channel、status等。每个进程中都有一个ngx_processes数组,数组元素就是这里的ngx_process_t结构体,也就是说每个进程都会通过ngx_processes数组保存其余进程的基本信息。其声明如下:
// 存储了nginx中所有的子进程数组,每个子进程都有一个对应的ngx_process_t结构体进行标记
extern ngx_process_t ngx_processes[NGX_MAX_PROCESSES];
这里我们就可以看出,每个进程都会一个与之对应的channel数组,这个数组的长度为2,其是与master进程进行交互的管道流。在master进程创建每一个子进程的之前,都会创建一个channel数组,该数组的创建方法为:int socketpair(int domain, int type, int protocol, int sv[2]);
这个方法的主要作用是创建一对匿名的已经连接的套接字,也就是说,如果在一个套接字中写入数据,那么在另一个套接字中就可以接收到写入的数据。通过这种方式,如果在父进程中往管道的一边写入数据,那么在子进程就可以在另一边接收到数据,这样就可以实现父子进程的数据通信了。在master进程启动完子进程之后,子进程会保有master进程中相应的数据,也包括这里的channel数组。如此,master进程就可以通过channel数组实现与子进程的通信了。
2. worker处理外部命令
对于外部命令,其本质上是通过signals数组中定义的各个信号以及回调方法进行处理的。在master进程初始化基本环境的时候,会将signals数组中指定的信号回调方法设置到对应的信号中。由于worker进程会继承master进程的基本环境,因而worker进程在接收到这里设置的信号之后,也会调用对应的回调方法。而该回调方法的主要逻辑也仅仅只是设置相应的标志位的值。关于nginx接收到信号之后如何设置对应的标志位,可以参照本人前面的文章(nginx master工作循环 超链接),这里不再赘述。
3. 源码讲解
master进程是通过ngx_start_worker_processes()方法启动各个子进程的,如下是该方法源码:
这里我们主要需要关注上面的启动子进程的方法调用,也即这里的ngx_spawn_process()方法,该方法的第二个参数是一个方法,在启动子进程之后,子进程就会进入该方法所指定的循环中。而在ngx_spawn_process()方法中,master进程会为当前新创建的子进程创建一个channel数组,以用于与当前子进程进行通信。如下是ngx_spawn_process()方法的源码:
ngx_spawn_process()方法最后会fork()一个子进程以执行其第二个参数所指定的回调方法。但是在这之前,我们需要说明的是,其通过socketpair()方法调用会创建一对匿名的socket,然后将其存储在当前进程的channel数组中,如此就完成了channel数组的创建。
worker进程启动之后会执行ngx_worker_process_cycle()方法,该方法首先会对worker进程进行初始化,其中就包括对继承而来的channel数组的处理。由于master进程和worker进程都保有channel数组所指代的socket描述符,而本质上master进程和各个worker进程只需要保有该数组的某一边的描述符即可。因而这里worker进程在初始化过程中,会关闭其所保存的另一边的描述符。在nginx中,master进程统一的会保留channel数组的0号位的socket描述符,关闭1号位的socket描述符,而worker进程则会关闭0号位的socket描述符,保留1号位的描述符。这样master进程需要与worker进程通信时,就只需要往channel[0]中写入数据,而worker进程则会监听channel[1],从而接收到master进程的数据写入。这里我们首先看一下worker进程的初始化方法ngx_worker_process_init()的源码:
该方法主要是对worker进程进行初始化,这里我们主要需要关注最后会遍历ngx_processes数组,这个数组中保存了当前nginx中各个进程的相关信息。在遍历过程中,会关闭当前进程保有的其余进程的channel[1]句柄,而保留有channel[0]句柄,这样当前进程如果需要与其他进程通信,也只需要往目标进程的channel[0]中写入数据即可。在遍历完成之后,当前进程就会关闭自身的channel[0]句柄,而保留channel[1]句柄。最后,会通过ngx_add_channel_event()方法为当前进程添加对channel[1]的监听事件,这里在调用ngx_add_channel_event()方法时传入的第二个参数是ngx_channel,该参数是在前面的ngx_spawn_process()方法中赋值的,指向的就是当前进程的channel[1]的socket句柄。
关于ngx_add_channel_event()方法,其本质就是创建一个ngx_event_t结构体的事件,然后将其添加到当前所使用的事件模型(比如epoll)句柄中。这里不再赘述该方法的实现源码,不过我们需要关注的是该事件触发时的回调方法,即调用ngx_add_channel_event()方法时传入的第三个参数ngx_channel_handler()方法。如下是该方法的源码:
在ngx_channel_handler()方法中,主要是读取所监听的socket句柄中的数据,而数据是以一个ngx_channel_t结构体所承载的,这个ngx_channel_t是nginx所统一使用的master与worker进程进行通信的结构体,其会指定当前发生的事件类型,以及发生该事件的进程信息。如下是ngx_channel_t结构体的声明:
在从当前进程的channel[1]中读取了ngx_channel_t结构体的数据之后,ngx_channel_handler()方法会根据发生的事件类型更新相应的标志位的状态,并且会更新当前进程的ngx_processes数组中对应的发生事件的进程的状态信息。
在处理了master进程所发送的事件之后,worker进程就会继续其循环,在该循环中会检查其所关注的标志位的状态,然后会根据这些状态执行对应的逻辑。如下是worker进程工作的循环的源码:
可以看到,worker进程主要处理了nginx是否退出相关的标志位,还处理了nginx是否重新读取了配置文件的标志位。
4. 小结
本文首先对master-worker进程交互的基本原理进行了讲解,然后深入到源码中讲解了nginx是如何实现master和worker进程的相互通信的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://my.oschina.net/zhangxufeng/blog/3163109