进程、线程、进程间通信Unix Domain Sockets (UDS)

进程、线程、UDS

  • 进程和线程
  • 进程间通信
  • [Unix Domain Sockets (UDS)](#Unix Domain Sockets (UDS))
  • 补充
    • [socketpair() 函数](#socketpair() 函数)
    • [struct msghdr 结构体](#struct msghdr 结构体)
    • [struct iovec](#struct iovec)
    • [struct cmsghdr](#struct cmsghdr)
    • [struct iovec 、struct msghdr 、struct cmsghdr三者关系](#struct iovec 、struct msghdr 、struct cmsghdr三者关系)
    • [sendmsg() 函数](#sendmsg() 函数)
    • [recvmsg() 函数](#recvmsg() 函数)

博客园------进程和线程_1
博客园------进程和线程_2
博客园------浅析Unix domain socket是什么、Java如何使用UnixSocket调用Docker API对容器进行操作(jnr-unixsocket的使用)

进程和线程

每个进程中的内容 每个线程中的内容
地址空间 程序计数器
全局变量 寄存器
打开文件 堆栈
子进程 状态
即将发生的定时器
信号与信号处理程序
账号信息
  1. 进程模型

    在进程模型中,计算机上所有可运行的软件,通常也包括操作系统本身,被组织成若干顺序进程,一个进程就是一个正在执行程序的实例,包括程序计算器、寄存器、变量当前值。

  2. 进程的状态

    进程运行,占用CPU资源执行程序

    就绪态,等待调度程序给进程分配CPU资源

    阻塞态,等待某种外部事件发生,比如用户输入

运行------》阻塞 :进程因为等待输入而被阻塞

运行------》就绪 :调度程序选择另外的进程执行

就绪------》运行 :调度程序选择了这个进程

阻塞------》就绪 :出现有效输入

  1. 进程的实现
    为了实现进程模型,操作系统维护了一张表格(结构数组)------进程表,每个进程占用一个表项------进程控制块,其包含了进程状态的重要信息,进程在从运行态切换到其它状态时,必须将一些必要的当前信息保存到进程控制块中。

进程控制块中的字段(部分)

进程管理 存储管理 文件管理
寄存器 正文段指针 根目录
程序计数器 数据段指针 工作目录
程序状态字 堆栈段指针 文件描述符
堆栈状态 用户ID
进程状态 组ID
优先级
调度参数
进程ID
父进程
进程组
信号
进程开始时间
使用的CPU时间
子进程的CPU时间
下次定时器时间
  1. 中断向量
    中断向量本质上是一个指针,指向中断服务程序(ISR, Interrupt Service Routine)的地址。中断向量存储了中断处理程序的内存地址,CPU通过该指针跳转到正确的代码位置执行来执行ISR。

所有的中断都是从保存寄存器开始,对于当前进程而言,通常是保存在进程控制块中,随后,会从堆栈中,删除由中断硬件机制存入堆栈的那部分信息,并将堆栈指针指向一个由进程处理程序所使用的临时堆栈。

一些诸如保存寄存器值和设置堆栈指针等操作,通常由一个短小的汇编语言例程来完成,当该例程结束后,它会调用一个C过程处理某个特定的终端类型剩下的工作。

在完成有关工作后,大概就会使相关的进程就绪,接着调用调度程序,决定下一个该运行的进程。决定好以后,会将控制转给一段汇编语言代码,为将要执行的进程装入寄存器值以及内存映射并启动该进程运行。

中断发生后操作系统最底层的工作步骤
1.硬件过程:压入堆栈程序计算器等
2.硬件从中断向量装入新的程序计数器
3.汇编语言过程:保存寄存器值
4.汇编语言过程:设置新的堆栈
5.C中断服务例程运行(读、缓冲写入)
6.调度程序决定下一个将要运行的进程
7.C过程:返回至汇编代码
8.汇编语言过程:开始运行新的当前进程

  1. POSIX线程
    IEEE在IEEE标准1003.1c中定义了线程的标准。它定义的线程包叫作pthread。
    大部分UNIX系统都支持该标准。

进程间通信

竞争条件:两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序

互斥:以某种手段确保当一个进程在使用一个共享数据时,其他进程不能做同样的操作

临界区:对共享内存进行访问的程序片段

互斥实现方案:

屏蔽终端、锁变量、严格轮转法、Peterson解法、TSL指令

Unix/Linux 系统中的进程间通信(IPC, Inter-Process Communication)机制:

  1. 管道(Pipe)

    匿名管道(单向通信):

    通过 pipe() 系统调用创建,用于父子进程或兄弟进程间的通信。

    数据单向流动(一个写端,一个读端)。

    示例:ls | grep "file"(Shell 中的竖线 | 就是匿名管道)。

    命名管道(FIFO):

    通过 mkfifo 命令或 mkfifo() 函数创建,存在于文件系统中。

    允许无亲缘关系的进程通信。

  2. 信号(Signal)

    用于通知进程某事件的发生(如 SIGKILL、SIGTERM)。

    通过 kill()、raise() 或键盘组合(如 Ctrl+C 发送 SIGINT)触发。

    异步通信,处理函数通过 signal() 或 sigaction() 注册。

  3. 共享内存(Shared Memory)

    最高效的 IPC 方式,多个进程直接访问同一块内存。

    通过 shmget() 创建共享内存段,shmat() 附加到进程地址空间。

    需配合同步机制(如信号量)避免竞争条件。

  4. 消息队列(Message Queue)

    进程通过 msgget() 创建队列,msgsnd() 和 msgrcv() 发送/接收消息。

    消息按类型存储,支持优先级。

    与管道不同,消息队列是持久的(除非显式删除)。

  5. 信号量(Semaphore)

    用于进程间同步,避免资源竞争。

    通过 semget()、semop() 等操作信号量集。

    分为 System V 信号量和 POSIX 信号量(后者更轻量)。

  6. 套接字(Socket)

    支持跨网络通信,也可用于本地进程间通信(Unix Domain Socket)。

    本地套接字通过文件系统路径标识(如 /tmp/mysocket)。

    示例:AF_UNIX 地址族的套接字。

  7. 内存映射文件(Memory-Mapped File)

    通过 mmap() 将文件映射到进程内存空间,多个进程可共享修改。

    适用于大文件读写或进程间共享数据。

  8. 文件锁(File Locking)

    通过 fcntl() 或 flock() 对文件加锁,实现进程间同步。

    分为建议性锁(需进程主动检查)和强制性锁(内核强制限制)。

  9. 远程过程调用(RPC, Remote Procedure Call)

    允许进程调用另一台机器上的函数(如 Sun RPC)。

    本地通信也可通过 RPC 框架实现。

Unix Domain Sockets (UDS)

Unix Domain Sockets (UDS) 是一种进程间通信(IPC)机制,允许同一台主机上的进程相互通信。与网络套接字不同,UDS不通过网络协议栈,而是通过文件系统进行通信,因此效率更高。

  1. 使用场景

    同一主机上的客户端/服务器通信

    数据库系统与本地客户端通信

    X Window系统

    Docker守护进程通信

    系统服务内部通信

  2. 使用方法

步骤 服务器端 客户端
1.创建 Socket socket(AF_UNIX, type, 0) socket(AF_UNIX, type, 0)
2.绑定地址 bind() + listen() connect()
3.通信 accept() → read/write read/write
4.关闭 close() + unlink() close()
  1. 与普通套接字的差异
    Unix Domain Sockets (UDS) 和普通网络套接字(如 TCP/UDP)在编程接口上非常相似,下面进行对比:

(1)地址结构不同

UDS 使用 sockaddr_un(文件系统路径或抽象名)

普通套接字使用 sockaddr_in(IP + 端口)

(2)通信范围不同

特性 Unix Domain Sockets (UDS) 普通套接字(TCP/UDP)
通信范围 仅限同一台主机上的进程 可以跨网络通信(本地或远程主机)
依赖网络协议栈 不依赖,直接走内核 IPC 机制 依赖 TCP/IP 或 UDP/IP 协议栈
性能 更高(无需序列化、校验和、路由等) 较低(有协议栈开销)

(3)绑定流程差异

UDS 需要处理文件系统路径

普通套接字绑定到 IP + 端口

(4)权限控制

UDS 受文件系统权限控制

普通套接字依赖网络权限

(5)数据传递能力

UDS 支持传递文件描述符和进程凭证

普通套接字只能传输原始数据

UDS的核心适用场景和用途

  1. 本地高性能进程间通信(IPC)
  2. 文件描述符与进程间状态传递
  3. 系统服务与守护进程通信
  4. 容器化环境(Container)内部通信
  5. 特权分离与安全沙盒
  6. 特殊场景:抽象命名空间(Linux 特有)

配置UDS的几种主要方式

具体取决于通信模型和需求

方法 适用场景 是否需要文件路径 特点
socketpair() 父子进程/线程间快速通信 否(匿名) 简单高效,但限于相关进程
常规 UDS 客户端-服务器模型 支持多客户端,需管理 socket 文件
抽象命名空间 Linux 匿名通信 否(但需特殊命名) 无文件残留,不可移植
SOCK_DGRAM 无连接消息传递 可选 类似 UDP,无可靠性保证
SCM_RIGHTS 传递文件描述符 依赖基础 UDS 跨进程资源共享的唯一标准方式
I/O 多路复用 高并发事件驱动 依赖基础 UDS 高性能,但实现复杂

socketpair() 基本配置流程

  1. 创建套接字对
  2. 创建子进程
  3. 关闭不需要的文件描述符
  4. 使用套接字进行通信
    下面是代码示例:
cpp 复制代码
//创建套接字对
int sockets[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == -1) {
    perror("socketpair");
    exit(EXIT_FAILURE);
}

参数说明:

AF_UNIX 或 AF_LOCAL:表示本地通信(同一主机)

SOCK_STREAM:提供面向连接的可靠通信(类似TCP)

0:默认协议

sockets:用于存储创建的套接字描述符的数组

cpp 复制代码
//创建子进程
pid_t pid = fork();
if (pid == -1) {
    perror("fork");
    exit(EXIT_FAILURE);
}
cpp 复制代码
关闭不需要的文件描述符

在子进程中
if (pid == 0) { // 子进程
    close(sockets[0]); // 关闭父进程使用的套接字
    // 现在可以使用 sockets[1] 与父进程通信
    // ... 通信代码 ...
    close(sockets[1]); // 通信结束后关闭
    exit(EXIT_SUCCESS);
}

在父进程中
else { // 父进程
    close(sockets[1]); // 关闭子进程使用的套接字
    // 现在可以使用 sockets[0] 与子进程通信
    // ... 通信代码 ...
    close(sockets[0]); // 通信结束后关闭
    wait(NULL); // 等待子进程结束
}
cpp 复制代码
可以使用 read()/write() 或 send()/recv() 进行通信:

父进程发送数据到子进程:

char message[] = "Hello from parent";
write(sockets[0], message, sizeof(message));

子进程接收数据:

char buffer[256];
ssize_t n = read(sockets[1], buffer, sizeof(buffer));
if (n > 0) {
    printf("Child received: %s\n", buffer);
}

socketpair() 进阶------传递文件描述符

socketpair() 创建的套接字还可以用于传递文件描述符(如示例代码中的 SendFD 和 RecvFD 方法):

  1. 使用 sendmsg() 和 recvmsg() 系统调用
  2. 通过辅助数据 (cmsghdr) 传递文件描述符
  3. 设置 msg_control 和 msg_controllen 字段
  4. 使用 SCM_RIGHTS 作为控制消息类型

补充

socketpair() 函数

cpp 复制代码
//创建一对相互连接的匿名套接字,适用于父子进程或线程间通信。
//与 pipe() 不同,socketpair 创建的套接字是全双工的(双向通信)。
//文件描述符通过 fork() 继承,无需暴露文件系统路径(匿名性)。
int socketpair(int domain, int type, int protocol, int sv[2]);
参数 取值/类型 作用
domain AF_LOCAL/AF_UNIX 指定协议族(Unix Domain Socket,本地通信)
type SOCK_STREAM 面向连接的可靠通信(类似 TCP,保证数据顺序)
SOCK_DGRAM 无连接的数据报(类似 UDP,不保证顺序)
protocol 0 通常为 0,表示默认协议
sv int[2] 用于返回两个关联的套接字描述符(sv[0] 和 sv[1])

struct msghdr 结构体

cpp 复制代码
struct msghdr {
    void         *msg_name;       // 目标地址(用于网络通信,UDS 中为 NULL)
    socklen_t     msg_namelen;    // 地址长度
    struct iovec *msg_iov;        // 数据块数组(分散/聚集 I/O)
    int           msg_iovlen;     // 数据块数量
    void         *msg_control;    // 控制信息(如文件描述符、凭证)
    socklen_t     msg_controllen; // 控制信息长度
    int           msg_flags;      // 接收时的标志位(通常忽略)
};

关键成员

成员 用途
msg_iov 指向 struct iovec 数组,支持分散读写(Scatter/Gather I/O)
msg_control 指向控制信息缓冲区(如传递文件描述符的 cmsghdr 结构)
msg_controllen 控制信息的实际长度(需对齐到 CMSG_SPACE)

struct iovec

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

struct cmsghdr

cpp 复制代码
struct cmsghdr {
    socklen_t cmsg_len;   // 控制信息总长度(含头部)
    int       cmsg_level; // 协议层级(如 SOL_SOCKET)
    int       cmsg_type;  // 信息类型(如 SCM_RIGHTS)
    // 随后是实际的控制数据(如 int fd)
};

struct iovec 、struct msghdr 、struct cmsghdr三者关系

  1. 这三个结构体共同用于 高级 I/O 操作(如 sendmsg/recvmsg),分工明确:
    iovec:管理普通数据(如字符串、二进制块)。
    msghdr:整合数据+控制信息,作为 I/O 操作的顶层容器。
    cmsghdr:专门处理控制信息(如文件描述符、进程凭证)。
  2. 它们的关系类似于 快递包裹:
    iovec = 包裹中的物品(数据内容)
    cmsghdr = 快递单上的备注(特殊指令)
    msghdr = 整个包裹(物品+快递单)

三者的内存布局

bash 复制代码
  struct msghdr
       +---------------------+
       | msg_iov  ---------->|--> struct iovec[0] {iov_base, iov_len}
       | msg_iovlen = 2      |    struct iovec[1] {iov_base, iov_len}
       |                     |
       | msg_control ------->|--> struct cmsghdr
       | msg_controllen      |    |-- cmsg_len
       +---------------------+    |-- cmsg_level (SOL_SOCKET)
                                  |-- cmsg_type (SCM_RIGHTS)
                                  |-- CMSG_DATA() -> 实际数据(如 int fd)

总结:

结构体 存储内容 所属层级 典型用途
iovec 普通数据(用户态缓冲区) 数据层 分散/聚集 I/O
cmsghdr 控制信息(内核级元数据) 控制层 传递文件描述符、进程凭证
msghdr 整合 iovec + cmsghdr 操作聚合层 sendmsg/recvmsg 的参数

sendmsg() 函数

cpp 复制代码
//发送数据和辅助数据(如文件描述符)。
//通过 msg_control 传递控制信息(需按 CMSG_SPACE 对齐)。
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
参数 类型 作用
sockfd int 套接字描述符(如 socketpair 返回的 pipes[1])
msg const msghdr* 包含数据和控制信息的消息结构体
flags int 发送标志(如 MSG_DONTWAIT 非阻塞,通常设为 0)

recvmsg() 函数

cpp 复制代码
//接收数据和辅助数据(如文件描述符)。
//通过 msg_control 提取控制信息(需检查 cmsg_type)。
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数 类型 作用
sockfd int 套接字描述符(如 socketpair 返回的 pipes[0])
msg struct msghdr* 接收数据的消息结构体(需预分配缓冲区)
flags int 接收标志(如 MSG_WAITALL 阻塞等待全部数据,通常设为 0)
相关推荐
淋过很多场雨1 小时前
现代c++获取linux所有的网络接口名称
java·linux·c++
await 4041 小时前
Windows查看和修改IP,IP互相ping通
linux·网络协议·tcp/ip
这个懒人2 小时前
C++后端服务器常见开发框架
c++·后端·框架
头顶秃成一缕光3 小时前
JVM快速入门
java·linux·jvm·ide·spring·spring cloud·servlet
橘颂TA3 小时前
【C++】数据结构 九种排序算法的实现
数据结构·c++·排序算法
大魔王(已黑化)3 小时前
LeetCode —— 572. 另一棵树的子树
c语言·数据结构·c++·算法·leetcode·职场和发展
Lw老王要学习4 小时前
Linux架构篇、第1章_01架构的介绍HTTP HTTPS 协议全面解析
linux·运维·服务器·http·架构·https·云计算
byte轻骑兵4 小时前
【C++类和数据抽象】消息处理示例(2)
开发语言·c++
千里镜宵烛4 小时前
C++ 红黑树
java·开发语言·c++
敲上瘾4 小时前
高并发内存池(五):性能测试与性能优化
c++·功能测试·缓存·性能优化·线程·高并发内存池