1. 程式人生 > >Nginx原始碼分析與實踐---程序間通訊機制(套接字)

Nginx原始碼分析與實踐---程序間通訊機制(套接字)

上一篇中,我們看到了nginx共享記憶體方式的程序間通訊。這次我們看下nginx使用套接字的程序間通訊方式。


同樣的幾個問題:

1.什麼時候需要使用套接字方式的程序間通訊機制呢?

舉個栗子:我們知道nginx有master程序和worker程序,那麼master程序是如何向worker程序傳送訊息的呢?worker程序又是如何接收master傳送過來的訊息呢?答案就是使用套接字。注意的是雖然套接字是雙工的,但目前套接字僅用於master程序管理worker程序,而沒用於worker發訊息給master,或者worker程序間通訊(參考:《深入理解Nginx》).


2.nginx的套接字是如何實現的呢?

nginx的套接字是本機套接字,即通過socketpair建立的,不是網路套接字(通過socket建立)。


socketpair:

int socketpair(int family, int type, int protocol, int sockfd[2]);

返回值:成功:0;出錯:-1


socketpair僅適用於unix域套接字,因此,family必須為AF_LOCAL,protocol必須為0。type可以為SOCK_STREAM或SOCK_DGRAM,表示使用TCP還是UDP。sockfd[2]是套接字對,套接字是雙工的。因此,通常都是父程序fork子程序之前呼叫socketpair建立套接字對,接著父程序fork子程序,子程序繼承套接字對。父程序關閉sockfd[1],子程序關閉sockfd[0]。父程序通過sockfd[0]傳送接收訊息,子程序通過sockfd[1]傳送接收訊息。


下面看一個關鍵函式: ngx_spawn_process

.../os/unix/ngx_process.c:

/* 建立套接字 派生子程序 */
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
    char *name, ngx_int_t respawn)
{
    ......

    if (respawn != NGX_PROCESS_DETACHED) {

        /* Solaris 9 still has no AF_LOCAL */

        /* 建立套接字用於master與worker程序間通訊 */
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          "socketpair() failed while spawning \"%s\"", name);
            return NGX_INVALID_PID;
        }

        ngx_log_debug2(NGX_LOG_DEBUG_CORE, cycle->log, 0,
                       "channel %d:%d",
                       ngx_processes[s].channel[0],
                       ngx_processes[s].channel[1]);

        /* 套接字設定為非阻塞 */
        if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1) {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
                          ngx_nonblocking_n " failed while spawning \"%s\"",
                          name);
            ngx_close_channel(ngx_processes[s].channel, cycle->log);
            return NGX_INVALID_PID;
        }

	......
	       /* master 建立worker程序 */
	        pid = fork();
	......

我們看到,在ngx_spawn_process函式內,先通過socketpair建立套接字,建立的套接字放在ngx_processes[s].channel內。而ngx_processes是個結構體陣列,元素型別是ngx_process_t


.../os/unix/ngx_process.h:

typedef struct {
    ngx_pid_t           pid;
    int                 status;
    ngx_socket_t        channel[2];    /* socketpair建立的套接字對,用於master與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;
socketpair建立的套接字正是放在ngx_process_t結構體的channel成員這。因為nginx一個程序是單執行緒的,每個程序要處理數以萬,以幾十萬計的連線,因此都非常忙,所以關鍵一點就是nginx的程序一定不能阻塞!!!一阻塞,nginx效率就會大幅度下降。因此,可以看到在ngx_spawn_process函式內部,建立完套接字後,接著就是通過ngx_nonblocking將套接字設定為非阻塞,然後才可以通過fork()建立worker程序。


3.master如何通過套接字向worker傳送訊息呢?

上面一節介紹瞭如何建立套接字,下面講如何通過套接字來發送訊息。介紹之前得先知道nginx目前只是將套接字用於master向worker傳送命令,而沒用於worker間通訊。

有了套接字就相當於有了傳送命令的視窗,接下來就是要構造包裝命令的工具,以及傳送接收命令等相配套的方法了。

nginx是通過ngx_channel_t(頻道)來包裝命令的。master通過頻道就可以向worker傳送訊息。


下面看ngx_channel_t的原始碼:

.../os/unix/ngx_channel.h

/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) Nginx, Inc.
 */


#ifndef _NGX_CHANNEL_H_INCLUDED_
#define _NGX_CHANNEL_H_INCLUDED_


#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_event.h>


/* 頻道定義 */
typedef struct {
    ngx_uint_t  command;    /* 傳遞的命令 */
    ngx_pid_t   pid;        /* 傳送命令方的程序ID */
    ngx_int_t   slot;       /* 傳送命令方在程序陣列ngx_processex中的序號 */
    ngx_fd_t    fd;         /* 套接字控制代碼 */
} ngx_channel_t;

/* 頻道相關的方法 */
ngx_int_t ngx_write_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size,
    ngx_log_t *log);
ngx_int_t ngx_read_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size,
    ngx_log_t *log);
ngx_int_t ngx_add_channel_event(ngx_cycle_t *cycle, ngx_fd_t fd,
    ngx_int_t event, ngx_event_handler_pt handler);
void ngx_close_channel(ngx_fd_t *fd, ngx_log_t *log);


#endif /* _NGX_CHANNEL_H_INCLUDED_ */


ngx_channel_t的command就是要傳送的命令,型別有:

.../os/unix/ngx_process_cycle.h:

/* 開啟頻道,使用頻道前必須先發送此命令 */
#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
通過這些命令,master程序就能控制worker程序了。之後是4個頻道配套的方法。

傳送頻道:

ngx_write_channel:s是建立的套接字,ch是頻道,size是頻道大小,log是日誌物件。可以看到函式通過套接字s傳送了頻道ch。

接收頻道:

ngx_read_channel。引數都一樣,注意的是master是用s[0]傳送,worker是用s[1]接收。
新增頻道事件:

ngx_add_channel_event。nginx的worker程序都是非常繁忙的,如何讓worker知道有頻道訊息過來了呢?方法就是將套接字加入到epoll中,使worker程序既能處理使用者請求,又能接收master程序的訊息。當woker從epoll讀取到相應的套接字,就可以呼叫ngx_read_channel讀取頻道訊息了。

cycle:程序的結構體;fd:接收套接字,即s[1];event:epoll事件,EPOLLIN;handler:回撥函式

關閉頻道:

ngx_close_channel。fd:套接字陣列,即s[2]


參考:《深入理解Nginx》