信号以及共享内存

以下是基于笔记整理的Linux 信号(软中断) 知识体系,分"基础概念""常用信号""处理流程""补充细节"四大模块,覆盖定义、通信机制、典型信号及工程实践:


一、基础概念:信号的本质与通信机制

1. 信号定义
  • 别称:软中断(软件层面模拟"中断",实现进程异步通知 )。
  • 作用
    • 进程间异步通信(发送方主动通知,接收方被动响应 )。
    • 控制进程行为(如终止、暂停、资源清理 )。
2. 同步 vs 异步(通信视角)
类型 说明 典型场景 信号体现
同步 进程执行有严格先后顺序(需等待响应 ) 函数调用(printf 需等输出完成 ) 无(信号是异步通知 )
异步 接收方无需主动等待,随时响应通知 定时器触发(alarm )、终端按键 信号(如 Ctrl+C 触发 SIGINT ,进程无需主动轮询 )

二、常用信号与系统支持

1. 信号查看与范围
  • 查看系统支持的信号:kill -l(列出所有信号编号与名称 )。
  • 常用信号范围:1~31(标准信号 )、34~64(实时信号 ,支持排队,避免丢失 )。
2. 典型信号速查表(按笔记整理+补充)
信号编号 名称 触发方式/场景 默认行为 补充说明
2 SIGINT Ctrl+C 终端中断 终止进程 可捕获自定义逻辑(如优雅退出 )
3 SIGQUIT Ctrl+\ 终端退出 终止进程+生成 core 调试时常用(生成 core dump )
9 SIGKILL kill -9 <pid> 强制终止 终止进程 无法捕获/忽略(内核强制 )
13 SIGPIPE 管道破裂(如写端关闭后读端继续写 ) 终止进程 网络编程中常见(如 socket 断开后写数据 )
17 SIGCHLD 子进程结束(退出/被终止 ) 忽略(需手动处理 ) 父进程捕获后回收子进程资源
19 SIGSTOP kill -19 <pid> 或调试器暂停 暂停进程 无法捕获/忽略(内核强制 )
14 SIGALRM 定时器超时(alarm/setitimer 终止进程 实现定时任务(如心跳检测 )
10 SIGUSR1 用户自定义(kill -10 <pid> 终止进程(默认 ) 业务逻辑中用作"自定义通知"
12 SIGUSR2 用户自定义(kill -12 <pid> 终止进程(默认 ) 同上(与 SIGUSR1 区分 )
3. 特殊信号说明
  • SIGCONT(18 号) :恢复暂停的进程(与 SIGSTOP 配合 )。
  • SIGSTOP(19 号) :暂停进程(SIGCONT 恢复 ),调试场景常用(如 gdb 控制进程 )。
  • SIGSEGV(11 号):段错误(如非法内存访问 ),默认终止进程+生成 core dump 。

三、信号处理流程(内核视角)

信号从"产生"到"进程响应"的核心步骤:

  1. 信号产生

    • 硬件触发(如 Ctrl+CSIGINT )。
    • 软件触发(如 kill 命令、alarm 定时器、raise 函数 )。
  2. 信号传递

    内核将信号标记到目标进程的信号 pending 队列(记录待处理的信号 )。

  3. 信号处理

    进程从内核态切换到用户态时,检查 pending 队列:

    • 若信号是默认/忽略:执行预设逻辑(如终止、忽略 )。
    • 若信号是捕获(自定义) :跳转到用户注册的信号处理函数执行。
  4. 恢复执行

    处理函数执行完毕后,进程回到被中断的执行流继续运行。

流程图解(简化):

默认/忽略 捕获 信号产生 内核标记 pending 检查处理方式 执行系统逻辑 执行自定义函数 进程继续/终止


四、补充细节与工程实践

1. 信号的"不可靠性"(标准信号)
  • 1~31 的标准信号可能丢失(如同一信号多次产生,内核可能只记录一次 )→ 需"实时信号"(34~64 )解决(支持排队 )。
  • 示例:连续发 10 次 SIGUSR1(标准信号 ),进程可能只收到 1 次通知。
2. 信号与终端的交互
  • 后台进程(& 运行)默认忽略 SIGINTCtrl+C ),但仍响应 SIGTERMkill <pid> )。

  • 守护进程需主动忽略无关终端信号(如 SIGTTOUSIGTTIN ):

    c 复制代码
    signal(SIGTTOU, SIG_IGN);
    signal(SIGTTIN, SIG_IGN);
3. 信号处理的"竞态条件"
  • 若信号处理函数与主逻辑共享全局变量 ,需用 volatile 修饰(防止编译器优化 ):

    c 复制代码
    volatile int flag = 0; // 信号处理函数可能修改 flag
    void handler(int sig) { flag = 1; }
4. 调试与故障排查
  • 生成 core dump:需开启 ulimit -c unlimited(允许生成 core 文件 ),配合 gdb 分析 SIGSEGV 等崩溃。
  • 追踪信号:用 strace -e signal 调试进程的信号接收与处理(如 strace -e signal ./a.out )。

五、总结:知识体系脑图

graph LR A[信号:软中断] --> B[基础概念: 异步通知、进程控制] A --> C[常用信号: SIGINT/SIGKILL/SIGCHLD 等] A --> D[处理流程: 产生→传递→响应→恢复] B --> B1[同步 vs 异步通信] C --> C1[信号速查表+系统支持] D --> D1[默认/忽略/捕获逻辑] A --> E[工程实践: 可靠信号、竞态条件、调试]

通过以上整理,可覆盖笔记核心内容,并补充信号处理流程工程细节(如可靠信号、调试方法 ),适配实际开发需求。

一、核心概念:信号的三种处理方式

进程收到信号时,有三类响应策略,通过 signal 函数配置:

处理方式 说明 代码示例(signal 调用) 典型场景
默认(缺省) 按系统预设逻辑执行(如终止、暂停) signal(SIGINT, SIG_DFL); 未特殊处理的通用信号(如 Ctrl+C 触发 SIGINT
忽略 收到信号不做任何操作 signal(SIGINT, SIG_IGN); 后台守护进程忽略无关终端信号
捕获(自定义) 执行用户注册的回调函数 signal(SIGINT, handler);handler 是自定义函数) 需自定义逻辑(如优雅退出、资源清理 )

二、signal 函数详解

1. 函数原型与头文件
c 复制代码
#include <signal.h>
// 定义函数指针类型:参数是信号值(int),无返回值
typedef void (*sighandler_t)(int);  
// 注册信号处理逻辑
sighandler_t signal(int signum, sighandler_t handler);  
2. 参数说明
  • signum
    要处理的信号编号 (如 SIGINTSIGALRM 等,完整列表可查 man 7 signal )。
  • handler
    • SIG_DFL:使用默认处理
    • SIG_IGN忽略信号。
    • 函数指针(如 handler ):自定义处理函数 (需符合 void func(int) 格式 )。
3. 返回值
  • 成功 :返回之前注册的处理函数地址(可用于恢复默认逻辑 )。
  • 失败 :返回 NULL(需检查 errno,如 EINVAL 表示信号编号无效 )。

三、自定义信号处理函数

1. 格式要求
c 复制代码
void handler(int signum) {  
    // signum:触发的信号编号(如 SIGINT=2,SIGALRM=14 )
    printf("%d signal coming!\n", signum);  
    // 可添加自定义逻辑(如释放资源、记录日志 )
}
2. 注册与触发示例
c 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void handler(int signum) {
    printf("捕获信号:%d,执行自定义逻辑\n", signum);
}

int main() {
    // 注册:用自定义函数处理 SIGINT(Ctrl+C 触发)
    signal(SIGINT, handler);  

    printf("进程运行中,按 Ctrl+C 测试信号捕获...\n");
    while (1) {
        sleep(1); // 保持进程运行,等待信号
    }
    return 0;
}

执行效果

  • Ctrl+C 时,触发 SIGINT,执行 handler 打印信息,而非默认终止进程。

四、补充细节与注意事项

1. 不可捕获/忽略的信号

部分信号无法被捕获或忽略(内核强制控制),典型:

  • SIGKILL(9 号):强制终止进程,无法拦截。
  • SIGSTOP(19 号):强制暂停进程,无法拦截。
2. 信号的"异步性"与竞态条件
  • 信号是异步事件 ,可能在进程任意执行流中触发 → 若处理函数与主逻辑共享全局变量 ,需用 volatile 修饰(防止编译器优化导致数据不同步 ):

    c 复制代码
    volatile int flag = 0;  
    void handler(int signum) { flag = 1; } // 主逻辑可感知 flag 变化
3. 信号处理函数的"可重入性"
  • 避免在处理函数中调用非可重入函数 (如 printf 内部有锁,可能导致死锁 )→ 优先用 write 等底层函数,或简化逻辑。
4. 恢复默认处理

若需临时自定义信号处理,后续恢复默认逻辑,可利用 signal 的返回值:

c 复制代码
// 保存旧的处理函数(默认或之前注册的)
sighandler_t old_handler = signal(SIGINT, handler);  
// ... 执行自定义逻辑 ...
// 恢复默认处理
signal(SIGINT, old_handler);  

五、总结:信号处理知识体系

模块 核心内容 关键代码/工具
处理方式 默认、忽略、自定义捕获 SIG_DFLSIG_IGN、自定义函数
signal 函数 注册信号处理逻辑,返回旧处理函数地址 #include <signal.h> 、函数指针
自定义处理 编写 void func(int) 格式函数,处理信号编号与业务逻辑 注意可重入性、共享变量用 volatile
补充细节 不可捕获信号、异步安全、恢复默认逻辑 SIGKILLSIGSTOP 特性

以下是对笔记中 "信号注册与发送" 知识点的系统梳理,分"核心注意事项""信号发送方式""补充细节"三大模块,结合 Linux 信号机制深化:


一、信号注册的核心注意事项

笔记中提到信号注册的 4 条规则,整理为工程实践原则

序号 规则原文 补充说明/实践场景
1 若信号不被注册,按默认方式运行 - 未注册的信号会触发系统预设逻辑(如 SIGINT 默认终止进程 ) - 示例:signal(SIGINT, SIG_DFL); 显式恢复默认
2 信号只需注册一次 - 多次注册以最后一次为准 ,但通常只需注册一次(避免覆盖逻辑 ) - 场景:守护进程启动时注册 SIGTERM 处理函数
3 每次信号到来,触发任务函数 - 信号是异步事件 ,每次触发都会调用处理函数(需注意"重入性" ) - 风险:处理函数中调用 printf(非可重入函数)可能导致死锁
4 信号尽可能早注册 - 防止进程启动初期收到信号(如 SIGCHLD 子进程退出信号 ),因未注册而走默认逻辑 - 最佳实践:main 函数开头优先注册关键信号

二、进程中发送信号的方式

信号发送是"主动通知进程"的核心手段,笔记覆盖 kill 命令系统调用killraise 等 ),补充细节如下:

1. kill 命令(终端/脚本触发)
  • 语法kill -<信号编号/名称> <pid>
  • 示例
    • 强制终止进程:kill -9 1234SIGKILL 无法捕获 )
    • 发送 SIGUSR1kill -USR1 1234
  • 补充
    • 批量发送:kill -9 $(pgrep <进程名>)(终止所有同名进程 )
    • 无信号编号时,默认发送 SIGTERM(15 号,可捕获 )
2. kill 系统调用(C 函数)
  • 原型#include <sys/types.h>
    #include <signal.h>
    int kill(pid_t pid, int sig);

  • 参数

    • pid:目标进程 ID(pid>0 发信号给指定进程;pid=0 发给同组进程;pid=-1 发给所有有权限进程 )
    • sig:信号编号(如 SIGINTSIGKILL
  • 返回值

    • 成功:0
    • 失败:-1errno 说明原因,如 ESRCH 进程不存在 )
  • 示例

    c 复制代码
    if (kill(1234, SIGUSR1) == -1) {
        perror("kill failed"); // 如进程不存在,输出 kill failed: No such process
    }
3. 其他发送函数
函数 原型 功能 补充说明
raise int raise(int sig); 当前进程发送信号 等价于 kill(getpid(), sig)
alarm unsigned int alarm(unsigned int seconds); 定时触发 SIGALRM 信号 重复调用会重置定时器
sigqueue int sigqueue(pid_t pid, int sig, const union sigval value); 发送信号并附加数据(实时信号) 需配合实时信号(34~64 )使用
对比表格(按笔记+补充)
发送方式 适用场景 特点/限制 典型函数/命令
kill 命令 终端/脚本快速触发 简单直接,无需编程 kill -9 <pid>
kill 系统调用 编程中主动发信号 灵活控制 pid 和信号 kill(pid, SIGUSR1)
raise 进程自陷信号(给自己发) 等价于 kill(getpid(), sig) raise(SIGINT)
alarm 定时触发(SIGALRM 仅触发 SIGALRM,可重置定时器 alarm(5);(5 秒后触发 )

三、特殊信号的"强制处理"(SIGKILLSIGSTOP

  • 规则SIGKILL(9 号)、SIGSTOP(19 号)无法被捕获/忽略(内核强制控制 )。
  • 原因
    • 系统需要"绝对终止/暂停进程"的能力(如强制回收资源、调试暂停 )。
    • 若允许捕获,恶意进程可能规避终止,导致系统不稳定。
  • 实践建议
    • 调试时优先用 SIGTERM(15 号,可捕获 ),允许进程优雅退出(如清理临时文件 )。
    • 仅当进程完全无响应时,用 SIGKILL 强制终止。

四、补充细节:信号发送的"权限与限制"

  • 权限
    • 普通用户只能向自己的进程发信号(或有权限的进程,如同组进程 )。
    • root 用户可向任意进程 发信号(需谨慎使用 SIGKILL )。
  • 错误场景
    • 发送信号失败常见原因:
      • ESRCH:进程 pid 不存在。
      • EPERM:无权限向目标进程发信号(如普通用户向 root 进程发信号 )。

五、知识体系总结

模块 核心内容 关键工具/函数 工程实践建议
信号注册 4 条原则(早注册、单次注册等),避免信号丢失/逻辑覆盖 signalsigaction(更可靠 ) 主函数开头注册关键信号
信号发送 命令(kill )、系统调用(killraise 等 ),覆盖主动通知场景 killraisealarm 优先用 SIGTERM 替代 SIGKILL
特殊信号 SIGKILLSIGSTOP 无法捕获,内核强制控制 kill -9kill -19 调试时慎用,避免破坏进程状态

通过以上整理,可完整覆盖笔记中 信号注册规则发送方式,补充"权限限制""特殊信号强制逻辑"等工程细节,适配实际开发需求。

以下是对笔记中 alarm 定时器pause 挂起 知识点的系统梳理,分"核心函数解析""示例代码拆解""补充细节""实践场景"四个模块:


一、核心函数解析

1. alarm:定时触发 SIGALRM 信号
  • 原型unsigned int alarm(unsigned int seconds);
  • 头文件<unistd.h>
  • 功能
    • 设定一个"定时器",seconds 秒后向当前进程 发送 SIGALRM 信号。
    • 重复调用会重置定时器(覆盖之前的定时 )。
  • 返回值
    • 成功:返回上一次定时器的剩余时间 (若之前未设置,返回 0 )。
    • 失败:无(alarm 不会失败,内核兜底处理 )
2. pause:进程挂起等待信号
  • 原型int pause(void);
  • 头文件<unistd.h>
  • 功能
    • 使进程主动挂起,直到捕获到任意信号后恢复执行。
    • 信号处理完成后,pause 返回(通常返回 -1errno 设为 EINTR )。

二、示例代码拆解(按笔记整理+补充)

笔记中的示例可拆解为 基础定时结合信号处理 两种场景:

场景 1:基础定时(未处理信号)
c 复制代码
#include <unistd.h>
#include <stdio.h>

int main(void) {
    // 5 秒后触发 SIGALRM 信号(默认终止进程)
    alarm(5);  

    while (1) {
        printf("hello, world!\n");
        sleep(1); 
    }

    return 0;
}

执行流程

  1. alarm(5) 设定 5 秒定时器。
  2. 进程进入死循环,每秒打印 hello, world!
  3. 5 秒后,内核发送 SIGALRM → 进程默认行为:终止(退出死循环 )。
场景 2:结合信号处理(自定义 SIGALRM 逻辑)
c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

void handler(int signum) {
    printf("捕获到信号:%d(SIGALRM)\n", signum);
    // 重置定时器(实现"周期性触发")
    alarm(5);  
}

int main(void) {
    // 注册 SIGALRM 处理函数
    signal(SIGALRM, handler);  

    // 首次定时 5 秒
    alarm(5);  

    while (1) {
        printf("hello, world!\n");
        sleep(1); 
    }

    return 0;
}

执行流程

  1. signal(SIGALRM, handler) 注册自定义处理函数。
  2. alarm(5) 启动定时器 → 5 秒后触发 SIGALRM
  3. handler 被调用:打印信息 + 重置定时器(alarm(5) )→ 实现周期性触发
  4. 进程持续运行,每秒打印 hello, world!,每 5 秒触发一次 handler
场景 3:pause 挂起等待信号
c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <signal.h>

void handler(int signum) {
    printf("捕获到信号:%d,唤醒进程!\n", signum);
}

int main(void) {
    // 注册信号处理函数(可捕获任意信号,如 SIGINT、SIGALRM )
    signal(SIGINT, handler);  

    printf("进程挂起,等待信号...\n");
    // 挂起,直到收到信号
    pause();  

    printf("进程恢复运行!\n");
    return 0;
}

执行流程

  1. signal 注册 SIGINT 处理函数(Ctrl+C 触发 )。
  2. pause 使进程挂起 → 控制台显示"进程挂起,等待信号..."。
  3. Ctrl+C → 触发 SIGINThandler 被调用 → 打印信息。
  4. pause 返回 → 进程继续执行,打印"进程恢复运行!"。

三、补充细节与注意事项

1. alarm 的"覆盖性"
  • 重复调用 alarm重置定时器 ,示例:

    c 复制代码
    alarm(10); // 首次定时 10 秒
    sleep(3);
    alarm(5);  // 剩余 7 秒被覆盖,新定时 5 秒 → 5 秒后触发 SIGALRM
2. pause 的"不可中断性"?
  • pause 会被任意信号中断 (包括 SIGALRMSIGINT 等 ),返回后需检查 errno

    c 复制代码
    int ret = pause();
    if (ret == -1 && errno == EINTR) {
        printf("被信号中断,恢复执行!\n");
    }
3. 信号处理的"异步性"与 alarm 结合
  • alarm 触发的 SIGALRM异步事件 ,可能在进程任意执行流中触发 → 若处理函数与主逻辑共享变量,需用 volatile 修饰:

    c 复制代码
    volatile int flag = 0;
    void handler(int signum) { flag = 1; }
    
    int main() {
        alarm(5);
        signal(SIGALRM, handler);
        while (!flag) { /* 等待 flag 被信号修改 */ }
        printf("定时器触发!\n");
    }
4. alarmsetitimer 的区别(进阶)
  • alarm简易定时器 (仅触发 SIGALRM ),但:
    • 同一进程只能有一个 alarm 定时器(重复调用覆盖 )。

    • 需更复杂定时(如"周期性触发""区分间隔/绝对时间" ),用 setitimer

      c 复制代码
      struct itimerval timer = {
          .it_interval = {1, 0}, // 间隔 1 秒
          .it_value    = {3, 0}  // 3 秒后首次触发
      };
      setitimer(ITIMER_REAL, &timer, NULL); // 触发 SIGALRM

四、实践场景与应用

1. 守护进程的"心跳检测"
  • alarm 周期性触发 SIGALRM → 处理函数中检查系统状态(如网络连接、资源使用 ):

    c 复制代码
    void heartbeat_handler(int signum) {
        // 检查网络连接、上报状态...
        alarm(60); // 1 分钟后再次触发,持续心跳
    }
    
    int main() {
        signal(SIGALRM, heartbeat_handler);
        alarm(60); // 首次心跳定时
        // 守护进程主逻辑...
    }
2. 限时任务(超时终止)
  • 结合 alarmSIGALRM 默认行为,实现"任务超时终止":

    c 复制代码
    // 模拟一个耗时任务,超时 5 秒则终止
    int main() {
        alarm(5); // 5 秒后终止进程
        // 执行耗时操作...
        while (1) { /* 模拟任务 */ }
        return 0;
    }
3. pause 实现"等待信号唤醒"
  • 进程启动后挂起,等待外部信号触发逻辑(如 Ctrl+C 启动任务 ):

    c 复制代码
    int main() {
        signal(SIGINT, start_task);
        printf("等待启动信号...\n");
        pause(); // 挂起直到收到 SIGINT
        return 0;
    }
    
    void start_task(int signum) {
        printf("开始执行任务...\n");
        // 任务逻辑...
    }

五、知识体系总结

函数 核心功能 典型用法 注意事项
alarm 定时触发 SIGALRM 信号 实现定时器、心跳检测 重复调用会重置定时器
pause 挂起进程,等待信号唤醒 等待外部事件触发(如用户按键 ) 会被任意信号中断,需检查 errno
signal 注册信号处理函数 自定义 SIGALRMSIGINT 逻辑 注意信号的"异步性"和"可重入"

通过以上梳理,可完整覆盖笔记中 alarmpause 的用法,补充"定时器覆盖性""pause 中断处理"等工程细节,适配实际开发需求(如守护进程、限时任务 )。

以下是对笔记中 共享内存(Shared Memory) 知识点的系统总结与补充,分"核心原理""操作流程""代码实践""补充细节"四大模块,适配 Linux 进程间通信(IPC)场景:


一、核心原理:共享内存为何是"效率最高的 IPC"?

1. 基本概念
  • 共享内存 :内核创建一块物理内存,映射到多个进程的虚拟地址空间 → 进程可直接读写同一块内存,无需数据拷贝。
  • 内存映射 :通过 mmapshmat,将内核管理的共享内存段映射到进程虚拟地址 → 实现"虚拟地址 → 物理地址"的直接关联。
2. 效率优势
IPC 方式 数据拷贝次数 典型场景 缺点
共享内存 0 次(直接读写物理内存) 高并发数据传输(如游戏服务器 ) 需同步机制(如信号量 )
管道/消息队列 2 次(用户→内核→用户) 低并发、异步通信 速度慢,数据量大时低效
3. 原理图解(简化)
graph TD A[物理内存:共享内存段] --> B[进程1虚拟地址空间] A --> C[进程2虚拟地址空间] B --> D[进程1读写数据] C --> E[进程2读写数据] D --> A E --> A

二、共享内存操作流程(6 步)

笔记梳理了 创建 → 映射 → 读写 → 解除映射 → 删除 的流程,补充细节如下:

1. 头文件依赖
c 复制代码
#include <sys/ipc.h>   // 通用 IPC 定义(如 key_t )
#include <sys/shm.h>   // 共享内存专用函数(shmget、shmat 等 )
2. 步骤拆解与函数说明
步骤 函数/操作 功能说明 代码示例/参数
1 创建 IPC key 生成唯一标识,用于关联共享内存 key_t key = ftok("/path/to/file", 'a'); - path 需是真实文件 - 'a' 是项目 ID
2 创建/获取共享内存段 在内核中申请共享内存,返回 shmid(共享内存 ID ) `int shmid = shmget(key, size, IPC_CREAT
3 映射共享内存到进程空间 将内核共享内存映射到进程虚拟地址,返回内存首地址 void *shm_addr = shmat(shmid, NULL, 0); - NULL:让内核选地址 - 0:默认权限
4 读写共享内存 直接通过映射后的地址读写数据 *(int *)shm_addr = 100;(写数据 ) printf("%d", *(int *)shm_addr);(读数据 )
5 解除内存映射 断开进程虚拟地址与共享内存的关联 shmdt(shm_addr); - 仅解除映射,不删除内核中的共享内存
6 删除共享内存段 在内核中销毁共享内存(需所有进程解除映射后生效 ) shmctl(shmid, IPC_RMID, NULL); - IPC_RMID:标记删除 - 实际销毁需等所有进程 shmdt

以下是对笔记中 ftokshmget 函数的系统总结与补充,分"核心功能""参数细节""实践避坑""关联流程"四个模块:


一、ftok:创建 IPC Key

1. 核心功能
  • 生成唯一的 IPC 标识符key_t 类型 ),用于关联共享内存、信号量、消息队列等 IPC 资源。
  • 本质:将文件的inode (文件唯一标识 )与 proj_id(项目 ID )结合,生成 key
2. 参数与返回值
函数原型 key_t ftok(const char *pathname, int proj_id);
pathname 必须是真实存在的文件路径 (如 "/tmp/ipc_file" ) - 作用:用文件的 inode 保证 key 唯一性
proj_id 项目 ID(通常取 1 字节,如 'a'10 ) - 作用:同一文件可生成多个 key(不同 proj_id
返回值 - 成功:生成的 key_t(非负整数 ) - 失败:-1errno 设为 ENOENT 等 )
3. 实践注意事项
  • 风险 1 :若 pathname 文件被删除重建inode 改变 ),新进程用 ftok 会生成key → 无法关联旧 IPC 资源。
    • 解决方案:改用固定 key(如 key_t key = 0x1234; ),或确保文件稳定(如用 /dev/zero )。
  • 风险 2proj_id 若超出 1 字节(如 256 ),会被截断为低 8 位 → 可能冲突。
    • 建议:proj_id0~255 范围内的值(如 'a'10 )。

二、shmget:创建/获取共享内存段

1. 核心功能
  • 在内核中申请/查找共享内存段 ,返回 shmid(共享内存 ID )→ 后续操作(shmatshmctl )依赖此 ID。
2. 参数与返回值
函数原型 int shmget(key_t key, size_t size, int shmflg);
key ftok 生成的 key,或固定值(如 0x1234
size 共享内存大小(字节) - 需是系统页大小的整数倍 (通常 4096 字节 ) - 不足时内核自动对齐(可能浪费内存 )
shmflg 标志位(组合使用,用 `
返回值 - 成功:shmid(非负整数 ) - 失败:-1errno 设为 EEXIST 等 )
3. 代码示例与解释
c 复制代码
// 1. 生成 key(依赖文件 /tmp/ipc_file 和 proj_id 'a')
key_t key = ftok("/tmp/ipc_file", 'a');
if (key == -1) { perror("ftok failed"); return 1; }

// 2. 创建共享内存(4096 字节,权限 0664,不存在则创建)
int shmid = shmget(key, 4096, IPC_CREAT | 0664);
if (shmid == -1) { perror("shmget failed"); return 1; }

参数细节

  • size=4096:刚好是系统页大小(getconf PAGE_SIZE 验证 ),无内存浪费。
  • IPC_CREAT | 0664:若 key 对应共享内存不存在,则创建;权限设为 rwxr--r--(同文件权限 )。
4. 扩展标志位(进阶)
  • IPC_EXCL :与 IPC_CREAT 配合,若共享内存已存在则失败(避免覆盖 )。

    c 复制代码
    // 仅创建新共享内存,存在则报错
    shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0664);
  • SHM_NORESERVE:不预留交换空间(适合大内存,风险:内存不足时进程崩溃 )。


三、ftok + shmget 完整流程(工程化示例)

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    // 1. 生成 IPC key
    key_t key = ftok("/tmp/ipc_file", 'a');
    if (key == -1) {
        perror("ftok error");
        return EXIT_FAILURE;
    }

    // 2. 创建共享内存(4KB,权限 0664,不存在则创建)
    int shmid = shmget(key, 4096, IPC_CREAT | 0664);
    if (shmid == -1) {
        perror("shmget error");
        return EXIT_FAILURE;
    }

    printf("共享内存创建成功,shmid = %d\n", shmid);

    // 3. 后续操作(映射、读写、解除映射、删除等)
    // ... 省略 shmat、shmdt、shmctl 代码 ...

    return EXIT_SUCCESS;
}

以下是对 shmat 函数知识点的总结与补充,从函数功能、参数、返回值、使用示例及注意事项等维度展开:

一、shmat 函数核心功能

shmat 是 Linux 系统中用于 共享内存映射 的关键函数,作用是将内核中已创建的共享内存段(通过 shmget 获取 shmid 标识 ),映射到当前进程的用户虚拟地址空间,使进程能直接读写共享内存,实现进程间高效数据交互。

二、函数原型与参数解析

函数原型:

c 复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);
1. 参数说明
  • shmid
    共享内存的唯一标识(shmget 函数的返回值 ),用于指定要映射的共享内存段。
  • shmaddr
    期望映射到的用户空间首地址 。若填 NULL,由系统自动选择空闲地址(推荐用法,避免地址冲突 );若手动指定地址,需确保该地址未被占用且满足内存对齐要求(复杂场景慎用 )。
  • shmflg
    控制映射权限的标志位,核心取值:
    • SHM_RDONLY只读映射,进程只能读取共享内存内容,无法修改。
    • 不设置 SHM_RDONLY(或用 !SHM_RDONLY 显式表示 ):可读可写映射,进程可读写共享内存。

三、返回值规则

  • 成功 :返回映射到的用户空间首地址void* 类型 ),后续通过该指针操作共享内存。
  • 失败 :返回 (void*)-1(需通过 errno 排查错误,常见错误如权限不足、shmid 无效等 )。

四、使用示例与逻辑拆解

笔记中示例代码可拆解为 映射流程错误检查数据操作 三部分:

1. 完整示例代码
c 复制代码
// 假设已通过 shmget 获取 shmid
int shmid = shmget(key, size, IPC_CREAT | 0664);  
if (shmid == -1) { perror("shmget error"); return -1; }  

// 1. 调用 shmat 映射共享内存
void *pmem = shmat(shmid, NULL, !SHM_RDONLY);  

// 2. 检查映射是否失败
if ((void*)-1 == pmem) {  
    perror("shmat error");  
    return -1;  
}  

// 3. 操作共享内存(示例:int 类型赋值、字符串拷贝)
// int 类型数据写入
*(int*)pmem = 10;  
// 字符串写入(需确保内存足够)
strcpy((char*)pmem, "hello");  

// 后续可继续读写共享内存...
2. 代码逻辑说明
  • 映射阶段shmat(shmid, NULL, !SHM_RDONLY) 中,NULL 让系统选地址,!SHM_RDONLY 设为可读可写。
  • 错误检查 :通过判断返回值是否为 (void*)-1,识别映射失败场景(如权限不足、shmid 无效 )。
  • 数据操作
    • 强转指针类型((int*)pmem(char*)pmem ),适配不同数据读写需求。
    • 直接通过指针操作共享内存,数据会同步到内核共享内存段(其他映射该段的进程可访问 )。

五、补充细节与注意事项

1. 内存权限与进程行为
  • 若以 SHM_RDONLY 映射只读内存,进程尝试写入时,会触发段错误SIGSEGV ),导致程序崩溃。

  • 示例:

    c 复制代码
    void *pmem = shmat(shmid, NULL, SHM_RDONLY);  
    *(int*)pmem = 10; // 触发段错误!进程被终止  
2. 多进程映射的一致性
  • 不同进程映射同一共享内存段时,系统会将其映射到各自虚拟地址空间 ,但最终操作的是同一块物理内存(确保数据全局可见 )。
  • 示例:进程 A 写入 *(int*)pmem = 10,进程 B 映射后读取 *(int*)pmem 也会得到 10
3. 映射后的资源释放
  • 共享内存映射后,需在进程退出前调用 shmdt 解除映射(shmdt(pmem); ),但不会删除内核中的共享内存段 (需用 shmctl 配合 IPC_RMID 标记删除 )。

以下是对笔记中 共享内存控制函数(shmdtshmctl 及相关命令的系统总结,分"核心函数解析""命令行工具""流程关联""补充细节"四个模块梳理:


一、核心函数解析:shmdt + shmctl

1. shmdt:解除内存映射
  • 原型int shmdt(const void *shmaddr);
  • 功能 :断开进程虚拟地址空间与共享内存段的映射关系(仅解除映射,不删除内核中的共享内存 )。
  • 参数
    • shmaddrshmat 返回的共享内存映射首地址 (必须是之前通过 shmat 获得的地址 )。
  • 返回值
    • 成功:0
    • 失败:-1errno 设为 EINVAL 等,如 shmaddr 无效 )
2. shmctl:操作共享内存段(内核级控制 )
  • 原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 功能 :对共享内存段执行查询、删除、设置属性等操作(核心用于"标记删除" )。
  • 参数
    • shmidshmget 返回的共享内存 ID
    • cmd:操作指令(常用 IPC_RMID )。
    • buf:用于存储/设置共享内存属性(struct shmid_ds ),删除时可填 NULL
  • 返回值
    • 成功:0(或返回属性查询结果 )
    • 失败:-1errno 设为 EINVAL 等 )
关键指令:IPC_RMID(标记删除共享内存 )
  • 作用
    • 标记共享内存段为"待删除" → 所有进程解除映射shmdt )后,内核才会真正销毁该段。
    • 若有进程未解除映射,共享内存段会残留(需等待所有进程 shmdt 后销毁 )。

二、命令行工具:ipcs + ipcrm

1. ipcs -a:查看 IPC 资源
  • 功能:列出系统中所有 IPC 资源(共享内存、消息队列、信号量 )。

  • 示例

    bash 复制代码
    ipcs -m  # 仅查看共享内存
  • 输出解析

    • shmid:共享内存 ID(与 shmget 返回值对应 )。
    • owner:创建者用户名。
    • segments:共享内存段大小、权限等。
2. ipcrm:删除 IPC 资源
  • 语法

    • ipcrm -m shmid:通过 shmid 删除共享内存。
    • ipcrm -M shmkey:通过 shmkeyftok 生成的 key )删除共享内存。
  • 示例

    bash 复制代码
    ipcrm -m 1234  # 删除 shmid=1234 的共享内存

以下是对笔记中 信号量集消息队列 知识点的系统总结与补充,分"核心原理""对比管道""工程实践"三个模块:


一、信号量集(Semaphore Set)

1. 核心功能
  • 定位 :实现进程/线程间同步(协调多个进程对共享资源的访问顺序 )。
  • 本质 :一个"计数器 + 等待队列"的组合 → 通过P/V 操作sem_wait/sem_post )控制资源访问:
    • P 操作(减 1 ):资源可用时占用,否则阻塞等待。
    • V 操作(加 1 ):释放资源,唤醒等待的进程。
2. 与共享内存的协同
  • 信号量本身不存储数据 ,需与共享内存 配合使用:
    • 共享内存:负责数据存储(多进程共享 )。
    • 信号量:负责同步控制(确保多进程读写共享内存时不冲突 )。
3. 关键函数(补充笔记未提及的代码细节)
c 复制代码
#include <sys/sem.h>

// 创建/获取信号量集
int semid = semget(key, 1, IPC_CREAT | 0664); 

// P 操作(申请资源)
struct sembuf op = {0, -1, 0};
semop(semid, &op, 1); 

// V 操作(释放资源)
struct sembuf op = {0, 1, 0};
semop(semid, &op, 1); 

// 标记删除信号量集
semctl(semid, 0, IPC_RMID); 
4. 典型场景
  • 生产者-消费者模型
    • 信号量控制"缓冲区空/满" → 生产者生产数据后 V 操作,消费者消费前 P 操作。
  • 多进程读写锁
    • 用信号量实现"读锁"(多进程可同时读,需等待写锁释放 )、"写锁"(独占,读/写进程需等待 )。

二、消息队列(Message Queue)

1. 核心功能
  • 定位 :进程间异步通信 (消息可缓存,无需实时交互 ),支持按优先级投递消息。
  • 优势
    • 对比管道:消息队列自带同步(队列缓存消息,进程无需忙等 )。
    • 对比共享内存:消息队列结构化存储struct msgbuf 定义消息格式 )。
2. 与管道的对比(补充笔记细节)
特性 消息队列 管道(无名/有名)
数据格式 结构化(struct msgbuf 无结构(字节流)
同步性 异步(队列缓存,进程无需等待 ) 同步(读/写需同时在线 )
优先级 支持(按 mtype 区分 ) 无(先进先出 )
持久化 内核中存在,进程退出后残留 无名管道随进程退出销毁
3. 关键函数与消息结构
c 复制代码
#include <sys/msg.h>

// 消息结构
struct msgbuf {
    long mtype; // 消息类型(>0,用于区分优先级 )
    char mtext[1024]; // 消息内容
};

// 创建/获取消息队列
int msgid = msgget(key, IPC_CREAT | 0664); 

// 发送消息(mtype=10,高优先级 )
struct msgbuf msg = {.mtype=10, .mtext="Urgent Data"};
msgsnd(msgid, &msg, sizeof(msg.mtext), 0); 

// 接收消息(只收 mtype=10 的 )
msgrcv(msgid, &msg, sizeof(msg.mtext), 10, 0); 
4. 优先级实践(补充笔记逻辑)
  • 消息队列通过 mtype 实现优先级
    • mtype 越小,不一定优先级越高 (需业务层定义规则,如 mtype=1 为最高优先级 )。

    • 示例:

      c 复制代码
      // 发送高优先级消息(mtype=1 )
      struct msgbuf high_msg = {.mtype=1, .mtext="Critical"};
      msgsnd(msgid, &high_msg, sizeof(high_msg.mtext), 0); 
      
      // 发送普通消息(mtype=10 )
      struct msgbuf normal_msg = {.mtype=10, .mtext="Normal"};
      msgsnd(msgid, &normal_msg, sizeof(normal_msg.mtext), 0); 
      
      // 接收时,优先取 mtype=1 的消息
      msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0); 
5. 持久化与清理
  • 消息队列是内核对象 → 进程退出后队列残留,需显式调用 msgctl(msgid, IPC_RMID, NULL) 删除。
  • 调试时用 ipcs -q 查看残留队列,ipcrm -q msgid 手动删除。

三、总结:IPC 知识体系对比

IPC 方式 核心场景 关键函数/结构 补充细节
信号量集 进程同步(共享资源协调 ) semgetsemopsemctl 需与共享内存配合
消息队列 异步通信(按优先级投递 ) msggetmsgsndmsgrcv 结构化消息,支持队列缓存
管道 简单字节流通信(父子进程 ) pipemkfifo 同步,无结构
共享内存 高效大数据传输 shmgetshmatshmdt 需信号量/锁同步

通过以上整理,补充了 信号量与共享内存的协同消息队列的优先级实践各 IPC 方式的对比 等细节,适配实际开发中"同步控制""异步通信"的需求。

三、共享内存完整流程(结合函数与命令)

1. 代码流程(C 示例)
c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>

int main() {
    // 1. 生成 key
    key_t key = ftok(".", 'a');
    if (key == -1) { perror("ftok"); return 1; }

    // 2. 创建共享内存
    int shmid = shmget(key, 1024, IPC_CREAT | 0664);
    if (shmid == -1) { perror("shmget"); return 1; }

    // 3. 映射共享内存到进程空间
    void *shmaddr = shmat(shmid, NULL, 0);
    if (shmaddr == (void *)-1) { perror("shmat"); return 1; }

    // 4. 操作共享内存(示例:写入数据)
    *(int *)shmaddr = 100;

    // 5. 解除映射
    if (shmdt(shmaddr) == -1) { perror("shmdt"); return 1; }

    // 6. 标记共享内存为待删除(进程退出前执行)
    if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl"); return 1; }

    return 0;
}
2. 命令行验证流程
  • 运行代码后,用 ipcs -m 查看:
    • shmctl(IPC_RMID) 执行成功,共享内存状态会标记为 dest(待删除 )。
    • 所有进程 shmdt 后,再次 ipcs -m 会发现该段已被销毁。

四、补充细节与避坑指南

1. shmdt 的"不可逆性"
  • shmdt 仅解除映射 → 进程无法再访问该共享内存段,但内核中的共享内存段未被删除 (需 shmctl(IPC_RMID) 标记删除 )。
2. 共享内存的"残留问题"
  • 若进程异常退出(未执行 shmdt/shmctl ),共享内存段会残留 → 新进程无法创建同名段。
  • 解决
    • 调试时用 ipcrm -m shmid 手动删除残留段。
    • 代码中注册 atexit 回调,确保进程退出时执行 shmdt + shmctl(IPC_RMID)
3. shmctl 的其他指令(进阶)
  • IPC_STAT :查询共享内存属性(如大小、创建时间 ),存入 struct shmid_ds
  • IPC_SET:设置共享内存属性(如权限 ),需 root 权限。
4. 权限问题
  • shmctl(IPC_RMID) 需要足够权限(通常创建者或 root 可执行 )→ 普通用户无法删除其他用户的共享内存段。

3. 完整示例(两个进程通信)

进程 1(写数据)

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    // 1. 创建 key
    key_t key = ftok(".", 'a');
    if (key == -1) { perror("ftok"); return 1; }

    // 2. 创建共享内存(1024 字节,权限 0664)
    int shmid = shmget(key, 1024, IPC_CREAT | 0664);
    if (shmid == -1) { perror("shmget"); return 1; }

    // 3. 映射到进程空间
    char *shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void *)-1) { perror("shmat"); return 1; }

    // 4. 写数据
    sprintf(shm_addr, "Hello from Process 1!");
    printf("Data written: %s\n", shm_addr);

    // 5. 等待进程 2 读取(模拟同步)
    sleep(3);

    // 6. 解除映射 + 删除共享内存
    shmdt(shm_addr);
    shmctl(shmid, IPC_RMID, NULL);

    return 0;
}

进程 2(读数据)

c 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    // 1. 创建 key(与进程 1 相同)
    key_t key = ftok(".", 'a');
    if (key == -1) { perror("ftok"); return 1; }

    // 2. 获取共享内存(不创建,仅关联)
    int shmid = shmget(key, 1024, 0);
    if (shmid == -1) { perror("shmget"); return 1; }

    // 3. 映射到进程空间
    char *shm_addr = shmat(shmid, NULL, 0);
    if (shm_addr == (void *)-1) { perror("shmat"); return 1; }

    // 4. 读数据
    printf("Data read: %s\n", shm_addr);

    // 5. 解除映射
    shmdt(shm_addr);

    return 0;
}

三、补充细节与避坑指南

1. ftok 的风险
  • ftok 依赖文件的 inode → 若文件被删除重建inode 改变 ),新进程用 ftok 会生成新 key → 无法关联旧共享内存。
  • 替代方案 :直接用固定 key(如 key_t key = 0x1234; ),或改用 shm_open(基于文件系统的共享内存 )。
2. 共享内存的"孤儿段"问题
  • 进程异常退出(未执行 shmdt/shmctl )→ 共享内存段残留(内核中未销毁 )→ 新进程无法创建同名段。
  • 解决
    • 调试时用 ipcs -m 查看残留段,ipcrm -m shmid 删除。
    • 代码中注册 atexit 回调,确保退出时 shmdt + shmctl(shmid, IPC_RMID, NULL)
3. 同步问题(必须注意!)
  • 共享内存无内置同步机制 → 多进程同时读写会导致数据竞争(如进程 1 写时,进程 2 同时读 )。
  • 解决方案 :配合信号量semgetsemop )或互斥锁pthread_mutex_t )使用。
4. 内存对齐与大小
  • shmgetsize 需是系统页大小的整数倍(通常 4096 字节 )→ 不足时内核自动对齐(可能浪费内存 )。
  • 检查页大小:getconf PAGE_SIZE(通常输出 4096 )。

四、总结:共享内存知识体系

模块 核心内容 关键函数/工具 典型场景
原理 映射物理内存到多进程虚拟地址,0 拷贝通信 shmgetshmat 高并发数据传输(如游戏服务器 )
操作流程 创建 key → 创建共享内存 → 映射 → 读写 → 解除映射 → 删除 ftokshmgetshmat 进程间大数据量、低延迟通信
避坑指南 解决孤儿段、同步问题、ftok 风险 ipcsipcrm、信号量 生产环境稳定运行
相关推荐
phoenix09819 分钟前
Linux入门DAY27
linux·运维·服务器
DokiDoki之父1 小时前
多线程—飞机大战排行榜功能(2.0版本)
android·java·开发语言
whatever who cares1 小时前
Java 中表示数据集的常用集合类
java·开发语言
xy_recording1 小时前
Day08 Go语言学习
开发语言·学习·golang
EndingCoder1 小时前
测试 Next.js 应用:工具与策略
开发语言·前端·javascript·log4j·测试·全栈·next.js
吧唧霸1 小时前
golang读写锁和互斥锁的区别
开发语言·算法·golang
还梦呦2 小时前
2025年09月计算机二级Java选择题每日一练——第一期
java·开发语言
SunnyKriSmile2 小时前
【冒泡排序】
c语言·算法·排序算法
答题卡上的情书2 小时前
java第一个接口
java·开发语言