socketpair系统调用及示例

好的,我们继续按照您的列表顺序,介绍下一个函数

1. 函数介绍

socketpair 是一个 Linux 系统调用,用于创建一对相互连接的匿名套接字。这对套接字就像一个双向的管道(bidirectional pipe),允许两个进程(通常是具有亲缘关系的进程,如父子进程或线程)通过套接字语义进行双向(全双工)通信。

你可以把它想象成一对连在一起的对讲机

  • 你和朋友各拿一个对讲机(sv[0] 和 sv[1])。
  • 你对你的对讲机说话,朋友能从他的对讲机里听到。
  • 朋友对他的对讲机说话,你能从你的对讲机里听到。
  • 这两个对讲机是预先配对好的,它们之间有直接的通信链路。

与 pipe 创建的单向管道不同,socketpair 创建的是双向的。虽然 socketpair 创建的是套接字,但它们是匿名的,没有关联网络地址(如 IP 和端口),只能用于本地进程间通信。


2. 函数原型

#include <sys/socket.h> // 必需

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

3. 功能

  • 创建套接字对: 请求内核创建两个未命名(匿名)的、相互连接的套接字。
  • 返回文件描述符: 将这两个套接字的文件描述符通过 sv 数组返回给调用者。
    • sv[0] 是第一个套接字的文件描述符。
    • sv[1] 是第二个套接字的文件描述符。
  • 建立连接: 这两个套接字在创建后就处于已连接状态,数据可以从 sv[0] 写入并从 sv[1] 读出,反之亦然。

4. 参数

  • int domain: 指定套接字的通信域。
    • AF_UNIX (或 AF_LOCAL): Unix 域套接字。这是 socketpair 最常用且在 POSIX 标准中唯一要求支持的域。它用于同一台主机上的进程间通信。
    • AF_INET: 在某些系统(如 Linux)上也支持,但这不是标准行为,且不常用。
  • int type: 指定套接字的类型。
    • SOCK_STREAM流式套接字。提供有序可靠双向的字节流服务。数据传输没有边界(像管道一样)。
    • SOCK_DGRAM数据报套接字。提供无连接不可靠有边界的数据报服务。每个 send/write 调用对应一个独立的消息单元。
    • (在 Linux 上,SOCK_SEQPACKET 也可能被支持,提供有序、可靠、有边界的连接)
  • int protocol: 指定具体的协议。
    • 通常设置为 0,表示使用给定 domain 和 type 的默认协议
  • int sv[2]: 这是一个包含两个整数的数组,用于接收 socketpair 调用返回的两个相互连接的套接字文件描述符
    • sv[0]: 第一个套接字的文件描述符。
    • sv[1]: 第二个套接字的文件描述符。

5. 返回值

  • 成功时: 返回 0。同时,sv[0] 和 sv[1] 被填充为有效的文件描述符。
  • 失败时: 返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EAFNOSUPPORT 不支持的地址族,EMFILE 进程打开的文件描述符已达上限,ENFILE 系统打开的文件总数已达上限,EOPNOTSUPP 指定的类型或协议在此域中不受支持,EPROTONOSUPPORT 不支持的协议等)。

6. 相似函数,或关联函数

  • pipe: 创建一个单向的匿名管道。socketpair 可以看作是 pipe 的双向版本。
  • socket / bind / connect: 用于创建和连接命名套接字(如网络套接字)。socketpair 创建的是匿名的、预先连接的套接字。
  • fork: 经常与 socketpair 结合使用,父进程和子进程各自保留套接字对的一端,用于彼此通信。
  • read / write / send / recv: 用于对 socketpair 返回的文件描述符进行数据传输。

7. 示例代码

示例 1:父子进程通过 socketpair 通信

这个经典的例子演示了如何使用 socketpair 在父进程和子进程之间建立双向通信通道。

// socketpair_fork.c
// 注意:为了编译此示例,需要链接 pthread 库: gcc -o socketpair_fork socketpair_fork.c
#include <sys/socket.h> // socketpair
#include <sys/wait.h>   // wait
#include <unistd.h>     // fork, read, write, close
#include <stdio.h>      // perror, printf
#include <stdlib.h>     // exit
#include <string.h>     // strlen

#define BUFFER_SIZE 256

int main() {
    int sv[2];          // 用于存储 socketpair 返回的两个文件描述符
    pid_t pid;
    char buf[BUFFER_SIZE];
    ssize_t bytes_read;

    // 1. 创建一对相互连接的 Unix 流式套接字
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("socketpair failed");
        exit(EXIT_FAILURE);
    }

    printf("Socket pair created: sv[0] = %d, sv[1] = %d\n", sv[0], sv[1]);

    // 2. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork failed");
        // 创建子进程失败,需要关闭已创建的套接字
        close(sv[0]);
        close(sv[1]);
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {
        // --- 子进程 ---
        // 关闭不需要的 sv[0]
        close(sv[0]);

        // 子进程通过 sv[1] 发送消息给父进程
        const char *msg_to_parent = "Hello from child process!";
        printf("Child: Sending message to parent...\n");
        if (write(sv[1], msg_to_parent, strlen(msg_to_parent)) != (ssize_t)strlen(msg_to_parent)) {
            perror("Child: write to parent failed");
            close(sv[1]);
            _exit(EXIT_FAILURE);
        }
        printf("Child: Message sent.\n");

        // 子进程通过 sv[1] 接收来自父进程的消息
        printf("Child: Waiting for message from parent...\n");
        bytes_read = read(sv[1], buf, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buf[bytes_read] = '\0'; // 确保字符串结束
            printf("Child: Received from parent: %s\n", buf);
        } else if (bytes_read == 0) {
            printf("Child: Parent closed the connection.\n");
        } else {
            perror("Child: read from parent failed");
        }

        // 子进程完成通信,关闭其套接字
        close(sv[1]);
        printf("Child: Exiting.\n");
        _exit(EXIT_SUCCESS);

    } else {
        // --- 父进程 ---
        // 关闭不需要的 sv[1]
        close(sv[1]);

        // 父进程通过 sv[0] 接收来自子进程的消息
        printf("Parent: Waiting for message from child...\n");
        bytes_read = read(sv[0], buf, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buf[bytes_read] = '\0';
            printf("Parent: Received from child: %s\n", buf);
        } else if (bytes_read == 0) {
            printf("Parent: Child closed the connection.\n");
        } else {
            perror("Parent: read from child failed");
        }

        // 父进程通过 sv[0] 发送消息给子进程
        const char *msg_to_child = "Hello from parent process!";
        printf("Parent: Sending message to child...\n");
        if (write(sv[0], msg_to_child, strlen(msg_to_child)) != (ssize_t)strlen(msg_to_child)) {
            perror("Parent: write to child failed");
        } else {
            printf("Parent: Message sent.\n");
        }

        // 父进程完成通信,关闭其套接字
        close(sv[0]);

        // 等待子进程结束
        int status;
        if (wait(&status) == -1) {
            perror("Parent: wait for child failed");
            exit(EXIT_FAILURE);
        }

        if (WIFEXITED(status)) {
            printf("Parent: Child exited with status %d.\n", WEXITSTATUS(status));
        } else {
            printf("Parent: Child did not exit normally.\n");
        }

        printf("Parent: Exiting.\n");
    }

    return 0;
}

代码解释:

  1. 调用 socketpair(AF_UNIX, SOCK_STREAM, 0, sv) 创建一对 Unix 域流式套接字。
  2. 调用 fork() 创建子进程。
  3. 子进程 (pid == 0):
    • 关闭不需要的 sv[0]
    • 通过 sv[1] 向父进程发送一条消息。
    • 通过 sv[1] 从父进程读取一条消息。
    • 通信完成后,关闭 sv[1] 并退出。
  4. 父进程 (pid > 0):
    • 关闭不需要的 sv[1]
    • 通过 sv[0] 从子进程读取一条消息。
    • 通过 sv[0] 向子进程发送一条消息。
    • 通信完成后,关闭 sv[0]
    • 调用 wait() 等待子进程结束并检查其退出状态。

示例 2:使用 socketpair 和 SOCK_DGRAM 进行有边界通信

这个例子演示了使用 SOCK_DGRAM 类型的 socketpair,它保留了消息边界。

// socketpair_dgram.c
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 256

int main() {
    int sv[2];
    char buf[BUFFER_SIZE];
    ssize_t bytes_read;

    // 创建一对 Unix 数据报套接字
    if (socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == -1) {
        perror("socketpair SOCK_DGRAM failed");
        exit(EXIT_FAILURE);
    }

    printf("SOCK_DGRAM Socket pair created: sv[0] = %d, sv[1] = %d\n", sv[0], sv[1]);

    // 通过 sv[0] 发送两条独立的消息
    const char *msg1 = "First message";
    const char *msg2 = "Second message is longer than the first one.";

    printf("Sending two messages through sv[0]...\n");
    if (write(sv[0], msg1, strlen(msg1)) != (ssize_t)strlen(msg1)) {
        perror("write msg1 failed");
        close(sv[0]);
        close(sv[1]);
        exit(EXIT_FAILURE);
    }
    if (write(sv[0], msg2, strlen(msg2)) != (ssize_t)strlen(msg2)) {
        perror("write msg2 failed");
        close(sv[0]);
        close(sv[1]);
        exit(EXIT_FAILURE);
    }
    printf("Messages sent.\n");

    // 通过 sv[1] 逐个接收消息 (保留边界)
    printf("Receiving messages through sv[1]...\n");

    bytes_read = read(sv[1], buf, BUFFER_SIZE - 1);
    if (bytes_read > 0) {
        buf[bytes_read] = '\0';
        printf("Received message 1 (%zd bytes): %s\n", bytes_read, buf);
    }

    bytes_read = read(sv[1], buf, BUFFER_SIZE - 1);
    if (bytes_read > 0) {
        buf[bytes_read] = '\0';
        printf("Received message 2 (%zd bytes): %s\n", bytes_read, buf);
    }

    // 如果再读一次,会因为没有数据而阻塞 (或返回 0 如果另一端关闭)
    // bytes_read = read(sv[1], buf, BUFFER_SIZE - 1);
    // printf("Third read returned: %zd\n", bytes_read); // Would block

    close(sv[0]);
    close(sv[1]);

    printf("Communication finished.\n");
    return 0;
}

代码解释:

  1. 调用 socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) 创建一对 Unix 域数据报套接字。
  2. 通过 sv[0] 连续发送两条长度不同的消息。
  3. 通过 sv[1] 连续调用 read 两次,每次读取一条完整的消息,展示了 SOCK_DGRAM 的消息边界特性。
  4. 如果再调用一次 read,它会因为没有更多数据而阻塞(对于阻塞套接字)。

示例 3:线程间使用 socketpair 通信

这个例子演示了如何在线程之间使用 socketpair 进行通信。

// socketpair_threads.c
// 编译时需要链接 pthread 库: gcc -o socketpair_threads socketpair_threads.c -lpthread
#define _GNU_SOURCE // 启用 GNU 扩展,如 pthread_yield
#include <sys/socket.h>
#include <pthread.h> // pthread_create, pthread_join
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 128

// 全局变量,用于在线程间共享 socketpair 文件描述符
int sv[2];

void* thread_function(void *arg) {
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;

    printf("Thread: Running...\n");

    // 线程通过 sv[1] 发送消息
    const char *msg = "Message from thread";
    printf("Thread: Sending message...\n");
    if (write(sv[1], msg, strlen(msg)) != (ssize_t)strlen(msg)) {
        perror("Thread: write failed");
        return NULL;
    }

    // 线程通过 sv[1] 接收消息
    printf("Thread: Waiting for reply...\n");
    bytes_read = read(sv[1], buffer, BUFFER_SIZE - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Thread: Received reply: %s\n", buffer);
    }

    printf("Thread: Exiting.\n");
    return NULL;
}

int main() {
    pthread_t thread;
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;

    // 创建 socketpair
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("socketpair failed");
        exit(EXIT_FAILURE);
    }

    printf("Main: Socket pair created.\n");

    // 创建线程
    if (pthread_create(&thread, NULL, thread_function, NULL) != 0) {
        perror("pthread_create failed");
        close(sv[0]);
        close(sv[1]);
        exit(EXIT_FAILURE);
    }

    // 主线程通过 sv[0] 接收消息
    printf("Main: Waiting for message from thread...\n");
    bytes_read = read(sv[0], buffer, BUFFER_SIZE - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("Main: Received from thread: %s\n", buffer);
    }

    // 主线程通过 sv[0] 发送回复
    const char *reply = "Reply from main";
    printf("Main: Sending reply...\n");
    if (write(sv[0], reply, strlen(reply)) != (ssize_t)strlen(reply)) {
        perror("Main: write reply failed");
    }

    // 等待线程结束
    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join failed");
    }

    close(sv[0]);
    close(sv[1]);
    printf("Main: Exiting.\n");

    return 0;
}

代码解释:

  1. 在 main 函数中创建 socketpair,并将返回的文件描述符存储在全局变量 sv 中。
  2. 创建一个新线程 thread
  3. 主线程:
    • 通过 sv[0] 从线程读取消息。
    • 通过 sv[0] 向线程发送回复。
    • 等待线程结束。
  4. 线程函数:
    • 通过 sv[1] 向主线程发送消息。
    • 通过 sv[1] 从主线程读取回复。
    • 退出。

重要提示与注意事项:

  1. 域的选择: 在可移植代码中,应始终使用 AF_UNIX 作为 domain 参数。
  2. 类型的选择:
    • SOCK_STREAM: 提供无边界的字节流,行为类似 pipe,但双向。
    • SOCK_DGRAM: 提供有边界的、独立的消息传递,行为类似 UDP,但本地。
  3. 与 pipe 的比较:
    • pipe 是单向的,socketpair 是双向的。
    • socketpair 提供了套接字的所有功能(如 sendrecv, 套接字选项等),而 pipe 只能用 read/write
  4. 关闭: 与管道和普通套接字一样,使用完毕后需要调用 close() 关闭两端的文件描述符。
  5. 错误处理: 始终检查返回值。失败可能由资源限制或不支持的参数引起。
  6. 用途: 常用于需要双向 IPC 的场景,特别是在 fork 之后或在多线程环境中。

总结:

socketpair 是一个强大的工具,用于在同一主机上创建一对相互连接的匿名套接字。它提供了比 pipe 更灵活的双向通信能力,并且具有套接字的所有特性。理解其参数和使用方法对于实现高效的本地进程间或线程间通信非常有帮助。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注