【Linux C | 网络编程】进程间传递文件描述符socketpair、sendmsg、recvmsg详解

我们的目的是,实现进程间传递文件描述符,是指 A进程打开文件fileA,获得文件描述符为fdA,现在 A进程要通过某种方法,传递fdA,使得另一个进程B,获得一个新的文件描述符fdB,这个fdB在进程B中的作用,跟fdA在进程A中的作用一样。即在 fdB上的操作,即是对fileA的操作。

可能很多人想到的是用管道pipe来实现,但是却没想象的那么容易。

1.了解fork函数

首先需要知道,在调用 fork() 函数之前打开的文件,其文件描述符会被复制到子进程中。这是因为在 fork() 函数执行时,操作系统会创建子进程,而子进程将复制父进程的地址空间,包括文件描述符表。因此,父进程打开的文件描述符会被子进程继承并复制到子进程的文件描述符表中。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main() {
    int file_fd;
    pid_t pid;
    char buf[1024];

    // 打开文件
    file_fd = open("file.txt", O_RDONLY);
    if (file_fd == -1) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        ssize_t num_read;

        // 使用父进程打开的文件描述符操作文件
        while ((num_read = read(file_fd, buf, sizeof(buf))) > 0) {
            write(STDOUT_FILENO, buf, num_read);
        }

        if (num_read == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        close(file_fd);  // 关闭文件描述符
        _exit(EXIT_SUCCESS);  // 退出子进程
    } else {
        // 父进程
        int status;

        // 等待子进程结束
        wait(&status);

        // 关闭文件描述符
        close(file_fd);

        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出状态码:%d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程异常终止,信号编号:%d\n", WTERMSIG(status));
        }
    }

    return 0;
}

其次,在父进程调用 fork() 函数之后打开的文件,子进程不会自动继承这些新打开的文件描述符。也就是说,父进程在 fork() 之后打开的文件描述符,只在父进程的文件描述符表中有效,子进程不会获得这些文件描述符。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main() {
    int file_fd;
    pid_t pid;
    char buf[1024];

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        printf("子进程开始...\n");

        // 尝试读取父进程在fork之后打开的文件描述符(这里没有实际的文件描述符,所以操作会失败)
        ssize_t num_read = read(3, buf, sizeof(buf)); // 假设文件描述符3是父进程在fork之后打开的
        if (num_read == -1) {
            perror("子进程无法读取文件(因为没有继承父进程在fork之后打开的文件描述符)");
        } else {
            write(STDOUT_FILENO, buf, num_read);
        }

        _exit(EXIT_SUCCESS);  // 退出子进程
    } else {
        // 父进程
        int status;

        // 在fork之后打开文件
        file_fd = open("file.txt", O_RDONLY);
        if (file_fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        printf("父进程打开的文件描述符: %d\n", file_fd);

        // 等待子进程结束
        wait(&status);

        // 关闭文件描述符
        close(file_fd);

        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出状态码:%d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("子进程异常终止,信号编号:%d\n", WTERMSIG(status));
        }
    }

    return 0;
}

结论:

  • fork() 之前打开的文件描述符,子进程会继承并可以使用。
  • fork() 之后打开的文件描述符,子进程不会继承,无法直接使用这些文件描述符。

2.使用管道pipe传递文件描述符的问题

下面是使用管道pipe传递文件描述符的示例代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

#define BUF_SIZE 1024

int main() {
    int pipe_fd[2]; // 管道的文件描述符数组
    int file_fd;    // 文件的文件描述符
    pid_t pid;
    char buf[BUF_SIZE];

    // 创建管道
    if (pipe(pipe_fd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        close(pipe_fd[1]); // 关闭子进程不需要的写入端

        // 从管道中读取父进程传递的文件描述符
        int received_fd;
        read(pipe_fd[0], &received_fd, sizeof(int));

        printf("received_fd:%d\n", received_fd);


        // 使用接收到的文件描述符操作文件A
        ssize_t num_read;
        while ((num_read = read(received_fd, buf, BUF_SIZE)) > 0) {
            write(STDOUT_FILENO, buf, num_read); // 在子进程中输出文件内容
        }
        if (num_read == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        close(received_fd); // 关闭文件描述符
        close(pipe_fd[0]);  // 关闭管道读取端
        _exit(EXIT_SUCCESS); // 退出子进程
    } else {
        // 父进程
        close(pipe_fd[0]); // 关闭父进程不需要的读取端

        // 打开文件A并获取文件描述符
        file_fd = open("fileA.txt", O_RDONLY);
        if (file_fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        printf("file_fd:%d\n", file_fd);

        // 将文件描述符写入管道,传递给子进程
        if (write(pipe_fd[1], &file_fd, sizeof(int)) != sizeof(int)) {
            perror("write");
            exit(EXIT_FAILURE);
        }

        close(file_fd);    // 关闭父进程中的文件描述符
        close(pipe_fd[1]); // 关闭管道写入端
        wait(NULL);        // 等待子进程结束

        exit(EXIT_SUCCESS); // 退出父进程
    }
}

失败的原因:在代码中,父进程使用 write(pipe_fd[1], &file_fd, sizeof(int))file_fd 写入管道的写入端。这一步骤本质上是将一个整数(文件描述符)写入到管道中。在Unix-like系统中,文件描述符是进程特定的,它们不能简单地通过整数值在不同的进程之间传递。直接通过管道传递文件描述符的整数值是无效的,因为文件描述符是相对于进程的。

要正确地传递文件描述符,必须使用sendmsgrecvmsg系统调用,以及SCM_RIGHTS控制消息。这些系统调用允许在进程间传递文件描述符,而不仅仅是它们的整数值。

3.相关操作函数

3.1socketpair 函数

socketpair 函数用于在本地创建一对连接的套接字,通常用于同一主机上的进程间通信。它创建了一个双向通道,使得两个相关联的套接字可以在两个进程之间传递数据,实现全双工通信。socketpair 通常用于需要在同一台主机上的不同进程之间进行高效通信的场景。例如,父子进程间或者同一程序的不同线程之间。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
参数详解
domain:指定套接字的协议族,通常为 AF_UNIX(Unix 域套接字),用于本地进程间通信。
type:指定套接字的类型,常见的有:
    SOCK_STREAM:提供面向连接的、可靠的数据传输服务(如 TCP)。
    SOCK_DGRAM:提供无连接、不可靠的数据传输服务(如 UDP)。
protocol:指定套接字使用的协议,一般为 0,表示使用默认协议。
sv[2]:一个整型数组,用于存放创建的套接字对的文件描述符。在调用 socketpair 函数后,sv[0] 和 sv[1] 分别包含这两个相关联的套接字的文件描述符。
返回值:
成功时返回 0。
失败时返回 -1,并设置 errno 指示错误的类型。

3.2 sendmsg函数

sendmsg 函数用于向指定套接字发送消息,支持发送多块数据和控制消息(例如文件描述符)。

cpp 复制代码
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数详解
sockfd:指定发送消息的套接字描述符。
msg:
指向 struct msghdr 结构体的指针,描述了要发送的消息的详细信息,包括数据块 (iovec 结构体数组) 和控制消息 (cmsghdr 结构体)。
flags:通常为 0,用于控制消息发送的附加选项。
返回值:
成功时返回发送的字节数。
失败时返回 -1,并设置 errno 指示错误的类型。

3.3 recvmsg函数

recvmsg 函数用于从指定套接字接收消息,支持接收多块数据和控制消息(例如文件描述符)。

cpp 复制代码
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数详解
sockfd:指定接收消息的套接字描述符。
msg:
指向 struct msghdr 结构体的指针,用于存放接收到的消息的详细信息,包括数据块 (iovec 结构体数组) 和控制消息 (cmsghdr 结构体)。
flags:通常为 0,用于控制消息接收的附加选项。
返回值:
成功时返回发送的字节数。
失败时返回 -1,并设置 errno 指示错误的类型。

3.4 struct msghdr 结构体详解

cpp 复制代码
struct msghdr {
    void         *msg_name;       // 指向目标地址的指针
    socklen_t    msg_namelen;     // 目标地址的长度
    struct iovec *msg_iov;        // 数据块的数组
    size_t       msg_iovlen;      // 数据块数组的长度
    void         *msg_control;    // 控制消息的指针
    size_t       msg_controllen;  // 控制消息的长度
    int          msg_flags;       // 消息的标志
};
结构体成员:
    msg_name 和 msg_namelen:用于指定目标地址的信息,通常用于 UDP 套接字。
    msg_iov 和 msg_iovlen:用于指定数据块的数组和数组长度,支持数据的分散/聚集操作。
    msg_control 和 msg_controllen:用于处理控制消息(如文件描述符)的指针和长度。
    msg_flags:用于指定消息的标志,例如 MSG_DONTWAIT 等。

struct iovec {
    void  *iov_base; // 指向数据缓冲区的指针
    size_t iov_len;  // 数据缓冲区的长度
};
成员解释:
    iov_base:指向数据缓冲区的指针,即存放数据的内存地址。
    iov_len:数据缓冲区的长度,即缓冲区中可以传输的数据的字节数。
  • 工作原理

    • sendmsg 函数通过 msg 结构体描述要发送的数据块和控制消息。
    • recvmsg 函数从套接字接收数据,并将接收到的数据填充到 msg 结构体中指定的缓冲区中。
  • 控制消息传递

    • sendmsgrecvmsg 支持通过 msg_controlmsg_controllen 参数传递控制消息(cmsghdr 结构体),特别是用于传递文件描述符等特殊信息。
  • 应用场景

    • 进程间通信(IPC):sendmsgrecvmsg 可以在进程间传递复杂的数据结构和文件描述符,适用于需要高效数据传输和资源共享的场景。
    • 网络编程:在网络编程中,这两个函数用于向套接字发送数据和从套接字接收数据,支持分散/聚集操作和控制消息传递。

作用

  • struct iovec 结构体主要用于描述 msg_iov 参数,即 struct msghdr 结构体中的数据块数组。它允许用户指定多个数据块的位置和长度,从而实现数据的分散(scatter)和聚集(gather)操作。

3.5 writev 和 readv 函数

操作 struct iovec 结构体的系统调用主要用于实现数据的分散读(scatter)和聚集写(gather)操作,这些操作通常用于高效地传输多个数据块。

功能

  • writev 函数将多个数据块从 iov 指定的缓冲区中写入到文件描述符 fd 所指定的文件中。
  • readv 函数从文件描述符 fd 所指定的文件中读取数据,并存储到 iov 指定的多个缓冲区中。
cpp 复制代码
#include <sys/uio.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
参数:
    fd:文件描述符,表示要读写的文件。
    iov:指向 struct iovec 结构体数组的指针,描述要读写的数据块及其位置。
    iovcnt:iov 数组中元素的个数,即要读写的数据块的数量。
返回值:
成功时返回读写的字节数。
失败时返回 -1,并设置 errno 指示错误的类型。
  • 工作原理

    • writev 函数将 iov 数组中描述的多个数据块依次写入到文件描述符 fd 指定的文件中。
    • readv 函数从文件描述符 fd 指定的文件中读取数据,并将数据依次存储到 iov 数组中指定的多个缓冲区中。
  • 数据传输

    • writevreadv 函数支持数据的分散(scatter)和聚集(gather)操作,可以高效地处理多个非连续数据块的读写。
  • 应用场景

    • 在网络编程中,writevreadv 可以用于同时发送或接收多个数据块,提高数据传输的效率。
    • 在文件操作中,对于需要同时读写多个缓冲区数据的场景,如日志文件写入等,也可以使用这两个函数。

4.示例代码

将父进程中得到文件描述符传递给子进程,子进程得到该文件描述符同样可以操作该文件,实现文件描述符在进程间真正的传递。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/uio.h>

void send_fd(int socket, int fd_to_send) {
    struct msghdr msg = {0};          // 定义消息头结构体并初始化
    struct iovec iov[1];              // 定义数据块结构体数组
    struct cmsghdr *cmsg;             // 定义控制消息头指针
    char control[CMSG_SPACE(sizeof(int))]; // 控制信息缓冲区
    char dummy = '*';                 // 用于填充数据块的虚拟字符

    iov[0].iov_base = &dummy;         // 设置数据块的基址为虚拟字符的地址
    iov[0].iov_len = 1;               // 设置数据块的长度为1字节

    msg.msg_iov = iov;                // 设置消息头的数据块数组
    msg.msg_iovlen = 1;               // 设置数据块数组的长度
    msg.msg_control = control;        // 设置消息头的控制信息
    msg.msg_controllen = sizeof(control); // 设置控制信息的长度

    cmsg = CMSG_FIRSTHDR(&msg);       // 获取第一个控制消息头
    cmsg->cmsg_level = SOL_SOCKET;    // 设置控制消息级别为套接字级别
    cmsg->cmsg_type = SCM_RIGHTS;     // 设置控制消息类型为传递文件描述符
    cmsg->cmsg_len = CMSG_LEN(sizeof(int)); // 设置控制消息长度

    *((int *) CMSG_DATA(cmsg)) = fd_to_send; // 将待发送的文件描述符复制到控制消息的数据部分

    if (sendmsg(socket, &msg, 0) == -1) { // 发送消息
        perror("sendmsg");
    }
}


int recv_fd(int socket) {
    struct msghdr msg = {0};          // 定义消息头结构体并初始化
    struct iovec iov[1];              // 定义数据块结构体数组
    struct cmsghdr *cmsg;             // 定义控制消息头指针
    char control[CMSG_SPACE(sizeof(int))]; // 控制信息缓冲区
    char dummy;                       // 用于填充数据块的虚拟字符

    iov[0].iov_base = &dummy;         // 设置数据块的基址为虚拟字符的地址
    iov[0].iov_len = 1;               // 设置数据块的长度为1字节

    msg.msg_iov = iov;                // 设置消息头的数据块数组
    msg.msg_iovlen = 1;               // 设置数据块数组的长度
    msg.msg_control = control;        // 设置消息头的控制信息
    msg.msg_controllen = sizeof(control); // 设置控制信息的长度

    if (recvmsg(socket, &msg, 0) == -1) { // 接收消息
        perror("recvmsg");
        return -1;
    }

    cmsg = CMSG_FIRSTHDR(&msg);       // 获取第一个控制消息头
    unsigned char *data = CMSG_DATA(cmsg); // 获取控制消息的数据部分

    int fd = *((int *) data);         // 提取文件描述符
    return fd;                        // 返回接收到的文件描述符
}


int main() {
    int socket_pair[2]; // 套接字对
    int file_fd;        // 文件的文件描述符
    pid_t pid;
    char buf[1024];

    // 创建套接字对
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, socket_pair) == -1) {
        perror("socketpair");
        exit(EXIT_FAILURE);
    }

    // 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // 子进程
        close(socket_pair[1]); // 关闭子进程不需要的写入端

        // 从套接字中读取父进程传递的文件描述符
        int received_fd = recv_fd(socket_pair[0]);

        // 使用接收到的文件描述符操作文件A
        ssize_t num_read;
        while ((num_read = read(received_fd, buf, sizeof(buf))) > 0) {
            write(STDOUT_FILENO, buf, num_read); // 在子进程中输出文件内容
        }
        if (num_read == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        close(received_fd); // 关闭文件描述符
        close(socket_pair[0]);  // 关闭套接字读取端
        _exit(EXIT_SUCCESS); // 退出子进程
    } else {
        // 父进程
        close(socket_pair[0]); // 关闭父进程不需要的读取端

        // 在fork后打开文件并获取文件描述符
        file_fd = open("fileA.txt", O_RDONLY);
        if (file_fd == -1) {
            perror("open");
            exit(EXIT_FAILURE);
        }

        // 将文件描述符通过套接字传递给子进程
        send_fd(socket_pair[1], file_fd);

        close(file_fd);    // 关闭父进程中的文件描述符
        close(socket_pair[1]); // 关闭套接字写入端
        wait(NULL);        // 等待子进程结束

        exit(EXIT_SUCCESS); // 退出父进程
    }

    return 0;
}
相关推荐
方竞7 分钟前
Linux空口抓包方法
linux·空口抓包
stm 学习ing8 分钟前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
sun0077001 小时前
ubuntu dpkg 删除安装包
运维·服务器·ubuntu
海岛日记1 小时前
centos一键卸载docker脚本
linux·docker·centos
oi772 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
AttackingLin2 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
吃肉不能购3 小时前
Label-studio-ml-backend 和YOLOV8 YOLO11自动化标注,目标检测,实例分割,图像分类,关键点估计,视频跟踪
运维·yolo·自动化
学Linux的语莫3 小时前
Ansible使用简介和基础使用
linux·运维·服务器·nginx·云计算·ansible