Linux 入门九:Linux 进程间通信

news2025/5/23 21:03:10

概述

进程间通信(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;  // 替换为实际进程 ID
    if (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_SET
        unsigned 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');  // 生成唯一 key
    int 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=1
    arg.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');  // 生成唯一 key
    int 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');  // 生成唯一 key
    int 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)排查问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2336823.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

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]…

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&#…

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

课程教材教法 “课程思政”视域下的高中数学教学探索与实践——以“函数概念的发展历程”为例 赵文博; 3-617 PBL教学模式下高中统计教学的探索与实践——以“随机抽样&#xff08;第一课时&#xff09;”为例 陈沛余; 7-10 “三新”背景下的高中数学教学困境与应对…

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;以及如何进行相关配置以开启智能编码…

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…

图论整理复习

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

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…

muduo库源码分析: TcpConnection

一. 主要成员: socket_&#xff1a;用于保存已连接套接字文件描述符。channel_&#xff1a;封装了上面的socket_及其各类事件的处理函数&#xff08;读、写、错误、关闭等事件处理函数&#xff09;。这个Channel中保存的各类事件的处理函数是在TcpConnection对象构造函数中注册…

RuoYi-Vue升级为https访问-后端安装SSL证书(单台Linux服务器部署)

一、前言 当Nginx已经作为反向代理并成功配置了SSL证书时,前端客户端与Nginx的通信已经是加密的。但Nginx和后端服务之间的连接可能仍然存在明文传输的风险。 如果Nginx和后端服务位于同一台物理机器或者通过安全的内部网络(如私有VLAN或防火墙保护的内网)进行通信,则可以…

博客文章文件名该怎么取?

文章目录 &#x1f9fe; 1. 博客文章文件名该怎么取&#xff1f;&#x1f4cc; 2. 为什么文件名重要&#xff1f;✅ 3. 推荐命名规范✅ 3.1 使用 **小写英文 中划线&#xff08;kebab-case&#xff09;**✅ 3.2 简短但具备语义✅ 3.3 如果是系列文章&#xff0c;可加前缀序号或…