深入解析:进程间通信(IPC)

在 Linux 系统中,进程是资源分配的基本单位,每个进程拥有独立的地址空间,彼此隔离。进程间通信(IPC)是指不同进程之间交换数据、同步操作或传递状态的机制。Linux 提供了多种 IPC 方式,适用于不同场景(如速度、同步需求、是否跨主机等)。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信,这个第三方资源实际上就是操作系统提供的一段内存区域
在这里插入图片描述
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件,内核缓冲等)。由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信的方式

IPC核心需求

进程间通信的核心目的包括:

  • 数据交换:进程间传递结构化或非结构化数据;
  • 同步:协调多个进程的执行顺序(如避免同时操作共享资源);
  • 通知:一个进程告知另一个进程发生了特定事件;
  • 共享资源:多个进程安全访问共同的资源(如内存、文件)。

管道

管道是最古老、最基础的 IPC 机制,适用于有亲缘关系的进程(如父子、兄弟进程),本质是内核维护的一个内存缓冲区。
例如:统计我们登录用户的个数
在这里插入图片描述
who和wc命令都是两个程序,当它们运行的时候就变成了两个进程,who进程通过标准输出将数据打到管道中,wc进程通过标准输入从管道中读取数据,这样便完成了数据传输
在这里插入图片描述

匿名管道

无文件名,仅存在于内存中,通过文件描述符传递,生命周期随进程。

原理:
内核创建一个半双工(单向)的缓冲区,进程通过两个文件描述符操作:fd[0](读端)和fd[1](写端)。数据从写端写入,读端读出,遵循 “先进先出”(FIFO)规则。
让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信

在这里插入图片描述
注意:
父子进程看到的同一份文件资源是由操作系统来维护的,当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝
操作系统一定不会把进程进行通信的数据刷新到磁盘中,因为这样做有IO参与会降低效率。这种文件是一批不会把数据写到磁盘当中的文件。简单来说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存中存在,而不会在磁盘当中存在。

pipe函数

创建匿名管道

  • 函数原型:int pipe(int pipefd[2])
  • 函数参数
    • pipefd[0]:读端
    • pipefd[1]:写端

在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,

1.父进程调用pipe函数创建管道
在这里插入图片描述

2.父进程创建子进程
在这里插入图片描述

3.父进程关闭写段,子进程关闭读端
在这里插入图片描述

注意:
管道是单向通信,因此当父进程创建完子进程后,需要确认父子进程谁写谁读,在关闭相应的读写端
从管道写端写入的数据会被存到内核缓冲,直到从管道的读端被读取

  • 管道中没有数据:write 返回成功写入的字节数,读端程 阻塞在 read 上
  • 管道中有数据没有满:write 返回成功写入的字节数,read 返回读取的字节数
  • 管道已满:写端程序阻塞在 write 上,read 返回读取的字节数
  • 写端全部关闭:read 正常读,返回读取的字节数(没有数据返回0,不阻塞)
  • 读端全部关闭:写端程 write 会异常终止停止进程(被信号 SIGPIPE 杀死的)

特点:

  • 半双工:数据只能单向流动,双向通信需创建两个管道;
  • 字节流:数据无结构,按顺序传递,无消息边界;
  • 阻塞特性:读端无数据时读操作阻塞,写端满时写操作阻塞;
  • 仅支持亲缘进程:依赖文件描述符的继承。

命名管道(FIFO)

匿名管道的局限是仅支持亲缘进程,命名管道(FIFO)通过文件系统路径标识,突破了这一限制,可用于无亲缘关系的进程

定义:
存在于文件系统中的特殊文件(类型为p),进程通过路径访问,生命周期独立于进程(需手动删除)。

原理:
与匿名管道类似,内核维护缓冲区,但通过文件名暴露给所有进程,任何进程都可打开该文件进行读写。

命名管道是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了
命名管道在磁盘有一个简单的映像,但这个映像大小永远为0,因为命名管道和匿名管道都不会把通信数据刷新到磁盘当中

创建命名管道

mkfifo fifo

在这里插入图片描述
可以看到,创建出来的文件类型是p,代表该文件是命名管道文件
在这里插入图片描述
使用这个文件就像使用普通文件一样,能够实现两个进程之间的通信了

mkfifo函数
创建命名管道

  • 头文件:

    • #include<sys/types.h>
    • #include<sys/stat.h>
  • 函数原型:

    • int mkfifo(const char *pathname, mode_t mode)
  • 函数参数:

    • pathname:表示要创建的命名管道文件
    • mode:表示创建命名管道文件的默认权限

命名管道的打开规则:

  • 读进程打开 FIFO,并且没有写进程打开时:

    • 没有 O_NONBLOCK(阻塞进程):阻塞直到有写进程打开该 FIFO
    • 有 O_NONBLOCK(非阻塞进程):立刻返回成功
  • 写进程打开 FIFO,并且没有读进程打开时:

    • 没有 O _NONBLOCK(阻塞进程):阻塞直到有读进程打开该 FIFO
    • 有 O _NONBLOCK(非阻塞进程):立刻返回失败,错误码为 ENXIO

特点:

  • 支持任意进程:通过路径访问,无需亲缘关系;
  • 半双工:同匿名管道,单向通信;
  • 阻塞特性:若以读方式打开,会阻塞直到有进程以写方式打开;反之亦然(可通过O_NONBLOCK设置非阻塞)。

信号

信号是 Linux 中最简洁的 IPC 机制,用于异步通知进程发生了特定事件(如异常、用户指令),无法传递大量数据,仅能传递 “事件类型”。

原理:
信号是内核向进程发送的 “软件中断”,每个信号对应一个整数(如SIGINT=2、SIGKILL=9),进程收到信号后会执行预设动作(默认 / 自定义)。

关键概念:

  • 信号类型:Linux 定义了 31 种标准信号(1-31)和实时信号(34+),常见信号如下:

    • SIGINT(2):用户按Ctrl+C,默认终止进程;
    • SIGKILL(9):强制终止进程,无法被捕获或忽略;
    • SIGSTOP(19):暂停进程,无法被捕获或忽略;
    • SIGALRM(14):定时器到期(alarm()触发)。
  • 信号处理:进程可通过函数自定义信号处理逻辑(除SIGKILL和SIGSTOP):

    • signal(int signum, sighandler_t handler):简单注册处理函数(兼容性好但功能有限);
    • sigaction(int signum, const struct sigaction *act, struct sigaction *oldact):更强大的接口,支持设置信号掩码、处理方式等。
  • 信号发送:进程可通过系统调用向其他进程发送信号:

    • kill(pid_t pid, int sig):向指定 PID 的进程发送信号;
    • raise(int sig):向自身发送信号;
    • alarm(unsigned int seconds):seconds 秒后向自身发送SIGALRM。

特点

  • 优点:简单高效,适用于紧急事件通知(如异常终止);
  • 缺点:仅能传递信号类型(无附加数据),且信号可能丢失(非实时信号不排队);
  • 典型场景:进程终止(SIGKILL)、超时处理(SIGALRM)、用户中断(Ctrl+C触发SIGINT)。

共享内存

共享内存是速度最快的 IPC 机制,因为进程直接访问物理内存,无需通过内核中转数据(其他机制如管道、消息队列需内核复制数据)。

原理:

  • 内核在物理内存中创建一块 “共享区域”;
  • 多个进程通过系统调用将该区域 “映射” 到自己的虚拟地址空间;
  • 进程对映射区域的读写直接反映到物理内存,其他进程可立即看到修改。

关键系统调用(System V 标准)

  • shmget(key_t key, size_t size, int shmflg):创建或获取共享内存
    • key:标识共享内存的键值(可通过ftok()生成);
    • size:共享内存大小(字节);
    • shmflg:权限(如0666)+ 操作(IPC_CREAT创建,IPC_EXCL不存在时创建)。
  • shmat(int shmid, const void *shmaddr, int shmflg):将共享内存附加到进程地址空间
    • 返回映射后的虚拟地址,进程通过该地址读写共享内存。
  • shmdt(const void *shmaddr):将共享内存从进程地址空间分离(仅断开映射,不删除)。
  • shmctl(int shmid, int cmd, struct shmid_ds *buf):控制共享内存(如删除)
    • cmd=IPC_RMID:标记共享内存,当最后一个进程分离后删除。

特点

  • 速度最快:无内核数据复制,直接访问内存;
  • 需同步机制:多个进程同时读写可能导致数据混乱,需配合信号量等同步;
  • 生命周期:内核维护,直到被显式删除或系统重启;
  • 大小限制:受系统内存和内核参数(如SHMMAX)限制。

典型场景

  • 高频、大数据量的进程间通信(如视频处理中多个模块共享帧数据);
  • 配合信号量实现 “共享内存 + 同步” 的高效组合。

内存映射

内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用用户通过修改内存就能修改磁盘文件。
映射分为两种:

  • 文件映射:将文件的一部分映射到调用进程的虚拟内存中。对文件映射部分的访问转化为相应内存区域的字节操作。映射页面会按需自动从文件加载。
  • 匿名映射:一个匿名映射没有对应的文件。其映射页面的内容会被初始化为 0。一个进程所映射的内存可以与其他进程的映射共享,分享的两种方式:
    • 两个进程对同一文件的同一区域映射。
    • fork() 创建的子进程继承父进程的映射。

在这里插入图片描述

mmap()函数
在调用进程的虚拟地址空间中创建一个新内存映射

  • 头文件
    • <sys/mman.h>
  • 函数原型
    • void mmap(void addr,size_t length,int prot,int flags,int fd,off_t offset)
  • 函数参数
    • addr:指向欲映射的内存起始地址,通常设为 NULL,代表系统自动选定地址。
    • length:映射的长度。
    • prot:映射区域的保护方式:
      • PROT_READ:映射区域可读取
      • PROT_WRITE:映射区域可修改
    • flags:影响映射区域的特性。必须指定 MAP_SHARED 或 MAP_PRIVATE
      • MAP_SHARED:创建共享映射,对映射的写入会写入文件里,其他共享映射的进程可见
      • MAP_PRIVATE:创建私有映射,对映射的写入不会写入文件里,其他映射进程不可见
      • MAP_ANONYMOUS:创建匿名映射,此时会忽略参数 fd (设为 -1),不涉及文件,没有血缘关系的进程不能共享
    • fd:要映射的文件描述符,匿名映射设为 -1
    • offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小(4k)的整数倍。

函数返回值
若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因
存于errno中

munmap()函数
解除映射区域

  • 头文件

    • <sys/mman.h>
  • 函数原型

    • int munmap(void*addr,size_t length)
  • 函数参数

    • addr:指向要解除映射的内存起始地址
    • length:解除映射的长度

消息队列

消息队列是内核维护的链表结构,进程可按 “类型” 发送 / 接收消息,实现结构化、异步通信。
消息队列是面向消息进行通信的,一次读取一条完整的消息,每条消息中还包含一个整数表示优先级,可以根据优先级读取消息。
进程 A 可以往队列中写入消息,进程 B 读取消息。并且,进程 A 写入消息后就可以终止,进程 B 在需要的时候再去读取。
在这里插入图片描述

每条消息通常具有以下属性:

  • 一个表示优先级的整数
  • 消息数据部分的长度
  • 消息数据本身

消息队列函数
头文件:

  • #include <fcntl.h>
  • #include <sys/stat.h>
  • #include <mqueue.h>

打开和关闭消息队列

  • mqd_t mq_open(const char *name, int oflag);
  • mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
  • int mq_close(mqd_t mqdes);

获取和设置消息队列属性

  • int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
  • int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);

在队列中写入和读取一条消息

  • int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
  • ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);

删除消息队列

  • int mq_unlink(const char *name);

函数参数和返回值

  • name:消息队列名

  • oflag:打开方式,类似 open 函数。

    • 必选项:O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(可读可写)
    • 可选项:O_NONBLOCK(非阻塞模式)、O_CREAT、O_EXCL
      • O_NONBLOCK:非阻塞模式。如果设置了这个标志,操作(比如读取或写入)在没有缓冲区可用时不会阻塞,而是立即返回错误。
      • O_CREAT:如果指定的队列不存在,就会创建新队列。创建时需要提供 mode 和 attr 参数,用于定义队列的权限和属性。
      • O_EXCL:与 O_CREAT 一起使用时,表示如果队列已存在,则打开会失败,否则会创建新队列。这用来确保不会覆盖已存在的队列。
      • mq_open("/myqueue", O_CREAT | O_WRONLY, 0644, NULL);
      • 表示以“创建并只写”模式打开(如果不存在则创建),权限为 0644。
  • mode:访问权限,oflag 中含有 O_CREAT 且消息队列不存在时提供该参数

  • attr:队列属性,open 时为 NULL 表示默认属性

  • mqdes:表示消息队列描述符

  • msg_ptr:指向缓冲区的指针

  • msg_len:缓冲区大小

  • msg_prio:消息优先级

返回值
成功返回 0 ,open 返回消息队列描述符,mq_receive 返回写入成功字符数
失败返回 -1

注意
在编译时报 undefined reference to mq_open、undefined reference to mq_close 时,除了要包含文件 <mqueue.h>,还需要加上编译选项 -lrt

特点

  • 结构化消息:消息包含类型,支持按优先级处理(如高类型消息优先接收);
  • 异步通信:发送进程无需等待接收进程,消息在队列中缓存;
  • 有限制:消息大小、队列容量受内核参数限制(如MSGMAX、MSGMNB);
  • 生命周期:内核维护,直到被删除或系统重启。

优点

  1. 异步通信能力
    消息队列允许发送进程在发送消息后立即返回,无需等待接收进程处理。消息会被内核缓存于队列中,接收进程可在合适时机读取,实现了进程间的解耦和异步协作。
  • 例如:日志收集进程可随时接收其他进程发送的日志消息,无需实时等待日志处理完成。
  1. 结构化消息与类型区分
    消息队列中的每个消息都包含类型标识(整数)和数据部分,接收进程可根据类型选择性读取(如只接收高优先级消息),实现了消息的分类处理。
  • 相比管道的 “无结构字节流”(无法区分消息边界),消息队列天然支持消息边界识别,避免了数据粘包问题。
  • 例如:系统监控程序可按 “错误”“警告”“信息” 等类型发送消息,接收进程可优先处理错误类型。
  1. 跨进程通信支持
    消息队列通过全局键值(System V)或文件路径(POSIX)标识,可被任意进程访问(无论是否有亲缘关系),突破了匿名管道仅支持亲缘进程的限制。
  • 例如:独立的服务进程和客户端进程可通过消息队列交换数据,无需依赖父子关系。
  1. 消息持久化与缓存
    消息队列由内核维护,即使发送进程退出,消息仍会保存在队列中,直到被接收进程读取或显式删除,实现了消息的临时持久化。
  • 相比信号(可能丢失),消息队列更可靠(非实时消息虽不排队,但不会因接收进程未就绪而丢失)。
  • 例如:任务调度进程发送任务消息后崩溃,接收进程重启后仍可读取未处理的任务。
  1. 无需共享内存的同步问题
    共享内存虽快,但需额外同步机制(如信号量)避免多进程并发读写冲突;而消息队列的发送 / 接收操作由内核保证原子性,天然避免了竞态条件,简化了编程。
  • 例如:多个进程向队列发送消息时,内核会按顺序存储,接收时不会出现数据错乱。
  1. 灵活性与可控性
  • 可通过系统调用(如msgctl)动态调整队列属性(如最大消息数、单个消息大小)。
  • 支持非阻塞操作(通过IPC_NOWAIT标志),避免进程因等待消息而长期阻塞。
  • 可查询队列状态(如当前消息数、占用内存),便于监控和调试。

消息队列的核心优势在于异步性、结构化、跨进程支持和内核级可靠性,适用于需要按类型处理数据、进程间解耦或避免实时交互的场景(如日志系统、任务调度、分布式组件通信等)。其性能虽不及共享内存,但在开发复杂度和适用范围上更具优势。

套接字

在 Linux 进程间通信(IPC)中,套接字(Socket)是一种功能强大且灵活的机制,尤其适用于需要跨进程甚至跨主机通信的场景。与其他 IPC 机制(如管道、消息队列)相比,套接字不仅支持本地进程间通信,还能无缝扩展到网络通信,提供了统一的编程接口。

核心概念
套接字本质是内核中的一个数据结构,用于标识 “通信端点”。进程通过套接字描述符(整数)操作该结构,实现数据收发。其核心属性包括:

1.域(Domain)
决定通信范围和地址格式,常用域:

  • AF_UNIX(或AF_LOCAL):用于本地进程间通信,地址是文件系统中的路径(如/tmp/my_socket)。
  • AF_INET:用于IPv4 网络通信,地址是(IP地址, 端口号)。
  • AF_INET6:用于 IPv6 网络通信。

2.类型(Type)
决定通信方式和数据传输特性,核心类型:

  • SOCK_STREAM(流式套接字):
    • 面向连接:通信前需建立连接(类似打电话)。
    • 可靠传输:数据无丢失、无重复、按序到达(通过重传机制保证)。
    • 字节流:数据无边界,需应用层自行处理消息分割。
    • 对应传输层协议:TCP。
  • SOCK_DGRAM(数据报套接字):
    • 无连接:通信前无需建立连接(类似发短信)。
    • 不可靠传输:数据可能丢失、重复或乱序(不保证送达)。
    • 有消息边界:每次接收操作对应一个完整消息。
    • 对应传输层协议:UDP。

3.协议(Protocol)
当域和类型确定后,通常使用默认协议(填0):
+ AF_INET+SOCK_STREAM默认使用 TCP(IPPROTO_TCP)。
+ AF_INET+SOCK_DGRAM默认使用 UDP(IPPROTO_UDP)。

Linux进程间通信中套接字的特殊性

在 Linux 中,套接字用于 IPC 时主要通过UNIX 域套接字(AF_UNIX/AF_LOCAL) 实现,它与网络套接字(AF_INET)共享相同的 API,但专为本地进程间通信优化:

  • 不依赖网络协议栈,数据直接在内核中传递,效率接近共享内存;
  • 通过文件系统路径标识通信端点,而非 IP 地址和端口;
  • 支持流式(SOCK_STREAM)和数据报(SOCK_DGRAM)两种通信模式,满足不同可靠性需求。

优势

相比网络套接字,UNIX 域套接字省去了 TCP/IP 协议栈的处理开销,数据通过内核缓冲区直接复制,性能接近共享内存。测试表明,其吞吐量通常比本地回环 TCP 套接字(127.0.0.1)高 30%~50%。

UNIX 域套接字的通信流程

1.流式套接字(SOCK_STREAM)通信流程
需经历 “建立连接→数据传输→关闭连接” 三阶段,类似 TCP 的 “三次握手→数据传输→四次挥手”。
面向连接、可靠传输(类似 TCP),数据无丢失、无重复、按序到达,适合需要确保数据完整性的场景(如配置同步)。
适用于需要可靠连接的场景,流程类似 “服务器 - 客户端” 模型。

(1)服务器端代码

#include <stdio.h>
  #include <stdlib.h>
    #include <string.h>
      #include <unistd.h>
        #include <sys/socket.h>
          #include <sys/un.h>
            #define SOCK_PATH "/tmp/stream_socket"
            int main() {
            int server_fd, client_fd;
            struct sockaddr_un server_addr, client_addr;
            socklen_t client_len = sizeof(client_addr);
            char buf[1024];
            // 1. 创建流式套接字
            if ((server_fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
            }
            // 2. 绑定地址(先删除旧文件,避免绑定失败)
            unlink(SOCK_PATH);
            server_addr.sun_family = AF_UNIX;
            strcpy(server_addr.sun_path, SOCK_PATH);
            if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("bind");
            exit(EXIT_FAILURE);
            }
            // 3. 监听连接(最大等待队列长度为5)
            if (listen(server_fd, 5) == -1) {
            perror("listen");
            exit(EXIT_FAILURE);
            }
            printf("Server listening on %s...\n", SOCK_PATH);
            // 4. 接受客户端连接(阻塞)
            if ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) == -1) {
            perror("accept");
            exit(EXIT_FAILURE);
            }
            printf("Client connected\n");
            // 5. 接收并回复数据
            ssize_t n;
            while ((n = recv(client_fd, buf, sizeof(buf)-1, 0)) >
            0) {
            buf[n] = '\0';
            printf("Received: %s\n", buf);
            send(client_fd, "Message received", 17, 0);
            // 回复客户端
            }
            // 6. 关闭连接
            close(client_fd);
            close(server_fd);
            unlink(SOCK_PATH);
            // 清理套接字文件
            return 0;
            }

(2)客户端代码

#include <stdio.h>
  #include <stdlib.h>
    #include <string.h>
      #include <unistd.h>
        #include <sys/socket.h>
          #include <sys/un.h>
            #define SOCK_PATH "/tmp/stream_socket"
            int main() {
            int client_fd;
            struct sockaddr_un server_addr;
            char buf[1024];
            // 1. 创建套接字
            if ((client_fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
            }
            // 2. 连接服务器
            server_addr.sun_family = AF_UNIX;
            strcpy(server_addr.sun_path, SOCK_PATH);
            if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("connect");
            exit(EXIT_FAILURE);
            }
            // 3. 发送数据并接收回复
            const char* msg = "Hello from client";
            send(client_fd, msg, strlen(msg), 0);
            ssize_t n = recv(client_fd, buf, sizeof(buf)-1, 0);
            if (n >
            0) {
            buf[n] = '\0';
            printf("Server reply: %s\n", buf);
            }
            // 4. 关闭连接
            close(client_fd);
            return 0;
            }
  1. 数据报套接字(SOCK_DGRAM)通信
    无连接、不可靠传输(类似 UDP),但速度更快,适合实时性要求高的场景(如状态通知)。
    适用于无连接、低延迟场景,无需建立连接即可直接发送数据。
    (1)服务器端代码
#include <stdio.h>
  #include <stdlib.h>
    #include <string.h>
      #include <unistd.h>
        #include <sys/socket.h>
          #include <sys/un.h>
            #define SOCK_PATH "/tmp/dgram_socket"
            int main() {
            int server_fd;
            struct sockaddr_un server_addr, client_addr;
            socklen_t client_len = sizeof(client_addr);
            char buf[1024];
            // 1. 创建数据报套接字
            if ((server_fd = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
            }
            // 2. 绑定地址
            unlink(SOCK_PATH);
            server_addr.sun_family = AF_UNIX;
            strcpy(server_addr.sun_path, SOCK_PATH);
            if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
            perror("bind");
            exit(EXIT_FAILURE);
            }
            printf("Server waiting for datagrams...\n");
            // 3. 接收并回复数据(无连接,每次需指定客户端地址)
            while (1) {
            ssize_t n = recvfrom(server_fd, buf, sizeof(buf)-1, 0,
            (struct sockaddr*)&client_addr, &client_len);
            if (n == -1) {
            perror("recvfrom");
            continue;
            }
            buf[n] = '\0';
            printf("Received: %s\n", buf);
            // 回复客户端(使用recvfrom获取的客户端地址)
            sendto(server_fd, "Datagram received", 17, 0,
            (struct sockaddr*)&client_addr, client_len);
            }
            // 4. 清理(实际中需信号处理退出)
            close(server_fd);
            unlink(SOCK_PATH);
            return 0;
            }

(2)客户端代码

#include <stdio.h>
  #include <stdlib.h>
    #include <string.h>
      #include <unistd.h>
        #include <sys/socket.h>
          #include <sys/un.h>
            #define SOCK_PATH "/tmp/dgram_socket"
            int main() {
            int client_fd;
            struct sockaddr_un server_addr;
            socklen_t server_len = sizeof(server_addr);
            char buf[1024];
            // 1. 创建数据报套接字
            if ((client_fd = socket(AF_UNIX, SOCK_DGRAM, 0)) == -1) {
            perror("socket");
            exit(EXIT_FAILURE);
            }
            // 2. 配置服务器地址(无需连接)
            server_addr.sun_family = AF_UNIX;
            strcpy(server_addr.sun_path, SOCK_PATH);
            // 3. 发送数据并接收回复
            const char* msg = "Hello from datagram client";
            sendto(client_fd, msg, strlen(msg), 0,
            (struct sockaddr*)&server_addr, server_len);
            ssize_t n = recvfrom(client_fd, buf, sizeof(buf)-1, 0, NULL, NULL);
            if (n >
            0) {
            buf[n] = '\0';
            printf("Server reply: %s\n", buf);
            }
            // 4. 关闭套接字
            close(client_fd);
            return 0;
            }

流式(Stream)和数据报(Datagram)的区别

维度流式(如 TCP、SOCK_STREAM)数据报(如 UDP、SOCK_DGRAM)
连接方式面向连接:通信前必须建立连接(类似打电话)无连接:通信前无需建立连接(类似发短信)
数据边界无边界:数据以连续字节流传输,接收方无法区分 “消息单元”有边界:每个数据报是独立单元,接收方一次读取一个完整数据报
可靠性可靠传输:保证数据无丢失、无重复、按序到达(通过重传、确认机制)不可靠传输:不保证送达,可能丢失、重复或乱序
传输效率较低:需维护连接状态、处理确认和重传,开销大较高:无需连接管理,协议简单,延迟低
拥塞控制有:根据网络状况动态调整发送速率(如 TCP 的慢启动)无:发送方不感知网络状态,可能加重拥塞
数据大小限制无:可传输任意大小数据(由应用层拆分)有:受限于数据报最大长度(如 UDP 通常不超过 65535 字节)
  1. 连接导向 vs 无连接(最核心区别)
  • 流式(面向连接)
    通信前必须通过 “三次握手” 建立连接(如 TCP),内核会为连接维护状态(如序号、窗口大小、重传计时器等)。发送方和接收方的套接字被 “绑定”,数据传输过程中始终关联该连接。
    类比:打电话时,需先拨号(建立连接),通话中双方持续占用线路,结束时需挂电话(断开连接)。
  • 数据报(无连接)
    无需建立连接,每个数据报都是独立的 “个体”,包含完整的目标地址,发送方直接发送,接收方收到后无需确认。内核不维护连接状态,每个数据报的传输相互独立。
    类比:发短信时,无需提前 “建立连接”,每条短信独立发送,接收方收到后也无需告知发送方 “已收到”。
  1. 数据传输的 “完整性” 与 “顺序性” 保障
  • 流式:
    内核通过 “序号” 和 “确认机制” 保证数据按发送顺序到达,若数据丢失,发送方会重传;若接收方收到重复数据,会自动丢弃。因此,应用层无需处理数据丢失或乱序问题。
    但由于是 “字节流”,发送方分 10 次发送 100 字节,与一次发送 100 字节对接收方来说完全相同,需应用层自行定义消息边界(如通过分隔符、长度前缀)。
  • 数据报:
    内核不保证数据报的送达,也不保证顺序。例如:发送方先发送数据报 A,再发送数据报 B,接收方可能只收到 B、只收到 A、或 A 和 B 乱序到达。
    但每个数据报是 “完整的”,接收方要么收到整个数据报,要么完全没收到(不会出现 “半个数据报”),天然具有消息边界。
  1. 协议开销与适用场景
  • 流式:
    为实现可靠性,协议开销大(需处理连接管理、确认、重传、拥塞控制等),延迟较高,但数据传输稳定。
    适用场景:需确保数据完整的场景,如文件传输(FTP)、网页浏览(HTTP)、数据库交互等。
  • 数据报:
    协议简单,无连接管理和重传机制,开销小,延迟低,但可靠性差。
    适用场景:实时性优先于可靠性的场景,如视频 / 音频流(丢失一两个数据包不影响整体播放)、实时游戏(延迟敏感)、广播 / 多播通信等。

UNIX 域套接字的优势与适用场景

优势

  • 高效性:数据在内核中直接传递,无网络协议开销,速度远超 TCP 回环通信。
  • 可靠性选择:流式套接字提供 TCP 级别的可靠性,数据报套接字提供 UDP 级别的高效性。
  • 权限控制:通过文件系统权限(如chmod 600 /tmp/socket)限制访问,安全性高。
  • 全双工通信:单个套接字可同时读写,无需像管道那样创建双向通道。
  • 跨进程无限制:支持任意进程通信(无论是否有亲缘关系),突破管道的亲缘限制。

在这里插入图片描述

适用场景

  • 本地服务程序与客户端通信(如 MySQL 服务器与应用程序);
  • 桌面应用组件间交互(如 GUI 进程与后端服务);
  • 需要高吞吐量的本地 IPC(如日志收集、数据同步);
  • 未来可能扩展到网络通信的程序(统一接口便于迁移)。

套接字的核心价值在于灵活性和扩展性:同一套 API 既能满足本地 IPC 需求,又能无缝支持网络通信,是开发跨环境通信程序的首选。

各IPC机制对比

机制类型核心特点速度数据格式通信方式跨进程支持可靠性典型应用场景
匿名管道内核维护的内存缓冲区,通过文件描述符传递无结构字节流半双工(单向)仅亲缘进程(父子 / 兄弟)可靠(FIFO)Shell 管道,父子进程数据传递
命名管道存在于文件系统的特殊文件,通过路径访问无结构字节流半双工任意进程可靠(FIFO)无亲缘关系的本地进程通信(如服务与客户端配置交换)
信号内核发送的异步事件通知,仅传递信号类型仅信号编号(无数据)异步通知任意进程可能丢失(非实时信号)进程终止(SIGKILL)、超时处理(SIGALRM)、用户中断
共享内存内核创建的共享物理内存区域,进程直接映射到虚拟地址空间最快自定义结构直接读写任意进程需手动同步(否则不可靠)高频、大数据量通信(如视频帧共享、实时数据处理)
消息队列内核维护的链表,消息包含类型和数据,支持按类型读取结构化消息(带类型)异步通信任意进程可靠(内核缓存)按优先级处理的异步通信(如日志分级收集、任务调度)
UNIX域套接字通过文件系统路径标识,支持流式和数据报两种模式字节流或数据报全双工任意进程流式可靠 / 数据报不可靠本地服务与客户端通信(如数据库交互、桌面程序组件)
网络套接字基于 IP: 端口标识,支持 TCP(流式)和 UDP(数据报)中(受网络影响)字节流或数据报全双工跨主机进程(运行在不同物理主机(或虚拟机、容器)上的进程)TCP 可靠 / UDP 不可靠跨主机通信(如 Web 服务、分布式系统节点交互)
posted @ 2025-07-29 20:27  yfceshi  阅读(14)  评论(0)    收藏  举报