计算机 · 2021年12月19日 0

UNIX域套接字

UNIX域套接字是同机器上IPC通信方式之一,这个机制的几点特性如下:

  • 具有和网络套接字一样的API,但是相比网络套接字更高效,因为它只拷贝数据,没有网络协议栈;
  • SOCK_DGRAM类型的UNIX域套接字和SOCK_STREAM类型套接字一样的可靠,没有丢包、乱序;
  • 可以用来传递文件描述符;

和UNIX域套接字相关的几个API:

socketpair

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

这个API创建了一对可以全双工通信的无名UNIX域套接字(实际上有些基于BSD的系统就是用UNIX域套接字来实现pipe的,就是调用socketpair函数后,把返回的第一个描述符的写关闭,把返回的第二个描述符的读关闭)。

和UNIX域套接字相关的定义:

Linux 3.2.0和Solaris 10中sockaddr_un的定义:

struct sockaddr_un {
	sa_family_t sun_family;		/*AF_UNIX*/
	char	sun_path[108];		/*pathname*/
};

FreeBSD 8.0和Mac OS X 10.6.8中的定义:

struct sockaddr_un {
	unsigned char sun_len;		/*sockaddr length*/
	sa_family_t	sun_family;	/*AF_UNIX*/
	char		sun_path[104];	/*pathname*/
};
  • TCP/IP协议通过sockaddr_in指定IP地址和端口,而UNIX域套接字通过sockaddr_un指定地址;
  • sun_path指定了代表该UNIX域套接字的一个S_IFSOCK类型文件的位置,当我们试图将一个套接字bind到指定sun_path时,如果该位置已经存在了一个文件,那么绑定就会失败。在套接字被关闭后,该S_IFSOCK类型文件并不会被自动移除,需要我们显示执行unlink才行;
  • 使用UNIX域套接字的客户端如果不显示bind一个地址的话,那么服务端获取到的客户端的地址的sun_path是空的,这和TCP/IP协议会给客户端自动分配端口是不同的;
  • sun_path可以设置为绝对路径或者相对路径,所以客户端在连接服务端时要确认它所连接的地址是否和服务端bind的是同一个地址,而不只是字面上的sun_path相同;
  • 由于sockaddr_un在不同系统中定义可能不同,所以bind地址时的代码最好按如下所示书写:
	size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);
	if (bind(fd, (struct sockaddr *)&un, size) < 0)
		err_sys("bind failed");

这里主要使用了offsetof这个来自于stddef.h的宏:#define offsetof(TYPE, MEMBER) ((int)&((TYPE *)0)->MEMBER)

  • 我们在设置UNIX域套接字的地址时,是不需要填充sun_len这个字段的(前提是如果这个字段真的存在的话),系统会根据我们调用bind参数时填的size参数去推导填充这个字段;
  • bind的最后一个参数是不包含UNIX域套接字对应路径末尾表示字符串结束的0的;当accept接受客户端连接的时候,其最后一个参数返回的值也是没有包含末尾的0的(因为我们bind的时候就没有把末尾那个0字节给拷贝进去),所以对于返回的客户端sun_path要注意在其后面添加0字节使用;

使用UNIX套接字传递文件描述符

UNIX套接字的一个特殊之处在于可以用它传递文件描述符,这个传递文件描述符并不是传递文件描述符这个数字,而是说接收文件描述符的一方会使用其可用的最小文件描述符来指向发送方传递的文件描述符对应的file table entry
UNIX套接字传递文件描述符的功能是通过CMSG的形式提供给应用程序的,为此我们需要熟悉以下api和相关定义:

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

struct msghdr {
	void	*msg_name;	/* optional address */
	socklen_t msg_namelen;	/* address size in bytes */
	struct iovec *msg_iov;	/* array of I/O buffers */
	int	msg_iovlen;	/* number of elements in array */
	void 	*msg_control;	/* ancillary data */
	socklen_t msg_controllen;	/* number of ancillary bytes */
	int 	msg_flags;	/* flags for received message */
};

struct cmsghdr {
	socklen_t	cmsg_len;	/* data byte count, including header */
	int		cmsg_level;	/* originating protocol	*/
	int		cmgs_type;	/* protocol-specific type */
	/* followed by the actual control message data */
};

/* 和CMSG相关的几个宏 */

#include <sys/socket.h>

// 返回cmsghdr结构体cp中数据的位置
// Returns: pointer to data associated with cmsghdr structure
unsigned char *CMSG_DATA(struct cmsghdr *cp);

// 返回和msghdr结构体mp关联的第一个cmsghdr
// Returns: pointer to the first cmsghdr structure associated with the msghdr 
// structure, or NULL if none exists
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);

// 返回和msghdr结构体mp关联的、且位于mp结构体之后的cmsghdr结构体的位置
// Returns: pointer to the next cmsghdr structure associated with the msghdr 
// structure given the current cmsghdr structure, or NULL if we're at the last one
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mp, struct cmsghdr *cp);

// 用于计算想要在cmsghdr结构体中存储n个字节的数据需要为这个结构体开辟多少字节的内存空间
// Returns: size to allocate for data object nbytes large
unsigned int CMSG_LEN(unsigned int nbytes);

对于发送文件描述符的一方来说,需要做的事情有:

  • 将文件描述符存到cmsghdr的data字段里,用int类型存;
  • 设置cmsghdrcmsg_levelSOL_SOCKET
  • 设置cmsghdrcmsg_typeSCM_RIGHTS
  • 设置cmsghdrcmsg_len为int类型长度;
  • 将cmsg用sendmsg方法发送;

可以参考apiue中发送文件描述符的示例代码:

#include "apue.h"
#include <sys/socket.h>

/* size of control buffer to send/recv one file descriptor */
#define	CONTROLLEN	CMSG_LEN(sizeof(int))

static struct cmsghdr	*cmptr = NULL;	/* malloc'ed first time */

/*
 * Pass a file descriptor to another process.
 * If fd<0, then -fd is sent back instead as the error status.
 */
int
send_fd(int fd, int fd_to_send)
{
	struct iovec	iov[1];
	struct msghdr	msg;
	char			buf[2];	/* send_fd()/recv_fd() 2-byte protocol */

	iov[0].iov_base = buf;
	iov[0].iov_len  = 2;
	msg.msg_iov     = iov;
	msg.msg_iovlen  = 1;
	msg.msg_name    = NULL;
	msg.msg_namelen = 0;

	if (fd_to_send < 0) {
		msg.msg_control    = NULL;
		msg.msg_controllen = 0;
		buf[1] = -fd_to_send;	/* nonzero status means error */
		if (buf[1] == 0)
			buf[1] = 1;	/* -256, etc. would screw up protocol */
	} else {
		if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)
			return(-1);
		cmptr->cmsg_level  = SOL_SOCKET;
		cmptr->cmsg_type   = SCM_RIGHTS;
		cmptr->cmsg_len    = CONTROLLEN;
		msg.msg_control    = cmptr;
		msg.msg_controllen = CONTROLLEN;
		*(int *)CMSG_DATA(cmptr) = fd_to_send;		/* the fd to pass */
		buf[1] = 0;		/* zero status means OK */
	}

	buf[0] = 0;			/* null byte flag to recv_fd() */
	if (sendmsg(fd, &msg, 0) != 2)
		return(-1);
	return(0);
}

对于接收文件描述符的一方来说,需要做的事情有:

  • 使用recvmsg接收数据,并且检查是否有收到cmsg数据,检查该cmsg数据的cmsg_levelcmsg_type,是否就是SOL_SOCKETSCM_RIGHTS
  • 需要注意的一点是recvmsg在接收时需要提前申请好用于保存cmsg的空间,并且需要将msg_control指向该空间和设置msg_controllen的值;

参考apiue中的示例代码:

#include "apue.h"
#include <sys/socket.h>		/* struct msghdr */

/* size of control buffer to send/recv one file descriptor */
#define	CONTROLLEN	CMSG_LEN(sizeof(int))

#ifdef LINUX
#define RELOP <
#else
#define RELOP !=
#endif

static struct cmsghdr	*cmptr = NULL;		/* malloc'ed first time */

/*
 * Receive a file descriptor from a server process.  Also, any data
 * received is passed to (*userfunc)(STDERR_FILENO, buf, nbytes).
 * We have a 2-byte protocol for receiving the fd from send_fd().
 */
int
recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t))
{
	int				newfd, nr, status;
	char			*ptr;
	char			buf[MAXLINE];
	struct iovec	iov[1];
	struct msghdr	msg;

	status = -1;
	for ( ; ; ) {
		iov[0].iov_base = buf;
		iov[0].iov_len  = sizeof(buf);
		msg.msg_iov     = iov;
		msg.msg_iovlen  = 1;
		msg.msg_name    = NULL;
		msg.msg_namelen = 0;
		if (cmptr == NULL && (cmptr = malloc(CONTROLLEN)) == NULL)
			return(-1);
		msg.msg_control    = cmptr;
		msg.msg_controllen = CONTROLLEN;
		if ((nr = recvmsg(fd, &msg, 0)) < 0) {
			err_ret("recvmsg error");
			return(-1);
		} else if (nr == 0) {
			err_ret("connection closed by server");
			return(-1);
		}

		/*
		 * See if this is the final data with null & status.  Null
		 * is next to last byte of buffer; status byte is last byte.
		 * Zero status means there is a file descriptor to receive.
		 */
		for (ptr = buf; ptr < &buf[nr]; ) {
			if (*ptr++ == 0) {
				if (ptr != &buf[nr-1])
					err_dump("message format error");
 				status = *ptr & 0xFF;	/* prevent sign extension */
 				if (status == 0) {
					if (msg.msg_controllen RELOP CONTROLLEN)
						err_dump("status = 0 but no fd");
					newfd = *(int *)CMSG_DATA(cmptr);
				} else {
					newfd = -status;
				}
				nr -= 2;
			}
		}
		if (nr > 0 && (*userfunc)(STDERR_FILENO, buf, nr) != nr)
			return(-1);
		if (status >= 0)	/* final data has arrived */
			return(newfd);	/* descriptor, or -status */
	}
}

在Linux中,还可以用UNIX域套接字来传递credentials,相应的cmsg_typeSCM_CREDENTIALS

struct ucred {
	pid_t	pid;	/* sender's process ID */
	uid_t 	uid;	/* sender's user ID */
	gid_t	gid;	/* sender's group ID */
};

示例代码就不贴了,直接查阅apiue的源码即可,从中还可以学到CMSG_NXTHDR这个宏的用法。