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命令发送信号)。
代码
#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(){while(1){printf("pid:%d\n",getpid());sleep(1);}return0;}编译&运行
gcc 11signaltest.c -o signaltest ./signaltest测试方式
打开新终端,执行kill -2 进程PID(发送SIGINT信号),程序会中断退出。
核心解析
通过getpid()获取当前进程PID,循环打印便于定位目标进程,是信号测试的基础模板。
示例2:信号发送工具(12kill.c)
功能
通过命令行参数指定目标进程PID和信号编号,发送信号(模拟kill命令的核心功能)。
代码
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<signal.h>#include<sys/types.h>intmain(intargc,char*argv[]){if(argc<3){fprintf(stderr,"usage: %s <pid> <signal_number>\n",argv[0]);exit(1);}pid_tpid=(pid_t)atoi(argv[1]);intsig=atoi(argv[2]);intret=kill(pid,sig);if(ret==-1){perror("kill error");exit(1);}return0;}编译&运行
gcc 12kill.c -o mykill# 向PID为1234的进程发送SIGINT(2号)信号./mykill12342核心解析
命令行参数校验:需传入2个参数(PID和信号编号),否则提示用法;
atoi():将字符串参数转为整数(PID和信号编号);kill():核心函数,向目标PID进程发送指定信号,失败通过perror()打印错误原因(如PID不存在、无权限)。
示例3:闹钟信号(13alarm.c)
功能
设置5秒闹钟,超时后内核发送SIGALRM信号,触发默认处理(终止进程)。
代码
#include<stdio.h>#include<unistd.h>#include<stdlib.h>intmain(intargc,char*argv[]){alarm(5);// 5秒后发送SIGALRM(14号)信号while(1){printf("i'm processing...\n");sleep(1);}return0;}编译&运行
gcc 13alarm.c -o alarmtest ./alarmtest现象
程序循环打印5秒后,自动退出(SIGALRM默认处理是终止进程)。
示例4:进程阻塞(14pause.c)
功能
演示pause()函数的阻塞特性:进程运行5秒后阻塞,直到收到可捕获的信号才继续。
代码
#include<stdio.h>#include<unistd.h>intmain(){inti=0;while(1){printf("i am listen music... \n");sleep(1);i++;if(i==5){pause();// 阻塞,等待信号}}return0;}编译&运行
gcc 14pause.c -o pausetest ./pausetest测试方式
程序打印5次后阻塞,打开新终端执行kill -10 进程PID(发送SIGUSR1信号),进程会继续打印。
核心解析
pause()使进程进入“可中断睡眠”状态,直到收到一个“可捕获”的信号(忽略的信号无法唤醒),被唤醒后返回-1,进程继续执行后续逻辑。
示例5:自定义闹钟处理(15signal_alarm.c)
功能
通过signal()注册自定义函数处理SIGALRM信号,避免进程被终止,实现“超时后切换状态”。
代码
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<signal.h>intflag=0;voidmyhandle(intnum)// 自定义信号处理函数{flag=1;// 收到信号后修改标志位}intmain(intargc,char*argv[]){signal(SIGALRM,myhandle);// 注册SIGALRM的处理函数为myhandlealarm(5);// 5秒后发送SIGALRMwhile(1){if(0==flag){printf("i'm processing...\n");}else{printf("i'm off duty....\n");// 超时后切换为该状态}sleep(1);}return0;}核心解析
自定义处理函数
myhandle():参数为信号编号,功能是修改全局标志位flag;signal(SIGALRM, myhandle):将SIGALRM信号的处理方式改为自定义函数;逻辑:前5秒
flag=0打印“processing”,5秒后收到SIGALRM,flag=1,切换为“off duty”,进程不终止。
示例6:信号唤醒阻塞(16sign_con.c)
功能
演示pause()阻塞后,通过自定义信号处理函数唤醒,且不终止进程。
代码
#include<stdio.h>#include<unistd.h>#include<signal.h>voidmyhandle(intnum)// 空处理函数(仅唤醒进程){}intmain(intargc,char*argv[]){signal(SIGCONT,myhandle);// 注册SIGCONT信号的处理函数inti=0;while(1){printf("i'm listen music...,pid:%d\n",getpid());sleep(1);i++;if(3==i){pause();// 第3次后阻塞}}return0;}测试方式
程序打印3次后阻塞,新终端执行kill -18 进程PID(发送SIGCONT信号),进程继续打印。
核心解析
自定义处理函数可以是空实现,核心作用是“捕获信号并唤醒pause()”,避免进程被信号的默认处理终止。
示例7:用户自定义信号处理(17signal_user.c)
功能
处理用户自定义信号SIGUSR1和SIGUSR2,实现“接收指定次数后切换处理方式”(忽略/默认)。
代码(修正拼写错误后)
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<signal.h>#include<string.h>voidsigusr1_handler(intsigno){staticintcount=0;count++;printf("Received SIGUSR1 %d times\n",count);if(3==count){signal(SIGUSR1,SIG_IGN);// 接收3次后忽略SIGUSR1}return;}voidsigusr2_handler(intsigno){staticintcount=0;count++;printf("Received SIGUSR2 %d times\n",count);if(4==count){signal(SIGUSR2,SIG_DFL);// 接收4次后恢复默认处理}return;}intmain(){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);}return0;}核心解析
修正代码错误:原代码中
sginal是拼写错误,应改为signal;SIGUSR1:接收3次后,通过
signal(SIGUSR1, SIG_IGN)设置为忽略,后续再发送SIGUSR1无响应;SIGUSR2:接收4次后,通过
signal(SIGUSR2, SIG_DFL)恢复默认处理(SIGUSR2默认处理是终止进程)。
示例8:处理SIGCHLD避免僵尸进程(18signal_child.c)
功能
父进程注册SIGCHLD信号处理函数,在子进程终止时自动回收资源,避免僵尸进程。
代码
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<signal.h>#include<string.h>#include<sys/types.h>#include<sys/wait.h>voidhandler(intsig){pid_trecycle=wait(NULL);// 回收子进程资源printf("pid:%d,Child %d terminated\n",getpid(),recycle);}intmain(){signal(SIGCHLD,handler);// 注册SIGCHLD处理函数pid_tpid=fork();if(pid>0){inti=10;while(i--){printf("I am parent pid:%d\n",getpid());sleep(1);}}elseif(pid==0){intj=3;while(j--){printf("I am child pid:%d\n",getpid());sleep(1);}exit(0);// 子进程3秒后终止}else{perror("fork error");exit(1);}return0;}核心解析
僵尸进程成因:子进程终止后,父进程未回收其资源(PCB);
解决方案:子进程终止时,内核向父进程发送SIGCHLD信号,父进程在处理函数中调用
wait()回收资源;现象:子进程3秒后终止,父进程立即打印“Child X terminated”,无僵尸进程残留。
示例9:单个处理函数处理多个信号(19signal_handlenum.c)
功能
用一个自定义处理函数处理SIGUSR1和SIGUSR2,通过信号编号区分不同信号,执行不同逻辑。
代码(修正逻辑错误后)
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<signal.h>#include<string.h>voidsig_handler(intsigno){if(SIGUSR1==signo){staticintcount=0;printf("father help\n");count++;if(count==3){signal(SIGUSR1,SIG_IGN);// 3次后忽略SIGUSR1}}elseif(SIGUSR2==signo)// 原代码逻辑错误,需移出SIGUSR1判断{staticintcount=0;printf("mother help\n");count++;if(count==4){signal(SIGUSR2,SIG_DFL);// 4次后恢复默认}}return;}intmain(){signal(SIGUSR1,sig_handler);signal(SIGUSR2,sig_handler);while(1){printf("playing pid:%d\n",getpid());sleep(1);}return0;}核心解析
原代码将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 常用管理命令
ipcs -a# 查看所有System V IPC对象(共享内存、信号量、消息队列)ipcs -m# 仅查看共享内存ipcrm -m shmid# 删除指定shmid的共享内存(强制清理残留)2.2 共享内存实战代码解析
以下两个示例实现“共享内存写进程”和“共享内存读进程”,完成进程间字符串传递。
示例1:共享内存写进程(20shm_w.c)
功能
生成key→申请共享内存→映射→写入字符串“hello”→撤销映射。
代码
#include<sys/types.h>#include<sys/ipc.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/shm.h>intmain(intargc,char*argv[]){// 1. 生成key(路径./,项目ID为'!')key_tkey=ftok("./",'!');if(-1==key){perror("ftok");return1;}printf("key: 0x%x\n",key);// 2. 申请4096字节的共享内存(权限0666,不存在则创建)intshmid=shmget(key,4096,IPC_CREAT|0666);if(-1==shmid){perror("shmget");return1;}printf("shmid is %d\n",shmid);// 3. 映射共享内存(内核自动分配地址,读写权限)void*p=shmat(shmid,NULL,0);// 原代码!SHM_RDONLY等价于0(读写)if((void*)-1==p){perror("shmat");return1;}// 4. 写入数据(memcpy适用于任意二进制数据,strcpy适用于字符串)charbuf[1024]="hello";memcpy(p,buf,strlen(buf)+1);// +1 包含字符串结束符'\0'// 5. 撤销映射shmdt(p);return0;}示例2:共享内存读进程(20shm_r.c)
功能
生成相同key→获取已存在的共享内存→映射→读取数据→撤销映射(可选删除)。
代码
#include<sys/types.h>#include<sys/ipc.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<string.h>#include<sys/shm.h>intmain(intargc,char*argv[]){// 1. 生成与写进程相同的key(路径和项目ID必须一致)key_tkey=ftok("./",'!');if(-1==key){perror("ftok");return1;}printf("key: 0x%x\n",key);// 2. 获取已存在的共享内存(大小和权限需与写进程一致)intshmid=shmget(key,4096,IPC_CREAT|0666);if(-1==shmid){perror("shmget");return1;}printf("shmid is %d\n",shmid);// 3. 映射共享内存(读写权限)void*p=shmat(shmid,NULL,0);if((void*)-1==p){perror("shmat");return1;}// 4. 读取数据(直接访问映射地址)printf("mem:%s\n",(char*)p);// 5. 撤销映射shmdt(p);// 6. (可选)删除共享内存(通常由最后一个进程执行)// shmctl(shmid,IPC_RMID,NULL);return0;}编译&运行步骤
# 编译写进程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 -m12345(替换为实际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的正确性,避免操作错误对象。