自强不息,厚德载物。
分类: LINUX
2015-01-18 01:02:19
本文的主要内容有:1. Linux为什么能在进程间传递SOCKET文件描述?让读者知其然,更知其所以然。2. 怎么样传输和传输中应该注意哪些问题,为什么。3. 结合上篇《初探SSL编程》讲讲如果是SSL的socket该怎么解决问题。
其实,不管是Linux还是windows都会有自己的进程空间,虽然Linux的子进程会自动继承父进程已经打开的文件描述符,但是在实际应用中,可能需要在fork子进程之后再传递打开的文件描述符。(我的下一篇文章就会分析为什么在我们的产品中不能通过每一个socket连接之后fork一个进程来进行通信)。今天我们就讲讲怎样在fork子进程之后再传递打开的socket文件描述符,最常见的就是accept函返回的SOCKET FD.(注意其他文件描述也是可以的)
首先,我们要先弄清楚为什么能传递?,众所周知,SOCKET描述也是文件描述符,只不过该文件描述所申请的文件是在网络文件系统的空间申请的,下面来看看调用下面这个Linux系统函数过程中,怎样分配SOCKET描述符和文件的,就弄清楚为什么能传递这个问题了。
int server_fd = socket(AF_INET, SOCK_STREAM, 0 );
这个函数会调用系统内核中的sys_socket(),而在这个函数中在初始化struct socket 和struct sock之后,就会调用sock_map_fd函数,来分配socket所用的文件描述符和文件空间。下面看看这个函数:
static int sock_map_fd(struct socket *sock, int flags)
{
struct file *newfile;
int fd = get_unused_fd_flags(flags);
if (unlikely(fd < 0))
return fd;
newfile = sock_alloc_file(sock, flags, NULL);
if (likely(!IS_ERR(newfile))) {
fd_install(fd, newfile);
return fd;
}
put_unused_fd(fd);
return PTR_ERR(newfile);
}
首先是从文件系统中分配一个文件描述符fd,至于怎么分配文件描述符不是本文重点,具体请看我的博客《Linux位图算法》。分配好文件描述符之后,就要调用sock_alloc_file到网络空间分配文件,这个函数较多, 我们只看关键代码两行就够了:
sock->file = file;
file->private_data = sock;
看到这里,我们就会有这么一个逻辑,原来对于没有socket文件描述符都打开了一个文件,系统在使用socket接口进行操作的时候,都要通过这个文件来获取socket结构,并对其相应的操作,那么我们只要有文件描述符,就可以中file结构中的private_data字段获取socket结构。而文件系统是个进程所共有的,所以在Linux中同意主机上的socket文件描述符是可以传递。第一个问题解决。
其次,第二个问题,怎样传输,这里只告诉传输方法和平时应用的时候注意哪些,为什么等,而不涉及内核是怎样支持其传递,因涉及的内容太多。(后面我的Linux网络通信源码分析系列都会讲清楚,敬请期待。),
第一步: 首先在父子进程间建立SOCKET Unix域连接作为消息传递的通道,这样父子进程就可以通信了,
第二步,假设accept函数返回的SOCKET描述符是client_fd,调用dup_fd =dup(client_fd) ;然后将dup_fd通过sendmsg函数发送子进程,关闭client_fd,close(client_fd);为什么需要这么做,而不是直接就把client_fd发送过去?1在发往子进程网络的过程,描述符依然还是可以接收数据,这时候父进程select捕捉到了该事件,应该读取数据,而导致子进程捕捉不到该事件,而没法读到数据。导致数据漏读。如果是先dup一个出来把原来的关闭,等到dup_fd到了子进程之后,即可相应该事件。
第三步,子进程通过recvmsg接收到dup_fd之后,也得调用 server_fd= dup(dup_fd),然后关闭dup_fd,close(dup_fd);为什么?同样是在传输中接收到数据,这样dup_fd没法捕捉到。dup之后就会获取响应事件。
接下来看看sendmsg和recvmsg函数,以及需要注意的地方:
#include
#include
int sendmsg(int s, const struct msghdr *msg, unsigned int flags);
int recvmsg(int s, struct msghdr *msg, unsigned int flags);
struct msghdr {
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags;
};
struct iovec {
ptr_t iov_base; /* Starting address */
size_t iov_len; /* Length in bytes */
};
struct cmsghdr {
socklen_t cmsg_len;
int cmsg_level;
int cmsg_type;
/* u_char cmsg_data[]; */
};
咋一看, 这文件描述符放哪儿呢?看看如下伪代码:
struct iovec iov[1];
// 有些系统使用的是旧的msg_accrights域来传递描述符,Linux下是新的msg_control字段
#ifdef HAVE_MSGHDR_MSG_CONTROL
union{
struct cmsghdr cm;
char control[CMSG_SPACE(sizeof(int))];
}control_un;
// 设置辅助缓冲区和长度
msg.msg_control = control_un.control;
msg.msg_controllen = sizeof(control_un.control);
// 只需要一组附属数据就够了,直接通过CMSG_FIRSTHDR取得
struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg);
// 设置必要的字段,数据和长度
cmptr->cmsg_len = CMSG_LEN(sizeof(int)); // fd类型是int,设置长度
cmptr->cmsg_level = SOL_SOCKET;
cmptr->cmsg_type = SCM_RIGHTS; // 指明发送的是描述符
//数据缓冲区,至少一个1个字节
iov[0].iov_base = ptr;
iov[0].iov_len = nbytes;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
*((int*)CMSG_DATA(cmptr)) = dup_fd; // 把dup出来的fd写入辅助数据中
至于子进程代码,这里就不多说了,各位可以根据父进程代码,自己去写就OK了,看看需要注意的地方:
msghdr 的 msg_control 缓冲区必须与 cmghdr 结构对齐,可以看到我们使用了现使用了一个 union 结构来保证这一点。
数据缓冲区中至少要有一个字节的数据。
最后我们用一张时序图来表示SSL套接口的传输,看看我们是怎么解决问题的。
HazeC2017-01-15 22:50:07
博主 你好
“假设accept函数返回的SOCKET描述符是client_fd,调用dup_fd =dup(client_fd) ;然后将dup_fd通过sendmsg函数发送子进程,关闭client_fd,close(client_fd);为什么需要这么做,而不是直接就把client_fd发送过去?1在发往子进程网络的过程,描述符依然还是可以接收数据,这时候父进程select捕捉到了该事件,应该读取数据,而导致子进程捕捉不到该事件,而没法读到数据。导致数据漏读。如果是先dup一个出来把原来的关闭,等到dup_fd到了子进程之后,即可相应该事件。”
为啥dup之后才能收到呢?
dup不是只把描述符复制一份么?