lchl0388 发表于 2016-12-28 10:57:33

从nginx角度看服务器多进程模型设计(一)

多进程你可能很熟悉,也许有一套自己的使用习惯和方法。这东西没有什么权威建议,书上只是给出了基本知识点,至于具体怎么去用,因人而异。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.channel, FIOASYNC, &on) == -1) {
...
}
if (fcntl(ngx_processes.channel, F_SETOWN, ngx_pid) == -1) {
...
}

...

ngx_channel = ngx_processes.channel;

...

// 当前产生的子进程在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.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_tch;
ch.command = NGX_CMD_OPEN_CHANNEL;
for (i = 0; i < n; i++) {
ch.pid = ngx_processes.pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes.channel;
ngx_pass_open_channel(cycle, &ch);
}
}


在worker进程的进程信息数组中,把当前worker进程信息结构(即ngx_process_slot对应的位置)中的channel关掉,

同时会把其他进程的channel关掉,而只把他们的channel留着,如下代码所示。那么这样做的意图是什么?



ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority) {
...
for (n = 0; n < ngx_last_process; n++) {
if (ngx_processes.pid == -1) {
continue;
}
if (n == ngx_process_slot) {
continue;
}
if (ngx_processes.channel == -1) {
continue;
}
if (close(ngx_processes.channel) == -1) {
...
}
}
if (close(ngx_processes.channel) == -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.channel,我们知道s是本次用来放置新创建进程信息的(在ngx_processes中)位置,即也是ngx_process_slot变量,因为这两个变量,ngx_channel和ngx_process_slot是在fork之前设置的,那么在随后子进程的init时,就会用到这两个刚刚在父进程中设置的值,ngx_add_channel_event中的ngx_channel就是当前子进程的channle,所以这里的意图也就明了了,该worker进程就是通过监听channel的读事件来获取信息。好了,然后我们搜遍所有代码,并没有看到在某个channel上监听写事件的动作,那么我们要问了,子进程有没有需要写一些数据的时候呢?好吧,我们全文搜索"channel",看看会发现什么。我想你肯定发现了ngx_write_channel函数,它的作用就是往channel中去写数据,那么是都是谁在写呢?




我们找到了ngx_pass_open_channel,也就是我上文所说的“广播”。你要知道的一点就是,父进程中通过向channle里写数据,可以在子进程中相应的channel中读取。好了到这里我们前面提到的一些疑问基本上都有了答案,父进程通过向各个channel中“广播”数据,子进程在其自己的channel中读取相应的数据,他们正是通过这种方式来通信的。那么nginx进程间通信的机制仅仅就是这些吗?远不止。。。




我们前面提到父进程写channel,子进程读channel,那么父进程都传了些啥?我们最容易看到的载体是ngx_channel_t结构,在ngx_pass_open_channel中,参数ch就是这样的一个结构:

/*
* command传递的命令
* pid本次产生的新进程pid
* slot新进程在ngx_processes数组中的位置
* fd新进程中channel
*/
typedef struct {
ngx_uint_tcommand;
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.pid = ch.pid;

ngx_processes.channel = ch.fd;




这个NGX_CMD_OPEN_CHANNEL算是最简单的命令了,其他的处理有什么特别的地方?




以上我们讨论的这些算是基础设施了,在这之上,nginx做了很多的好东西。目前看到的只是在nginx初始化时做的事情,那么在实际运行中,进程间又有哪些交互和通信呢?后面的文章讨论。一篇讨论文章太长的话,大家都蛋疼,你懂得。。。
页: [1]
查看完整版本: 从nginx角度看服务器多进程模型设计(一)