35、Linux IPC进阶:信号与System V共享内存

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 共享内存编程步骤

共享内存的使用遵循固定流程,核心是"申请→映射→读写→撤销→删除":

  1. 生成唯一键值(key):通过ftok()函数,由文件路径和项目ID生成;

  2. 申请共享内存:通过shmget()函数,向内核申请指定大小的共享内存;

  3. 内存映射:通过shmat()函数,将内核共享内存映射到当前进程的用户空间;

  4. 读写操作:直接操作映射后的内存地址(如memcpy()strcpy());

  5. 撤销映射:通过shmdt()函数,断开进程与共享内存的映射关系;

  6. 删除共享内存:通过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机制各有侧重,实际开发中常协同使用:

  1. 信号:负责"异步通知"(如进程间状态同步、异常处理),适合传递简单控制信息,不适合传递大量数据;

  2. 共享内存:负责"高效数据共享",适合传递大量数据,但无同步机制,需信号/信号量辅助避免数据竞争。

核心学习要点:

  • 信号:掌握kill()发送信号、signal()注册自定义处理函数,理解SIGALRM、SIGCHLD等常用信号的应用场景;

  • 共享内存:牢记"key→shmget→shmat→读写→shmdt→shmctl"的固定流程,注意key一致性和资源清理;

  • 实操避坑:编译信号代码无需额外链接库,共享内存代码需包含完整头文件;测试时注意进程PID和共享内存ID的正确性,避免操作错误对象。

相关推荐
不爱吃糖的程序媛1 小时前
基于Ascend C开发的Vector算子模板库-ATVOSS 技术深度解读
人工智能·算法·机器学习
惊鸿一博1 小时前
Linux文件同步/镜像—rsync
linux·运维
守城小轩2 小时前
基于Chrome140的Quora账号自动化(关键词浏览)——脚本撰写(二)
运维·自动化·chrome devtools·浏览器自动化·浏览器开发
SunnyDays10112 小时前
Python 实现 PDF 文档压缩:完整指南
linux·开发语言·python
Cx330❀2 小时前
《C++ 动态规划》第001-002题:第N个泰波拉契数,三步问题
开发语言·c++·算法·动态规划
LYFlied2 小时前
【每日算法】LeetCode 114. 二叉树展开为链表:从树结构到线性结构的优雅转换
数据结构·算法·leetcode·链表·面试·职场和发展
金海境科技2 小时前
【服务器数据恢复】H3C华三Ceph分布式存储文件丢失数据恢复案例
服务器·经验分享·分布式·ceph
xinyu_Jina2 小时前
局域网文件传输:P2P应用层协议——元数据握手与数据通道的生命周期管理
数据库·asp.net·p2p
weixin_307779132 小时前
Jenkins Pipeline: Input Step插件详解与实践指南
运维·开发语言·自动化·jenkins·etl