IPC 通讯
目的:数据传输、进程间控制、进程同步、事件通知
1. 管道
匿名管道(PIPE)
- 核心定义:匿名管道是一个没有名字(没有文件系统路径名)的内核级缓冲区。它通过文件描述符(fd[0] 为读端,fd[1] 为写端)进行访问。
- 亲缘关系:仅适用于具有血缘关系的进程之间通信(如父子进程、兄弟进程)。
- 单向通信:数据只能单向流动。若需双向通信,必须创建两个管道。
- 底层原理:父进程创建管道后调用 fork(),子进程会继承父进程的文件描述符表。父子进程通过读写指向同一个内核缓冲区的文件描述符来实现数据交互。
- 生命周期:随进程的结束而销毁。当所有读端或写端关闭时,管道会自动关闭。
- 核心 API:Linux: pipe(int fd[2])
命名管道(FIFO)
- 核心定义:命名管道(FIFO,First In, First Out)是一种特殊的文件类型。它在文件系统中拥有一个明确的路径名,但数据实际上驻留在内存中(没有 Data block,文件大小显示为 0)。
- 无亲缘限制:任何具有访问权限的进程,只要知道管道的路径名,都可以打开它进行通信。
- 持久性:即使创建它的进程退出,管道文件依然存在于文件系统中,直到被显式删除。
- 本质上是半双工(单向)的,但可以通过创建两个命名管道来实现全双工通信。
- 读写操作遵循严格的阻塞规则:如果写端关闭,读端读完数据后 read() 返回 0;如果读端全部关闭,写端继续写入会触发 SIGPIPE 信号。
- C语言创建: mkfifo(const char *pathname, mode_t mode)
标准流管道
- 核心定义:这是一种基于标准 I/O 文件流(FILE *)的管道机制,主要用于创建一个连接到另一个可执行程序(如 Shell 命令)的管道。
- 工作流封装:它将创建管道、fork() 子进程、关闭不需要的文件描述符、调用 exec 函数族执行命令等一系列复杂操作,高度封装在了一个函数中。
- 必须使用标准 I/O 函数(如 fgets, fputs)进行操作,不能使用底层的 read/write。
- 关闭时必须使用对应的 pclose() 函数,它会关闭流并等待命令执行结束。
- 创建: FILE*popen(const char command, const chartype) (type 为 "r" 或 "w")
- 关闭: int pclose(FILE*stream)
核心对比总结
| | | 标准流管道 (Standard Stream Pipe) |
|---|
| 受限:仅限具有亲缘关系的进程(如父子、兄弟进程)。 | 广泛:支持任意进程间通信,只要知道路径名且有权限。 | 广泛:通常用于与另一个程序(子进程)进行通信,支持任意可执行程序。 |
| | 单向/双向:本质半双工,但可通过建立两个命名管道实现全双工。 | 单向:通常是单向的(popen 模式决定是读还是写),双向较复杂。 |
| 底层 I/O:使用文件描述符 (int fd),配合 read() / write() 系统调用。 | 底层 I/O:使用文件描述符 (int fd),配合 read() / write() 系统调用(也可用标准 I/O 封装)。 | 标准 I/O:使用流指针 (FILE *fp),配合 fgets() / fputs() / fprintf() 等库函数。 |
| 临时性:随进程结束而销毁,存在于内存中,无文件系统路径。 | 持久性:作为特殊文件存在于文件系统中,直到被显式删除(unlink)。 | 临时性:随 pclose() 调用或程序结束而关闭,底层依赖匿名管道。 |
| | 命令行 mkfifo 或系统调用 mkfifo() | 标准库函数 popen(const char *command, const char *type) |
2. 共享内存
- 共享内存(Shared Memory)是
最快的进程间通信(IPC)机制。它允许多个不相关的进程直接访问同一块物理内存区域,从而实现“零拷贝”(Zero-Copy)通信。
1. 共享内存的分类
System V 共享内存:历史较悠久,通过 shmget、shmat 等函数族进行操作,生命周期由内核管理,支持跨进程持久化。POSIX 共享内存:较新的标准,通过 shm_open 和 mmap 进行操作,与文件系统路径结合更紧密,API 设计更符合现代 POSIX 规范。
2. System V 共享内存核心 API
- 作用:创建一个新的共享内存段,或获取一个已存在的共享内存段。
- 原型:int shmget(key_t key, size_t size, int shmflg);
- 作用:将共享内存段附加(映射)到当前进程的虚拟地址空间。成功后,进程可以通过返回的指针像操作普通内存一样直接读写数据。
- 原型:
void *shmat(int shmid, const void*shmaddr, int shmflg);
- 作用:将共享内存段从当前进程的地址空间中分离。注意:这仅仅是解除映射,并不会从系统中删除该共享内存。
- 原型:
int shmdt(const void *shmaddr);
IPC_RMID:删除/销毁共享内存段(释放物理内存资源)。IPC_SET:设置共享内存段的属性(如权限、所有者等)。
3. 同步机制与信号量操作规范
- 由于共享内存没有内置的同步和互斥机制,多个进程并发读写极易导致数据竞争(脏读、写覆盖等),因此必须配合信号量(Semaphore)等机制使用。
- 原因:如果持有锁的进程在 P 操作之后、V 操作之前意外崩溃,内核会在进程退出时自动撤销(Undo)该 P 操作,从而释放信号量,防止死锁和资源泄漏。
- 原因:V 操作本身就是为了释放资源而设计的。如果进程在 V 操作前崩溃,P 操作的 SEM_UNDO 机制已经会自动补偿释放;若 V 操作也设置 SEM_UNDO,反而可能导致信号量被错误地多次释放,破坏同步状态。
3. 消息队列
- 一条消息队列,最大容量是 16384字节(16K);一条消息最大长度为8192字节(8K).
核心API与流程
- 生成键值 (ftok):通过文件路径和项目ID生成一个唯一的 key_t 键值,用于标识消息队列。
- 创建/获取队列 (msgget):根据键值创建新的消息队列或获取已存在的队列,需指定权限(如 IPC_CREAT | 0666)。
- 发送消息 (msgsnd):将消息写入队列。消息结构体必须以 long mtype(消息类型,必须大于0)开头,后面跟着消息正文。
- 接收消息 (msgrcv):从队列读取消息并自动将其从队列中删除。其最大特点是支持按类型过滤接收: msgtyp = 0:按FIFO顺序接收第一条消息;msgtyp > 0:只接收指定类型的第一条消息;msgtyp < 0:接收类型值小于等于其绝对值的最小类型消息。
- 控制队列 (msgctl):用于删除队列(IPC_RMID)、获取状态信息(IPC_STAT)或修改权限(IPC_SET)。
开发避坑指南
- System V 的残留问题:System V 消息队列是随内核持续的,如果程序异常退出且未调用 msgctl(IPC_RMID) 删除队列,消息队列会残留在系统中。需使用 ipcs -q 查看并用 ipcrm -q 手动清理。
- 内存泄漏防范:在自定义消息队列中,出队(mq_pop)后务必 free 掉消息体(body)、消息结构体(msg)以及队列节点(node),防止内存泄漏。
- 销毁队列的规范:销毁内存队列时,不仅要销毁互斥锁和条件变量,还要遍历链表释放所有残留的消息节点。
查看IPC 信号量对象命令: ipcs -s查看IPC 共享内存对象命令:ipcs -m查看IPC 消息队列对象命令:ipcs -q查看IPC 全部对象命令: ipcs -a 或者 ipc -pms删除IPC 信号量对象命令:ipcrm -s semid删除IPC 共享内存对象命令:ipcrm -m shmid删除IPC 消息队列对象命令:ipcrm -q msgid
4. 信号量
- 本质:Dijkstra 提出的整数计数器,通过原子操作解决并发竞态条件,控制共享资源访问。
- P 操作(Wait/Down):S减1。若 S < 0,进程阻塞入队;若 S >= 0,继续执行。
- V 操作(Signal/Up):S加1。若 S <= 0,唤醒队列中- 一个进程;若 S > 0,继续执行。
- 二进制信号量:值为 0 或 1,用于互斥(保护临界区)。
- 计数信号量:值为非负整数,用于同步(控制 N 个同类资源的并发访问)。
- 与互斥锁区别:互斥锁有所有权(谁加锁谁解锁);信号量无所有权(任何进程/线程均可 V 操作释放)。
- 初始化:sem_init() / sem_open()
- 销毁:sem_destroy() / sem_close()
5. 信号
- 本质:软件层面的“中断”,用于通知进程异步事件(如 Ctrl+C 触发 SIGINT)。
- 生命周期:产生 → 未决(Pending,已产生但未处理) → 递达与处理。
- 阻塞机制:通过信号屏蔽字控制,被阻塞的信号停留在“未决”状态,解除阻塞后才递达。
- 处理方式:默认动作、忽略、自定义捕获函数(Handler)。
- 注册:signal() / sigaction()
- 阻塞/未决:sigprocmask() / sigpending()
- 分类:标准信号(1-31,不可靠,不排队);实时信号(32+,可靠,支持排队)。
6. 套接字
UDP 协议
UDP 单对单
广播
- 设置特殊的ip地址(广播地址): 192.168.12.255 / 255.255.255.255 (INADDR_BROADCAST)
- 接收端绑定的ip应该使用任何ip(0.0.0.0---INADDR_ANY)---推荐;接收端绑定当前主机所在ip网段的广播地址(192.168.x.255),并且发送端必须给192.168.x.255地址发送数据,接收端才能收到;接收端绑定地址信息是不指定ip地址,让系统默认
组播
224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet;224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效;239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
UDP实现组播--接收端
- 组播地址范围:组播 IP 属于 D 类地址(224.0.0.0 ~ 239.255.255.255)。 建议:局域网测试推荐使用本地管理范围的私有组播地址 239.0.0.0 ~ 239.255.255.255,避免与公网真实组播流冲突。
- 接收端核心逻辑:接收端不关心发送端是谁,只需“加入”指定的组播组,操作系统内核会自动过滤并接收发往该组播 IP 和端口的数据包。
核心实现步骤
- 设置地址复用选项 -> setsockopt--SO_REUSEADDR
- 加入组播组 -> setsockopt--IP_ADD_MEMBERSHIP
- 离开组播组 -> setsockopt--IP_DROP_MEMBERSHIP
标准跨平台 struct ip_mreq:通过 IP 地址指定网卡。
structip_mreqmreq;mreq.imr_multiaddr.s_addr = inet_addr("239.1.2.3");// 组播组 IPmreq.imr_interface.s_addr = htonl(INADDR_ANY);// 默认网卡
Linux 专属 struct ip_mreqn:支持通过网卡名称获取索引(imr_ifindex),在多网卡环境下更精确。
structip_mreqnmreqn;mreqn.imr_multiaddr.s_addr = inet_addr("239.1.2.3");mreqn.imr_address.s_addr = htonl(INADDR_ANY);mreqn.imr_ifindex = if_nametoindex("ens33");// 通过网卡名获取索引
流程图

UDP实现组播--接收端
- 组播地址范围 与接收端一致,发送端也应使用 D 类地址(224.0.0.0 ~ 239.255.255.255)。局域网测试推荐使用私有组播地址 239.0.0.0 ~ 239.255.255.255。
核心实现步骤
- 设置组播 TTL (可选) -> setsockopt(IP_MULTICAST_TTL),限制组播包跨越的路由器跳数(默认通常为 1,仅限本地网段)。
- 指定发送网卡 (可选) -> setsockopt(IP_MULTICAST_IF),在多网卡环境下指定从哪个网卡发出组播包。
- 发送数据 -> sendto,目标地址为组播 IP 和端口。
流程图

TCP 协议
服务器端
- 监听客户端连接 ---> listenbacklog 是指允许的连接数,当客户端发起连接请求而服务器正忙于处理其他任务时,新到达的连接请求会被暂存在一个队列中等待处理,这个队列就是 backlog。它指定了该未完成连接队列的最大长度。
普通 Web 服务(如 Nginx/PHP-FPM):通常设置为 1024 ~ 2048 即可满足大部分需求。短连接高频服务(如 API 网关、HTTP 负载均衡):由于连接建立频繁且速度快,建议设置为 1024 ~ 4096。也可以根据公式估算:峰值 QPS × 平均连接建立耗时(秒)。高并发/长连接服务(如 WebSocket、IM 服务、Swoole):由于并发量大,建议设置为 8192 ~ 16384 甚至更高。
客户端
- 发起服务器连接 ---> connect 连接超时与重试:客户端在调用
connect 时,若服务器未响应或网络异常,通常会触发超时。建议设置合理的超时时间(如 3~5 秒),并配合重试机制(如指数退避算法)以提高弱网环境下的连接成功率。
普通 Web 客户端:默认超时 3~5 秒,重试 1~2 次。移动端 App:考虑弱网与网络切换,超时可设为 5~10 秒,重试 2~3 次,并增加网络状态监听。高频 API 调用客户端:超时 1~3 秒,重试 1 次,避免阻塞主线程。长连接客户端(如 IM、WebSocket):首次连接超时 5~10 秒,断线后采用指数退避重连(如 1s → 2s → 4s → 8s → 最大 30s)。
客户端与服务端关键差异总结:
并发服务器设计
- 初始化监听:创建Socket,绑定IP和端口,开启监听。
- 接收连接:主线程循环调用 accept,获取新连接。
- 并发分发:将新连接分配给独立线程、线程池或IO多路复用(如epoll)处理。
- 业务处理:工作单元负责与客户端的数据读写及业务逻辑。
- 同步与安全:使用锁等机制保护共享资源,防止数据竞争。
- 资源回收:通信结束后,及时关闭Socket并释放相关内存。
- 线程池是多线程并发处理任务的一种实现方式,它将任务分配给多个线程,并管理这些线程的生命周期。
任务提交流程(核心流程)
关键规则
优先创建核心线程:线程数未达到 corePoolSize 时,即使有空闲线程,也会创建新线程最后扩容:队列满了才创建非核心线程(corePoolSize → maximumPoolSize)最终拒绝:队列满 + 线程数达到上限 → 执行拒绝策略
Worker 线程执行流程
TCP为什么要三次握手,两次不行吗
TCP采用三次握手的方式建立连接,主要是为了确保连接的可靠性和防止因网络延迟或其他原因引起的错误连接。
具体来说,三次握手的过程如下: 1、客户端向服务器发送一个连接请求报文(SYN)。 2、服务器收到请求报文后,回复一个确认报文(SYN+ACK)表示已经收到请求。 3、客户端再次回复一个确认报文(ACK),表示已经收到服务器的确认。
通过这个过程,客户端和服务器能够确保双方都能正常收发数据。
如果只进行两次握手,那么就存在以下问题: 1.如果只进行两次握手,那么服务器只能确认客户端的请求,但是客户端无法确认服务器是否已经收到自己的请求,从而无法保证连接的可靠性。 2.可能存在历史连接的延续。假设客户端发送一个连接请求,但是由于某种原因导致服务器没有收到请求,客户端可能会认为连接已经建立,但是服务器并不知道。如果后来有其他客户端向服务器发送请求,而请求中恰好包含了与之前客户端相同的源地址和端口号,那么服务器就会误认为这是之前客户端发送的请求,从而建立连接,这就导致了历史连接的延续,可能会给网络带来安全隐患。因此,为了保证连接的可靠性和安全性,TCP采用了三次握手的方式建立连接。
为什么要四次挥手,为什么不能是三次挥手
至于为什么不能是三次挥手,这主要是因为TCP连接是双向的,也就是说,通信的双方都可以发送和接收数据。在关闭连接时,需要确保双方都知道了连接已经关闭,并且所有的数据都已经传输完毕。如果使用三次挥手,可能会存在一方已经关闭了连接,但另一方仍然尝试发送数据的情况,这会导致数据丢失或混乱。因此,为了确保数据的完整性和可靠性,TCP采用了四次挥手的方式来关闭连接。
请注意,TCP没有半开方法的传输方法,但有半关传输方法。半关指的是一方发了FIN表明不再发送数据,但是另外一方不发FIN,这时候不发FIN的一方还可以继续发数据。这也是TCP采用四次挥手而不是三次的一个重要原因。