以下是基于笔记整理的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 。
三、信号处理流程(内核视角)
信号从"产生"到"进程响应"的核心步骤:
-
信号产生:
- 硬件触发(如
Ctrl+C
→SIGINT
)。 - 软件触发(如
kill
命令、alarm
定时器、raise
函数 )。
- 硬件触发(如
-
信号传递 :
内核将信号标记到目标进程的信号 pending 队列(记录待处理的信号 )。
-
信号处理 :
进程从内核态切换到用户态时,检查 pending 队列:
- 若信号是默认/忽略:执行预设逻辑(如终止、忽略 )。
- 若信号是捕获(自定义) :跳转到用户注册的信号处理函数执行。
-
恢复执行 :
处理函数执行完毕后,进程回到被中断的执行流继续运行。
流程图解(简化):
默认/忽略 捕获 信号产生 内核标记 pending 检查处理方式 执行系统逻辑 执行自定义函数 进程继续/终止
四、补充细节与工程实践
1. 信号的"不可靠性"(标准信号)
- 1~31 的标准信号可能丢失(如同一信号多次产生,内核可能只记录一次 )→ 需"实时信号"(34~64 )解决(支持排队 )。
- 示例:连续发 10 次
SIGUSR1
(标准信号 ),进程可能只收到 1 次通知。
2. 信号与终端的交互
-
后台进程(
&
运行)默认忽略SIGINT
(Ctrl+C
),但仍响应SIGTERM
(kill <pid>
)。 -
守护进程需主动忽略无关终端信号(如
SIGTTOU
、SIGTTIN
):csignal(SIGTTOU, SIG_IGN); signal(SIGTTIN, SIG_IGN);
3. 信号处理的"竞态条件"
-
若信号处理函数与主逻辑共享全局变量 ,需用
volatile
修饰(防止编译器优化 ):cvolatile 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
)。
五、总结:知识体系脑图
通过以上整理,可覆盖笔记核心内容,并补充信号处理流程 、工程细节(如可靠信号、调试方法 ),适配实际开发需求。
一、核心概念:信号的三种处理方式
进程收到信号时,有三类响应策略,通过 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
:
要处理的信号编号 (如SIGINT
、SIGALRM
等,完整列表可查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
修饰(防止编译器优化导致数据不同步 ):cvolatile 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_DFL 、SIG_IGN 、自定义函数 |
signal 函数 |
注册信号处理逻辑,返回旧处理函数地址 | #include <signal.h> 、函数指针 |
自定义处理 | 编写 void func(int) 格式函数,处理信号编号与业务逻辑 |
注意可重入性、共享变量用 volatile |
补充细节 | 不可捕获信号、异步安全、恢复默认逻辑 | SIGKILL 、SIGSTOP 特性 |
以下是对笔记中 "信号注册与发送" 知识点的系统梳理,分"核心注意事项""信号发送方式""补充细节"三大模块,结合 Linux 信号机制深化:
一、信号注册的核心注意事项
笔记中提到信号注册的 4 条规则,整理为工程实践原则:
序号 | 规则原文 | 补充说明/实践场景 |
---|---|---|
1 | 若信号不被注册,按默认方式运行 | - 未注册的信号会触发系统预设逻辑(如 SIGINT 默认终止进程 ) - 示例:signal(SIGINT, SIG_DFL); 显式恢复默认 |
2 | 信号只需注册一次 | - 多次注册以最后一次为准 ,但通常只需注册一次(避免覆盖逻辑 ) - 场景:守护进程启动时注册 SIGTERM 处理函数 |
3 | 每次信号到来,触发任务函数 | - 信号是异步事件 ,每次触发都会调用处理函数(需注意"重入性" ) - 风险:处理函数中调用 printf (非可重入函数)可能导致死锁 |
4 | 信号尽可能早注册 | - 防止进程启动初期收到信号(如 SIGCHLD 子进程退出信号 ),因未注册而走默认逻辑 - 最佳实践:main 函数开头优先注册关键信号 |
二、进程中发送信号的方式
信号发送是"主动通知进程"的核心手段,笔记覆盖 kill
命令 、系统调用 (kill
、raise
等 ),补充细节如下:
1. kill
命令(终端/脚本触发)
- 语法 :
kill -<信号编号/名称> <pid>
- 示例 :
- 强制终止进程:
kill -9 1234
(SIGKILL
无法捕获 ) - 发送
SIGUSR1
:kill -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
:信号编号(如SIGINT
、SIGKILL
)
-
返回值 :
- 成功:
0
- 失败:
-1
(errno
说明原因,如ESRCH
进程不存在 )
- 成功:
-
示例 :
cif (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 秒后触发 ) |
三、特殊信号的"强制处理"(SIGKILL
、SIGSTOP
)
- 规则 :
SIGKILL
(9 号)、SIGSTOP
(19 号)无法被捕获/忽略(内核强制控制 )。 - 原因 :
- 系统需要"绝对终止/暂停进程"的能力(如强制回收资源、调试暂停 )。
- 若允许捕获,恶意进程可能规避终止,导致系统不稳定。
- 实践建议 :
- 调试时优先用
SIGTERM
(15 号,可捕获 ),允许进程优雅退出(如清理临时文件 )。 - 仅当进程完全无响应时,用
SIGKILL
强制终止。
- 调试时优先用
四、补充细节:信号发送的"权限与限制"
- 权限 :
- 普通用户只能向自己的进程发信号(或有权限的进程,如同组进程 )。
- root 用户可向任意进程 发信号(需谨慎使用
SIGKILL
)。
- 错误场景 :
- 发送信号失败常见原因:
ESRCH
:进程pid
不存在。EPERM
:无权限向目标进程发信号(如普通用户向 root 进程发信号 )。
- 发送信号失败常见原因:
五、知识体系总结
模块 | 核心内容 | 关键工具/函数 | 工程实践建议 |
---|---|---|---|
信号注册 | 4 条原则(早注册、单次注册等),避免信号丢失/逻辑覆盖 | signal 、sigaction (更可靠 ) |
主函数开头注册关键信号 |
信号发送 | 命令(kill )、系统调用(kill 、raise 等 ),覆盖主动通知场景 |
kill 、raise 、alarm |
优先用 SIGTERM 替代 SIGKILL |
特殊信号 | SIGKILL 、SIGSTOP 无法捕获,内核强制控制 |
kill -9 、kill -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
返回(通常返回-1
,errno
设为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;
}
执行流程:
alarm(5)
设定 5 秒定时器。- 进程进入死循环,每秒打印
hello, world!
。 - 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;
}
执行流程:
signal(SIGALRM, handler)
注册自定义处理函数。alarm(5)
启动定时器 → 5 秒后触发SIGALRM
。handler
被调用:打印信息 + 重置定时器(alarm(5)
)→ 实现周期性触发。- 进程持续运行,每秒打印
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;
}
执行流程:
signal
注册SIGINT
处理函数(Ctrl+C
触发 )。pause
使进程挂起 → 控制台显示"进程挂起,等待信号..."。- 按
Ctrl+C
→ 触发SIGINT
→handler
被调用 → 打印信息。 pause
返回 → 进程继续执行,打印"进程恢复运行!"。
三、补充细节与注意事项
1. alarm
的"覆盖性"
-
重复调用
alarm
会重置定时器 ,示例:calarm(10); // 首次定时 10 秒 sleep(3); alarm(5); // 剩余 7 秒被覆盖,新定时 5 秒 → 5 秒后触发 SIGALRM
2. pause
的"不可中断性"?
-
pause
会被任意信号中断 (包括SIGALRM
、SIGINT
等 ),返回后需检查errno
:cint ret = pause(); if (ret == -1 && errno == EINTR) { printf("被信号中断,恢复执行!\n"); }
3. 信号处理的"异步性"与 alarm
结合
-
alarm
触发的SIGALRM
是异步事件 ,可能在进程任意执行流中触发 → 若处理函数与主逻辑共享变量,需用volatile
修饰:cvolatile int flag = 0; void handler(int signum) { flag = 1; } int main() { alarm(5); signal(SIGALRM, handler); while (!flag) { /* 等待 flag 被信号修改 */ } printf("定时器触发!\n"); }
4. alarm
与 setitimer
的区别(进阶)
alarm
是简易定时器 (仅触发SIGALRM
),但:-
同一进程只能有一个
alarm
定时器(重复调用覆盖 )。 -
需更复杂定时(如"周期性触发""区分间隔/绝对时间" ),用
setitimer
:cstruct itimerval timer = { .it_interval = {1, 0}, // 间隔 1 秒 .it_value = {3, 0} // 3 秒后首次触发 }; setitimer(ITIMER_REAL, &timer, NULL); // 触发 SIGALRM
-
四、实践场景与应用
1. 守护进程的"心跳检测"
-
用
alarm
周期性触发SIGALRM
→ 处理函数中检查系统状态(如网络连接、资源使用 ):cvoid heartbeat_handler(int signum) { // 检查网络连接、上报状态... alarm(60); // 1 分钟后再次触发,持续心跳 } int main() { signal(SIGALRM, heartbeat_handler); alarm(60); // 首次心跳定时 // 守护进程主逻辑... }
2. 限时任务(超时终止)
-
结合
alarm
和SIGALRM
默认行为,实现"任务超时终止":c// 模拟一个耗时任务,超时 5 秒则终止 int main() { alarm(5); // 5 秒后终止进程 // 执行耗时操作... while (1) { /* 模拟任务 */ } return 0; }
3. pause
实现"等待信号唤醒"
-
进程启动后挂起,等待外部信号触发逻辑(如
Ctrl+C
启动任务 ):cint main() { signal(SIGINT, start_task); printf("等待启动信号...\n"); pause(); // 挂起直到收到 SIGINT return 0; } void start_task(int signum) { printf("开始执行任务...\n"); // 任务逻辑... }
五、知识体系总结
函数 | 核心功能 | 典型用法 | 注意事项 |
---|---|---|---|
alarm |
定时触发 SIGALRM 信号 |
实现定时器、心跳检测 | 重复调用会重置定时器 |
pause |
挂起进程,等待信号唤醒 | 等待外部事件触发(如用户按键 ) | 会被任意信号中断,需检查 errno |
signal |
注册信号处理函数 | 自定义 SIGALRM 、SIGINT 逻辑 |
注意信号的"异步性"和"可重入" |
通过以上梳理,可完整覆盖笔记中 alarm
和 pause
的用法,补充"定时器覆盖性""pause
中断处理"等工程细节,适配实际开发需求(如守护进程、限时任务 )。
以下是对笔记中 共享内存(Shared Memory) 知识点的系统总结与补充,分"核心原理""操作流程""代码实践""补充细节"四大模块,适配 Linux 进程间通信(IPC)场景:
一、核心原理:共享内存为何是"效率最高的 IPC"?
1. 基本概念
- 共享内存 :内核创建一块物理内存,映射到多个进程的虚拟地址空间 → 进程可直接读写同一块内存,无需数据拷贝。
- 内存映射 :通过
mmap
或shmat
,将内核管理的共享内存段映射到进程虚拟地址 → 实现"虚拟地址 → 物理地址"的直接关联。
2. 效率优势
IPC 方式 | 数据拷贝次数 | 典型场景 | 缺点 |
---|---|---|---|
共享内存 | 0 次(直接读写物理内存) | 高并发数据传输(如游戏服务器 ) | 需同步机制(如信号量 ) |
管道/消息队列 | 2 次(用户→内核→用户) | 低并发、异步通信 | 速度慢,数据量大时低效 |
3. 原理图解(简化)
二、共享内存操作流程(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 |
以下是对笔记中 ftok
和 shmget
函数的系统总结与补充,分"核心功能""参数细节""实践避坑""关联流程"四个模块:
一、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 (非负整数 ) - 失败:-1 (errno 设为 ENOENT 等 ) |
3. 实践注意事项
- 风险 1 :若
pathname
文件被删除重建 (inode
改变 ),新进程用ftok
会生成新key
→ 无法关联旧 IPC 资源。- 解决方案:改用固定
key
(如key_t key = 0x1234;
),或确保文件稳定(如用/dev/zero
)。
- 解决方案:改用固定
- 风险 2 :
proj_id
若超出 1 字节(如256
),会被截断为低 8 位 → 可能冲突。- 建议:
proj_id
取0~255
范围内的值(如'a'
、10
)。
- 建议:
二、shmget
:创建/获取共享内存段
1. 核心功能
- 在内核中申请/查找共享内存段 ,返回
shmid
(共享内存 ID )→ 后续操作(shmat
、shmctl
)依赖此 ID。
2. 参数与返回值
函数原型 | int shmget(key_t key, size_t size, int shmflg); |
---|---|
key |
ftok 生成的 key ,或固定值(如 0x1234 ) |
size |
共享内存大小(字节) - 需是系统页大小的整数倍 (通常 4096 字节 ) - 不足时内核自动对齐(可能浪费内存 ) |
shmflg |
标志位(组合使用,用 ` |
返回值 | - 成功:shmid (非负整数 ) - 失败:-1 (errno 设为 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
),导致程序崩溃。 -
示例:
cvoid *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
标记删除 )。
以下是对笔记中 共享内存控制函数(shmdt
、shmctl
) 及相关命令的系统总结,分"核心函数解析""命令行工具""流程关联""补充细节"四个模块梳理:
一、核心函数解析:shmdt
+ shmctl
1. shmdt
:解除内存映射
- 原型 :
int shmdt(const void *shmaddr);
- 功能 :断开进程虚拟地址空间与共享内存段的映射关系(仅解除映射,不删除内核中的共享内存 )。
- 参数 :
shmaddr
:shmat
返回的共享内存映射首地址 (必须是之前通过shmat
获得的地址 )。
- 返回值 :
- 成功:
0
- 失败:
-1
(errno
设为EINVAL
等,如shmaddr
无效 )
- 成功:
2. shmctl
:操作共享内存段(内核级控制 )
- 原型 :
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能 :对共享内存段执行查询、删除、设置属性等操作(核心用于"标记删除" )。
- 参数 :
shmid
:shmget
返回的共享内存 ID。cmd
:操作指令(常用IPC_RMID
)。buf
:用于存储/设置共享内存属性(struct shmid_ds
),删除时可填NULL
。
- 返回值 :
- 成功:
0
(或返回属性查询结果 ) - 失败:
-1
(errno
设为EINVAL
等 )
- 成功:
关键指令:IPC_RMID
(标记删除共享内存 )
- 作用 :
- 标记共享内存段为"待删除" → 所有进程解除映射 (
shmdt
)后,内核才会真正销毁该段。 - 若有进程未解除映射,共享内存段会残留(需等待所有进程
shmdt
后销毁 )。
- 标记共享内存段为"待删除" → 所有进程解除映射 (
二、命令行工具:ipcs
+ ipcrm
1. ipcs -a
:查看 IPC 资源
-
功能:列出系统中所有 IPC 资源(共享内存、消息队列、信号量 )。
-
示例 :
bashipcs -m # 仅查看共享内存
-
输出解析 :
shmid
:共享内存 ID(与shmget
返回值对应 )。owner
:创建者用户名。segments
:共享内存段大小、权限等。
2. ipcrm
:删除 IPC 资源
-
语法 :
ipcrm -m shmid
:通过shmid
删除共享内存。ipcrm -M shmkey
:通过shmkey
(ftok
生成的 key )删除共享内存。
-
示例 :
bashipcrm -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 方式 | 核心场景 | 关键函数/结构 | 补充细节 |
---|---|---|---|
信号量集 | 进程同步(共享资源协调 ) | semget 、semop 、semctl |
需与共享内存配合 |
消息队列 | 异步通信(按优先级投递 ) | msgget 、msgsnd 、msgrcv |
结构化消息,支持队列缓存 |
管道 | 简单字节流通信(父子进程 ) | pipe 、mkfifo |
同步,无结构 |
共享内存 | 高效大数据传输 | shmget 、shmat 、shmdt |
需信号量/锁同步 |
通过以上整理,补充了 信号量与共享内存的协同 、消息队列的优先级实践 、各 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 同时读 )。
- 解决方案 :配合信号量 (
semget
、semop
)或互斥锁 (pthread_mutex_t
)使用。
4. 内存对齐与大小
shmget
的size
需是系统页大小的整数倍(通常 4096 字节 )→ 不足时内核自动对齐(可能浪费内存 )。- 检查页大小:
getconf PAGE_SIZE
(通常输出 4096 )。
四、总结:共享内存知识体系
模块 | 核心内容 | 关键函数/工具 | 典型场景 |
---|---|---|---|
原理 | 映射物理内存到多进程虚拟地址,0 拷贝通信 | shmget 、shmat |
高并发数据传输(如游戏服务器 ) |
操作流程 | 创建 key → 创建共享内存 → 映射 → 读写 → 解除映射 → 删除 | ftok 、shmget 、shmat 等 |
进程间大数据量、低延迟通信 |
避坑指南 | 解决孤儿段、同步问题、ftok 风险 | ipcs 、ipcrm 、信号量 |
生产环境稳定运行 |