天天看點

nginx源碼分析1———程序間的通信機制六(UNIX域協定)

相關介紹

Unix域協定并不是一個實際的協定族,而是在單機上用戶端與伺服器通信的一種方法。但是可以通過與網絡通信中使用的相同插口API來通路它們。當然也和TCP/IP有很多不同之處。

網際TCP/IP協定:當客戶程序通過TCP往伺服器程序發送資料時,資料首先由TCP輸出處理,然後再經過IP輸出處理,最後發往環回驅動器(lo),在環回驅動器中(lo),資料首先被放到IP輸入隊列,然後經過IP輸入和TCP輸入處理,最後傳送到伺服器。這樣工作很好,并且對于在相同主機上的對等端來說是透明的。然而,在TCP/IP協定棧裡需要大量的處理過程,當資料沒有離開主機時,這些處理過程實際上是不需要的。

UNIX域協定:UNIX域協定由于知道資料不會離開主機,是以隻需要較少的處理過程,這樣書記傳輸的更快。不需要進行檢驗和的計算和驗證,資料也不會失序,由于核心能控制客服程序和伺服器程序的執行過程,流量控制也被大大簡化了,等等。UNIX域的優點還在于它們可以使用的接口與網絡程式使用的接口完全一樣。

相關系統調用

#include <sys/socket.h>
int socketpair(int family, int type, int protocol, int sockfd[]);
           

family參數必須為AF_UNIX(AF_LOCAL)。

type參數可以是SOCK_STREAM(TCP),也可以是SOCK_DGRAM(UDP)

protocol參數為0

sockfd新建立的兩個套接字作為sockfd傳回

相關結構

struct msghdr {
    void         *msg_name;     
    socklen_t     msg_namelen;    
    struct iovec *msg_iov;        
    size_t        msg_iovlen;    
    void         *msg_control;    
    socklen_t     msg_controllen; 
    int           msg_flags;      
};
           

1,msg_name與msg_namelen用于未連接配接的場景,msg_name指向套接字的位址結構,msg_namelen用于指定位址結構的長度,當使用TCP或者已經連接配接的UDP的時候,msg_name應該為空,msg_namelen應該為0。

2,msg_iov與msg_iovlen指定我将要發送的資料,由于資料是數組形式的,是以需要msg_iovlen來指定數組的長度。

3,msg_control與msg_controllen是發送輔助資料的結構和輔助資料結構長度

typedef struct {
     ngx_uint_t  command;
     ngx_pid_t   pid;
     ngx_int_t   slot;
     ngx_fd_t    fd;
} ngx_channel_t;
           

ngx_channel_t是用于發送消息的内容。

struct cmsghdr{
    socklen_t cmsg_len;
    int cmsg_level;
    int cmsg_type;
    /* followed by nsigned char cmsg_data[] */
};
           

cmsghdr是發送控制資料的結構體,cmsg_len指定輔助資料的長度,cmsg_level和cmsg_type指定輔助資料的等級與輔助資料的類型,最後還會跟上輔助資料,也就是上面注釋的地方。

我們會用到輔助資料的類型如下

協定 cmsg_level cmsg_type 說明
UNIX域 SOL_SOCKET SCM_RIGHTS 發送或者接受描述符
UNIX域 SOL_SOCKET SCM_CREDS 發送或者接受使用者憑證

建立

ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data, char *name, ngx_int_t respawn)
{
   .../* 省略 */
        /* Solaris 9 still has no AF_LOCAL */
        //建立套接字,其中channel就是一個int數組
        if (socketpair(AF_UNIX, SOCK_STREAM, , ngx_processes[s].channel) == -)
        {
            ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "socketpair() failed while spawning \"%s\"", name);
            return NGX_INVALID_PID;
        }
        //在建立成功以後将調用fcntl和ioctl進行設定描述符控制操作,然後進行fork建立多程序。
   .../* 省略 */
}
           

寫資料

nginx寫資料采用套接字api,使用裡面的sendmsg發送資料。

ngx_int_t
ngx_write_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size,
    ngx_log_t *log)
{
    ssize_t             n;
    ngx_err_t           err;
    struct iovec        iov[];
    struct msghdr       msg;
    union {
        struct cmsghdr  cm;
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;
    //如果不需要傳遞fd,那麼也不需要輔助資料。
    if (ch->fd == -) {
        msg.msg_control = NULL;
        msg.msg_controllen = ;
    //如果需要傳遞fd,那麼需要輔助資料,
    } else {
        //指定輔助資料位址與長度
        msg.msg_control = (caddr_t) &cmsg;
        msg.msg_controllen = sizeof(cmsg);

        //初始化cmsg結構
        ngx_memzero(&cmsg, sizeof(cmsg));

        //由于隻需要輔助資料傳遞描述符,那麼指定為描述符的大小
        cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
        //指定為套接層
        cmsg.cm.cmsg_level = SOL_SOCKET;
        //指定type為SCM_RIGHTS,表示發送和接受描述符
        cmsg.cm.cmsg_type = SCM_RIGHTS;

        /*
         * We have to use ngx_memcpy() instead of simple
         *   *(int *) CMSG_DATA(&cmsg.cm) = ch->fd;
         * because some gcc 4.4 with -O2/3/s optimization issues the warning:
         *   dereferencing type-punned pointer will break strict-aliasing rules
         *
         * Fortunately, gcc with -O1 compiles this ngx_memcpy()
         * in the same simple assignment as in the code above
         */
        //将fd拷貝到輔助資料data的第一個位元組上面
        ngx_memcpy(CMSG_DATA(&cmsg.cm), &ch->fd, sizeof(int));
    }


    msg.msg_flags = ;

    //将ngx_channel_t(将要發送消息内容)給msg_iov
    iov[].iov_base = (char *) ch;
    //并指定ngx_channel_t長度
    iov[].iov_len = size;

    //這兩項設定為空,隻有在未連接配接的時候才需要
    msg.msg_name = NULL;
    msg.msg_namelen = ;

    //指定msg_iov,并指定其數組大小為1,因為隻傳遞一個msg_iov
    msg.msg_iov = iov;
    msg.msg_iovlen = ;

    //sendmsg
    n = sendmsg(s, &msg, );

    if (n == -) {
        err = ngx_errno;
        if (err == NGX_EAGAIN) {
            return NGX_AGAIN;
        }
        ngx_log_error(NGX_LOG_ALERT, log, err, "sendmsg() failed");
        return NGX_ERROR;
    }
    return NGX_OK;
}
           

讀取資料

nginx讀資料采用套接字api,使用裡面的recvmsg發送資料。

ngx_int_t
ngx_read_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size, ngx_log_t *log)
{
    ssize_t             n;
    ngx_err_t           err;
    struct iovec        iov[];
    struct msghdr       msg;

    union {
        struct cmsghdr  cm;
        char space[CMSG_SPACE(sizeof(int))];
    } cmsg;

    //設定資料結構,讓recvmsg去填充
    iov[].iov_base = (char *) ch;
    iov[].iov_len = size;

    msg.msg_name = NULL;
    msg.msg_namelen = ;
    //因為每次發送隻有一個,是以msg_iovlen為1就可以收到
    msg.msg_iov = iov;
    msg.msg_iovlen = ;

    //設定接受輔助資料的結構,以供于填充
    msg.msg_control = (caddr_t) &cmsg;
    msg.msg_controllen = sizeof(cmsg);

    n = recvmsg(s, &msg, );
    //recvmsg出錯
    if (n == -) {
        err = ngx_errno;
        if (err == NGX_EAGAIN) {
            return NGX_AGAIN;
        }
        ngx_log_error(NGX_LOG_ALERT, log, err, "recvmsg() failed");
        return NGX_ERROR;
    }
    //沒有資料
    if (n == ) {
        ngx_log_debug0(NGX_LOG_DEBUG_CORE, log, , "recvmsg() returned zero");
        return NGX_ERROR;
    }
    //資料不夠
    if ((size_t) n < sizeof(ngx_channel_t)) {
        ngx_log_error(NGX_LOG_ALERT, log, ,
                      "recvmsg() returned not enough data: %z", n);
        return NGX_ERROR;
    }

    //解析資料,command相當于nginx自定義傳輸的指令,如果指令是打開套接字,需要擷取輔助資料擷取套接字。
    if (ch->command == NGX_CMD_OPEN_CHANNEL) {
        //如果控制資料不夠
        if (cmsg.cm.cmsg_len < (socklen_t) CMSG_LEN(sizeof(int))) {
            ngx_log_error(NGX_LOG_ALERT, log, , "recvmsg() returned too small ancillary data");
            return NGX_ERROR;
        }
        //如果輔助資料的等級或者類型不對
        if (cmsg.cm.cmsg_level != SOL_SOCKET || cmsg.cm.cmsg_type != SCM_RIGHTS)
        {
            ngx_log_error(NGX_LOG_ALERT, log, ,         
                "recvmsg() returned invalid ancillary data "
                "level %d or type %d",
                cmsg.cm.cmsg_level, cmsg.cm.cmsg_type);
            return NGX_ERROR;
        }
        //将輔助資料拷貝到ngx_channel_t,來填充ngx_channel_t的fd參數
        /* ch->fd = *(int *) CMSG_DATA(&cmsg.cm); */
        ngx_memcpy(&ch->fd, CMSG_DATA(&cmsg.cm), sizeof(int));
    }

    //如果資料被截斷,傳回錯誤
    if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC)) {
        ngx_log_error(NGX_LOG_ALERT, log, ,
                      "recvmsg() truncated data");
    }
    return n;
}
           

關閉

關閉就是關閉建立時生成的一對fd,最後普及一下基礎知識,close隻是對描述符的引用減1操作。

void
ngx_close_channel(ngx_fd_t *fd, ngx_log_t *log)
{
    if (close(fd[]) == -) {
        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, "close() channel failed");
    }

    if (close(fd[]) == -) {
        ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, "close() channel failed");
    }
}
           

總結

unix域套接字主要用于nginx中master與worker的通信,他們之間的通信用這個最合适不過了,如果用網際tcp會性能特别低。在ngx_channel.c中,還有ngx_add_channel_event函數沒有講解,主要是将套接字放到epoll中去,将在以後講解。

相關參考資料

1,UNIX網絡程式設計卷1

2,TCP/IP詳解卷3

3,man手冊

繼續閱讀