[Linux]进程间传递文件描述符(一看就懂)

测试代码

#include <stdio.h>
#include <sys/socket.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>

void send_fd(int pipe_send, int fd_to_send)
{
    struct msghdr msg;
    struct iovec iov[1];
    char buf[0]; // 实际运用中不会这样使用,会在堆上开辟空间,由于后续用不到,所以就随便定义一个

    //下面这一批为必要的初始化内容,必须要做。但其实做了对后续的传递文件描述符也没有什么用
    iov[0].iov_base = buf; // 不能指向null,这会导致后续的读写操作失败
    iov[0].iov_len = 1;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    struct cmsghdr cm;                   // 描述控制消息的结构体
    cm.cmsg_len = CMSG_LEN(sizeof(int)); // 控制消息的总长度,即后面CMSG_DATA数据的长度
    cm.cmsg_level = SOL_SOCKET;          // 设置控制消息的级别
    cm.cmsg_type = SCM_RIGHTS;           // 告知对方传递的是文件描述符
    *(int *)CMSG_DATA(&cm) = fd_to_send; // 指向数据部分,这里存储了文件描述符

    msg.msg_control = &cm;                      // 指向对应的控制消息
    msg.msg_controllen = CMSG_LEN(sizeof(int)); // 设置控制消息的长度

    sendmsg(pipe_send, &msg, 0); // 通过管道告诉父进程,要传递文件描述符
}

int recv_fd(int pipe_recv)
{
    /*这部分同上*/
    struct iovec iov[1];
    struct msghdr msg;
    char buf[0];

    iov[0].iov_base = buf;
    iov[0].iov_len = 1;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    struct cmsghdr cm;
    msg.msg_control = &cm;
    msg.msg_controllen = CMSG_LEN(sizeof(int));

    recvmsg(pipe_recv, &msg, 0); //msg是输出型参数,将传来的文件描述符保存在里面

    int fd_to_read = *(int *)CMSG_DATA(&cm); //获取存在msg里面的文件描述符
    printf("fd_to_read:%d\n", fd_to_read);
    return fd_to_read;
}

int main()
{
    int pipefd[2];
    // pipe(pipefd); // 这里不能使用匿名管道通信,可以自行尝试。因为pipe主要用于字节流传输,并不支持传递控制消息(如文件描述符)。
    int ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, pipefd); // 创建一对连接的套接字

    pid_t pid = fork();
    if (pid == 0) // 子进程
    {
        close(pipefd[0]);                                  // 关闭读端
        int fd_to_pass = open("test.txt", O_RDONLY, 0666); // 将test文件从子进程传递给父进程
        send_fd(pipefd[1], fd_to_pass);
        close(fd_to_pass);
        exit(0);
    }
    else // 父进程
    {
        close(pipefd[1]);            // 关闭写端
        int fd = recv_fd(pipefd[0]); // 读取子进程发送的test文件描述符
        char buf[1024] = {0};
        read(fd, buf, sizeof(buf));
        printf("father receive msg: %s from fd:%d\n", buf, fd);
        close(fd);
    }
    return 0;
}

该代码摘自《Linux高性能服务器编程》,略有改动

原理

在这里插入图片描述

  • 每一个进程对应一个文件描述符表,该文件描述符表以下标来区分不同的文件(图示忽略了默认的0、1、2fd,将其均视为了普通文件)
  • 文件描述符表中的每项均指向一个struct file结构,该结构中存储了文件的许多信息,其中最重要的就是文件的inode(表示磁盘的某一个文件)

所以进程之间传递文件描述符的方式就是将struct file结构传递给另一个进程(就是这么简单!)

我们将自顶向下,层层解密具体的传递方案

传递struct file结构

tips:下文将传递struct file结构统称为传递文件描述符(两者概念相等)

父子进程间传递

适用场合: 如多进程reactor模型中,父进程接收新链接,传递给子进程去处理。此时如果是多线程,就不会这么麻烦了,而多进程则需要将父进程接收到的新链接对应的fd传递给子进程,才能让子进程持续处理客户端请求。

对于父子进程的传递,最常见的就是pipe×),绝对不能采用管道来传递文件描述符,!!!

  • pipe 主要用于字节流传输,并不支持传递控制消息(如文件描述符)。
  • 当使用 pipe 传递的消息大小超过管道缓冲区时,会导致数据丢失或阻塞。

需要采用socketpair来构建父子进程之间的通信信道(创建的是套接字)

  • 双向通信: 创建的两个套接字是全双工的,可以同时进行读写。
  • 无须绑定: 不需要像其他套接字那样进行绑定(bind)。
  • 适用于本地进程: 通常用于本地进程间通信,不适用于跨网络的通信。

不相干进程间传递

适用场合: 进程间通信 ,在需要多个进程协作时,可以通过传递文件描述符来实现高效的进程间通信。例如,一个进程可以创建一个管道或套接字,并将其描述符传递给其他进程,以便它们可以共同通信。

采用Unix 域间套接字,与创建网络套接字方法一样socket(AF_UNIX, SOCK_STREAM, 0);

  1. 创建套接字:使用 socket() 创建一个 Unix 域套接字。
  2. 绑定套接字:使用 bind() 将套接字绑定到一个文件系统路径。
  3. 监听和接受连接 (对于服务器):如果是服务器进程,使用 listen()accept() 来等待客户端连接。
  4. 发送和接收消息:使用 send()recv() 发送和接收消息。
  5. 关闭套接字:在通信完成后,使用 close() 关闭套接字。

传递载体—struct msghdr

tips:这两个话题不讨论具体的方式,以介绍结构体为主。“文件描述符的发送方与接收方” 部分会讲述具体的方式

在了解如何传递之后,我们还需要了解到底要传什么(当然是文件描述符啦,只不过具体要传什么结构呢?)

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

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

通过如上两个系统调用来传递文件描述符

sockfd:进程之间的通信信道(文件描述符)

msg:描述要发送或接收的消息的结构体(此处可以理解为struct file结构的封装)

flag:设为0就行

struct msghdr {
    void         *msg_name;        // 目的地址(发送方的地址)
    socklen_t    msg_namelen;     // 目的地址长度
    struct iovec *msg_iov;         // 指向 iovec 结构体数组的指针
    size_t       msg_iovlen;       // iovec 数组的长度
    
    int          msg_flags;        // 发送或接收时的标志
    
    void         *msg_control;     // 控制信息的指针(如传递文件描述符)
    size_t       msg_controllen;   // 控制信息的长度
};

此处只做传递文件描述符所需的必要说明

  • msg_name:不管(设为空就行)

  • msg_namelen:不管(设为0就行)

  • msg_iov:不管(但是系统规定必须初始化,所以咱也没办法。具体初始化方案见开头测试代码)

  • msg_iovlen:不管(设为1就行)

  • msg_control: 指向控制信息的缓冲区(struct cmsghdr结构),用于传递额外信息(可以用来传递文件描述符!)

    msg.msg_control = &cm;

  • msg_controllen: 控制信息的长度(对于文件描述符就是int,下文讨论对应的CMSG_LEN宏,用来初始化该字段)

    msg.msg_controllen = CMSG_LEN(sizeof(int));

最后2个参数和下文的结构体密切相关,这2个结构体互相合作就能完成文件描述符的存储

传递载体中的核心结构struct cmsghdr

struct cmsghdr {
    socklen_t cmsg_len;   // 控制消息的总长度
    int       cmsg_level; // 控制消息的级别(如 SOL_SOCKET)
    int       cmsg_type;  // 控制消息的类型(如 SCM_RIGHTS)
    // 后面跟着控制数据
};

先介绍两个宏,这两个宏为后续操作提供了方便:

  • CMSG_DATA宏:

    用于从 struct cmsghdr 结构体中获取控制消息的数据部分的指针(说白了,指向数据部分)。
    #define CMSG_DATA(cmsg) ((unsigned char *) ((struct cmsghdr *) (cmsg) + 1))

  • CMSG_LEN宏:

    用于计算控制消息的总长度。
    #define CMSG_LEN(len) (sizeof(struct cmsghdr) + (len))

  • cmsg_len: 控制消息的总长度,包括 cmsghdr 结构体本身和后续的控制数据。可以使用CMSG_LEN(sizeof(int))来快速初始化。

  • cmsg_level: 指定控制消息的级别,通常为 SOL_SOCKET,表示这是一个套接字层的控制消息。

  • cmsg_type: 控制消息的类型,常见的值包括 SCM_RIGHTS(用于传递文件描述符)等。

文件描述符的发送方与接收方

tips:这里对于struct iovec的初始化不做讨论,因为他们为必须初始化内容,与文件描述符的传递无关。

发送方

发送方需要先初始化struct cmsghdr结构(称其对象为cm)

  1. cm.cmsg_level = SOL_SOCKET;

    设置控制消息的级别

  2. cm.cmsg_type = SCM_RIGHTS;

    告知对方传递的是文件描述符

  3. cm.cmsg_len = CMSG_LEN(sizeof(int));

    控制消息的总长度,使用宏初始化方便计算控制+数据部分总长度(int表示数据部分是文件描述符)

  4. *(int *)CMSG_DATA(&cm) = fd_to_send;

    cm中的数据字段赋值成了需要传递的文件描述符fd_to_send

  5. msg.msg_control = &cm;

    设置要发送的msghdr结构体,使其指向cm对象

  6. msg.msg_controllen = CMSG_LEN(sizeof(int));

    设置要发送的msghdr结构体的长度,使用宏方便初始化

  7. sendmsg(fd, &msg, 0);

    一切准备就绪,发送文件描述符数据到信道里!!!

接收方

接收方要做的事就比较简单了,除了必须要初始化的一些字段外,只剩下如下字段

  1. struct msghdr msg;

    接收方需要构造msg结构体,以便在recvmsg中当输出参数

  2. struct cmsghdr cm; msg.msg_control = &cm; msg.msg_controllen = CMSG_LEN(sizeof(int));

    初始化msg内部的具体参数

  3. recvmsg(fd, &msg, 0);

    通过信道接收另一个进程发来的msg信息,其中在cmsghdr结构体中就存放有需要传递的文件描述符

  4. int fd_to_read = *(int *)CMSG_DATA(&cm);

    将这个文件描述符拿出来,就可以用啦!!!!

感悟

对于多线程的文件描述符的传递,往往是十分简单的,因为他们根本不需要传递(共享同一个文件描述符,谁还需要传!)

对于多进程的文件描述符的传递,因为进程具有独立性,且是资源划分的最小单位,所以不同进程间文件描述符表不同就是意料之内的了。但是不得不说,多进程文件描述符的传递确实费时费力,这时候我就感觉到《Linux高性能服务器编程》中第8章的高效的半同步/半异步的模型的爽点了(作者管这个模型这样叫,但我总感觉还是缺点啥)

该模型采用多线程方式(进程也一样,后续我就当作进程来讨论),父进程只负责监听新链接的到来,通过IPC的方式通知其余进程,让其余进程竞争锁(我写的版本并没有加锁,目前看来好像也没啥问题)。竞争到的进程就去获取这个链接,然后对该链接所对应客户的后续操作保驾护航

我的基于多进程的高效的半同步/半异步模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值