nginx 进程通信
运行在多进程模型的nginx在正常工作时,自然就会有多个进程实例,比如下图是在配置“worker_processes 4;”情况下的显示,nginx设置的进程title能很好的帮助我们区分监控进程与工作进程,不过带上选项f的ps命令以树目录的形式打印各个进程信息也能帮助我们做这个区分。多进程联合工作必定要牵扯到进程之间的通信问题,下面就来看看nginx是如何做的。
采用socketpair()函数创造一对未命名的unix域套接字来进行进程之间的双向通信.
基本的数据结构:
typedef struct { /** * 进程id */ ngx_pid_t pid; /** * 进程退出的状态 */ int status; /** * 进程的channel,通过socketpair创建的 */ ngx_socket_t channel[2]; /** * 进程的初始化函数,在每次创建完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;
typedef struct { /** * 向worker进程发送的命令 */ ngx_uint_t command; /** * 对应进程的id */ ngx_pid_t pid; /** * 对应的进程在ngx_processes数组中的下标 */ ngx_int_t slot; /** * 文件描述符 */ ngx_fd_t fd; } ngx_channel_t;
static void ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type) { .....for (i = 0; i < n; i++) { cpu_affinity = ngx_get_cpu_affinity(i); ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL, "worker process", type); //创建子进程后调用 ngx_worker_process_cycle
......
ngx_pass_open_channel(cycle, &ch);
}
}
ngx_spawn_process函数:
#define NGX_MAX_PROCESSES 1024 ngx_process_t ngx_processes[NGX_MAX_PROCESSES];
ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data, char *name, ngx_int_t respawn) { if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) {........ } .............. ngx_channel = ngx_processes[s].channel[1]; ......... pid = fork(); //创建进程 ngx_close_channel(ngx_processes[s].channel, cycle->log);//子进程调用 proc(cycle, data); //主进程掉用,ngx_worker_process_cycle
}
在该函数进行fork()之前,先调用了socketpair()创建一对socket描述符,存放在变量ngx_processes[s].chanel内,(其中s标志在ngx_processes数组内第一个可用元素的下标,比如最开始产生第一个工作进程时,可用元素的下标s为0),而在fork() 之后,由于子进程继承了父进程的资源,那么父子进程都拥有一对socket描述符,而nginx将channel[0]给父进程使用, channel[1]给子进程使用,这样分别错开使用不同的描述符,即可实现父子进程之间的双向通信.
除此之外,对于各个子进程也能进行双向通信.父子进程的通信channel设定是自然而然的事情,而子进程之间的通信channel设定就涉及到进程之间文件描述符(socket描述符也属于文件描述符)的传递,因为虽然后生成的子进程通过继承的channel[0]能够往前生成的子进程发送信息,但前生成的子进程无法获知后生成子进程的channel[0]而不能发送信息,所以后生成的子进程必须利用已知的前生成子进程的channel[0]进行主动告知。
在子进程的初始话函数ngx_worker_process_init()里,会把ngx_channel(channel[1])加入到读事件监听集里,对应回调处理函数ngx_channel_handler();
ngx_worker_process_init() //worker进程初始化
static void ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority) { ...... if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT, ngx_channel_handler) }
而在父进程fork()生成一个新子进程后,就会立即通过ngx_pass_open_channel()函数把这个子进程的相关信息告知给其前面已生成的子进程:
static void ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch) { ngx_int_t i; for (i = 0; i < ngx_last_process; i++) { if (i == ngx_process_slot || ngx_processes[i].pid == -1 || ngx_processes[i].channel[0] == -1) { continue; } ........... ngx_write_channel(ngx_processes[i].channel[0], ch, sizeof(ngx_channel_t), cycle->log); } }
其中参数ch里包括刚创建新子进程的pid,进程信息全局数组的ngx_processes的下标,socket描述符channel[0]等信息,这里通过循环所有的存活的其他子进程,然后调用函数ngx_write_channel()通过继承的channel[0]描述符进行信息主动告知,而接收到这信息的子进程将执行回调函数ngx_channel_handler(),把接收到的新子进程A的相关信息存储在ngx_processes全局数组内。
static void ngx_channel_handler(ngx_event_t *ev) { ...... for ( ;; ) { n = ngx_read_channel(c->fd, &ch, sizeof(ngx_channel_t), ev->log);//读取channel ........ switch (ch.command) { ......... case NGX_CMD_OPEN_CHANNEL: ngx_processes[ch.slot].pid = ch.pid; ngx_processes[ch.slot].channel[0] = ch.fd; break; .....
} }
最后,就目前nginx代码来看,子进程并没有往父进程发送任何消息,子进程之间也没有相互通信的逻辑,也许是因为nginx有其它一些更好的进程通信方式,比如共享内存等,所以这种channel通信目前仅做为父进程往子进程发送消息使用,但由于有这个基础在这,如果未来要使用channel做这样的事情,的确是可以的。