多进程你可能很熟悉,也许有一套自己的使用习惯和方法。这东西没有什么权威建议,书上只是给出了基本知识点,至于具体怎么去用,因人而异。nginx在多进程设计方面有很多值得学习和借鉴的东西,我认为是一套比较好的实现方案。你也许认为这东西很简单,是老生常谈的东西了,但是我这里要提醒你一下,俗话道酒是陈的香,越经典的东西越值得去琢磨,不要对自己太自信。善于思考的家伙总是会在一些老的技术上给你许多新鲜的见解,这种牛人你不会没遇到过吧!扯淡罢了,回到正题。
看函数ngx_spawn_process的骨架:
for (s = 0; s < ngx_last_process; s++) {
...
}
...
/*
* 实现异步通知的两个步骤,在nginx事件模型的rtsig机制中会用到
* 参考:http://blog.csdn.net/adc0809608/article/details/7368119
*/
if (ioctl(ngx_processes[s].channel[0], FIOASYNC, &on) == -1) {
...
}
if (fcntl(ngx_processes[s].channel[0], F_SETOWN, ngx_pid) == -1) {
...
}
...
ngx_channel = ngx_processes[s].channel[1];
...
// 当前产生的子进程在ngx_processes数组中的下标
ngx_process_slot = s;
/*
* 在fork之后,父进程的ngx_processes数组,“传递”给了子进程,但是这时子进程拿到的数组是截至创建该进程之前其他进程的信息。
* 由于子进程是父进程fork得到的,那么在之后父进程的操作结果在子进程中就不可见了。假设当前诞生的是进程1,用p1表示,当父进程
* 创建p5时,那么p2-p5的进程信息在p1中是缺失的,那么p1需要这些信息吗?如果需要的话,该通过什么手段给它呢?
* 见ngx_start_worker_processes相关分析
*/
ngx_pid = fork();
switch (pid) {
...
case 0:
ngx_pid = ngx_getpid();
proc(cycle, data);
break;
default:
break;
}
...
ngx_processes[s].pid = pid;
...
if (s == ngx_last_process) {
ngx_last_process++;
}
/*
* 父进程,也就是master进程,依次创建work子进程,为了确保ngx_processes数组在子进程间同步,每次创建完一个子进程,
* 就通过ngx_pass_open_channel,做一次广播,告诉先前已经创建的子进程: "新进程诞生,注意更新进程数组相关项"
* 那么这些子进程又是如何更新这些信息的呢?答案在ngx_worker_process_init中。
*/
ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
ngx_int_t i;
ngx_channel_t ch;
ch.command = NGX_CMD_OPEN_CHANNEL;
for (i = 0; i < n; i++) {
ch.pid = ngx_processes[ngx_process_slot].pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes[ngx_process_slot].channel[0];
ngx_pass_open_channel(cycle, &ch);
}
}
在worker进程的进程信息数组中,把当前worker进程信息结构(即ngx_process_slot对应的位置)中的channel[0]关掉,
同时会把其他进程的channel[1]关掉,而只把他们的channel[0]留着,如下代码所示。那么这样做的意图是什么?
ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority) {
...
for (n = 0; n < ngx_last_process; n++) {
if (ngx_processes[n].pid == -1) {
continue;
}
if (n == ngx_process_slot) {
continue;
}
if (ngx_processes[n].channel[1] == -1) {
continue;
}
if (close(ngx_processes[n].channel[1]) == -1) {
...
}
}
if (close(ngx_processes[ngx_process_slot].channel[0]) == -1) {
...
}
}
在ngx_worker_process_init的最后,调用了函数ngx_add_channel_event:
ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT, ngx_channel_handler)
主要的目的是向epoll中注册读事件。读数据的fd为变量ngx_channel,处理函数是ngx_channel_handler,那么ngx_channel是什么, 这个处理函数又做了些什么呢?
在ngx_spawn_process函数中,有句:ngx_channel = ngx_processes[s].channel[1],我们知道s是本次用来放置新创建进程信息的(在ngx_processes中)位置,即也是ngx_process_slot变量,因为这两个变量,ngx_channel和ngx_process_slot是在fork之前设置的,那么在随后子进程的init时,就会用到这两个刚刚在父进程中设置的值,ngx_add_channel_event中的ngx_channel就是当前子进程的channle[1],所以这里的意图也就明了了,该worker进程就是通过监听channel[1]的读事件来获取信息。好了,然后我们搜遍所有代码,并没有看到在某个channel上监听写事件的动作,那么我们要问了,子进程有没有需要写一些数据的时候呢?好吧,我们全文搜索"channel[0]",看看会发现什么。我想你肯定发现了ngx_write_channel函数,它的作用就是往channel[0]中去写数据,那么是都是谁在写呢?
我们找到了ngx_pass_open_channel,也就是我上文所说的“广播”。你要知道的一点就是,父进程中通过向channle[0]里写数据,可以在子进程中相应的channel[1]中读取。好了到这里我们前面提到的一些疑问基本上都有了答案,父进程通过向各个channel[0]中“广播”数据,子进程在其自己的channel[1]中读取相应的数据,他们正是通过这种方式来通信的。那么nginx进程间通信的机制仅仅就是这些吗?远不止。。。
我们前面提到父进程写channel[0],子进程读channel[1],那么父进程都传了些啥?我们最容易看到的载体是ngx_channel_t结构,在ngx_pass_open_channel中,参数ch就是这样的一个结构:
/*
* command传递的命令
* pid本次产生的新进程pid
* slot新进程在ngx_processes数组中的位置
* fd新进程中channel[0]
*/
typedef struct {
ngx_uint_t command;
ngx_pid_t pid;
ngx_int_t slot;
ngx_fd_t fd;
} ngx_channel_t;
对于command,在当前为NGX_CMD_OPEN_CHANNEL,也就是告诉其他的子进程,“某个新的进程刚刚诞生,注意同步相关信息”。那么子进程得到这个信息会怎么做呢?
还记得这个ngx_channel_handler函数吗,在注册读事件时设置的,它里面会调用ngx_read_channel来或者这个channel数据。如果发现是NGX_CMD_OPEN_CHANNEL命令,那么就在ngx_processes的相应位置上更新新诞生进程的信息:
ngx_processes[ch.slot].pid = ch.pid;
ngx_processes[ch.slot].channel[0] = ch.fd;
ngx_processes[ch.slot].channel[0] = ch.fd;
这个NGX_CMD_OPEN_CHANNEL算是最简单的命令了,其他的处理有什么特别的地方?
以上我们讨论的这些算是基础设施了,在这之上,nginx做了很多的好东西。目前看到的只是在nginx初始化时做的事情,那么在实际运行中,进程间又有哪些交互和通信呢?后面的文章讨论。一篇讨论文章太长的话,大家都蛋疼,你懂得。。。