进程间文件描述符传递原理
进程中文件的管理以及fork
每个进程的文件描述符是独立的,即一个进程打开的文件描述符是记录在进程对象上的(task_struct)。
task_struct {
files_struct *files;
}
files_struct {
struct fdtable __rcu *fdt;
}
fdtable {
struct file __rcu **fd;
}
下图展示了一个进程打开3个文件时,内核 task_struct
是如何描述自己打开文件的。
下图展示了一个进程fork后,子进程和父进程各自打开1个文件后,进程中文件如何管理的。
fork如何完成文件描述符的继承
调用链:
copy_process -> copy_files -> dup_fd
将老的 遍历 老的 current->files->fdt 中的元素,然后通过get_file
将其引用计数 加 1 。
for (i = open_files; i != 0; i--) {
struct file *f = *old_fds++;
if (f) {
get_file(f);
} else {
/*
* The fd may be claimed in the fd bitmap but not yet
* instantiated in the files array if a sibling thread
* is partway through open(). So make sure that this
* fd is available to the new process.
*/
__clear_open_fd(open_files - i, new_fdt);
}
rcu_assign_pointer(*new_fds++, f);
}
上面的逻辑解释了图二中新老进程共享已打开文件的原理。2个父子关系的进程,共享文件fork前的描述符,是"天赋",下面聊聊2个独立的进程如何共享文件描述符。
两个独立进程之间如何共享文件描述符
从上面fork的例子可以猜到,两个独立的进程(或者fork后各自独立的进程)希望共享已打开的文件,那么已打开文件进程的fd1 对应的 file
告诉目标进程,目标进程创建一个fd2,然后将fd2对应的file指向fd1对应的file,然后完成文件的共享。
Linux提供和scm方式来完成上述的逻辑。
scm: Socket control messages
。
例子
先看一段例子:
逻辑很简单,一个是为了完成进程间的通信,建立的一个socketpari
,你也可以使用其他进程间通信的方式,这里采用了socketpari
。其次,通过sendmsg
发送fd,然后通过recvmsg
接收fd。
发送方的fd可能和接收到的fd值不一样。
该方式的核心在于2个点,首先,创建的socket类型是UNIX socket
,其次发送时设置msg.msg_control
,其值为struct cmsghdr
且cmptr->cmsg_type
设置成SCM_RIGHTS
,内核根据cmsg_type
值,会对发送方的fd进行转换,转换成接收到的fd,并且将接收方的fd映射为发送方fd对应的文件。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#define CONTROLLEN CMSG_LEN(sizeof(int))
int send_fd(int fd, int fd_to_send) {
struct iovec iov[1];
struct msghdr msg;
char buf[1];
struct cmsghdr *cmptr = NULL;
iov[0].iov_base = buf;
iov[0].iov_len = 1;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
cmptr = malloc(CONTROLLEN);
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;
buf[0] = 0;
printf("[father]: fd_to_send %d\n", fd_to_send);
if (sendmsg(fd, &msg, 0) != 1) {
return -1;
}
return 0;
}
int recv_fd(int fd, int *fd_to_recv) {
int nr;
char buf[1];
struct iovec iov[1];
struct msghdr msg;
struct cmsghdr *cmptr = NULL;
iov[0].iov_base = buf;
iov[0].iov_len = 1;
msg.msg_iov = iov;
msg.msg_iovlen = 1;
msg.msg_name = NULL;
msg.msg_namelen = 0;
cmptr = malloc(CONTROLLEN);
msg.msg_control = cmptr;
msg.msg_controllen = CONTROLLEN;
if(recvmsg(fd, &msg, 0) < 0) {
printf("recvmsg error\n");
return -1;
}
if(msg.msg_controllen < CONTROLLEN) {
printf("recv_fd get invalid fd\n");
return -1;
}
*fd_to_recv = *(int*)CMSG_DATA(cmptr);
printf("[child]: fd_to_recv %d\n", *fd_to_recv);
return 0;
}
int main() {
int fd;
pid_t pid;
int sockpair[2];
int status;
char fname[256];
status = socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair);
if (status < 0) {
printf("Call socketpair error, errno is %d\n", errno);
return errno;
}
pid = fork();
if (pid == 0) {
close(sockpair[1]);
status = recv_fd(sockpair[0], &fd);
if (status != 0) {
printf("[child]: recv error, errno is %d\n", status);
return status;
}
printf("[child]:write fd %d\n", fd);
status = write(fd, "123", sizeof("123") - 1);
if (status < 0) {
printf("[child]: write error, errno is %d\n", status);
return -1;
} else {
printf("[child]: append logo successfully\n");
}
close(fd);
exit(0);
}
printf("[father]: enter the filename:\n");
scanf("%s", fname);
fd = open(fname, O_RDWR | O_APPEND);
if (fd < 0) {
perror("[father]");
return -1;
}
status = send_fd(sockpair[1], fd);
if (status != 0) {
perror("[father]");
return -1;
}
close(fd);
wait(NULL);
return 0;
}
原理
sendmsg 如果是正常调用,即没有设置cmsghdr
,那么只是简单的send操作,但是如果设置了cmsghdr
值且cmptr->cmsg_type
设置成了SCM_RIGHTS
,那么会有如下一个scm_fp_copy
操作:
sendmsg->unix_stream_sendmsg->scm_send->__scm_sen->scm_fp_copy
scm_fp_copy
将获得 fd 对应 的 struct file *
保存在scm_fp_list
中。
static int scm_fp_copy(struct cmsghdr *cmsg, struct scm_fp_list **fplp)
{
......
for (i=0; i< num; i++)
{
int fd = fdp[i];
struct file *file;
if (fd < 0 || !(file = fget_raw(fd)))
return -EBADF;
*fpp++ = file;
fpl->count++;
}
....
}
接着:
unix_stream_sendmsg->unix_scm_to_skb->unix_attach_fds
接着在 unix_scm_to_skb
时,借用 skb->cb 指向scm_fp_list
。
static int unix_attach_fds(struct scm_cookie *scm, struct sk_buff *skb)
{
......
UNIXCB(skb).fp = scm_fp_dup(scm->fp);
......
}
skb是数据报文传递的载体,对于domainsocket(UNIX SOCKET),skb直接回扔给接收方,而不会进行二三层网络传输。
我们来怎么取的
unix_stream_recvmsg->unix_stream_read_generic
首先将skb中的fd拷贝到自己sock结构体中,方便后续处理
if (UNIXCB(skb).fp)
siocb->scm->fp = scm_fp_dup(UNIXCB(skb).fp);
接着调用 scm_recv->scm_detach_fds
,这是接收方的核心函数,完成了接收方进程fd的创建以及文件的映射。
void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
{
......
for (i=0, cmfptr=(__force int __user *)CMSG_DATA(cm); i<fdmax;
i++, cmfptr++)
{
struct socket *sock;
int new_fd;
err = security_file_receive(fp[i]);
if (err)
break;
//获得当前进程未使用的fd
err = get_unused_fd_flags(MSG_CMSG_CLOEXEC & msg->msg_flags
? O_CLOEXEC : 0);
if (err < 0)
break;
new_fd = err;
err = put_user(new_fd, cmfptr);
if (err) {
put_unused_fd(new_fd);
break;
}
/* Bump the usage count and install the file. */
sock = sock_from_file(fp[i], &err);
if (sock) {
sock_update_netprioidx(sock->sk);
sock_update_classid(sock->sk);
}
//当前进程fd关联 到 发送进程的file
fd_install(new_fd, get_file(fp[i]));
}
......
}