傳送文件描述符是高併發網路服務編程的一種常見實現方式。 "Nebula" 高性能通用網路框架即採用了UNIX域套接字傳遞文件描述符設計和實現。本文詳細說明一下傳送文件描述符的應用。 1. TCP伺服器程式設計範式 開發一個伺服器程式,有較多的的程式設計 ...
傳送文件描述符是高併發網路服務編程的一種常見實現方式。Nebula 高性能通用網路框架即採用了UNIX域套接字傳遞文件描述符設計和實現。本文詳細說明一下傳送文件描述符的應用。
1. TCP伺服器程式設計範式
開發一個伺服器程式,有較多的的程式設計範式可供選擇,不同範式有其自身的特點和實用範圍,明瞭不同範式的特性有助於我們伺服器程式的開發。常見的TCP伺服器程式設計範式有以下幾種:
- 迭代伺服器
- 併發伺服器,每個客戶請求fork一個子進程
- 預先派生子進程,每個子進程無保護地調用accept
- 預先派生子進程,使用文件上鎖保護accept
- 預先派生子進程,使用線程互斥鎖上鎖保護accept
- 預先派生子進程,父進程向子進程傳遞套接字描述符
- 併發伺服器,每個客戶端請求創建一個線程
- 預先創建線程伺服器,使用互斥鎖上鎖保護accept
- 預先創建線程伺服器,由主線程調用accept
當系統負載較輕時,傳統的併發伺服器程式模型就夠了。相對於傳統的每個客戶一次fork設計,預先創建一個進程池或線程池可以減少進程式控制制CPU時間,大約可減少10倍以上。
某些實現允許多個子進程或線程阻塞在accept上,然而在另一些實現中,我們必須使用文件鎖、線程互斥鎖或其他類型的鎖來確保每次只有一個子進程或線程在accept。
一般來講,所有子進程或線程都調用accept要比父進程或主線程調用accept後將描述字傳遞個子進程或線程來得快且簡單。
2. Nebula 為什麼採用傳遞文件描述符方式?
Nebula框架是預先創建多進程,由Manager主進程accept後傳遞文件描述符到Worker子進程的服務模型(Nebula進程模型)。為什麼不採用像nginx那樣多線程由子線程使用互斥鎖上鎖保護accept的服務模型?而且這種服務模型的實現比傳遞文件描述符來得還簡單一些。
Nebula框架採用無鎖設計,進程之前完全不共用數據,不存在需要互斥訪問的地方。沒錯,會存在數據多副本問題,但這些多副本往往只是些配置數據,占用不了太大記憶體,與加鎖解鎖帶來的代碼複雜度及鎖開銷相比這點記憶體代價更划算也更簡單。
同一個Nebula服務的工作進程間不相互通信,採用進程和線程並無太大差異,之所以採用進程而不是線程的最重要考慮是Nebula是出於穩定性和容錯性考慮。Nebula是通用框架,完全業務無關,業務都是通過動態載入的方式或通過將Nebula鏈接進業務Server的方式來實現。Nebula框架無法預知業務代碼的質量,但可以保證在服務因業務代碼導致coredump或其他情況時,框架可以實時監控到並立刻拉起服務進程,最大程度保障服務可用性。
決定Nebula採用傳遞文件描述符方式的最重要一點是:Nebula定位是高性能分散式服務集群解決方案的基礎通信框架,其設計更多要為構建分散式服務集群而考慮。集群不同服務節點之間通過TCP通信,而所有邏輯都是Worker進程負責,這意味著節點之間通信需要指定到Worker進程,而如果採用子進程競爭accept的方式無法保證指定的子進程獲得資源,那麼第一個通信數據包將會路由錯誤。採用傳遞文件描述符方式可以很完美地解決這個問題,而且傳遞文件描述符也非常高效。
3. 文件描述符傳遞函數和數據結構
文件描述符傳遞通過調用sendmsg()函數發送,調用recvmsg()函數接收:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
這兩個函數與sendto和recvfrom函數相似,只不過可以傳輸更複雜的數據結構,不僅可以傳輸一般數據,還可以傳輸額外的數據,即文件描述符。下麵來看結構體msghdr及其相關結構體 :
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
/* iovec結構體 */
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
/* cmsghdr結構體 */
struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including header */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by unsigned char cmsg_data[]; */
};
msghdr結構成員說明:
- msg_name :即對等方的地址指針,不關心時設為NULL即可.
- msg_namelen:地址長度,不關心時設置為0即可.
- msg_iov:結構體iovec指針; iovec的成員iov_base 可以認為是傳輸正常數據時的buf,iov_len 是buf 的大小。
- msg_iovlen:iovec類型的元素的個數,每一個緩衝區的起始地址和大小由iovec類型自包含。當有n個iovec結構體時,此值為n。
- msg_control:是一個指向cmsghdr 結構體的指針,用來發送或接收控制信息。
- msg_controllen :控制信息所占用的位元組數。註意,msg_controllen與前面的msg_iovlen不同,msg_iovlen是指的由成員msg_iov所指向的iovec型的數組的元素個數,而msg_controllen,則是所有控制信息所占用的總的位元組數。
- msg_flags : 用來描述接受到的消息的性質,由調用recvmsg時傳入的flags參數設置。
為了對齊,可能存在一些填充位元組,跟不同系統的實現有關控制信息的數據部分,是直接存儲在cmsghdr結構體的cmsg_type之後的。但中間可能有一些由於對齊產生的填充位元組,由於這些填充數據的存在,對於這些控制數據的訪問,必須使用Linux提供的一些專用巨集來完成:
#include <sys/socket.h>
/* 返回msgh所指向的msghdr類型的緩衝區中的第一個cmsghdr結構體的指針。*/
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
/* 返回傳入的cmsghdr類型的指針的下一個cmsghdr結構體的指針。 */
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
/* 根據傳入的length大小,返回一個包含了添加對齊作用的填充數據後的大小。 */
size_t CMSG_ALIGN(size_t length);
/* 傳入的參數length指的是一個控制信息元素(即一個cmsghdr結構體)後面數據部分的位元組數,返回的是這個控制信息的總的位元組數,即包含了頭部(即cmsghdr各成員)、數據部分和填充數據的總和。*/
size_t CMSG_SPACE(size_t length);
/* 根據傳入的cmsghdr指針參數,返回其後面數據部分的指針。*/
size_t CMSG_LEN(size_t length);
/* 傳入的參數是一個控制信息中的數據部分的大小,返回的是這個根據這個數據部分大小,需要配置的cmsghdr結構體中cmsg_len成員的值。這個大小將為對齊添加的填充數據也包含在內。*/
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);
4. 文件描述符傳遞要點
sendmsg提供了可以傳遞控制信息的功能,要實現的傳遞描述符這一功能必須要用到這個控制信息。在msghdr變數的cmsghdr成員中,由控制頭cmsg_level和cmsg_type來設置傳遞文件描述符這一屬性,並將要傳遞的文件描述符作為數據部分,保存在cmsghdr變數的後面。這樣就可以實現傳遞文件描述符這一功能,這種情況是不需要使用msg_iov來傳遞數據的。
具體地說,為msghdr的成員msg_control分配一個cmsghdr的空間,將該cmsghdr結構的cmsg_level設置為SOL_SOCKET,cmsg_type設置為SCM_RIGHTS,並將要傳遞的文件描述符作為數據部分,調用sendmsg即可。其中SCM表示socket-level control message,SCM_RIGHTS表示我們要傳遞訪問許可權。
跟發送部分一樣,為控制信息配置好屬性,併在其後分配一個文件描述符的數據部分後,在成功調用recvmsg後,控制信息的數據部分就是在接收進程中的新的文件描述符了,接收進程可直接對該文件描述符進行操作。
文件描述符傳遞並不是將文件描述符數字傳遞,而是文件描述符對應數據結構。在主進程accept的到的文件描述符7傳遞到子進程後文件描述符有可能是7,更有可能是7以外的其他數值,但無論是什麼數值並不重要,重要的是傳遞之後的連接跟傳遞之前的連接是同一個連接。
通常在完成文件描述符傳遞後,接收進程接管文件描述符,發送進程則應調用close關閉已傳遞的文件描述符。發送進程關閉描述符並不造成關閉該文件或設備,因為該描述符對應的文件仍被視為由接收者進程打開(即使接收進程尚未接收到該描述符)。
文件描述符傳遞可經由基於STREAMS的管道,也可經由UNIX域套接字。兩種方式在《UNIX網路編程》中均有描述,Nebula採用的UNIX域套接字傳遞文件描述符。
創建用於傳遞文件描述符的UNIX域套接字用到socketpair函數:
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int d, int type, int protocol, int sv[2]);
傳入的參數sv為一個整型數組,有兩個元素。當調用成功後,這個數組的兩個元素即為2個文件描述符。一對連接起來的Unix匿名域套接字就建立起來了,它們就像一個全雙工的管道,每一端都既可讀也可寫。
5. Nebula框架中傳遞文件描述符的實現
Nebula框架的文件描述符屬於SocketChannel的基本屬性,文件描述符傳遞方法是SocketChannel的靜態方法。
文件描述符傳遞方法聲明:
static int SendChannelFd(int iSocketFd, int iSendFd, int iCodecType, std::shared_ptr<NetLogger> pLogger);
static int RecvChannelFd(int iSocketFd, int& iRecvFd, int& iCodecType, std::shared_ptr<NetLogger> pLogger);
文件描述符發送方法實現:
/**
* @brief 發送文件描述符
* @param iSocketFd 由socketpair()創建的UNIX域套接字,用於傳遞文件描述符
* @param iSendFd 待發送的文件描述符
* @param iCodecType 通信通道編解碼類型
* @param pLogger 日誌類指針
* @return errno 錯誤碼
*/
int SocketChannel::SendChannelFd(int iSocketFd, int iSendFd, int iCodecType, std::shared_ptr<NetLogger> pLogger)
{
ssize_t n;
struct iovec iov[1];
struct msghdr msg;
tagChannelCtx stCh;
int iError = 0;
stCh.iFd = iSendFd;
stCh.iCodecType = iCodecType;
union
{
struct cmsghdr cm;
char space[CMSG_SPACE(sizeof(int))];
} cmsg;
if (stCh.iFd == -1)
{
msg.msg_control = NULL;
msg.msg_controllen = 0;
}
else
{
msg.msg_control = (caddr_t) &cmsg;
msg.msg_controllen = sizeof(cmsg);
memset(&cmsg, 0, sizeof(cmsg));
cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
cmsg.cm.cmsg_level = SOL_SOCKET;
cmsg.cm.cmsg_type = SCM_RIGHTS;
*(int *) CMSG_DATA(&cmsg.cm) = stCh.iFd;
}
msg.msg_flags = 0;
iov[0].iov_base = (char*)&stCh;
iov[0].iov_len = sizeof(tagChannelCtx);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
n = sendmsg(iSocketFd, &msg, 0);
if (n == -1)
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "sendmsg() failed, errno %d", errno);
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(iError);
}
return(ERR_OK);
}
文件描述符接收方法實現:
/**
* @brief 接收文件描述符
* @param iSocketFd 由socketpair()創建的UNIX域套接字,用於傳遞文件描述符
* @param iRecvFd 接收到的文件描述符
* @param iCodecType 接收到的通信通道編解碼類型
* @param pLogger 日誌類指針
* @return errno 錯誤碼
*/
int SocketChannel::RecvChannelFd(int iSocketFd, int& iRecvFd, int& iCodecType, std::shared_ptr<NetLogger> pLogger)
{
ssize_t n;
struct iovec iov[1];
struct msghdr msg;
tagChannelCtx stCh;
int iError = 0;
union {
struct cmsghdr cm;
char space[CMSG_SPACE(sizeof(int))];
} cmsg;
iov[0].iov_base = (char*)&stCh;
iov[0].iov_len = sizeof(tagChannelCtx);
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_control = (caddr_t) &cmsg;
msg.msg_controllen = sizeof(cmsg);
n = recvmsg(iSocketFd, &msg, 0);
if (n == -1) {
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() failed, errno %d", errno);
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(iError);
}
if (n == 0) {
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() return zero, errno %d", errno);
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(ERR_CHANNEL_EOF);
}
if ((size_t) n < sizeof(tagChannelCtx))
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "rrecvmsg() returned not enough data: %z, errno %d", n, errno);
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(iError);
}
if (cmsg.cm.cmsg_len < (socklen_t) CMSG_LEN(sizeof(int)))
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() returned too small ancillary data");
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(iError);
}
if (cmsg.cm.cmsg_level != SOL_SOCKET || cmsg.cm.cmsg_type != SCM_RIGHTS)
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__,
"recvmsg() returned invalid ancillary data level %d or type %d", cmsg.cm.cmsg_level, cmsg.cm.cmsg_type);
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(iError);
}
stCh.iFd = *(int *) CMSG_DATA(&cmsg.cm);
if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC))
{
pLogger->WriteLog(neb::Logger::ERROR, __FILE__, __LINE__, __FUNCTION__, "recvmsg() truncated data");
iError = (errno == 0) ? ERR_TRANSFER_FD : errno;
return(iError);
}
iRecvFd = stCh.iFd;
iCodecType = stCh.iCodecType;
return(ERR_OK);
}
Manager進程的void Manager::CreateWorker()方法創建用於傳遞文件描述符的UNIX域套接字:
int iControlFds[2];
int iDataFds[2];
if (socketpair(PF_UNIX, SOCK_STREAM, 0, iControlFds) < 0)
{
LOG4_ERROR("error %d: %s", errno, strerror_r(errno, m_szErrBuff, 1024));
}
if (socketpair(PF_UNIX, SOCK_STREAM, 0, iDataFds) < 0)
{
LOG4_ERROR("error %d: %s", errno, strerror_r(errno, m_szErrBuff, 1024));
}
Manager進程發送文件描述符:
int iCodec = m_stManagerInfo.eCodec; // 將編解碼方式和文件描述符一同發送給Worker進程
int iErrno = SocketChannel::SendChannelFd(worker_pid_fd.second, iAcceptFd, iCodec, m_pLogger);
if (iErrno == 0)
{
AddWorkerLoad(worker_pid_fd.first);
}
else
{
LOG4_ERROR("error %d: %s", iErrno, strerror_r(iErrno, m_szErrBuff, 1024));
}
close(iAcceptFd); // 發送完畢,關閉文件描述符
Worker進程接收文件描述符:
int iAcceptFd = -1;
int iCodec = 0; // 這裡的編解碼方式在RecvChannelFd方法中獲得
int iErrno = SocketChannel::RecvChannelFd(m_stWorkerInfo.iManagerDataFd, iAcceptFd, iCodec, m_pLogger);
至此,Nebula框架的文件描述符傳遞分享完畢,下麵再看看nginx中的文件描述符傳遞實現。
6. Nginx文件描述符傳遞代碼實現
Nginx的文件描述符傳遞代碼在os/unix/ngx_channel.c文件中。
nginx中發送文件描述符代碼:
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[1];
struct msghdr msg;
#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
union {
struct cmsghdr cm;
char space[CMSG_SPACE(sizeof(int))];
} cmsg;
if (ch->fd == -1) {
msg.msg_control = NULL;
msg.msg_controllen = 0;
} else {
msg.msg_control = (caddr_t) &cmsg;
msg.msg_controllen = sizeof(cmsg);
ngx_memzero(&cmsg, sizeof(cmsg));
cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
cmsg.cm.cmsg_level = SOL_SOCKET;
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
*/
ngx_memcpy(CMSG_DATA(&cmsg.cm), &ch->fd, sizeof(int));
}
msg.msg_flags = 0;
#else
if (ch->fd == -1) {
msg.msg_accrights = NULL;
msg.msg_accrightslen = 0;
} else {
msg.msg_accrights = (caddr_t) &ch->fd;
msg.msg_accrightslen = sizeof(int);
}
#endif
iov[0].iov_base = (char *) ch;
iov[0].iov_len = size;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
n = sendmsg(s, &msg, 0);
if (n == -1) {
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中接收文件描述符代碼:
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[1];
struct msghdr msg;
#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
union {
struct cmsghdr cm;
char space[CMSG_SPACE(sizeof(int))];
} cmsg;
#else
int fd;
#endif
iov[0].iov_base = (char *) ch;
iov[0].iov_len = size;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
msg.msg_control = (caddr_t) &cmsg;
msg.msg_controllen = sizeof(cmsg);
#else
msg.msg_accrights = (caddr_t) &fd;
msg.msg_accrightslen = sizeof(int);
#endif
n = recvmsg(s, &msg, 0);
if (n == -1) {
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 == 0) {
ngx_log_debug0(NGX_LOG_DEBUG_CORE, log, 0, "recvmsg() returned zero");
return NGX_ERROR;
}
if ((size_t) n < sizeof(ngx_channel_t)) {
ngx_log_error(NGX_LOG_ALERT, log, 0,
"recvmsg() returned not enough data: %z", n);
return NGX_ERROR;
}
#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
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, 0,
"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, 0,
"recvmsg() returned invalid ancillary data "
"level %d or type %d",
cmsg.cm.cmsg_level, cmsg.cm.cmsg_type);
return NGX_ERROR;
}
/* 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, 0,
"recvmsg() truncated data");
}
#else
if (ch->command == NGX_CMD_OPEN_CHANNEL) {
if (msg.msg_accrightslen != sizeof(int)) {
ngx_log_error(NGX_LOG_ALERT, log, 0,
"recvmsg() returned no ancillary data");
return NGX_ERROR;
}
ch->fd = fd;
}
#endif
return n;
}
Nebula框架系列技術分享 之 《通過UNIX域套接字傳遞文件描述符》。 如果覺得這篇文章對你有用,如果覺得Nebula框架還可以,幫忙到Nebula的Github或碼雲給個star,謝謝。Nebula不僅是一個框架,還提供了一系列基於這個框架的應用,目標是打造一個高性能分散式服務集群解決方案。
參考資料:
- 《UNIX網路編程》
- 《UNIX環境高級編程》
- 進程間傳遞文件描述符
- linux網路編程之socket(十六):通過UNIX域套接字傳遞描述符和 sendmsg/recvmsg 函數