在进程间传递一个打开的文件描述符的能力是非常强大的。它可以导向不同的设计C/S应用的方法。它允许一个进程(通常是一个服务器)来做被请求的所
有事来打开一个文件(涉及诸如翻译一个网络名到一个网络地址、对猫拨号、为文件拿到锁,等等)并简单地把一个可以和所有I/O函数一起使用的描述符传递回
给调用进程。所有打开这个文件或设备所涉及的细节都对客户隐藏。
我们必须更明确我们说从一个进程向另一个“传递一个打开的文件描述符”的意思。回想3.10节里的图,它展示两个打开相同文件的进程。尽管它们共享同一个v-node,但是每个进程有它们自己的文件表项。
当我们从一个进程向另一个传递一个打开的文件描述时,我们想传递进程和招收进程都共享相同的文件表项。
技
术上,我们正把一个指向一个打开的文件表项的指针从一个进程传递到另一个里。这个指针被赋成接收进程里的第一个可用描述符。(说我们正传递一个打开的描述
符误导我们以为接收进程的描述符号和发送进程的相同,但通常不是这样。)让两个进程共享一个打开的文件表正是在fork之后所发生的事。
当一个描述符从一个进程传递到另一个时发生的事是,发送进程在传递完描述符后然后关闭这个描述符。被发送者关闭的描述符并不真正关闭文件或设备,因为描述符在接收进程里仍视为打开的(即使接收者还没有明确地收到这个描述符。)
我们定义以下的三个函数,我们在本章用它们发送和接收文件描述符。本节稍后,我们将展示这三个函数的代码,包括STREAMS版本和套接字版本。
- #include "apue.h"
- int send_fd(int fd, int fd_to_send);
- int send_err(int fd, int status, const char *errmsg);
- 成功返回0,错误返回-1。
- int recv_fd(int fd, ssize_t (*userfunc)(int, const void *, size_t));
- 成功返回文件描述符,错误返回负值。
一个想传递一个描述符给另一个进程的进程(通常是一个服务器)调用send_fd或send_err。等待接收描述符的进程(客户)调用recv_fd。
send_fd函数发送描述符fd_to_send,通过fd表示的STREAMS管道或UNIX域套接字。
我们将使用术语s-pipe来引用一个双向通信渠道,它可以由STREAMS管道或UNIX域流套接字实现。
send_err函数使用fd发送errmsg,接着是status字节。status的值必须是-1到-255的范围。
客
户调用recv_fd来接收一个描述符。如果成功(发送者调用send-fd),那么这个函数返回非负的描述符。否则返回值是由send_err发送的
status(一个-1到-255的负值)。此外,如果一个错误信息被一个服务器发送,那么客户的userfunc被调用来处理这个消息。
userfunc的第一个参数是常STDERR_FILENO,接着是指向错误消息的指针和它的长度。userfunc的返回值是写入的字节数据或在错误
是的一个负数。经常,客户指定普通的write函数作为userfunc。
我们实现被这三个函数使用的我们自己的协议。为了发送一个描述
符,send_fd发送两个0字节,接着是真实的描述符。为了发送一个错误,send_err发送errmsg,接着是一个0字节,再接着是status
字节的绝对值(1到255)。recv_fd函数读取在s-pipe上的所有东西,直到它碰到一个空字节。读到这个点为止所得到的所有数据都被传递给调用
者的userfunc。从recv_fd读到的下一个字节是status字节。如果status字节为0,一个描述符被返回,否则,没有描述符可接收。
函数send_err调用send_fd函数,在向s-pipe写入错误消息后。如下面的代码所示。
- /*
- * Used when we had planned to send an fd using send_fd(),
- * but encountered an error instead. We send the error back
- * using the send_fd()/recv_fd() protocol.
- */
- int
- send_err(int fd, int errcode, const char *msg)
- {
- int n;
- if ((n = strlen(msg)) > 0)
- if (writen(fd, msg, n) != n) /* send the error message */
- return(-1);
- if (errcode >= 0)
- errcode = -1; /* must be negative */
- if (send_fd(fd, errcode) < 0)
- return(-1);
- return(0);
- }
在后面两节,我们将看到send_fd和recv_fd函数的实现。
17.4.1 通过基于STREAMS的管道来传递文件描述符(Passing File Descriptors over STREAMS-based Pipes)
通过使用STREAMS管道,文件描述符使用两个ioctl命令来被交换:I_SENDFD和I_RECVFD。为了发送一个描述符,我们把ioctl的第三个参数设为真实的描述符。如下面代码所示:
- #include "apue.h"
- #include <stropts.h>
- /*
- * 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)
- {
- char
- buf[2];
- /* send_fd()/recv_fd() 2-byte protocol */
- buf[0] = 0;
- /* null byte flag to recv_fd() */
- if (fd_to_send < 0) {
- buf[1] = -fd_to_send;
- /* nonzero status means error */
- if (buf[1] == 0)
- buf[1] = 1; /* -256, etc. would screw up protocol */
- } else {
- buf[1] = 0;
- /* zero status means OK */
- }
- if (write(fd, buf, 2) != 2)
- return(-1);
- if (fd_to_send >= 0)
- if (ioctl(fd, I_SENDFD, fd_to_send) < 0)
- return(-1);
- return(0);
- }
当我们接收一个描述符时,ioctl的第三个参数是指向一个strrecvfd结构体的指针:struct strrecvfd {
int fd; /* new descriptor */ uid_t uid; /* effective user ID of
sender */ gid_t gid; /* effective group ID of sender */ char
fill[8];};recv_fd函数读取STREAMS管道,直到2字节协议的第一个字(空字节)被收到。当我们执行I_RECVFD的ioctl命
令时,下一个在流头的读队列上的消息必须是从I_SENDFD调用而来的一个描述符,否则我们得到一个错误。下面的代码显示了这个函数。
- #include "apue.h"
- #include <stropts.h>
- /*
- * Receive a file descriptor from another process (a server).
- * In addition, any data received from the server 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, nread, flag, status;
- char *ptr;
- char buf[MAXLINE];
- struct strbuf dat;
- struct strrecvfd recvfd;
- status = -1;
- for ( ; ; ) {
- dat.buf = buf;
- dat.maxlen = MAXLINE;
- flag = 0;
- if (getmsg(fd, NULL, &dat, &flag) < 0)
- err_sys("getmsg error");
- nread = dat.len;
- if (nread == 0) {
- err_ret("connection closed by server");
- return(-1);
- }
- /*
- * See if this is the final data with null & status.
- * Null must be next to last byte of buffer, status
- * byte is last byte. Zero status means there must
- * be a file descriptor to receive.
- */
- for (ptr = buf; ptr < &buf[nread]; ) {
- if (*ptr++ == 0) {
- if (ptr != &buf[nread-1])
- err_dump("message format error");
- status = *ptr & 0xFF; /* prevent sign extension */
- if (status == 0) {
- if (ioctl(fd, I_RECVFD, &recvfd) < 0)
- return(-1);
- newfd = recvfd.fd; /* new descriptor */
- } else {
- newfd = -status;
- }
- nread -= 2;
- }
- }
- if (nread > 0)
- if ((*userfunc)(STDERR_FILENO, buf, nread) != nread)
- return(-1);
- if (status >= 0) /* final data has arrived */
- return(newfd); /* descriptor, or -status */
- }
- }
17.4.2 通过UNIX域套接字来传递文件描述符(Passing File Descriptors over UNIX Domain Sockets)
为了用UNIX域套接字交换文件描述符,我们调用sendmsg和recvmsg函数(16.5节)。两个函数都接收一个指向一个msghdr结构体的指针,这个结构体包含所有我们想要发送或接收的信息。在你的系统上这个结构体可能看起来类似于:
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 */
};
前
两个元素通常用来在一个网络连接上发送数据报,而目标地址可以和各个数据报一起指定。接下来两个元素允许我们指定一个缓冲数组(分散读或集合写),正如我
们在14.7节描述的readv和writev一样。msg_flags域包含描述收到的消息的标志,如16.5节里的表汇总的一样。
两个元素处理控制信息的发送或接收。msg_control域指向一个cmsghdr(control message header)结构体,而msg_controllen域包含控制信息字节数。
struct cmsghdr {
socklen_t cmsg_len; /* data byte count, including header */
int cmsg_level; /* originating protocol */
int cmsg_type; /* protocol-specific type */
/* followed by the actual control message data */
};
为
了发送一个文件描述符,我们设置cmsg_len为cmsghdr结构体的尺寸加上一个整型(描述符)的尺寸。cmsg_level域被设为
SOL_SOCKET,而cmsg_type被设为SCM_RIGHTS,来指明我们正传递访问权利。(SCM代表socket-level
control
message。)访问权利只可以通过一个UNIX域套接字传递。描述符就存储在cmsg_type域的后面,使用宏CMSG_DATA来得到这个整型的
指针。
三个宏被用来访问控制数据,一个宏用来帮助计算用于cmsg_len的值。
- #include <sys/socket.h>
- unsigned char *CMSG_DATA(struct cmsghdr *cp);
- 返回和cmsghdr结构体相关的数据的指针。
- struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mp);
- 返回和msghdr结构体相关联的第一个cmsghdr结构体的指针,或者没有一个存在时返回NULL。
- struct cmsghdr *CMSG_NXTHDR(struct msghdr *mp, struct cmsghdr *cp);
- 给定当前cmsghdr结构体,返回和msghdr结构体相关联的下一个cmsghdr结构体的指针,或我们已经在最后一个上时返回NULL
- unsigned int CMSG_LEN(unsigned int nbytes);
- 返回为nbytes大的数据对象分配的尺寸。
CMSG_LEN宏在加上cmsghdr结构体的尺寸、为处理器架构所需的任何对齐限制做的调整、以及往上取约之后,返回存储一个尺寸为nbytes的数据对象所需的字节数。
下面的代码是UNIX域套接字版本的send_fd。
- #ifndef _GNU_SOURCE /* Linux上这个宏可以开启SCM_CREDENTIALS宏 */
- # define _GNU_SOURCE 1
- #endif
- #include <sys/socket.h>
- #include <stddef.h>
- /* size of control buffer to sned/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);
- }
在sendmsg调用里, 我们同时发送协议数据(null和状态字节)和描述符。
为了接收一个描述符,我们为一个cmsghdr结构体和一个描述符分配足够的空间,设置msg_control来指向被分配的区域,并调用recvmsg。我们使用CMSG_LEN宏来计算需要的空间量。
我们从套接字读,直到我们在最终状态字节前讲到一个空字节。所有在这个空字节之前的东西都是一个从发送者而来的错误消息。看下面的代码。
- #include <sys/socket.h> /* struct msghdr */
- #include <unistd.h>
- #define MAXLINE 4096
- /* 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 */
- /*
- * 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) {
- printf("recvmsg error\n");
- exit(1);
- } else if (nr == 0) {
- printf("connection closed by server\n");
- 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])
- printf("message format error\n");
- status = *ptr & 0xFF; /* prevent sign extension */
- if (status == 0) {
- if (msg.msg_controllen != CONTROLLEN)
- printf("status = 0 but no fd\n");
- 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 */
- }
- }
既然我们总是准备好接收一个描述符(我们在每次调用recvmsg之前设置msg_control和msg_controllen),但是仅当msg_controllen在返回时非空时我们才收到一个描述符。
当涉及到传递文件描述符时,在UNIX域套接字和STREAMS管道之间的一个区别是我们得到STREAMS管道的发送进程的身份。一些版本的UNIX域套接字提供相似的功能,但它们的接口不同。
FreeBSD 5.2.1和Linux2.4.22提供了从UNIX域套接字发送身份的支持,但是它们做法不同。Mac OS X10.3部分继承于FreeBSD,但是禁用了身份发送。Solaris 9不支持通过UNIX域套接字发送身份。
通过使用FreeBSD,身份作为一个cmsgcred结构体被发送:
#define CMGROUP_MAX 16
struct cmsgcred {
pid_t cmcred_pid; /* sender's process ID */
uid_t cmcred_uid; /* sender's real UID */
uid_t cmcred_euid; /* sender's effective UID */
gid_t cmcred_gid; /* sender's real GID */
short cmcred_ngroups; /* number of groups */
gid_t cmcred_groups[CMGROUP_MAX]; /* groups */
};
当我们传送身份时,我们只需要为cmsgcred结构体预留空间。内核会为我们填充它以阻止一个应用假装拥有一个不同的身份。
在Linux上,身份作为一个ucred结构体传送:
struct ucred {
uint32_t pid; /* sender's process ID */
uint32_t uid; /* sender's user ID */
uint32_t gid; /* sender's group ID */
}
不像FreeBSD,Linux要求我们在传送之间初始化结构体。内核会确保应用或使用对于于调用者的值或有恰当的权限来使用其它值。
下面的代码展示了更新的send_fd函数来包含发送进程的身份。
- #include <sys/socket.h>
- #include <stddef.h>
- #if defined(SCM_CREDS) /* BSD interface */
- # define CREDSTRUCT cmsgcred
- # define SCM_CREDTYPE SCM_CREDS
- #elif defined(SCM_CREDENTIALS) /* Linux interface */
- # define CREDSTRUCT ucred
- # define SCM_CREDTYPE SCM_CREDENTIALS
- #else
- # error passing credentials is
- #endif
- /* size of control buffer to sned/recv one file descriptor */
- #define RIGHTSLEN CMSG_LEN(sizeof(int))
- #define CREDSLEN CMSG_LEN(sizeof(struct CREDSTRUCT))
- #define CONTROLLEN CMSG_LEN(RIGHTSLEN + CREDSLEN)
- 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 CREDSTRUCT *credp;
- struct cmsghdr *cmp;
- 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);
- msg.msg_control = cmptr;
- msg.msg_controllen = CONTROLLEN;
- cmp = cmptr;
- cmptr->cmsg_level = SOL_SOCKET;
- cmptr->cmsg_type = SCM_RIGHTS;
- cmptr->cmsg_len = RIGHTSLEN;
- *(int *)CMSG_DATA(cmp) = fd_to_send; /* the fd to pass */
- cmp = CMSG_NXTHDR(&msg, cmp);
- cmp->cmsg_level = SOL_SOCKET;
- cmp->cmsg_type = SCM_CREDTYPE;
- cmp->cmsg_len = CREDSLEN;
- credp = (struct CREDSTRUCT *)CMSG_DATA(cmp);
- #if defined(SCM_CREDENTIALS)
- credp->uid = geteuid();
- credp->gid = getegid();
- credp->pid = getpid();
- #endif
- 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);
- }
注意我们只需要在Linux上初始化身份结构体。
下面的函数是recv_fd的一个修改版本,被称为recv_ufd,它通过一个引用的参数返回发送者的用户ID。
- #ifndef _GNU_SOURCE /* Linux上这个宏可以开启SCM_CREDENTIALS宏 */
- # define _GNU_SOURCE 1
- #endif
- #include <sys/socket.h> /* struct msghdr */
- #include <unistd.h>
- #define MAXLINE 4096
- #if defined(SCM_CREDS) /* BSD interface */
- # define CREDSTRUCT cmsgcred
- # define CR_UID cmcred_uid
- # define CREDOPT LOCAL_PEERCRED
- # define SCM_CREDTYPE SCM_CREDS
- #elif defined(SCM_CREDENTIALS) /* Linux interface */
- # define CREDSTRUCT ucred
- # define CR_UID uid
- # define CREDOPT SO_PASSCRED
- # define SCM_CREDTYPE SCM_CREDENTIALS
- #else
- # error passing credentials is
- #endif
- /* size of control buffer to send/recv one file descriptor */
- #define RIGHTSLEN CMSG_LEN(sizeof(int))
- #define CREDSLEN CMSG_LEN(sizeof(struct CREDSTRUCT))
- #define CONTROLLEN CMSG_LEN(sizeof(int))
- 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_ufd(int fd, uid_t *uidptr,
- ssize_t (*userfunc)(int, const void *, size_t))
- {
- struct cmsghdr *cmp;
- struct CREDSTRUCT *credp;
- int newfd, nr, status;
- char *ptr;
- char buf[MAXLINE];
- struct iovec iov[1];
- struct msghdr msg;
- const int on = 1;
- status = -1;
- newfd = -1;
- if (setsockopt(fd, SOL_SOCKET, CREDOPT, &on, sizeof(int)) < 0) {
- printf("setsockopt failed\n");
- return(-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) {
- printf("recvmsg error\n");
- exit(1);
- } else if (nr == 0) {
- printf("connection closed by server\n");
- 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])
- printf("message format error\n");
- status = *ptr & 0xFF; /* prevent sign extension */
- if (status == 0) {
- if (msg.msg_controllen != CONTROLLEN)
- printf("status = 0 but no fd\n");
- /* process the control data */
- for (cmp = CMSG_FIRSTHDR(&msg);
- cmp != NULL; cmp = CMSG_NXTHDR(&msg, cmp)) {
- if (cmp->cmsg_level != SOL_SOCKET)
- continue;
- switch (cmp->cmsg_type) {
- case SCM_RIGHTS:
- newfd = *(int *)CMSG_DATA(cmp);
- break;
- case SCM_CREDTYPE:
- credp = (struct CREDSTRUCT*)CMSG_DATA(cmp);
- *uidptr = credp->CR_UID;
- }
- }
- } 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 */
- }
- }
在FreeBSD上,我们指定SCM_CREDS来传送身份;在Linux上,我们使用SCM_CREDENTIALS。