Linux C 进程间高级通信

在之前学习进程间通信时,我们只接触了一些基础的进程间通信知识。例如管道通信、内存映射等,这些通信方式传值时没有任何问题,当时当传送一些特殊的内存指针或是变量就有可能出问题。由于进程之间彼此是内存隔离的,不能直接访问其他进程的内存空间,每个进程都有自己打开的文件对象或是网络套接字。当进程想把自己已经打开的文件对象分享给其他进程使用,仅通过管道传递一个文件描述符肯定是不行的,它只适用于自己。这个时候我们就可以使用一些特殊的进程通信系统调用,使其可以共享进程内的对象。

本地套接字

父进程和子进程的地址空间是隔离的,如果两个进程之间需要进行通信,那就要选择一种合适的进程间通信的手段,在本项目中,比较合适的方法是管道。除了之前所使用的 pipe 系统调用可以在父子进程间创建管道以外,还有一种方法是本地套接字。使用系统调用 socketpair 可以在父子进程间利用 socket 创建一个全双工的管道。

除此以外,本地套接字可以在同一个操作系统的两个进程之间传递文件描述符。一般 socketpair 之后会配合 fork 函数一起使用,从而实现父子进程之间的通信。从数据传递使用上面来看,本地套接字和网络套接字是完全一致的,但是本地套接字的效率更高,因为它在拷贝数据的时候不需要处理协议相关内容。

socketpair

在 Linux 中,socketpair() 函数用于创建一对相互连接的套接字。这对套接字可以用于进程间通信(IPC)。

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

int socketpair(int domain, int type, int protocol, int sv[2]);

参数说明

  • domain:

    • 指定套接字的通信域(协议族)。对于 socketpair(),通常使用 AF_UNIXAF_LOCAL,表示本地通信。对于父子进程通信必须选 AF_LOCAL。
  • type:

    • 指定套接字的类型。常见的类型包括:

      • SOCK_STREAM:流式套接字,提供可靠的双向字节流。

      • SOCK_DGRAM:数据报套接字,提供无连接的、不可靠的、固定大小的数据报。

      • SOCK_SEQPACKET:有序的、可靠的、固定大小的数据报。

      • SOCK_RAW:原始套接字,用于直接访问协议层。

  • protocol:

    • 指定使用的协议。对于 AF_UNIX通常设置为 0,表示默认协议。
  • sv[2]:

    • 一个数组,用于存储创建的两个套接字的文件描述符。sv[0]sv[1] 是一对相互连接的套接字。

返回值

  • 成功 :返回 0,并通过 sv 参数返回两个套接字的文件描述符。

  • 失败 :返回 -1,并通过 errno 设置错误码。

示例:使用socketpair进行简单进程通信

cpp 复制代码
int main() {
    int sv[2];
    pid_t pid;

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

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

    if (pid == 0) {
        // 子进程
        close(sv[0]); // 关闭父进程端的套接字
        char buffer[100];
        int n;

        // 从父进程接收数据
        if ((n = read(sv[1], buffer, sizeof(buffer))) == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        buffer[n] = '\0'; // 确保字符串以空字符结尾
        printf("Child received: %s\n", buffer);

        // 向父进程发送数据
        const char *msg = "Hello from child\n";
        if (write(sv[1], msg, strlen(msg)) == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }

        close(sv[1]); // 关闭子进程端的套接字
        exit(EXIT_SUCCESS);
    } else {
        // 父进程
        close(sv[1]); // 关闭子进程端的套接字
        const char *msg = "Hello from parent\n";

        // 向子进程发送数据
        if (write(sv[0], msg, strlen(msg)) == -1) {
            perror("write");
            exit(EXIT_FAILURE);
        }

        char buffer[100];
        int n;

        // 从子进程接收数据
        if ((n = read(sv[0], buffer, sizeof(buffer))) == -1) {
            perror("read");
            exit(EXIT_FAILURE);
        }
        buffer[n] = '\0'; // 确保字符串以空字符结尾
        printf("Parent received: %s\n", buffer);

        close(sv[0]); // 关闭父进程端的套接字
        wait(NULL);   // 等待子进程退出
    }

    return 0;
}

父子进程共享文件描述符

那么父进程向子进程到底需要传递哪些信息呢?除了传递一般的控制信息和文本信息(比如

上传)以外,需要特别注意的是需要传递已连接套接字的文件描述符。

父进程会监听特定某个 IP:PORT ,如果有某个客户端连接之后,子进程需要能够连上 accept 得到的已连接套接字的文件描述符,这样子进程才能和客户端进行通信。这种文件描述符的传递不是简单地传输一个整型数字就行了,而是需要让父子进程共享一个套接字文件对象。

但是这里会遇到麻烦,因为 accept 调用是在 fork 之后的,所以父子进程之间并不是天然地共享文件对象。倘若想要在父子进程之间共享 acccept 调用返回的已连接套接字,需要采用一些特别的手段:一方面,父子进程之间需要使用本地套接字来通信数据。另一方面需要使用 sendmsg 和 recvmsg 函数来传递数据。

相关数据类型

struct iovec

iovec 是一个结构体,用于描述分散(scatter)或聚集(gather)I/O 操作中的内存区域。它通常与 readv()writev() 等系统调用一起使用,允许程序一次性从多个内存区域读取或写入数据,而无需将数据先拷贝到一个连续的缓冲区中。这种方式可以显著提高 I/O 操作的效率。

cpp 复制代码
struct iovec {
    void  *iov_base;  // 缓冲区的起始地址
    size_t iov_len;   // 缓冲区的长度
};

字段说明

  • iov_base:

    • 类型为 void *,指向缓冲区的起始地址。这个地址可以是任意类型的指针,指向存储数据的内存区域。

    • 例如,可以是一个字符数组的地址,用于存储字符串数据。

  • iov_len:

    • 类型为 size_t,表示缓冲区的长度(以字节为单位)。

    • 这个值必须与 iov_base 指向的内存区域的实际大小一致,以避免缓冲区溢出。

使用场景

iovec 结构体通常用于以下场景:

  • 高效 I/O 操作

    • 使用 readv()writev() 等系统调用时,iovec 用于描述多个分散的内存区域。

    • 这种方式可以减少系统调用的次数和内存拷贝的开销,从而提高 I/O 操作的效率。

  • 网络编程

    • 在处理网络数据时,可以将数据直接写入或从多个缓冲区中读取,避免中间拷贝。

    • 例如,将多个消息片段一次性发送到网络套接字中,或从套接字中一次性读取多个消息片段。

  • 文件操作

    • 在处理文件时,可以将文件内容分散到多个缓冲区中,避免一次性读取或写入大块数据。

struct msghdr

msghdr 结构体是用于描述消息头的通用结构,通常与 sendmsg()recvmsg() 系统调用一起使用。它封装了消息的各个组成部分,包括数据缓冲区、辅助数据(控制信息)以及目标地址等。通过 msghdr,程序可以灵活地处理复杂的 I/O 操作,例如散列(scatter/gather)I/O 和辅助数据的传递。

cpp 复制代码
#include <sys/socket.h>

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:

    • 类型为 void *,指向目标地址或源地址。对于 sendmsg(),它是目标地址;对于 recvmsg(),它是源地址。

    • 如果套接字是已连接的(如 TCP 套接字),通常设置为 NULL0

  • msg_namelen:

    • 类型为 socklen_t,表示 msg_name 指向的地址的长度。

    • 如果 msg_nameNULL,则 msg_namelen 也应为 0

  • msg_iov:

    • 类型为 struct iovec *,指向一个 iovec 数组。每个 iovec 描述了一个数据缓冲区。
  • msg_iovlen:

    • 类型为 size_t,表示 msg_iov 数组中的元素数量。
  • msg_control:

    • 类型为 void *,指向辅助数据(控制信息)的缓冲区。

    • 辅助数据可以包含文件描述符、时间戳等协议特定的信息。

  • msg_controllen:

    • 类型为 size_t,表示辅助数据的长度。

    • 在发送时,必须正确设置 msg_controllen,以确保接收方能够正确解析辅助数据。

    • 在接收时,msg_controllen 由系统设置,表示接收到的辅助数据的长度。

  • msg_flags:

    • 类型为 int,表示消息的标志。

    • 在发送时,通常设置为 0

    • 在接收时,msg_flags 由系统设置,表示消息的实际状态(如是否为带外数据)。

使用场景

msghdr 结构体通常用于以下场景:

  • 散列(scatter/gather)I/O

    • 通过 msg_iovmsg_iovlen,可以将数据分散到多个缓冲区中,避免一次性拷贝到一个大缓冲区。

    • 提高 I/O 操作的效率,特别适用于处理大量数据。

  • 辅助数据传递

    • 通过 msg_controlmsg_controllen,可以传递文件描述符、时间戳等辅助数据。

    • 例如,使用 SCM_RIGHTS 传递文件描述符,或使用 SO_TIMESTAMP 获取时间戳。

  • 网络编程

    • 在处理网络数据时,可以将数据直接写入或从多个缓冲区中读取,避免中间拷贝。

    • 例如,将多个消息片段一次性发送到网络套接字中,或从套接字中一次性读取多个消息片段。

struct cmsghdr

cmsghdr 结构体用于描述辅助数据(控制信息),通常与 sendmsg()recvmsg() 系统调用一起使用。辅助数据可以包含各种协议特定的信息,例如文件描述符(通过 SCM_RIGHTS)、时间戳(通过 SO_TIMESTAMP)等。cmsghdr 结构体是这些辅助数据的通用头部。

cpp 复制代码
#include <sys/socket.h>

struct cmsghdr {
    size_t cmsg_len;    // 控制信息的长度(包括头部和数据)
    int    cmsg_level;  // 协议级别(例如 SOL_SOCKET)
    int    cmsg_type;   // 控制信息的类型(例如 SCM_RIGHTS)
};

字段说明

  • cmsg_len:

    • 类型为 size_t,表示控制信息的总长度,包括 cmsghdr 头部和后续的数据部分。

    • 在发送时,必须正确设置 cmsg_len,以确保接收方能够正确解析控制信息。

    • 在接收时,cmsg_len 由系统设置,表示接收到的控制信息的长度。

  • cmsg_level:

    • 类型为 int,表示控制信息的协议级别。

    • 常见的值包括:

      • SOL_SOCKET表示控制信息与套接字协议相关(例如 SCM_RIGHTS)。

      • 其他协议级别(如 IPPROTO_IPSOL_TCP)可能用于特定协议的控制信息。

  • cmsg_type:

    • 类型为 int,表示控制信息的具体类型。

    • 常见的值包括:

      • SCM_RIGHTS用于传递文件描述符。

      • SCM_CREDENTIALS用于传递进程凭证(用户ID、组ID等)。

      • 其他协议特定的类型。

辅助宏

如果存在多个控制信息,会构成一个控制信息序列,规范要求使用者绝不能直接操作控制信息序列,而是需要用一系列的 cmsg 宏来间接操作。

为了方便操作 cmsghdr 结构体,Linux 提供了一些辅助宏:

CMSG_SPACE:

  • 用于计算包含控制信息的总空间大小(包括头部和数据)。

  • CMSG_SPACE(size_t length) 计算的是 cmsghdr 头部加上 length 大小(实际的数据部分)的数据部分所需的总空间。用于设置 msg->msg_controllen,传入 cmsghdr 中数据字段大小,返回数据字段和 cmsghdr 头部大小的和。

cpp 复制代码
size_t CMSG_SPACE(size_t length);

CMSG_LEN:

  • 用于计算控制信息的长度(包括头部和数据)。用于设置 cmsg->cmsg_len
cpp 复制代码
size_t CMSG_LEN(size_t length);

CMSG_FIRSTHDR:

  • 用于获取第一个控制信息的头部指针。

  • 如果 msg_controllen 小于 sizeof(struct cmsghdr),则返回 NULL,表示没有控制信息。

  • 如果缓冲区大小足够,返回 msg_control 指向的缓冲区的起始地址,强制转换为 struct cmsghdr 类型。

cpp 复制代码
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *mhdr);

示例:设置单个控制信息

cpp 复制代码
struct msghdr msg = {0};
struct iovec iov[1];
char buffer[BUFFER_SIZE] = "Hello from parent";
struct cmsghdr *cmsg;  //注意控制信息这是一个指针,未申请内存
int *fd_ptr;

// 初始化 msghdr 结构
msg.msg_iov = iov;
msg.msg_iovlen = 1;
iov[0].iov_base = buffer;
iov[0].iov_len = strlen(buffer) + 1;

// 初始化控制信息
msg.msg_control = malloc(CMSG_SPACE(sizeof(int))); // 分配总空间大小
msg.msg_controllen = CMSG_SPACE(sizeof(int)); // 设置控制信息的总长度
cmsg = CMSG_FIRSTHDR(&msg);    // 设置存储控制信息的位置,获取第一个控制信息头部
cmsg->cmsg_level = SOL_SOCKET; 
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int)); // 设置控制信息的长度
fd_ptr = (int *)CMSG_DATA(cmsg); // 获取数据部分
*fd_ptr = fd_to_send;

CMSG_NXTHDR:

  • 用于获取下一个控制信息的头部指针。
cpp 复制代码
struct cmsghdr *CMSG_NXTHDR(struct msghdr *mhdr, struct cmsghdr *cmsg);

示例:设置多个控制信息

cpp 复制代码
    struct msghdr msg = {0};
    struct iovec iov[1];
    char buffer[BUFFER_SIZE] = "Hello from parent";
    struct cmsghdr *cmsg1, *cmsg2;
    int *fd_ptr;

    // 初始化 msghdr 结构
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    iov[0].iov_base = buffer;
    iov[0].iov_len = strlen(buffer) + 1;

    // 分配足够的空间以存储两个控制信息
    msg.msg_control = malloc(CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(int)));
    msg.msg_controllen = CMSG_SPACE(sizeof(int)) + CMSG_SPACE(sizeof(int));

    // 初始化第一个控制信息
    cmsg1 = CMSG_FIRSTHDR(&msg);
    cmsg1->cmsg_level = SOL_SOCKET;
    cmsg1->cmsg_type = SCM_RIGHTS;
    cmsg1->cmsg_len = CMSG_LEN(sizeof(int));
    fd_ptr = (int *)CMSG_DATA(cmsg1);
    *fd_ptr = fd1;

    // 初始化第二个控制信息
    cmsg2 = (struct cmsghdr *)((char *)cmsg1 + CMSG_LEN(sizeof(int)));
    cmsg2->cmsg_level = SOL_SOCKET;
    cmsg2->cmsg_type = SCM_RIGHTS;
    cmsg2->cmsg_len = CMSG_LEN(sizeof(int));
    fd_ptr = (int *)CMSG_DATA(cmsg2);
    *fd_ptr = fd2;

CMSG_DATA:

  • 用于获取控制信息的数据部分。传入 cmsghdr 的首地址,返回 cmsghdr 中的 data 字段首地址。

  • CMSG_DATA 宏的作用是计算 cmsghdr 结构体中数据部分的起始地址。它通过将 cmsghdr 结构体的地址加上 cmsghdr 头部的大小,得到数据部分的地址。

  • CMSG_DATA 宏返回一个指向数据部分的指针,类型为 void *。你可以将这个指针强制转换为所需的数据类型(例如 int *struct timeval *),然后读取或设置数据。

cpp 复制代码
void *CMSG_DATA(struct cmsghdr *cmsg);

使用场景

cmsghdr 结构体通常用于以下场景:

  • 传递文件描述符

    • 使用 SCM_RIGHTS,可以在进程间传递文件描述符。
  • 传递进程凭证

    • 使用 SCM_CREDENTIALS,可以在进程间传递用户ID、组ID等信息。
  • 协议特定的控制信息

    • 例如,传递 IP 选项或 TCP 选项。

msghdrcmsghdr 的关系

  • msghdr 是消息头的总体描述

    • msghdr 结构体描述了消息的整体结构,包括数据缓冲区、目标地址、辅助数据等。

    • 它是一个高层的描述符,用于封装消息的所有组成部分。

  • cmsghdr 是辅助数据的描述

    • cmsghdr 结构体描述了辅助数据(控制信息)的具体内容,例如文件描述符、时间戳等。

    • 它是 msghdrmsg_control 指向的缓冲区的一部分,用于描述每一块辅助数据。

具体关系

  • msghdr 包含 cmsghdr

    • msghdr 中的 msg_control 字段指向一个缓冲区,这个缓冲区中包含了多个 cmsghdr 结构体。

    • 每个 cmsghdr 结构体描述了一块辅助数据,这些辅助数据可以是文件描述符、时间戳等。

  • CMSG_FIRSTHDRCMSG_NXTHDR

    • CMSG_FIRSTHDR 宏用于从 msghdr 中获取第一个 cmsghdr

    • CMSG_NXTHDR 宏用于从当前 cmsghdr 获取下一个 cmsghdr

    • 这些宏帮助程序遍历 msg_control 缓冲区中的所有 cmsghdr 结构体。

bash 复制代码
msghdr
+-----------------------------+
| msg_name          |        |  <- 目标地址或源地址
| msg_namelen       |        |  <- 地址长度
| msg_iov           |        |  <- 数据缓冲区数组
| msg_iovlen        |        |  <- 数据缓冲区数组的长度
| msg_control       |------->|  <- 辅助数据缓冲区
| msg_controllen    |        |  <- 辅助数据缓冲区的长度
| msg_flags         |        |  <- 消息标志
+-----------------------------+

msg_control
+-----------------------------------------------+
| cmsghdr1 | cmsghdr2 | ... | cmsghdrN |        |
+-----------------------------------------------+

cmsghdr
+-----------------------------------------------+
| cmsg_len | cmsg_level | cmsg_type | cmsg_data |
+-----------------------------------------------+

sendmsg

sendmsg() 是一个用于发送消息的系统调用,它比 send()write() 更为通用和强大。它允许发送带有多种附加信息(如文件描述符)的消息,通常用于高级的套接字编程。

cpp 复制代码
#include <sys/socket.h>

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

参数说明

  • sockfd:

    • 套接字文件描述符,表示要发送消息的套接字。
  • msg:

    • 指向 struct msghdr 结构的指针,该结构定义了要发送的消息的内容和格式。struct msghdr 的定义如下:
cpp 复制代码
struct msghdr {
    void         *msg_name;       // 可选的地址
    socklen_t     msg_namelen;    // 地址长度
    struct iovec *msg_iov;        // 散列(scatter/gather)数组
    size_t        msg_iovlen;     // 散列数组中的元素数量
    void         *msg_control;    // 可选的控制信息
    size_t        msg_controllen; // 控制信息的长度
    int           msg_flags;      // 标志(通常为 0)
};
  • msg_namemsg_namelen :用于指定目标地址(例如在 UDP 套接字中)。对于已连接的套接字(如 TCP),通常设置为 NULL0

  • msg_iovmsg_iovlen :定义了要发送的数据。msg_iov 是一个指向 struct iovec 数组的指针,每个 struct iovec 包含以下内容:

cpp 复制代码
struct iovec {
    void  *iov_base;    // 数据的起始地址
    size_t iov_len;     // 数据的长度
};
  • flags:

    • 用于控制消息发送的行为。常见的标志包括:

      • MSG_DONTWAIT:非阻塞模式,即使套接字设置为阻塞模式,也会立即返回。

      • MSG_NOSIGNAL:防止发送 SIGPIPE 信号(当对方关闭连接时)。

      • MSG_EOR:表示消息结束(仅适用于某些协议)。

返回值

  • 成功:返回发送的字节数。

  • 失败 :返回 -1,并通过 errno 设置错误码。

使用场景

sendmsg() 的主要优势在于它可以同时发送多个数据块(通过 msg_iovmsg_iovlen)和控制信息(通过 msg_controlmsg_controllen)。这使得它特别适合以下场景:

  • 发送文件描述符 :通过 SCM_RIGHTS,可以在进程间传递文件描述符。

  • 发送大量数据:通过散列(scatter/gather)I/O,可以高效地发送多个内存块。

  • 高级协议控制:支持协议特定的控制信息。

recvmsg

recvmsg() 是一个用于接收消息的系统调用,与 sendmsg() 配合使用,支持从套接字接收复杂的消息。它不仅可以接收普通的数据,还可以接收控制信息(如文件描述符)。

cpp 复制代码
#include <sys/socket.h>

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

参数说明

  • sockfd:

    • 套接字文件描述符,表示要接收消息的套接字。
  • msg:

    • 指向 struct msghdr 结构的指针,该结构定义了接收消息的格式和内容。
  • msg_namemsg_namelen :用于接收发送方的地址(例如在 UDP 套接字中)。对于已连接的套接字(如 TCP),通常设置为 NULL0

  • msg_iovmsg_iovlen :定义了接收数据的缓冲区。msg_iov 是一个指向 struct iovec 数组的指针。

  • msg_controlmsg_controllen :用于接收控制信息(如辅助数据)。控制信息通常用于接收文件描述符(通过 SCM_RIGHTS)或其他协议特定的信息。

  • msg_flags :由 recvmsg() 设置,表示消息的接收状态(如是否为带外数据)。

  • flags:

    • 用于控制消息接收的行为。常见的标志包括:

      • MSG_DONTWAIT:非阻塞模式,即使套接字设置为阻塞模式,也会立即返回。

      • MSG_PEEK:查看消息但不消耗它(消息仍然保留在套接字接收队列中)。

      • MSG_WAITALL:等待直到所有请求的数据都被接收(仅适用于阻塞套接字)。

返回值

  • 成功:返回接收到的字节数。

  • 失败 :返回 -1,并通过 errno 设置错误码。

注意事项

  • 控制信息大小msg_controllen 必须足够大,以容纳控制信息。使用 CMSG_SPACE() 宏来计算所需的空间。

  • 文件描述符传递 :通过 SCM_RIGHTS 传递文件描述符时,接收端会获得一个有效的文件描述符,但发送端的文件描述符不会被关闭。

  • 协议支持recvmsg()sendmsg() 主要用于套接字编程,但某些协议(如 TCP)可能不支持某些控制信息。

  • msg_flags :接收完成后,msg_flags 会被设置为消息的实际状态(如是否为带外数据)。如果需要检查这些标志,可以在调用后检查 msg_flags 的值。

示例:父子进程使用 sendmsg 和 readmsg 进行通信

cpp 复制代码
int sendFd(int sockfd, int fdToSend){
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg)); //name->NULL namelen->0 flag->0
    
    char *buf = "hello";
    struct iovec vec[1];        //数组记录离散区域
    vec[0].iov_base = buf;
    vec[0].iov_len = 5;

    msg.msg_iov = vec;
    msg.msg_iovlen = 1;
    //以下为控制字段
    struct cmsghdr *cmsg = (struct cmsghdr*)calloc(1, CMSG_LEN(sizeof(int)));
    cmsg->cmsg_len =  CMSG_LEN(sizeof(int));
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    *(int *)CMSG_DATA(cmsg) = fdToSend;

    msg.msg_control = cmsg;
    msg.msg_controllen = CMSG_LEN(sizeof(int));
    
    int ret = sendmsg(sockfd, &msg, 0);
    ERROR_CHECK(ret, -1, "sendmsg");
    return 0;
}

int recvFd(int sockfd, int *pfdToRecv){
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    
    char buf[6] = {0};
    struct iovec vec[1];
    vec[0].iov_base = buf;
    vec[0].iov_len = 5;

    msg.msg_iov = vec;
    msg.msg_iovlen = 1;
    
    struct cmsghdr *cmsg = (struct cmsghdr *)malloc(CMSG_LEN(sizeof(int)));
    cmsg->cmsg_len = CMSG_LEN(sizeof(int));
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type = SCM_RIGHTS;
    
    msg.msg_control = cmsg;
    msg.msg_controllen = CMSG_LEN(sizeof(int));
    msg.msg_iov = vec;
    int ret = recvmsg(sockfd, &msg, 0);
    ERROR_CHECK(ret, -1, "recvmsg");

    printf("buf = %s,fd = %d\n", buf, *(int *)CMSG_DATA(cmsg));
    
    *pfdToRecv = *(int *)CMSG_DATA(cmsg);
    return 0;

}

int main(int argc, char const *argv[])
{
    int fds[2];
    char *buf = "hello world";
    socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
    if(fork()){
        close(fds[0]);
        int fdfile = open(argv[1], O_RDWR);
        printf("child fdfile = %d\n", fdfile);
        ERROR_CHECK(fdfile, -1, "open");
        write(fdfile, buf, strlen(buf));
        sendFd(fds[1], fdfile);
        wait(NULL);
    }else{
        close(fds[1]);
        int fdfile;
        recvFd(fds[0], &fdfile);
        printf("parent file = %d\n", fdfile);
        lseek(fdfile, 0, SEEK_SET);
        char buf[20];
        read(fdfile, buf, sizeof(buf));
        printf("buf is :%s\n",buf);
    }
    return 0;
}

为什么 sendmsg() 和 recvmsg() 可以共享文件描述符,而管道不行?

  • sendmsg():

    • sendmsg() 使用 SCM_RIGHTS 机制,内核会为接收进程创建一个新的文件描述符,这个文件描述符指向与发送进程相同的文件对象。

    • 接收进程可以直接使用这个新的文件描述符访问文件对象。

  • 管道:

    • 管道只能传递字节流数据,不能直接传递文件描述符。

    • 传递文件描述符的数值没有意义,因为文件描述符的数值在不同的进程中没有直接关联。

我们看一下上面程序的输出结果:

cpp 复制代码
(base) ubuntu@ubuntu:~/MyProject/processPool$ gcc shareFd.c -o shareFd
(base) ubuntu@ubuntu:~/MyProject/processPool$ ./shareFd file1
child fdfile = 3
buf = hello,fd = 4
parent file = 4
buf is :hello world

可以看到,父进程和子进程对于同一文件对象所打开的文件描述符是不同的,原因如下:

父进程首先通过 socketpair 创建了本地套接字 fds[0] 和 fds[1],创建子进程后,父进程关闭了fds[0],3号文件描述符空闲,子进程关闭了fds[1],4号文件描述符空闲。随后父进程打开了file文件,并将其共享给子进程,此时内核把空闲的3号和4号文件描述符分配了父子进程。如下图所示:

相关推荐
wmm_会飞的@鱼37 分钟前
FlexSim-汽车零部件仓库布局优化与仿真
服务器·前端·网络·数据库·数学建模·汽车
Deutsch.1 小时前
负载均衡Haproxy
运维·负载均衡·haproxy
猫猫的小茶馆1 小时前
【STM32】FreeRTOS 任务的删除(三)
java·linux·stm32·单片机·嵌入式硬件·mcu·51单片机
-XWB-1 小时前
【安全漏洞】网络守门员:深入理解与应用iptables,守护Linux服务器安全
linux·服务器·网络
不做无法实现的梦~1 小时前
mid360连接机载电脑,远程桌面连接不上的情况
运维·服务器·电脑
运维成长记2 小时前
关于linux运维 出现高频的模块认知
运维·职场和发展·云计算
kura_tsuki2 小时前
[Linux入门] Linux 远程访问及控制全解析:从入门到实战
linux·服务器·安全
lunz_fly19922 小时前
统信 UOS 安装 svn 指南
linux
Antonio9152 小时前
【Redis】Linux 配置Redis
linux·数据库·redis
张火火isgudi2 小时前
CentOS8 使用 Docker 搭建 Jellyfin 家庭影音服务器
服务器·docker·容器