Linux IPC进阶:信号与System V共享内存
一、信号:进程间的异步通知机制
信号是Linux内核向进程发送的"事件通知",用于处理异常、同步或异步交互(如进程终止、定时提醒)。信号的特点是"异步性"------进程无需主动等待,内核会在合适时机中断进程当前操作,执行信号处理逻辑。
1.1 信号核心基础
1.1.1 信号的核心概念
-
每个信号对应一个唯一编号(可通过
kill -l查看所有信号,共64种,其中1-31为普通信号,34-64为实时信号); -
信号的三种处理方式:
-
SIG_DFL:默认处理(如SIGTERM终止进程、SIGINT中断进程); -
SIG_IGN:忽略处理(进程不响应该信号); -
自定义处理:通过
signal()函数注册自定义回调函数。
-
-
常用关键信号:
-
SIGINT(2):键盘中断(Ctrl+C);
-
SIGTERM(15):默认终止信号(kill命令默认发送);
-
SIGALRM(14):闹钟超时信号(alarm()函数触发);
-
SIGCHLD(17):子进程终止/暂停时,内核向父进程发送的信号;
-
SIGUSR1(10)/SIGUSR2(12):用户自定义信号,可自由分配用途。
-
1.1.2 核心信号函数
| 函数原型 | 功能 | 关键参数说明 | 返回值 |
|---|---|---|---|
| int kill(pid_t pid, int sig); | 向指定PID的进程发送信号sig | pid:目标进程PID(0表示同组进程,-1表示所有进程);sig:信号编号 | 成功0,失败-1 |
| sighandler_t signal(int signum, sighandler_t handler); | 注册信号处理函数(修改信号的响应方式) | signum:信号编号;handler:SIG_DFL/SIG_IGN/自定义函数指针 | 成功返回旧处理函数,失败返回SIG_ERR |
| unsigned int alarm(unsigned int seconds); | 设置闹钟,seconds秒后内核发送SIGALRM信号 | seconds:超时时间(秒),0表示取消之前的闹钟 | 返回剩余秒数(若之前有闹钟),否则0 |
| int pause(void); | 使进程阻塞,直到收到一个可捕获的信号 | 无参数 | 被信号唤醒后返回-1,errno设为EINTR |
1.2 信号实战代码解析
以下结合10个实战代码,覆盖"信号发送、定时、阻塞、自定义处理"等核心场景,每个示例含功能说明、编译运行步骤及关键逻辑解析。
示例1:信号测试基础(11signaltest.c)
功能
循环打印当前进程PID,用于测试信号接收(配合kill命令发送信号)。
代码
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
while (1) {
printf("pid:%d\n",getpid());
sleep(1);
}
return 0;
}
编译&运行
bash
gcc 11signaltest.c -o signaltest
./signaltest
测试方式
打开新终端,执行 kill -2 进程PID(发送SIGINT信号),程序会中断退出。
核心解析
通过getpid()获取当前进程PID,循环打印便于定位目标进程,是信号测试的基础模板。
示例2:信号发送工具(12kill.c)
功能
通过命令行参数指定目标进程PID和信号编号,发送信号(模拟kill命令的核心功能)。
代码
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char *argv[]) {
if (argc < 3)
{
fprintf(stderr,"usage: %s <pid> <signal_number>\n", argv[0]);
exit(1);
}
pid_t pid = (pid_t)atoi(argv[1]);
int sig = atoi(argv[2]);
int ret = kill(pid, sig);
if (ret == -1)
{
perror("kill error");
exit(1);
}
return 0;
}
编译&运行
bash
gcc 12kill.c -o mykill
# 向PID为1234的进程发送SIGINT(2号)信号
./mykill 1234 2
核心解析
-
命令行参数校验:需传入2个参数(PID和信号编号),否则提示用法;
-
atoi():将字符串参数转为整数(PID和信号编号); -
kill():核心函数,向目标PID进程发送指定信号,失败通过perror()打印错误原因(如PID不存在、无权限)。
示例3:闹钟信号(13alarm.c)
功能
设置5秒闹钟,超时后内核发送SIGALRM信号,触发默认处理(终止进程)。
代码
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
alarm(5); // 5秒后发送SIGALRM(14号)信号
while(1)
{
printf("i'm processing...\n");
sleep(1);
}
return 0;
}
编译&运行
bash
gcc 13alarm.c -o alarmtest
./alarmtest
现象
程序循环打印5秒后,自动退出(SIGALRM默认处理是终止进程)。
示例4:进程阻塞(14pause.c)
功能
演示pause()函数的阻塞特性:进程运行5秒后阻塞,直到收到可捕获的信号才继续。
代码
c
#include <stdio.h>
#include <unistd.h>
int main()
{
int i =0;
while (1) {
printf("i am listen music... \n");
sleep(1);
i++;
if (i==5) {
pause(); // 阻塞,等待信号
}
}
return 0;
}
编译&运行
bash
gcc 14pause.c -o pausetest
./pausetest
测试方式
程序打印5次后阻塞,打开新终端执行 kill -10 进程PID(发送SIGUSR1信号),进程会继续打印。
核心解析
pause()使进程进入"可中断睡眠"状态,直到收到一个"可捕获"的信号(忽略的信号无法唤醒),被唤醒后返回-1,进程继续执行后续逻辑。
示例5:自定义闹钟处理(15signal_alarm.c)
功能
通过signal()注册自定义函数处理SIGALRM信号,避免进程被终止,实现"超时后切换状态"。
代码
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int flag = 0 ;
void myhandle(int num) // 自定义信号处理函数
{
flag = 1; // 收到信号后修改标志位
}
int main(int argc, char *argv[])
{
signal(SIGALRM, myhandle); // 注册SIGALRM的处理函数为myhandle
alarm(5); // 5秒后发送SIGALRM
while(1)
{
if(0 == flag)
{
printf("i'm processing...\n");
}
else
{
printf("i'm off duty....\n"); // 超时后切换为该状态
}
sleep(1);
}
return 0;
}
核心解析
-
自定义处理函数
myhandle():参数为信号编号,功能是修改全局标志位flag; -
signal(SIGALRM, myhandle):将SIGALRM信号的处理方式改为自定义函数; -
逻辑:前5秒
flag=0打印"processing",5秒后收到SIGALRM,flag=1,切换为"off duty",进程不终止。
示例6:信号唤醒阻塞(16sign_con.c)
功能
演示pause()阻塞后,通过自定义信号处理函数唤醒,且不终止进程。
代码
c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void myhandle(int num) // 空处理函数(仅唤醒进程)
{
}
int main(int argc, char *argv[])
{
signal(SIGCONT, myhandle); // 注册SIGCONT信号的处理函数
int i = 0;
while(1)
{
printf("i'm listen music...,pid:%d\n",getpid());
sleep(1);
i++;
if(3 == i)
{
pause(); // 第3次后阻塞
}
}
return 0;
}
测试方式
程序打印3次后阻塞,新终端执行 kill -18 进程PID(发送SIGCONT信号),进程继续打印。
核心解析
自定义处理函数可以是空实现,核心作用是"捕获信号并唤醒pause()",避免进程被信号的默认处理终止。
示例7:用户自定义信号处理(17signal_user.c)
功能
处理用户自定义信号SIGUSR1和SIGUSR2,实现"接收指定次数后切换处理方式"(忽略/默认)。
代码(修正拼写错误后)
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
void sigusr1_handler(int signo)
{
static int count = 0;
count++;
printf("Received SIGUSR1 %d times\n", count);
if (3 == count)
{
signal(SIGUSR1, SIG_IGN); // 接收3次后忽略SIGUSR1
}
return;
}
void sigusr2_handler(int signo)
{
static int count = 0;
count++;
printf("Received SIGUSR2 %d times\n", count);
if (4 == count)
{
signal(SIGUSR2, SIG_DFL); // 接收4次后恢复默认处理
}
return;
}
int main()
{
if (signal(SIGUSR1, sigusr1_handler) == SIG_ERR)
{
perror("signal SIGUSR1 error");
exit(1);
}
if (signal(SIGUSR2, sigusr2_handler) == SIG_ERR)
{
perror("signal SIGUSR2 error");
exit(1);
}
while (1)
{
printf("playing pid:%d\n",getpid());
sleep(1);
}
return 0;
}
核心解析
-
修正代码错误:原代码中
sginal是拼写错误,应改为signal; -
SIGUSR1:接收3次后,通过
signal(SIGUSR1, SIG_IGN)设置为忽略,后续再发送SIGUSR1无响应; -
SIGUSR2:接收4次后,通过
signal(SIGUSR2, SIG_DFL)恢复默认处理(SIGUSR2默认处理是终止进程)。
示例8:处理SIGCHLD避免僵尸进程(18signal_child.c)
功能
父进程注册SIGCHLD信号处理函数,在子进程终止时自动回收资源,避免僵尸进程。
代码
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
void handler(int sig){
pid_t recycle = wait(NULL); // 回收子进程资源
printf("pid:%d,Child %d terminated\n",getpid(), recycle);
}
int main(){
signal(SIGCHLD, handler); // 注册SIGCHLD处理函数
pid_t pid = fork();
if(pid >0 ){
int i =10;
while(i--){
printf("I am parent pid:%d\n", getpid());
sleep(1);
}
}else if(pid == 0){
int j =3;
while(j--){
printf("I am child pid:%d\n", getpid());
sleep(1);
}
exit(0); // 子进程3秒后终止
}
else{
perror("fork error");
exit(1);
}
return 0;
}
核心解析
-
僵尸进程成因:子进程终止后,父进程未回收其资源(PCB);
-
解决方案:子进程终止时,内核向父进程发送SIGCHLD信号,父进程在处理函数中调用
wait()回收资源; -
现象:子进程3秒后终止,父进程立即打印"Child X terminated",无僵尸进程残留。
示例9:单个处理函数处理多个信号(19signal_handlenum.c)
功能
用一个自定义处理函数处理SIGUSR1和SIGUSR2,通过信号编号区分不同信号,执行不同逻辑。
代码(修正逻辑错误后)
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
void sig_handler(int signo)
{
if (SIGUSR1 == signo)
{
static int count = 0;
printf("father help\n");
count++;
if (count == 3)
{
signal(SIGUSR1, SIG_IGN); // 3次后忽略SIGUSR1
}
}
else if (SIGUSR2 == signo) // 原代码逻辑错误,需移出SIGUSR1判断
{
static int count = 0;
printf("mother help\n");
count++;
if (count == 4)
{
signal(SIGUSR2, SIG_DFL); // 4次后恢复默认
}
}
return;
}
int main()
{
signal(SIGUSR1, sig_handler);
signal(SIGUSR2, sig_handler);
while (1)
{
printf("playing pid:%d\n",getpid());
sleep(1);
}
return 0;
}
核心解析
原代码将SIGUSR2的逻辑写在SIGUSR1的判断内部,导致无法响应SIGUSR2。修正后通过if-else区分信号编号,实现"一个函数处理多个信号"的需求。
二、System V共享内存:高效的进程间数据共享
System V共享内存是由Unix System V标准定义的IPC机制,核心优势是高效------多个进程直接映射同一块内核内存区域到自己的地址空间,无需数据拷贝(管道、消息队列需内核/用户空间拷贝)。但共享内存本身无同步机制,需搭配信号、信号量等使用,避免数据竞争。
2.1 共享内存核心基础
2.1.1 与管道的核心区别
| 特性 | 管道(匿名/有名) | System V共享内存 |
|---|---|---|
| 读写方式 | 半双工,需顺序读写 | 全双工,任意进程可读写 |
| 阻塞特性 | 读空/写满会阻塞 | 无阻塞(需手动同步) |
| 数据拷贝 | 用户→内核→用户(2次拷贝) | 无拷贝(直接操作共享内存) |
| 数据持久化 | 随进程退出/管道关闭销毁 | 随内核生命周期,需手动删除 |
| 数据结构 | 内核队列(FIFO) | 连续内存区域(类似字符数组) |
2.1.2 共享内存编程步骤
共享内存的使用遵循固定流程,核心是"申请→映射→读写→撤销→删除":
-
生成唯一键值(key):通过
ftok()函数,由文件路径和项目ID生成; -
申请共享内存:通过
shmget()函数,向内核申请指定大小的共享内存; -
内存映射:通过
shmat()函数,将内核共享内存映射到当前进程的用户空间; -
读写操作:直接操作映射后的内存地址(如
memcpy()、strcpy()); -
撤销映射:通过
shmdt()函数,断开进程与共享内存的映射关系; -
删除共享内存:通过
shmctl()函数,删除内核中的共享内存对象(避免残留)。
2.1.3 核心函数解析
| 函数原型 | 功能 | 关键参数说明 | 返回值 |
|---|---|---|---|
| key_t ftok(const char *pathname, int proj_id); | 生成唯一的IPC键值 | pathname:任意存在的文件路径;proj_id:1-255的整数(通常用ASCII单字符) | 成功返回key,失败-1 |
| int shmget(key_t key, size_t size, int shmflg); | 申请/获取共享内存 | key:ftok生成的键值;size:申请大小(字节);shmflg:权限(8进制)+ 标志(IPC_CREAT创建,IPC_EXCL检测存在) | 成功返回共享内存ID(shmid),失败-1 |
| void *shmat(int shmid, const void *shmaddr, int shmflg); | 映射共享内存到用户空间 | shmid:共享内存ID;shmaddr:映射地址(NULL表示内核自动分配);shmflg:0(读写)/SHM_RDONLY(只读) | 成功返回映射地址,失败(void*)-1 |
| int shmdt(const void *shmaddr); | 撤销共享内存映射 | shmaddr:shmat返回的映射地址 | 成功0,失败-1 |
| int shmctl(int shmid, int cmd, struct shmid_ds *buf); | 控制共享内存(删除/查询属性) | shmid:共享内存ID;cmd:IPC_RMID(删除);buf:NULL(仅删除,无需查询属性) | 成功0,失败-1 |
2.1.4 常用管理命令
bash
ipcs -a # 查看所有System V IPC对象(共享内存、信号量、消息队列)
ipcs -m # 仅查看共享内存
ipcrm -m shmid # 删除指定shmid的共享内存(强制清理残留)
2.2 共享内存实战代码解析
以下两个示例实现"共享内存写进程"和"共享内存读进程",完成进程间字符串传递。
示例1:共享内存写进程(20shm_w.c)
功能
生成key→申请共享内存→映射→写入字符串"hello"→撤销映射。
代码
c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
int main(int argc, char *argv[])
{
// 1. 生成key(路径./,项目ID为'!')
key_t key = ftok("./",'!');
if(-1 == key)
{
perror("ftok");
return 1;
}
printf("key: 0x%x\n",key);
// 2. 申请4096字节的共享内存(权限0666,不存在则创建)
int shmid = shmget(key,4096,IPC_CREAT|0666);
if(-1 == shmid)
{
perror("shmget");
return 1;
}
printf("shmid is %d\n",shmid);
// 3. 映射共享内存(内核自动分配地址,读写权限)
void* p = shmat(shmid,NULL,0); // 原代码!SHM_RDONLY等价于0(读写)
if((void *) -1 == p)
{
perror("shmat");
return 1;
}
// 4. 写入数据(memcpy适用于任意二进制数据,strcpy适用于字符串)
char buf[1024]="hello";
memcpy(p,buf,strlen(buf)+1); // +1 包含字符串结束符'\0'
// 5. 撤销映射
shmdt(p);
return 0;
}
示例2:共享内存读进程(20shm_r.c)
功能
生成相同key→获取已存在的共享内存→映射→读取数据→撤销映射(可选删除)。
代码
c
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/shm.h>
int main(int argc, char *argv[])
{
// 1. 生成与写进程相同的key(路径和项目ID必须一致)
key_t key = ftok("./",'!');
if(-1 == key)
{
perror("ftok");
return 1;
}
printf("key: 0x%x\n",key);
// 2. 获取已存在的共享内存(大小和权限需与写进程一致)
int shmid = shmget(key,4096,IPC_CREAT|0666);
if(-1 == shmid)
{
perror("shmget");
return 1;
}
printf("shmid is %d\n",shmid);
// 3. 映射共享内存(读写权限)
void* p = shmat(shmid,NULL,0);
if((void *) -1 == p)
{
perror("shmat");
return 1;
}
// 4. 读取数据(直接访问映射地址)
printf("mem:%s\n",(char*)p);
// 5. 撤销映射
shmdt(p);
// 6. (可选)删除共享内存(通常由最后一个进程执行)
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}
编译&运行步骤
bash
# 编译写进程
gcc 20shm_w.c -o shm_w
# 编译读进程
gcc 20shm_r.c -o shm_r
# 终端1:运行写进程(生成共享内存并写入数据)
./shm_w
# 输出:key: 0x2128019b(示例值),shmid is 12345(示例值)
# 终端2:运行读进程(读取共享内存数据)
./shm_r
# 输出:key: 0x2128019b,shmid is 12345,mem:hello
# (可选)清理共享内存
ipcrm -m 12345(替换为实际shmid)
核心注意事项
-
key一致性:读/写进程的
ftok()参数(路径和proj_id)必须完全一致,否则生成的key不同,无法访问同一共享内存; -
共享内存残留:共享内存随内核生命周期存在,进程退出后不会自动删除,需通过
shmctl(shmid, IPC_RMID, NULL)或ipcrm命令手动删除; -
同步问题:当前示例未加同步机制,若写进程未写完,读进程读取会得到不完整数据。实际使用需搭配信号(如写完成后发送信号通知读进程)或信号量;
-
映射权限:
shmat()的shmflg设为SHM_RDONLY时,进程仅能读取共享内存,写入会触发段错误。
三、总结:信号与共享内存的协同应用
本文解析的两类IPC机制各有侧重,实际开发中常协同使用:
-
信号:负责"异步通知"(如进程间状态同步、异常处理),适合传递简单控制信息,不适合传递大量数据;
-
共享内存:负责"高效数据共享",适合传递大量数据,但无同步机制,需信号/信号量辅助避免数据竞争。
核心学习要点:
-
信号:掌握
kill()发送信号、signal()注册自定义处理函数,理解SIGALRM、SIGCHLD等常用信号的应用场景; -
共享内存:牢记"key→shmget→shmat→读写→shmdt→shmctl"的固定流程,注意key一致性和资源清理;
-
实操避坑:编译信号代码无需额外链接库,共享内存代码需包含完整头文件;测试时注意进程PID和共享内存ID的正确性,避免操作错误对象。