Sword 进程间传递打开的文件描述符(demo)

demo

/*进程间传递打开的文件描述符*/
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include <unistd.h>
#include<sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define CONTROLLEN CMSG_LEN(sizeof(int))

/**
 * @brief       发送目标文件描述符
 * @param s        传递信息的 UNIX 域 文件描述符
 * @param fd    待发送的文件描述符
 */
void send_fd(int s, int fd)
{
    struct iovec iov[1];
    struct msghdr msg;
    /*
    设计说明:
        buf[0]用来标记状态, buf[1]固定值为NULL
    */
    char buf[2];
    struct cmsghdr* cmptr = NULL;

    // 1.参数校验

    // 协议设置
    msg.msg_name = NULL;
    msg.msg_namelen = 0;

    // 缓冲区设置
    iov[0].iov_base = buf;
    iov[0].iov_len = sizeof(buf);
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    // 设置辅助数据
    if (fd < 0)
    {
        // 无效文件描述符,不用传递辅助数据
        msg.msg_control = NULL;
        msg.msg_controllen = 0;

        // 设置状态异常
        buf[0] = 1;
    }
    else
    {
        cmptr = malloc(CONTROLLEN);

        cmptr->cmsg_len = CONTROLLEN;
        cmptr->cmsg_level = SOL_SOCKET;
        cmptr->cmsg_type = SCM_RIGHTS;

        msg.msg_control = cmptr;
        msg.msg_controllen = CONTROLLEN;

        *(int*)CMSG_DATA(cmptr) = fd;

        // 设置状态正常
        buf[0] = 0;
    }

    if (sendmsg(s, &msg, 0) != 2)
    {
        printf("sendmsg failed .\n");
    }

    if (cmptr)
    {
        free(cmptr);
        cmptr = NULL;
    }
}


/**
 * @brief 接受文件描述符
 * @param fd 传递信息的 UNIX 域 文件描述符
 */
int recv_fd(int s)
{
    struct iovec         iov[1];
    struct msghdr        msg;
    char                 buf[2];
    struct cmsghdr     * cmptr;
    ssize_t              nr;
    int                  sfd = -1;

    // 参数校验
    do 
    {
        // 协议设置
        msg.msg_name = NULL;
        msg.msg_namelen = 0;

        // 缓冲区设置
        iov[0].iov_base = buf;
        iov[0].iov_len = sizeof(buf);
        msg.msg_iov = iov;
        msg.msg_iovlen = 1;

        // 开辟辅助数据
        cmptr = malloc(CONTROLLEN);

        msg.msg_control = cmptr;
        msg.msg_controllen = CONTROLLEN;

        nr = recvmsg(s, &msg, 0);
        if (nr < 0)
        {
            printf("recvmsg failed.\n");
            break;
        }
        else if (0 == nr)
        {
            printf("connection closed by peer.\n");
            break;
        }

        // 判断状态
        if (buf[0])
        {
            // 对方数据出错
            printf("data error.\n");
            break;
        }

        // 提取文件描述符
        if (msg.msg_controllen != CONTROLLEN)
        {
            // 接收的辅助数据太短,数据异常
            printf("no fd.\n");
            break;
        }

        sfd = *(int*)CMSG_DATA(cmptr);

    } while (0);
    
    if (cmptr)
    {
        free(cmptr);
        cmptr = NULL;
    }

    return sfd;
}

void child_proc(int s)
{
    // 子进程逻辑

    int fd = -1;

    // 1.参数校验

    do
    {
        // 2.打开一个文件描述符
        fd = open("a.txt", O_RDWR, 0666);
        if (fd < 0)
        {
            printf("open file failed.\n");
            break;
        }

        // 4.向父进程发送文件描述符
        send_fd(s, fd);

        // 睡眠
        //sleep(30);

    } while (0);

    // 释放资源
    if (fd >= 0)
    {
        close(fd);
    }

    printf("Subprocess exit.\n");

}

void father_proc(int s)
{
    // 父进程逻辑

    int rfd = -1;
    char buf[1024] = { 0 };

    // 1.参数校验

    do
    {
        // 2.读取对端文件描述符
        rfd = recv_fd(s);
        if (rfd < 0)
        {
            break;
        }

        // 3.读取文件内容
        read(rfd, buf, 1024);

        printf("father read text: %s\n", buf);

    } while (0);

    if (rfd)
    {
        close(rfd);
    }

    sleep(100);

}

void test()
{
    int ret = 0;
    int fd[2];
    pid_t pid;


    // 1.创建 unix 域通信套接字
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, fd);
    if (ret)
    {
        // 创建通信失败
        printf("socketpair failed.\n");
        return;
    }

    // 2.创建子进程
    pid = fork();

    switch (pid)
    {

    case -1:
        // fork失败
        printf("fork failed.\n");
        close(fd[0]);
        close(fd[1]);
        return;

    case 0:
        // 子进程
        close(fd[0]);
        child_proc(fd[1]);
        // 子进程退出
        exit(0);
        break;

    default:
        // 父进程
        break;
    }

    // 父进程关闭fd[1]
    close(fd[1]);

    father_proc(fd[0]);

}

int main()
{
    test();
    return 0;
}

 

注意点

关于struct cmsghdr结构体错误用法

void send_fd(int s, int fd)
{
    /*
    错误说明:
        为啥这段代码执行sendmsg会报错"Bad file descriptor"?
        问题的原因是因为将struct cmsghdr定义成栈变量,当struct cmsghdr是栈变量的时候,其长度是固定值16个字节,
        可以看出struct cmsghdr栈变量没有为数据部分预留空间,如果不用辅助数据其实也没有问题,如果用了辅助数据就有问题了,
        因为struct cmsghdr数据部分没有内存空间,通过观察发现实际上struct cmsghdr的数据部分被存放到msg_iov中了,
        因此只要你修改buf的值就会破坏struct cmsghdr数据部分,从而导致被传递的文件描述符是无效的。
    */

    struct iovec iov[1];
    struct msghdr msg;
    char buf[2];
    // 错误点①
    struct cmsghdr cm;
    ssize_t nw;

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

    cm.cmsg_len = CONTROLLEN;
    cm.cmsg_level = SOL_SOCKET;
    cm.cmsg_type = SCM_RIGHTS;
    *(int*)CMSG_DATA(&cm) = fd;
    msg.msg_control = &cm;
    msg.msg_controllen = CONTROLLEN;

    // 错误点②
    buf[0] = 1;
    buf[1] = 3;

    nw = sendmsg(s, &msg, 0);
    printf("sendmsg return %ld, error message %s\n", nw, strerror(errno));
}

关于父子进程之间通信说明

在技术上,发送进程实际上向接收进程传送一个指向一打开文件表项的指针。该指针被分配存放在接收进程的第一个可用描述符项中。
(注意,不要造成错觉,以为发送进程和接收进程中的描述符编号是相同的,通常它们是不同的。)两个进程共享同一打开文件表项,
在这一点上与fork之后,父、子进程共享打开文件表项的情况完全相同.
当发送进程将描述符传送给接收进程后,通常它关闭该描述符。发送进程关闭该描述符并不造成关闭该文件或设备,
其原因是该描述符对应的文件仍被视为由接收者进程打开(即使接收进程尚未接收到该描述符)。

posted on 2023-02-25 13:52  寒魔影  阅读(20)  评论(0编辑  收藏  举报

导航