当前位置: 首页 > article >正文

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/writeopen/read/writefread/fwrite
典型场景父子进程数据传递跨进程日志监控执行 shell 命令并处理输出

总结

管道是 Linux IPC 的基础,无名管道适合亲缘进程快速通信,有名管道突破进程关系限制,标准流管道简化命令交互。掌握管道的阻塞机制、文件描述符操作和信号处理,是深入理解进程通信的关键。后续可结合信号量、共享内存等进阶方式,实现更复杂的同步与数据交换。

二、信号(Signal)

信号是 Linux 中一种轻量级的进程间通信方式,通过软件模拟硬件中断机制,用于通知进程发生了异步事件(如用户输入、子进程结束、非法内存访问等)。进程可以通过捕获信号执行特定操作,或选择忽略、使用默认处理方式。

1. 信号基础:从 “软中断” 到进程通知

核心概念
  • 异步事件:信号的到来无需进程主动等待,类似硬件中断(如键盘按下 Ctrl+C 发送 SIGINT 信号)。
  • 信号类型:系统预定义 30+ 种信号(如 SIGKILLSIGCHLD),每个信号有唯一编号(如 SIGINT 对应 2)和默认行为(如终止进程、忽略、内存转储等)。
  • 信号处理:进程可自定义信号处理逻辑,或沿用系统默认行为,或忽略信号(但 SIGKILL 和 SIGSTOP 不可被忽略或捕获)。
查看系统信号列表
kill -l  # 列出所有信号及其编号

2. 信号处理三方式:默认、忽略、捕获

方式 1:默认处理(系统预设行为)
  • 示例:收到 SIGINTCtrl+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. 常见信号分类与典型用途

信号名称编号默认行为典型场景
SIGINT2终止进程用户按下 Ctrl+C,中断程序运行
SIGKILL9强制终止进程(不可捕获 / 忽略)紧急终止无响应进程(kill -9 <pid>
SIGCHLD17忽略子进程结束时通知父进程,触发资源回收
SIGPIPE13终止进程向已关闭读端的管道写数据时触发(需处理避免崩溃)
SIGTERM15终止进程(可捕获)优雅终止进程(kill <pid> 默认发送此信号)
SIGUSR1/SIGUSR210/12终止进程用户自定义信号,用于进程间自定义通信

6. 信号处理最佳实践

  1. 避免僵尸进程:永远为 SIGCHLD 信号注册处理函数,使用 waitpid 非阻塞回收子进程。
  2. 谨慎处理 SIGPIPE:网络编程或管道通信时,捕获该信号并忽略,避免程序意外终止:
    signal(SIGPIPE, SIG_IGN);  // 忽略管道破裂信号
    
  3. 区分信号类型SIGKILL 用于强制终止,SIGTERM 用于正常终止(允许进程清理资源)。
  4. 可重入性:信号处理函数中仅调用可重入函数(如 _exitwrite),避免使用全局变量或复杂逻辑。

总结

信号是 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 操作和信号量集实现复杂的资源管理。新手需重点掌握:

  1. 信号量初始化semget + semctl);
  2. PV 操作实现semop 与 struct sembuf);
  3. 多信号量协作(如生产者 - 消费者问题中的互斥与资源计数)。
    实际开发中,结合共享内存、消息队列等 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 绝对值的最小类型消息。
    • 其他参数同 msgsndmsgflg 支持 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 msqidipcs -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 中 “结构化通信” 的首选,适合需要分类处理数据的场景。新手需重点掌握:

  1. 消息结构体定义(以 mtype 开头);
  2. 按类型读取消息msgrcv 的 msgtyp 参数);
  3. 队列的生命周期管理(创建、发送、接收、删除)。
    实际应用中,结合信号量实现队列操作的互斥,或与共享内存结合传输大数据,可构建更复杂的分布式通信系统。

五、共享内存

共享内存是 Linux 中最高效的进程间通信(IPC)方式,允许多个进程直接访问同一块物理内存区域。这种机制避免了数据在内核与用户空间之间的拷贝,特别适合高频、大数据量的通信场景(如实时视频处理、数据库缓存)。

1. 核心原理:从物理内存到虚拟地址映射

共享内存的底层机制
  • 物理内存分配:通过 shmget 函数创建共享内存段时,内核为其分配一块物理内存区域。
  • 虚拟地址映射:进程通过 shmat 函数将共享内存段映射到自身的虚拟地址空间,实现直接读写。
  • 数据一致性:多个进程对共享内存的修改会直接反映到物理内存,其他进程无需额外操作即可看到变化。
与其他 IPC 机制的性能对比
机制数据拷贝次数上下文切换次数适用场景
共享内存00高频、大数据量通信
管道 / FIFO22简单数据传输
消息队列22分类消息传递
套接字22跨主机通信

优势:共享内存直接在用户空间操作,无需内核干预,性能比其他机制提升数倍。

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::stringstd::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 + shmatmmap
底层实现System V IPCPOSIX 内存映射
适用场景任意进程间通信亲缘进程或文件映射
同步机制需手动实现(如信号量)自动同步(基于文件)
内存释放显式调用 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 的 “高速公路”,通过直接内存访问实现高性能通信。新手需重点掌握:

  1. 函数调用流程shmget → shmat → 读写 → shmdt → shmctl(删除)。
  2. 同步机制:结合信号量或互斥锁确保数据一致性。
  3. 性能优化:大页内存、预分配、避免动态数据结构。
    实际开发中,根据场景选择 shmget(通用)或 mmap(文件映射 / 亲缘进程),并配合调试工具(如 ipcspmap)排查问题。

相关文章:

Linux 入门九:Linux 进程间通信

概述 进程间通信&#xff08;IPC&#xff0c;Inter-Process Communication&#xff09;是指在不同进程之间传递数据和信息的机制。Linux 提供了多种 IPC 方式&#xff0c;包括管道、信号、信号量、消息队列、共享内存和套接字等。 方式 一、管道&#xff08;Pipe&#xff09…...

Spark-SQL核心编程实战:自定义函数与聚合函数详解

在大数据处理领域&#xff0c;Spark-SQL是极为重要的工具。今天和大家分享一下在Spark-SQL开发中的自定义函数和聚合函数的使用&#xff0c;这些都是基于实际项目开发经验的总结。 在Spark-SQL开发时&#xff0c;第一步是搭建开发环境。在IDEA中创建Spark-SQL子模块&#xff0c…...

[Mysql][Mybatis][Spring]配置文件未能正确给驱动赋值,.properties文件username值被替换

这是最初的.properties配置文件&#xff1a; drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/qykf usernameroot password123456 在Mybatis中引入后进行赋值&#xff1a; <environments default"development"><environment id"deve…...

go 指针接收者和值接收者的区别

go 指针接收者和值接收者的区别 指针接收者和值接收者的区别主要有两点&#xff1a; Go 中函数传参是传值&#xff0c;因此指针接收者传递的是接收者的指针拷贝&#xff0c;值接收者传递的是接收者的拷贝---在方法中指针接收者的变量会被修改&#xff0c;而值接收者的成员变量…...

Redis之缓存更新策略

缓存更新策略 文章目录 缓存更新策略一、策略对比二、常见的缓存更新策略三、如何选择策略四、实际应用示例五、使用 Cache-Aside TTL 的方式&#xff0c;实现缓存商铺信息详情1.引入StringRedisTemplate2.将查询商铺信息加入缓存3.更新商铺信息时移除缓存总结 六、注意事项 一…...

【leetcode100】杨辉三角

1、题目描述 给定一个非负整数 numRows&#xff0c;生成「杨辉三角」的前 numRows 行。 在「杨辉三角」中&#xff0c;每个数是它左上方和右上方的数的和。 示例 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 指针 到指定的提交&#xff0c;并可选择是否修改工作区和暂存区。 ⚠️ 注意&#xff1a;若提交已被推送到远程仓库&#xff0c;强制重置&#xff08;--hard&#xff09;后需谨慎操作&#xff0c;避免影响协作。 二、三种模…...

Selenium2+Python自动化:利用JS解决click失效问题

文章目录 前言一、遇到的问题二、点击父元素问题分析解决办法实现思路 三、使用JS直接点击四、参考代码 前言 在使用Selenium2和Python进行自动化测试时&#xff0c;我们有时会遇到这样的情况&#xff1a;元素明明已经被成功定位&#xff0c;代码运行也没有报错&#xff0c;但…...

OpenStack Yoga版安装笔记(十九)启动一个实例(Self-service networks)

1、概述 1.1 官方文档 Launch an instancehttps://docs.openstack.org/install-guide/launch-instance.html 《OpenStack Yoga版安装笔记&#xff08;十四&#xff09;启动一个实例》文档中&#xff0c;已经按照Option1: Provider networks创建网络。 本文按照Option2&#…...

数据结构(java)栈与队列

栈&#xff1a;&#xff08;先进后出&#xff09; 入栈: 1.普通栈一定要放、最小栈放的原则是: *如果最小栈是空的&#xff0c;那么放 *如果最小栈的栈顶元素没有当前的元素小&#xff0c;则放 2.如果要放的的元素小于等于最小栈栈顶元素可以放吗?放 出栈: 需要…...

Flask+Plotly结合动态加载图形页面实践

1. DeepSeek帮我实践 1.1. 我的提问既设计方案 原有如下主页:dashboard.html,现增加“预测模型学习”,对感知机神经网络描述如下: 1、输入与输出为固定值,例如输入层215,输出层48; 2、模型为回归神经网络; 3、中层是可动态调整的,例如定义如下:第二层,200,第三层…...

数学教学通讯杂志数学教学通讯杂志社数学教学通讯编辑部2025年第6期目录

课程教材教法 “课程思政”视域下的高中数学教学探索与实践——以“函数概念的发展历程”为例 赵文博; 3-617 PBL教学模式下高中统计教学的探索与实践——以“随机抽样&#xff08;第一课时&#xff09;”为例 陈沛余; 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 命名空间下的高性能键值对集合&#xff0c;其核心实现基于​​哈希表​​和​​链地址法&#xff08;Separate Chaining&#xff09;。 .Net4.8 Dictionary<TKey,TValue>源码地址&#xff1a; dictionary…...

在 Visual Studio Code 中安装通义灵码 - 智能编码助手

高效的编码工具对于提升开发效率和代码质量至关重要。 通义灵码作为一款智能编码助手&#xff0c;为开发者提供了全方位的支持。 本文将详细介绍如何在 Visual Studio Code&#xff08;简称 VSCode&#xff09;中安装通义灵码&#xff0c;以及如何进行相关配置以开启智能编码…...

【AutoTest】自动化测试工具大全(Java)

&#x1f60a; 如果您觉得这篇文章有用 ✔️ 的话&#xff0c;请给博主一个一键三连 &#x1f680;&#x1f680;&#x1f680; 吧 &#xff08;点赞 &#x1f9e1;、关注 &#x1f49b;、收藏 &#x1f49a;&#xff09;&#xff01;&#xff01;&#xff01;您的支持 &#x…...

idea报错java: 非法字符: ‘\ufeff‘解决方案

解决方案步骤以及说明 BOM是什么&#xff1f;1. BOM的作用2. 为什么会出现 \ufeff 错误&#xff1f;3. 如何解决 \ufeff 问题&#xff1f; 最后重新编译&#xff0c;即可运行&#xff01;&#xff01;&#xff01; BOM是什么&#xff1f; \ufeff 是 Unicode 中的 BOM&#xff0…...

PHY芯片与网络变压器接线设计指南——不同速率与接口的硬件设计原则

一、PHY与网络变压器的核心作用 • PHY芯片&#xff08;物理层芯片&#xff09; • 功能&#xff1a;实现数据编码&#xff08;如Manchester、PAM4&#xff09;、时钟恢复、链路协商&#xff08;Auto-Negotiation&#xff09;。 • 接口类型&#xff1a;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语言文件操作复习 文件 内容 属性。所有对文件的操作有两部分&#xff1a;a.对内容的操作&#xff1b;b.对属性的操作。内容是数据&#xff0c;属性其实也是数据-存储文件&#xff0c…...

记录一次后台项目的打包优化

文章目录 前言分析问题寻找切入点根据切入点逐一尝试cdn引入node包遇到的一些问题记录最终结果 前言 优化&#xff0c;所有开发者到一定的程度上&#xff0c;都绕不开的问题之一 例如&#xff1a; 首页加载优化白屏优化列表无限加载滚动优化&#xff0c;图片加载优化逻辑耦合…...

问题记录(四)——拦截器“失效”?null 还是“null“?

拦截器“失效”&#xff1f;null 还是"null"&#xff1f; 问题描述 这个问题本身并不复杂&#xff0c;但是却是一个容易被忽略的问题。 相信大家在项目中一定实现过强制登录的逻辑吧&#xff0c;巧了&#xff0c;所要介绍的问题就出现在测试强制登录接口的过程中&am…...

前端面试-HTML5与CSS3

HTML5/CSS3 1. HTML5语义化标签的作用是什么&#xff1f;请举例说明5个常用语义化标签及其适用场景 解答&#xff1a; 语义化标签通过标签名称直观表达内容结构&#xff0c;有利于&#xff1a; 提升可访问性&#xff08;屏幕阅读器识别&#xff09;改善SEO&#xff08;搜索引…...

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. 结构错误调试&#xff1a;标签未正确嵌套 2. 语法问题调试&#xff1a;缺失引号 3. 断点调试&#xff1a;动态生成内容时的 JavaScript 错误 4. 网络调试&#xff1a;资源加载错误 5. 性能调试&#xff1a;页面加载性能 总结&#xff1a; 2.CSS 1. 定位…...

图论整理复习

回溯&#xff1a; 模板&#xff1a; void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择&#xff1a;本层集合中元素&#xff08;树中节点孩子的数量就是集合的大小&#xff09;) {处理节点;backtracking(路径&#xff0c;选择列表); // 递归回溯&#xff…...

MIMO预编码与检测算法的对比

在MIMO系统中&#xff0c;预编码&#xff08;发送端处理&#xff09;和检测算法&#xff08;接收端处理&#xff09;的核心公式及其作用对比如下&#xff1a; 1. 预编码算法&#xff08;发送端&#xff09; 预编码的目标是通过对发送信号进行预处理&#xff0c;优化空间复用或…...

C++修炼:vector模拟实现

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》、《C修炼之路》 欢迎点赞&#xff0c;关注&am…...

案例-索引对于并发Insert性能优化测试

前言 最近因业务并发量上升,开发反馈对订单表Insert性能降低。应开发要求对涉及Insert的表进行分析并提供优化方案。   一般对Insert 影响基本都在索引,涉及表已按创建日期做了分区表,索引全部为普通索引未做分区索引。 优化建议: 1、将UNIQUE改为HASH(64) GLOBAL IND…...

[区块链lab2] 构建具备加密功能的Web服务端

实验目标&#xff1a; 掌握区块链中密码技术的工作原理。在基于Flask框架的服务端中实现哈希算法的加密功能。 实验内容&#xff1a; 构建Flash Web服务器&#xff0c;实现哈希算法、非对称加密算法的加密功能。 实验步骤&#xff1a; 哈希算法的应用&#xff1a;创建hash…...