Linux 入门九:Linux 进程间通信
概述
进程间通信(IPC,Inter-Process Communication)是指在不同进程之间传递数据和信息的机制。Linux 提供了多种 IPC 方式,包括管道、信号、信号量、消息队列、共享内存和套接字等。
方式
一、管道(Pipe)
管道是 Linux 中最基础的进程间通信方式,通过内核缓冲区实现数据传输,分为无名管道和有名管道,适用于不同场景的进程通信。
1. 无名管道(PIPE)

核心特点
- 亲缘关系限制:仅支持父子 / 兄弟等有亲缘关系的进程通信(通过 fork 继承文件描述符)。
- 半双工通信:数据单向流动,固定
fd[0]为读端,fd[1]为写端。 - 内存级文件:无实际磁盘文件,数据存在于内核缓冲区,进程退出后数据消失。
关键函数:pipe
#include <unistd.h>
int pipe(int fd[2]); // fd[0]=读端,fd[1]=写端,成功返回 0,失败返回 -1
通信步骤(以父子进程为例)
步骤 1:创建管道
int fd[2];
if (pipe(fd) == -1) {perror("pipe create failed");exit(1);
}
- 作用:在内核中创建缓冲区,返回两个文件描述符。
步骤 2:fork 生成子进程
pid_t pid = fork();
if (pid < 0) {perror("fork failed");exit(1);
}
- 目的:通过子进程继承父进程的管道文件描述符。
步骤 3:父子进程分工读写
- 父进程(写端):
if (pid > 0) {close(fd[0]); // 关闭读端,仅用写端const char *msg = "Hello from parent!";write(fd[1], msg, strlen(msg)); // 向管道写数据close(fd[1]); // 写完关闭写端wait(NULL); // 等待子进程结束 } - 子进程(读端):
else {close(fd[1]); // 关闭写端,仅用读端char buf[1024] = {0};ssize_t n = read(fd[0], buf, sizeof(buf)); // 从管道读数据if (n > 0) {printf("Child read: %s\n", buf);}close(fd[0]); } - 关键点:读写端分离,避免双向干扰。
步骤 4:处理关闭顺序(重要!)
- 先关写端:读端
read返回 0(管道空且关闭)。 - 先关读端:写端
write会触发SIGPIPE信号(需用signal处理或忽略)。
// 示例:处理 SIGPIPE 信号
signal(SIGPIPE, SIG_IGN); // 忽略该信号,避免进程终止
完整示例(父子进程通信)
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>void handle_pipe_signal(int sig) {printf("Caught SIGPIPE (signal %d)\n", sig);
}int main() {int fd[2];signal(SIGPIPE, handle_pipe_signal); // 注册信号处理函数if (pipe(fd) == -1) {perror("pipe");exit(1);}pid_t pid = fork();if (pid == -1) {perror("fork");exit(1);} else if (pid == 0) {// 子进程:读端close(fd[1]);char buf[1024];ssize_t n = read(fd[0], buf, sizeof(buf));if (n > 0) {printf("Child received: %s\n", buf);}close(fd[0]);} else {// 父进程:写端close(fd[0]);const char *msg = "Hello, child!";write(fd[1], msg, strlen(msg));close(fd[1]); // 关闭写端后,子进程读端会收到 EOF}return 0;
}
2. 有名管道(FIFO)

核心特点
- 无亲缘限制:通过文件系统路径名(如
/tmp/myfifo)实现任意进程通信。 - 持久化存储:管道以文件形式存在(类型为
p),可通过ls -l查看。 - 阻塞机制:读 / 写端未打开时,
open会阻塞(除非用O_NONBLOCK)。
创建方式
方式 1:命令行创建(mkfifo)
mkfifo /tmp/myfifo # 创建有名管道文件
ls -l /tmp/myfifo # 查看:类型为 p(pipe)
方式 2:编程创建(mkfifo 函数)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode); // 成功返回 0,失败返回 -1
// mode:文件权限,如 0666(读写权限)
读写流程(以客户端 - 服务器模式为例)
步骤 1:创建管道(服务端执行)
// server.c
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>int main() {if (mkfifo("/tmp/myfifo", 0666) == -1 && errno != EEXIST) {perror("mkfifo");return 1;}// 后续打开管道进行读写...return 0;
}
步骤 2:打开管道(读端 / 写端)
- 读端(阻塞模式):
int fd_read = open("/tmp/myfifo", O_RDONLY); // 阻塞直到写端打开 - 写端(非阻塞模式):
int fd_write = open("/tmp/myfifo", O_WRONLY | O_NONBLOCK); // 不阻塞
步骤 3:读写数据(与普通文件一致)
- 写端示例(客户端):
// client.c(写端) #include <fcntl.h> #include <stdio.h> #include <string.h> #include <unistd.h>int main() {int fd = open("/tmp/myfifo", O_WRONLY);if (fd == -1) {perror("open write");return 1;}char *msg = "Hello, FIFO!";write(fd, msg, strlen(msg));close(fd);return 0; } - 读端示例(服务端):
// server.c(续) int fd = open("/tmp/myfifo", O_RDONLY); char buf[1024] = {0}; ssize_t n = read(fd, buf, sizeof(buf)); if (n > 0) {printf("Server read: %s\n", buf); } close(fd); unlink("/tmp/myfifo"); // 清理管道文件(非必须,可保留)
注意事项
- 文件权限:需确保读写进程对管道文件有对应权限(读端需
O_RDONLY,写端需O_WRONLY)。 - 阻塞行为:
- 读端
open时,若写端未打开,会阻塞直到写端打开。 - 写端
open时,若读端未打开,默认阻塞(加O_NONBLOCK则返回 -1)。
- 读端
3. 标准流管道(popen/pclose)
核心功能
- 简化 shell 命令调用:通过管道连接进程与外部命令的标准输入 / 输出。
- 文件流接口:使用
FILE*指针,兼容fread/fwrite等标准 IO 函数。
关键函数
popen:创建管道并执行命令
#include <stdio.h>
FILE* popen(const char *command, const char *mode);
// command:要执行的 shell 命令(如 "ls -l")
// mode:"r"(读命令输出)或 "w"(向命令输入)
// 返回:成功返回文件流指针,失败返回 NULL
pclose:关闭管道
int pclose(FILE *stream); // 成功返回命令退出码,失败返回 -1
示例:读取命令输出(如 cat /etc/os-release)
#include <stdio.h>int main() {FILE *fp = popen("cat /etc/os-release", "r"); // 执行命令,获取读流if (fp == NULL) {perror("popen");return 1;}char buf[256];while (fgets(buf, sizeof(buf), fp)) { // 逐行读取命令输出printf("%s", buf);}pclose(fp); // 关闭管道,释放资源return 0;
}
示例:向命令输入数据(如 wc -w 统计单词数)
#include <stdio.h>
#include <string.h>int main() {const char *text = "hello world\nlinux ipc\n";FILE *fp = popen("wc -w", "w"); // 向命令标准输入写数据if (fp == NULL) {perror("popen");return 1;}fwrite(text, sizeof(char), strlen(text), fp); // 写入数据pclose(fp); // 关闭时命令执行完毕,输出统计结果return 0;
}
4. 管道对比与适用场景
| 特性 | 无名管道(PIPE) | 有名管道(FIFO) | 标准流管道(popen) |
|---|---|---|---|
| 进程关系 | 亲缘关系 | 任意进程 | 主进程与外部命令 |
| 持久化 | 内存中,进程退出消失 | 文件系统存在(需手动删除) | 临时管道,pclose 后消失 |
| 接口方式 | read/write | open/read/write | fread/fwrite |
| 典型场景 | 父子进程数据传递 | 跨进程日志监控 | 执行 shell 命令并处理输出 |
总结
管道是 Linux IPC 的基础,无名管道适合亲缘进程快速通信,有名管道突破进程关系限制,标准流管道简化命令交互。掌握管道的阻塞机制、文件描述符操作和信号处理,是深入理解进程通信的关键。后续可结合信号量、共享内存等进阶方式,实现更复杂的同步与数据交换。
二、信号(Signal)
信号是 Linux 中一种轻量级的进程间通信方式,通过软件模拟硬件中断机制,用于通知进程发生了异步事件(如用户输入、子进程结束、非法内存访问等)。进程可以通过捕获信号执行特定操作,或选择忽略、使用默认处理方式。
1. 信号基础:从 “软中断” 到进程通知
核心概念
- 异步事件:信号的到来无需进程主动等待,类似硬件中断(如键盘按下
Ctrl+C发送SIGINT信号)。 - 信号类型:系统预定义 30+ 种信号(如
SIGKILL、SIGCHLD),每个信号有唯一编号(如SIGINT对应 2)和默认行为(如终止进程、忽略、内存转储等)。 - 信号处理:进程可自定义信号处理逻辑,或沿用系统默认行为,或忽略信号(但
SIGKILL和SIGSTOP不可被忽略或捕获)。
查看系统信号列表
kill -l # 列出所有信号及其编号
2. 信号处理三方式:默认、忽略、捕获
方式 1:默认处理(系统预设行为)
- 示例:收到
SIGINT(Ctrl+C)时,进程默认终止。 - 适用场景:无需特殊处理的信号(如
SIGCHLD子进程结束信号,默认忽略)。
方式 2:忽略信号(丢弃事件)
#include <signal.h>
signal(SIGINT, SIG_IGN); // 忽略 SIGINT 信号,Ctrl+C 无效
- 注意:
SIGKILL(编号 9)和SIGSTOP(编号 19)不可被忽略,用于强制终止 / 暂停进程。
方式 3:捕获信号(自定义处理逻辑)
通过 signal 函数注册信号处理函数,进程收到信号时自动调用该函数。
#include <signal.h>
typedef void (*sighandler_t)(int); // 信号处理函数原型,参数为信号编号
sighandler_t signal(int signum, sighandler_t handler);
// signum:目标信号(如 SIGINT)
// handler:处理函数指针,或 SIG_IGN(忽略)、SIG_DFL(默认)
步骤 1:定义信号处理函数
void handle_sigint(int sig) {printf("收到信号 %d(SIGINT),不终止进程,继续运行...\n", sig);// 可在此添加自定义逻辑(如清理资源、保存状态)
}
步骤 2:注册处理函数
int main() {signal(SIGINT, handle_sigint); // 将 SIGINT 信号绑定到 handle_sigint 函数printf("程序运行中,按 Ctrl+C 触发信号处理...\n");while (1) sleep(1); // 循环运行,演示信号异步触发return 0;
}
关键点
- 异步触发:信号处理函数会打断进程当前操作,类似 “回调函数”。
- 可重入函数:处理函数中避免使用非可重入函数(如
printf是安全的,但全局变量操作需加锁)。
3. 信号发送:主动通知进程的 “信使”
函数 1:kill — 向指定进程发信号
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
// pid:目标进程 ID(`pid > 0` 为单个进程,`pid = 0` 为当前进程组所有进程)
// sig:信号类型(如 SIGTERM、SIGKILL)
// 返回:成功 0,失败 -1(如 pid 不存在)
示例:杀死指定进程
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>int main() {pid_t target_pid = 12345; // 替换为实际进程 IDif (kill(target_pid, SIGTERM) == -1) { // 发送终止信号(可捕获)perror("kill failed");return 1;}printf("已向进程 %d 发送 SIGTERM 信号\n", target_pid);return 0;
}
- 命令行等价:
kill -SIGTERM 12345或kill -15 12345。
函数 2:raise — 向自身发信号
#include <signal.h>
int raise(int sig); // 等价于 kill(getpid(), sig)
// 示例:主动触发 SIGUSR1 信号
raise(SIGUSR1);
函数 3:pause — 挂起进程直到收到信号
#include <unistd.h>
int pause(void); // 成功时不返回(仅当捕获信号且处理函数未终止进程时继续执行)
示例:等待信号唤醒
printf("等待信号唤醒...\n");
pause(); // 进程在此阻塞,直到收到任意信号(除忽略的信号)
printf("信号处理完成,继续执行...\n");
4. 实战场景:僵尸进程与信号处理
什么是僵尸进程?
子进程退出后,父进程未调用 wait/waitpid 回收资源,导致子进程状态保留在系统中,成为 “僵尸进程”(状态为 Z)。
问题影响
- 占用进程表资源,过多会导致系统性能下降。
- 父进程退出后,僵尸进程由
init进程(PID 1)接管,但仍需手动清理。
解决方案:捕获 SIGCHLD 信号
子进程结束时,系统自动向父进程发送 SIGCHLD 信号,父进程可通过处理该信号回收资源。
步骤 1:注册信号处理函数
#include <signal.h>
#include <sys/wait.h>void handle_child(int sig) {pid_t pid;int status;while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { // 非阻塞回收所有子进程if (WIFEXITED(status)) { // 子进程正常退出printf("子进程 %d 退出,状态码 %d\n", pid, WEXITSTATUS(status));} else { // 子进程被信号终止printf("子进程 %d 被信号 %d 终止\n", pid, WTERMSIG(status));}}
}
步骤 2:在父进程中绑定信号
int main() {signal(SIGCHLD, handle_child); // 绑定 SIGCHLD 信号处理函数// 创建子进程示例pid_t pid = fork();if (pid == 0) {// 子进程逻辑exit(0); // 子进程退出,触发 SIGCHLD 信号} else {// 父进程继续运行,无需阻塞等待子进程while (1) sleep(1);}return 0;
}
关键函数 waitpid
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
// pid:-1 表示任意子进程,0 表示同进程组子进程
// status:存储子进程退出状态(NULL 时忽略)
// options:WNOHANG 表示非阻塞,无退出子进程时立即返回
5. 常见信号分类与典型用途
| 信号名称 | 编号 | 默认行为 | 典型场景 |
|---|---|---|---|
| SIGINT | 2 | 终止进程 | 用户按下 Ctrl+C,中断程序运行 |
| SIGKILL | 9 | 强制终止进程(不可捕获 / 忽略) | 紧急终止无响应进程(kill -9 <pid>) |
| SIGCHLD | 17 | 忽略 | 子进程结束时通知父进程,触发资源回收 |
| SIGPIPE | 13 | 终止进程 | 向已关闭读端的管道写数据时触发(需处理避免崩溃) |
| SIGTERM | 15 | 终止进程(可捕获) | 优雅终止进程(kill <pid> 默认发送此信号) |
| SIGUSR1/SIGUSR2 | 10/12 | 终止进程 | 用户自定义信号,用于进程间自定义通信 |
6. 信号处理最佳实践
- 避免僵尸进程:永远为
SIGCHLD信号注册处理函数,使用waitpid非阻塞回收子进程。 - 谨慎处理
SIGPIPE:网络编程或管道通信时,捕获该信号并忽略,避免程序意外终止:signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号 - 区分信号类型:
SIGKILL用于强制终止,SIGTERM用于正常终止(允许进程清理资源)。 - 可重入性:信号处理函数中仅调用可重入函数(如
_exit、write),避免使用全局变量或复杂逻辑。
总结
信号是 Linux 进程间通信的 “轻骑兵”,适合异步通知和简单控制(如终止、暂停进程)。掌握 signal 函数的使用、信号发送机制(kill/raise)以及僵尸进程处理,是编写健壮程序的基础。后续可结合信号量、共享内存等方式,实现更复杂的进程同步与协作。
三、System V 信号量

System V 信号量是 Linux 中用于进程同步的核心机制之一,通过维护一个非负整数计数器(信号量值),控制多个进程对共享资源的访问。它支持 “信号量集”(多个信号量的集合),适用于复杂的同步场景,如生产者 - 消费者问题、资源竞争控制等。
1. 信号量核心概念:从 “资源计数器” 到进程同步
什么是信号量?
- 本质:一个非负整数变量,用于表示可用资源的数量。
- PV 操作:
- P 操作(申请资源):信号量值减 1,若结果为负则阻塞进程,直到资源可用。
- V 操作(释放资源):信号量值加 1,若有进程阻塞则唤醒。
- 信号量集:包含多个信号量的集合(类似数组),每个信号量通过编号(从 0 开始)唯一标识,适用于管理多种资源。
典型应用场景
- 互斥访问:确保共享资源同一时间仅被一个进程访问(二进制信号量,初始值为 1)。
- 资源计数:控制同时访问资源的进程数量(如连接池,初始值为资源总数)。
2. 关键函数:信号量集的创建、操作与控制
函数 1:semget — 创建 / 获取信号量集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
- 参数解释:
key:信号量集的唯一标识(可通过ftok函数生成,或用IPC_PRIVATE创建私有信号量集)。nsems:信号量集中信号量的数量(创建时需指定,获取已有集合时设为 0)。flag:- 权限标志:如
0666(读写权限)。 - 创建标志:
IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT合用,确保创建新集合)。
- 权限标志:如
- 返回值:成功返回信号量集 ID(整数),失败返回 -1。
示例:生成唯一 key
key_t key = ftok("/tmp/semaphore.txt", 'S'); // 基于文件路径和项目 ID 生成 key
函数 2:semop — 执行信号量操作(PV 操作)
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nops);
- 参数解释:
semid:信号量集 ID(由semget返回)。sops:指向struct sembuf数组的指针,每个元素描述一个信号量操作:struct sembuf {short sem_num; // 信号量在集合中的编号(0 开始)short sem_op; // 操作值:-1(P 操作,申请资源)、+1(V 操作,释放资源)short sem_flg; // 标志位,常用 `SEM_UNDO`(进程退出时自动恢复信号量值) };nops:操作数组的长度(一次可操作多个信号量,实现原子操作)。
- 返回值:成功返回 0,失败返回 -1。
函数 3:semctl — 控制信号量集(初始化、删除等)
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...); // 可变参数依赖于 cmd
- 参数解释:
semid:信号量集 ID。semnum:目标信号量编号(单个信号量操作时使用,集合操作时设为 0)。cmd:操作类型(常用值):SETVAL:设置信号量初始值(需第四个参数union semun)。GETVAL:获取信号量当前值(返回值为信号量值)。IPC_RMID:删除信号量集(无需第四个参数)。
union semun自定义(需用户声明):union semun {int val; // 用于 SETVAL 初始化信号量struct semid_ds *buf; // 用于 IPC_STAT/IPC_SETunsigned short *array; // 用于 GETALL/SETALL(操作信号量集) };
3. 使用步骤:从创建到销毁,手把手实现信号量同步
场景:父子进程同步,轮流打印 “父” 和 “子”
步骤 1:创建信号量集并初始化
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdio.h>union semun {int val;
};int main() {key_t key = ftok(".", 'S'); // 生成唯一 keyint semid = semget(key, 1, IPC_CREAT | 0666); // 创建包含 1 个信号量的集合union semun arg;arg.val = 1; // 初始化信号量为 1(表示可用资源数)semctl(semid, 0, SETVAL, arg); // 对第 0 号信号量设置初始值// 后续步骤...
}
- 关键点:
SETVAL必须在semop之前调用,否则信号量值未定义。
步骤 2:定义 PV 操作函数
// P 操作:申请资源(信号量减 1,阻塞直到 >= 0)
void sem_p(int semid) {struct sembuf sop = {0, -1, SEM_UNDO}; // 操作第 0 号信号量,减 1,自动恢复semop(semid, &sop, 1);
}// V 操作:释放资源(信号量加 1)
void sem_v(int semid) {struct sembuf sop = {0, +1, SEM_UNDO};semop(semid, &sop, 1);
}
SEM_UNDO作用:若进程异常退出,内核自动将信号量恢复为操作前的值,避免资源泄漏。
步骤 3:父子进程通过信号量同步
pid_t pid = fork();
if (pid == 0) {// 子进程:先等待父进程释放信号量for (int i = 0; i < 5; i++) {sem_p(semid); // 申请资源(阻塞直到信号量 >= 1)printf("子\n");sem_v(semid); // 释放资源,允许父进程继续}
} else {// 父进程:先占用信号量,再释放for (int i = 0; i < 5; i++) {sem_p(semid);printf("父\n");sem_v(semid);}wait(NULL); // 等待子进程结束
}
- 执行效果:父与子交替打印,确保每次仅一个进程执行。
步骤 4:删除信号量集(避免残留)
semctl(semid, 0, IPC_RMID); // 立即删除信号量集,唤醒所有阻塞进程
4. 进阶应用:生产者 - 消费者问题(多信号量协作)
场景描述
- 共享缓冲区大小为
N,生产者向缓冲区放数据,消费者取数据。 - 信号量设计:
empty:空缓冲区数量(初始值N,V 操作表示释放空缓冲区)。full:满缓冲区数量(初始值 0,P 操作表示获取满缓冲区)。mutex:互斥信号量(初始值 1,确保缓冲区操作互斥)。
代码框架(简化版)
#define N 5 // 缓冲区大小
int semid;// 初始化信号量集(3 个信号量:empty, full, mutex)
void init_sem() {union semun arg;key_t key = ftok(".", 'S');semid = semget(key, 3, IPC_CREAT | 0666);// 初始化 empty=N, full=0, mutex=1arg.val = N; semctl(semid, 0, SETVAL, arg); // empty(0 号)arg.val = 0; semctl(semid, 1, SETVAL, arg); // full(1 号)arg.val = 1; semctl(semid, 2, SETVAL, arg); // mutex(2 号)
}// 生产者:放入数据
void producer() {for (int i = 0; i < 10; i++) {struct sembuf ops[] = {{0, -1, SEM_UNDO}, // P(empty):申请空缓冲区{2, -1, SEM_UNDO}, // P(mutex):互斥访问缓冲区// 放入数据的逻辑...{2, +1, SEM_UNDO}, // V(mutex):释放互斥锁{1, +1, SEM_UNDO} // V(full):增加满缓冲区};semop(semid, ops, 4); // 原子操作 4 个信号量}
}// 消费者:取出数据
void consumer() {for (int i = 0; i < 10; i++) {struct sembuf ops[] = {{1, -1, SEM_UNDO}, // P(full):申请满缓冲区{2, -1, SEM_UNDO}, // P(mutex):互斥访问缓冲区// 取出数据的逻辑...{2, +1, SEM_UNDO}, // V(mutex):释放互斥锁{0, +1, SEM_UNDO} // V(empty):增加空缓冲区};semop(semid, ops, 4);}
}
- 关键点:
semop支持一次操作多个信号量,确保多个 PV 操作的原子性(要么全成功,要么全失败)。
5. 信号量操作注意事项
(1)信号量初始值设置
- 二进制信号量(互斥):初始值为 1,确保唯一进程访问资源。
- 计数信号量(资源池):初始值为资源总数(如数据库连接数为 5,则初始值设为 5)。
(2)避免死锁
- 操作顺序:多个信号量操作时,确保所有进程以相同顺序申请信号量(如先申请
mutex,再申请full)。 - 超时机制:结合
semop的非阻塞标志(但 System V 信号量不直接支持,需通过其他方式实现)。
(3)信号量集清理
- 手动删除:通过
semctl(semid, 0, IPC_RMID)删除,避免信号量集残留(可用ipcs -s查看系统信号量,ipcrm -s semid命令删除)。 - 自动回收:信号量集在内核中持久化,即使进程退出也不会自动删除,必须显式调用
semctl删除。
6. 代码示例:完整的信号量互斥演示
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdio.h>union semun { int val; };// 初始化信号量
int init_sem(key_t key, int initial_val) {int semid = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);if (semid == -1) {semid = semget(key, 1, 0); // 获取已存在的信号量集return semid;}union semun arg;arg.val = initial_val;semctl(semid, 0, SETVAL, arg);return semid;
}// P 操作
void sem_p(int semid) {struct sembuf sop = {0, -1, SEM_UNDO};semop(semid, &sop, 1);
}// V 操作
void sem_v(int semid) {struct sembuf sop = {0, +1, SEM_UNDO};semop(semid, &sop, 1);
}int main() {key_t key = ftok(".", 'S');int semid = init_sem(key, 1); // 初始化信号量为 1(互斥)if (fork() == 0) {// 子进程:尝试获取信号量sem_p(semid);printf("子进程获取信号量,开始执行...\n");sleep(2); // 模拟占用资源sem_v(semid);} else {// 父进程:等待子进程释放信号量sleep(1);sem_p(semid);printf("父进程获取信号量,开始执行...\n");sem_v(semid);wait(NULL);semctl(semid, 0, IPC_RMID); // 删除信号量集}return 0;
}
总结
System V 信号量是进程同步的强大工具,通过 PV 操作和信号量集实现复杂的资源管理。新手需重点掌握:
- 信号量初始化(
semget+semctl); - PV 操作实现(
semop与struct sembuf); - 多信号量协作(如生产者 - 消费者问题中的互斥与资源计数)。
实际开发中,结合共享内存、消息队列等 IPC 方式,可构建高效的进程间通信与同步方案。
四、消息队列
消息队列是 Linux 中一种灵活的进程间通信方式,允许进程以 “消息” 为单位进行数据交换。每个消息包含类型和内容,接收方可以根据类型选择性获取消息,适用于需要分类处理数据的场景(如日志系统、任务调度)。
1. 核心概念:从 “消息信封” 到分类通信
什么是消息队列?
- 本质:内核中维护的消息链表,每个消息由类型(长整型)和内容(用户自定义数据)组成。
- 核心特性:
- 类型过滤:接收方可按消息类型(
mtype)读取特定消息(如优先处理紧急消息)。 - 异步解耦:发送方和接收方无需同时运行,消息持久化存储直到被读取(依赖内核,进程重启后消失)。
- 多对多通信:多个进程可同时向队列写消息,多个进程可按类型读消息。
- 类型过滤:接收方可按消息类型(
与管道 / FIFO 的区别
| 特性 | 消息队列 | 管道 / FIFO |
|---|---|---|
| 数据单位 | 结构化消息(带类型) | 字节流(无类型) |
| 读取方式 | 按类型选择性读取 | 先进先出(FIFO) |
| 进程关系 | 无亲缘限制 | 无名管道仅限亲缘进程 |
| 持久化 | 内核存储,进程退出不消失 | 内存 / 文件存储,关闭后消失 |
2. 关键函数:消息队列的全生命周期操作
函数 1:msgget — 创建 / 获取消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- 参数解释:
key:消息队列的唯一标识(通过ftok生成,或用IPC_PRIVATE创建私有队列)。msgflg:- 权限标志:如
0666(读写权限)。 - 创建标志:
IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT合用,确保创建新队列)。
- 权限标志:如
- 返回值:成功返回队列 ID(整数),失败返回 -1。
函数 2:msgsnd — 发送消息到队列
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 参数解释:
msqid:消息队列 ID(由msgget返回)。msgp:指向消息结构体的指针(必须以长整型mtype开头)。msgsz:消息体的大小(不包含mtype,最大不超过系统限制,通常 4KB)。msgflg:0:阻塞直到队列有空间。IPC_NOWAIT:非阻塞,队列满时返回 -1。
- 返回值:成功 0,失败 -1。
函数 3:msgrcv — 从队列接收消息
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数解释:
msgtyp:期望的消息类型,支持三种模式:0:接收队列中的第一个消息(不考虑类型)。>0:接收类型等于msgtyp的第一个消息。<0:接收类型小于等于msgtyp绝对值的最小类型消息。
- 其他参数同
msgsnd,msgflg支持IPC_NOWAIT(队列空时非阻塞)。
- 返回值:成功返回消息体大小,失败 -1。
函数 4:msgctl — 控制消息队列(删除、查询状态)
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 常用
cmd:IPC_RMID:删除消息队列(立即标记为删除,后续操作拒绝)。IPC_STAT:获取队列状态(存入buf)。
3. 使用步骤:从创建到销毁,实现进程间消息交换
场景:非亲缘进程通信(客户端 - 服务器模式)
步骤 1:定义消息结构体(必须以 mtype 开头)
// common.h(共享头文件)
struct msgbuf {long mtype; // 消息类型(自定义,如 1 表示请求,2 表示响应)char mtext[100]; // 消息内容(用户自定义数据)
};
步骤 2:服务器端 — 创建队列并接收消息
// server.c
#include "common.h"
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>int main() {key_t key = ftok(".", 'M'); // 生成唯一 keyint msqid = msgget(key, IPC_CREAT | 0666); // 创建消息队列struct msgbuf msg;while (1) {// 接收类型为 1 的消息(客户端请求)msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0);printf("服务器收到消息:%s\n", msg.mtext);// 发送响应消息(类型 2)msg.mtype = 2;strcpy(msg.mtext, "服务器已收到消息");msgsnd(msqid, &msg, sizeof(msg.mtext), 0);}msgctl(msqid, IPC_RMID, NULL); // 删除队列(实际场景中可能由特定条件触发)return 0;
}
步骤 3:客户端 — 发送请求并接收响应
// client.c
#include "common.h"
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>int main() {key_t key = ftok(".", 'M');int msqid = msgget(key, 0666); // 获取已存在的队列struct msgbuf msg;strcpy(msg.mtext, "你好,服务器!");msg.mtype = 1; // 发送类型 1 的请求消息msgsnd(msqid, &msg, sizeof(msg.mtext), 0);// 接收类型为 2 的响应消息msgrcv(msqid, &msg, sizeof(msg.mtext), 2, 0);printf("客户端收到响应:%s\n", msg.mtext);return 0;
}
步骤 4:编译运行(需先运行服务器)
gcc server.c -o server
gcc client.c -o client
./server & # 后台运行服务器
./client # 客户端发送消息并接收响应
4. 进阶用法:亲缘进程通信与消息类型过滤
场景:父子进程通过消息队列传递不同类型的状态信息
步骤 1:创建私有消息队列(IPC_PRIVATE)
c
int msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0666); // 生成仅当前进程可见的队列
步骤 2:父进程发送控制消息,子进程按类型处理
// 父进程(发送类型 1:开始任务,类型 2:终止任务)
pid_t pid = fork();
if (pid > 0) {struct msgbuf msg;msg.mtype = 1;strcpy(msg.mtext, "开始执行任务");msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 发送开始命令sleep(2);msg.mtype = 2;strcpy(msg.mtext, "任务终止");msgsnd(msqid, &msg, sizeof(msg.mtext), 0); // 发送终止命令wait(NULL);
} else {struct msgbuf msg;while (1) {msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0); // 接收任意类型消息if (msg.mtype == 1) {printf("子进程:收到开始命令,执行任务...\n");} else if (msg.mtype == 2) {printf("子进程:收到终止命令,退出\n");exit(0);}}
}
关键点:
IPC_PRIVATE生成的队列需通过进程间共享的 ID(如通过管道或共享内存传递)实现通信。- 子进程通过
msgtyp=0接收所有类型消息,再根据mtype分支处理。
5. 消息队列操作最佳实践
(1)消息结构体设计
- 必须以
long mtype开头,否则msgrcv无法正确解析消息类型。 - 消息体大小限制:避免超过系统限制(可通过
sysctl -a | grep msgmnb查看,通常为 4096 字节)。
(2)阻塞与非阻塞模式选择
- 阻塞模式(默认):适合同步场景(如客户端等待服务器响应)。
- 非阻塞模式(
IPC_NOWAIT):适合异步场景(如日志收集器不阻塞主线程)。
(3)队列清理
- 手动删除:通过
msgctl(msqid, IPC_RMID, NULL)或命令ipcrm -q msqid(ipcs -q查看队列 ID)。 - 避免内存泄漏:服务器进程退出前务必删除队列,或通过
atexit注册清理函数。
(4)错误处理
- 队列满 / 空处理:发送时检查
msgsnd返回值,接收时结合errno == EAGAIN判断非阻塞场景。
6. 完整示例:带错误处理的消息队列通信
// 错误处理增强版 client.c
#include "common.h"
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>int main() {key_t key = ftok(".", 'M');int msqid = msgget(key, 0666);if (msqid == -1) {perror("msgget failed");return 1;}struct msgbuf msg;strcpy(msg.mtext, "测试消息");msg.mtype = 1;// 发送消息,处理队列满的情况if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {if (errno == EAGAIN) {printf("队列已满,发送失败\n");} else {perror("msgsnd failed");}return 1;}// 接收响应,处理队列空的情况memset(&msg, 0, sizeof(msg));ssize_t n = msgrcv(msqid, &msg, sizeof(msg.mtext), 2, 0);if (n == -1) {perror("msgrcv failed");return 1;}printf("收到响应:%s\n", msg.mtext);return 0;
}
总结
消息队列是 Linux IPC 中 “结构化通信” 的首选,适合需要分类处理数据的场景。新手需重点掌握:
- 消息结构体定义(以
mtype开头); - 按类型读取消息(
msgrcv的msgtyp参数); - 队列的生命周期管理(创建、发送、接收、删除)。
实际应用中,结合信号量实现队列操作的互斥,或与共享内存结合传输大数据,可构建更复杂的分布式通信系统。
五、共享内存
共享内存是 Linux 中最高效的进程间通信(IPC)方式,允许多个进程直接访问同一块物理内存区域。这种机制避免了数据在内核与用户空间之间的拷贝,特别适合高频、大数据量的通信场景(如实时视频处理、数据库缓存)。
1. 核心原理:从物理内存到虚拟地址映射
共享内存的底层机制
- 物理内存分配:通过
shmget函数创建共享内存段时,内核为其分配一块物理内存区域。 - 虚拟地址映射:进程通过
shmat函数将共享内存段映射到自身的虚拟地址空间,实现直接读写。 - 数据一致性:多个进程对共享内存的修改会直接反映到物理内存,其他进程无需额外操作即可看到变化。
与其他 IPC 机制的性能对比
| 机制 | 数据拷贝次数 | 上下文切换次数 | 适用场景 |
|---|---|---|---|
| 共享内存 | 0 | 0 | 高频、大数据量通信 |
| 管道 / FIFO | 2 | 2 | 简单数据传输 |
| 消息队列 | 2 | 2 | 分类消息传递 |
| 套接字 | 2 | 2 | 跨主机通信 |
优势:共享内存直接在用户空间操作,无需内核干预,性能比其他机制提升数倍。
2. 关键函数:共享内存的全生命周期管理
函数 1:shmget — 创建 / 获取共享内存段
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 参数解析:
key:共享内存的唯一标识(可通过ftok生成,或用IPC_PRIVATE创建私有段)。size:共享内存大小(实际分配按页对齐,如 4096 字节)。shmflg:- 权限标志:如
0666(读写权限)。 - 创建标志:
IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT合用,确保创建新段)。
- 权限标志:如
- 返回值:成功返回共享内存 ID,失败返回 -1。
函数 2:shmat — 映射共享内存到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数解析:
shmid:共享内存 ID(由shmget返回)。shmaddr:指定映射地址(通常设为NULL,由内核自动选择)。shmflg:SHM_RDONLY:只读映射。SHM_RND:若指定shmaddr,地址按页对齐。
- 返回值:成功返回映射后的内存指针,失败返回
(void *)-1。
函数 3:shmdt — 解除内存映射
int shmdt(const void *shmaddr);
- 参数:
shmaddr为shmat返回的指针。 - 作用:断开进程与共享内存的映射,但不删除内存段。
函数 4:shmctl — 控制共享内存(删除、查询状态)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 常用
cmd:IPC_RMID:删除共享内存段(标记为删除,所有进程断开后释放)。IPC_STAT:获取共享内存状态(存入buf)。
3. 使用步骤:从创建到销毁,实现进程间数据共享
场景:父子进程通过共享内存交换数据
步骤 1:创建共享内存段并初始化
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdio.h>int main() {key_t key = ftok(".", 'S'); // 生成唯一 keyint shmid = shmget(key, 1024, IPC_CREAT | 0666); // 创建 1KB 共享内存// 映射共享内存到进程地址空间char *shm_ptr = (char *)shmat(shmid, NULL, 0);if (shm_ptr == (char *)-1) {perror("shmat failed");return 1;}// 写入数据strcpy(shm_ptr, "Hello, shared memory!");// 解除映射(不删除内存段)shmdt(shm_ptr);// 创建子进程pid_t pid = fork();if (pid == 0) {// 子进程重新映射共享内存char *child_ptr = (char *)shmat(shmid, NULL, 0);printf("子进程读取数据:%s\n", child_ptr);shmdt(child_ptr);return 0;} else {wait(NULL);// 父进程删除共享内存段shmctl(shmid, IPC_RMID, NULL);return 0;}
}
关键点:
- 父子进程同步:父进程先写入数据,子进程通过
wait确保数据已写入。 - 内存清理:
shmctl(shmid, IPC_RMID, NULL)必须显式调用,否则共享内存段会残留(可用ipcs -m查看,ipcrm -m shmid手动删除)。
4. 进阶用法:非亲缘进程通信与同步机制
场景:客户端 - 服务器通过共享内存交换数据(需信号量同步)
步骤 1:定义共享内存结构体(含信号量)
// common.h
struct shared_data {int flag; // 同步标志(0:未写入,1:已写入)char buffer[1024];
};
步骤 2:服务器端 — 写入数据并通知客户端
// server.c
#include "common.h"
#include <sys/sem.h>int main() {// 创建共享内存key_t shm_key = ftok(".", 'S');int shmid = shmget(shm_key, sizeof(struct shared_data), IPC_CREAT | 0666);// 创建信号量(初始值 0)key_t sem_key = ftok(".", 'M');int semid = semget(sem_key, 1, IPC_CREAT | 0666);union semun { int val; } arg;arg.val = 0;semctl(semid, 0, SETVAL, arg);// 映射共享内存struct shared_data *shm_ptr = (struct shared_data *)shmat(shmid, NULL, 0);// 写入数据并通知客户端strcpy(shm_ptr->buffer, "服务器数据");shm_ptr->flag = 1;semop(semid, &(struct sembuf){0, 1, 0}, 1); // V 操作,释放信号量shmdt(shm_ptr);return 0;
}
步骤 3:客户端 — 等待数据并读取
// client.c
#include "common.h"
#include <sys/sem.h>int main() {// 获取共享内存和信号量key_t shm_key = ftok(".", 'S');int shmid = shmget(shm_key, 0, 0);key_t sem_key = ftok(".", 'M');int semid = semget(sem_key, 0, 0);// 映射共享内存struct shared_data *shm_ptr = (struct shared_data *)shmat(shmid, NULL, 0);// 等待数据(P 操作)semop(semid, &(struct sembuf){0, -1, 0}, 1);printf("客户端读取数据:%s\n", shm_ptr->buffer);shmdt(shm_ptr);return 0;
}
关键点:
- 信号量同步:服务器通过
semop释放信号量,客户端阻塞直到获取信号量。 - 权限管理:共享内存和信号量需设置相同的
key,确保进程间正确访问。
5. 共享内存操作最佳实践
(1)数据结构设计规范
- 避免动态内存分配:共享内存要求数据连续存储,禁止使用
std::string、std::vector等动态容器,改用固定大小数组:struct Data {int id;char name[32]; // 固定长度字符串int data[100]; // 固定大小数组 }; - 原子操作:对简单类型(如
int)可使用std::atomic保证操作原子性(需 C++11 支持)。
(2)错误处理与调试
- 内存映射失败:检查
shmat返回值是否为-1,并通过perror输出错误信息。 - 内存不足:
shmget可能因系统限制失败,需捕获ENOSPC错误。 - 调试命令:
ipcs -m # 查看系统共享内存段 ipcrm -m 688 # 删除 ID 为 688 的共享内存段 pmap -X 1234 # 查看进程 1234 的内存映射(含共享内存)
(3)性能优化
- 大页支持:通过
shmget的SHM_HUGETLB标志申请大页内存(如 2MB),减少页表开销。 - 内存预分配:提前分配足够大的共享内存,避免频繁调整大小。
6. 对比与拓展:mmap 实现共享内存
场景:父子进程通过 mmap 实现匿名共享内存
#include <sys/mman.h>
#include <fcntl.h>int main() {int *shared_var = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS, -1, 0);if (shared_var == MAP_FAILED) {perror("mmap failed");return 1;}*shared_var = 42; // 父进程写入数据pid_t pid = fork();if (pid == 0) {printf("子进程读取数据:%d\n", *shared_var);munmap(shared_var, sizeof(int));return 0;} else {wait(NULL);munmap(shared_var, sizeof(int));return 0;}
}
mmap 与 shmget 的区别
| 特性 | shmget + shmat | mmap |
|---|---|---|
| 底层实现 | System V IPC | POSIX 内存映射 |
| 适用场景 | 任意进程间通信 | 亲缘进程或文件映射 |
| 同步机制 | 需手动实现(如信号量) | 自动同步(基于文件) |
| 内存释放 | 显式调用 shmctl | 调用 munmap |
7. 完整示例:带同步的共享内存通信
#include <sys/shm.h>
#include <sys/sem.h>
#include <stdio.h>// 定义共享内存结构体(含同步信号量)
struct shared_data {int value;int semid; // 信号量 ID
};int main() {// 创建共享内存key_t shm_key = ftok(".", 'S');int shmid = shmget(shm_key, sizeof(struct shared_data), IPC_CREAT | 0666);struct shared_data *shm_ptr = (struct shared_data *)shmat(shmid, NULL, 0);// 创建信号量(初始值 0)key_t sem_key = ftok(".", 'M');shm_ptr->semid = semget(sem_key, 1, IPC_CREAT | 0666);union semun arg;arg.val = 0;semctl(shm_ptr->semid, 0, SETVAL, arg);// 写入数据并通知shm_ptr->value = 100;semop(shm_ptr->semid, &(struct sembuf){0, 1, 0}, 1);// 子进程读取数据pid_t pid = fork();if (pid == 0) {semop(shm_ptr->semid, &(struct sembuf){0, -1, 0}, 1);printf("子进程读取值:%d\n", shm_ptr->value);shmdt(shm_ptr);return 0;} else {wait(NULL);shmctl(shmid, IPC_RMID, NULL);semctl(shm_ptr->semid, IPC_RMID, NULL);return 0;}
}
总结
共享内存是 Linux IPC 的 “高速公路”,通过直接内存访问实现高性能通信。新手需重点掌握:
- 函数调用流程:
shmget→shmat→ 读写 →shmdt→shmctl(删除)。 - 同步机制:结合信号量或互斥锁确保数据一致性。
- 性能优化:大页内存、预分配、避免动态数据结构。
实际开发中,根据场景选择shmget(通用)或mmap(文件映射 / 亲缘进程),并配合调试工具(如ipcs、pmap)排查问题。
相关文章:
Linux 入门九:Linux 进程间通信
概述 进程间通信(IPC,Inter-Process Communication)是指在不同进程之间传递数据和信息的机制。Linux 提供了多种 IPC 方式,包括管道、信号、信号量、消息队列、共享内存和套接字等。 方式 一、管道(Pipe)…...
Spark-SQL核心编程实战:自定义函数与聚合函数详解
在大数据处理领域,Spark-SQL是极为重要的工具。今天和大家分享一下在Spark-SQL开发中的自定义函数和聚合函数的使用,这些都是基于实际项目开发经验的总结。 在Spark-SQL开发时,第一步是搭建开发环境。在IDEA中创建Spark-SQL子模块,…...
[Mysql][Mybatis][Spring]配置文件未能正确给驱动赋值,.properties文件username值被替换
这是最初的.properties配置文件: drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/qykf usernameroot password123456 在Mybatis中引入后进行赋值: <environments default"development"><environment id"deve…...
go 指针接收者和值接收者的区别
go 指针接收者和值接收者的区别 指针接收者和值接收者的区别主要有两点: Go 中函数传参是传值,因此指针接收者传递的是接收者的指针拷贝,值接收者传递的是接收者的拷贝---在方法中指针接收者的变量会被修改,而值接收者的成员变量…...
Redis之缓存更新策略
缓存更新策略 文章目录 缓存更新策略一、策略对比二、常见的缓存更新策略三、如何选择策略四、实际应用示例五、使用 Cache-Aside TTL 的方式,实现缓存商铺信息详情1.引入StringRedisTemplate2.将查询商铺信息加入缓存3.更新商铺信息时移除缓存总结 六、注意事项 一…...
【leetcode100】杨辉三角
1、题目描述 给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中,每个数是它左上方和右上方的数的和。 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例 2: 输入: numRows 1 输出: [[1]…...
git reset详解
一、git reset 的核心作用 用于 移动当前分支的 HEAD 指针 到指定的提交,并可选择是否修改工作区和暂存区。 ⚠️ 注意:若提交已被推送到远程仓库,强制重置(--hard)后需谨慎操作,避免影响协作。 二、三种模…...
Selenium2+Python自动化:利用JS解决click失效问题
文章目录 前言一、遇到的问题二、点击父元素问题分析解决办法实现思路 三、使用JS直接点击四、参考代码 前言 在使用Selenium2和Python进行自动化测试时,我们有时会遇到这样的情况:元素明明已经被成功定位,代码运行也没有报错,但…...
OpenStack Yoga版安装笔记(十九)启动一个实例(Self-service networks)
1、概述 1.1 官方文档 Launch an instancehttps://docs.openstack.org/install-guide/launch-instance.html 《OpenStack Yoga版安装笔记(十四)启动一个实例》文档中,已经按照Option1: Provider networks创建网络。 本文按照Option2&#…...
数据结构(java)栈与队列
栈:(先进后出) 入栈: 1.普通栈一定要放、最小栈放的原则是: *如果最小栈是空的,那么放 *如果最小栈的栈顶元素没有当前的元素小,则放 2.如果要放的的元素小于等于最小栈栈顶元素可以放吗?放 出栈: 需要…...
Flask+Plotly结合动态加载图形页面实践
1. DeepSeek帮我实践 1.1. 我的提问既设计方案 原有如下主页:dashboard.html,现增加“预测模型学习”,对感知机神经网络描述如下: 1、输入与输出为固定值,例如输入层215,输出层48; 2、模型为回归神经网络; 3、中层是可动态调整的,例如定义如下:第二层,200,第三层…...
数学教学通讯杂志数学教学通讯杂志社数学教学通讯编辑部2025年第6期目录
课程教材教法 “课程思政”视域下的高中数学教学探索与实践——以“函数概念的发展历程”为例 赵文博; 3-617 PBL教学模式下高中统计教学的探索与实践——以“随机抽样(第一课时)”为例 陈沛余; 7-10 “三新”背景下的高中数学教学困境与应对…...
整活 kotlin + springboot3 + sqlite 配置一个 SQLiteCache
要实现一个 SQLiteCache 也是很简单的只需要创建一个 cacheManager Bean 即可 // 如果配置文件中 spring.cache.sqlite.enable false 则不启用 Bean("cacheManager") ConditionalOnProperty(name ["spring.cache.sqlite.enable"], havingValue "t…...
C#容器源码分析 --- Dictionary<TKey,TValue>
Dictionary<TKey, TValue> 是 System.Collections.Generic 命名空间下的高性能键值对集合,其核心实现基于哈希表和链地址法(Separate Chaining)。 .Net4.8 Dictionary<TKey,TValue>源码地址: dictionary…...
在 Visual Studio Code 中安装通义灵码 - 智能编码助手
高效的编码工具对于提升开发效率和代码质量至关重要。 通义灵码作为一款智能编码助手,为开发者提供了全方位的支持。 本文将详细介绍如何在 Visual Studio Code(简称 VSCode)中安装通义灵码,以及如何进行相关配置以开启智能编码…...
【AutoTest】自动化测试工具大全(Java)
😊 如果您觉得这篇文章有用 ✔️ 的话,请给博主一个一键三连 🚀🚀🚀 吧 (点赞 🧡、关注 💛、收藏 💚)!!!您的支持 &#x…...
idea报错java: 非法字符: ‘\ufeff‘解决方案
解决方案步骤以及说明 BOM是什么?1. BOM的作用2. 为什么会出现 \ufeff 错误?3. 如何解决 \ufeff 问题? 最后重新编译,即可运行!!! BOM是什么? \ufeff 是 Unicode 中的 BOM࿰…...
PHY芯片与网络变压器接线设计指南——不同速率与接口的硬件设计原则
一、PHY与网络变压器的核心作用 • PHY芯片(物理层芯片) • 功能:实现数据编码(如Manchester、PAM4)、时钟恢复、链路协商(Auto-Negotiation)。 • 接口类型:MII/RMII/GMII/RGMII/…...
【学习笔记】计算机网络(八)—— 音频/视频服务
第8章 互联网上的音频/视频服务 文章目录 第8章 互联网上的音频/视频服务8.1概述8.2 流式存储音频/视频8.2.1 具有元文件的万维网服务器8.2.2 媒体服务器8.2.3 实时流式协议 RTSP 8.3 交互式音频/视频8.3.1 IP 电话概述8.3.2 IP电话所需要的几种应用协议8.3.3 实时运输协议 RTP…...
linux: 文件描述符fd
目录 1.C语言文件操作复习 2.底层的系统调用接口 3.文件描述符的分配规则 4.重定向 1.C语言文件操作复习 文件 内容 属性。所有对文件的操作有两部分:a.对内容的操作;b.对属性的操作。内容是数据,属性其实也是数据-存储文件,…...
记录一次后台项目的打包优化
文章目录 前言分析问题寻找切入点根据切入点逐一尝试cdn引入node包遇到的一些问题记录最终结果 前言 优化,所有开发者到一定的程度上,都绕不开的问题之一 例如: 首页加载优化白屏优化列表无限加载滚动优化,图片加载优化逻辑耦合…...
问题记录(四)——拦截器“失效”?null 还是“null“?
拦截器“失效”?null 还是"null"? 问题描述 这个问题本身并不复杂,但是却是一个容易被忽略的问题。 相信大家在项目中一定实现过强制登录的逻辑吧,巧了,所要介绍的问题就出现在测试强制登录接口的过程中&am…...
前端面试-HTML5与CSS3
HTML5/CSS3 1. HTML5语义化标签的作用是什么?请举例说明5个常用语义化标签及其适用场景 解答: 语义化标签通过标签名称直观表达内容结构,有利于: 提升可访问性(屏幕阅读器识别)改善SEO(搜索引…...
blender 导出衣服mesh为fbx,随后导入UE5,坐标轴如何保存一致
When exporting a clothing mesh from Blender to UE5 as an FBX file, maintaining consistent coordinate axes is crucial for proper positioning and orientation. Heres how to ensure coordinate consistency throughout the workflow: 当从 Blender 导出衣服 mesh 为 U…...
前端开发中的问题排查与定位:HTML、CSS、JavaScript(报错的解决方式)
目录 1.html 1. 结构错误调试:标签未正确嵌套 2. 语法问题调试:缺失引号 3. 断点调试:动态生成内容时的 JavaScript 错误 4. 网络调试:资源加载错误 5. 性能调试:页面加载性能 总结: 2.CSS 1. 定位…...
图论整理复习
回溯: 模板: void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯ÿ…...
MIMO预编码与检测算法的对比
在MIMO系统中,预编码(发送端处理)和检测算法(接收端处理)的核心公式及其作用对比如下: 1. 预编码算法(发送端) 预编码的目标是通过对发送信号进行预处理,优化空间复用或…...
C++修炼:vector模拟实现
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路! 我的博客:<但凡. 我的专栏:《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞,关注&am…...
案例-索引对于并发Insert性能优化测试
前言 最近因业务并发量上升,开发反馈对订单表Insert性能降低。应开发要求对涉及Insert的表进行分析并提供优化方案。 一般对Insert 影响基本都在索引,涉及表已按创建日期做了分区表,索引全部为普通索引未做分区索引。 优化建议: 1、将UNIQUE改为HASH(64) GLOBAL IND…...
[区块链lab2] 构建具备加密功能的Web服务端
实验目标: 掌握区块链中密码技术的工作原理。在基于Flask框架的服务端中实现哈希算法的加密功能。 实验内容: 构建Flash Web服务器,实现哈希算法、非对称加密算法的加密功能。 实验步骤: 哈希算法的应用:创建hash…...
