Linux系统:进程信号的产生

系列文章目录


前言

学习进程的信号是因为信号是 Linux 中 进程和内核之间异步通信的重要机制。它可以用来控制进程(如 Ctrl+C 发送 SIGINT 终止程序)、实现进程间事件通知(如子进程退出触发 SIGCHLD)、支持定时器和调试(如 alarm()gdb 的断点),并帮助程序优雅处理错误和安全退出(如捕获 SIGSEGVSIGTERM)。掌握信号能让我们理解操作系统如何管理进程,写出健壮的系统级程序,并为并发与网络编程打下基础


一,进程信号是什么?

信号(signal)是 软件层面的中断。它让操作系统或一个进程,可以 异步 地通知另一个进程“发生了某个事件”。
信号会打断进程的正常执行,转去执行 信号处理函数,类似于硬件中断打断 CPU 执行程序。

  • 简单的信号
int main()
{
    while(true)
    {
        cout<<"I am a process"<<endl;
        sleep(1);
    }
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am a process
I am a process
I am a process
I am a process
I am a process
^C

这里我们设置了一个死循环函数,每隔一秒打印一次信息,但是当我们按住键盘Ctrl+c后进程的循环就断开了,这就是所谓的信号。我们用键盘给进程发送了一个Ctrl+c的信号,而该信号对应的执行函数是让这个进程的执行中断。


  • 配置信号的系统函数signal

函数原型:

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
  • signal 是函数名
  • 它的第一个参数:
    • int signum → 信号编号,比如 SIGINT (2)、SIGTERM (15)
  • 它的第二个参数:
    • void (*handler)(int) → 一个函数指针,指向信号处理函数
    • 这个函数必须接受一个 int 参数(信号编号),返回 void
  • 返回值:
    • signal 返回一个 函数指针,指向原来处理该信号的函数
    • 这样你就可以在需要时恢复旧的处理函数

这样可能不太好理解,尤其是对一些同学来讲,连什么是函数指针都不懂换一种写法,将返回值和函数原型拆开来看

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

这个sighandler_t就是一个函数指针指向了一个返回值为void,函数参数只有一个int类型参数的自定义类型

当我们在Ctrl+c给进程发送信号后,进程接收到了这个信号然后执行了这个信号对应的函数,这个函数是一个能够中止进程的执行的函数,那我们设想一下,如果用signal重新定义Ctrl+c会发生什么

void reaction(int signals)
{
    cout<<"I am a process and I received a signal: "<<signals<<endl;
}
int main()
{
    while(true)
    {
        signal(SIGINT,reaction);
        cout<<"I am a process"<<endl;
        sleep(1);
    }
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process

这里的SIGINT是一个宏,对应的数值是2,当我们按Ctrl+c时其实就是向进程发送一个2的信号,然后进程再执行对应的函数,但是这里我们将函数改成我们自己编写的函数,所以当我们Ctrl+c后并不会中止进程,而是执行我们自己的函数reaction,这里的reaction的执行并不会影响到主函数的执行,所以我们的信号的执行对于进程来讲是异步


  • 前台进程和后台进程

我们使用键盘向进程发送信号是发送给前台进程的信号,我们再执行进程时,默认是前端进程,再执行命令后面加&则成为后台进程

  • 前台进程
    • 指 占据终端,直接与用户交互的进程
    • 用户在终端输入命令,输出会直接显示在终端
    • 前台进程会 独占当前终端,直到执行完毕或被中断
./a.out

此时 a.out 就是前台进程,它会运行并在屏幕上输出内容。如果它不结束,你就不能在这个终端继续输入新命令。

  • 后台进程
    • 不占用终端输入 的进程,可以在后台运行
    • 通过在命令后加 & 启动
    • 后台进程仍然可以输出到终端,但不会阻塞你在终端输入新命令
./a.out &

此时 a.out 在后台运行,你还能继续在终端输入别的命令

  • 前后台切换
    在 Linux 里,可以通过 作业控制 (job control) 来切换:
    • 暂停前台进程:Ctrl+Z → 把进程挂起(T 状态)
    • 转后台继续运行:bg %job_number
    • 转前台运行:fg %job_number
./a.out        # 启动前台
^Z             # Ctrl+Z 暂停
bg %1          # 把作业 1 放到后台运行
fg %1          # 把作业 1 拉回前台

  • 信号的概念

通过kill -l命令查看所有信号类型

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	
  • 标准信号
    • 范围:1–31
    • 特点:不排队,同一种信号在未处理前,只会记录“来过一次”,不会计数,响应速度快,但可能丢失信号
  • 实时信号
    • 范围:34-64
    • 特点:可排队,多个相同信号不会丢失,按顺序进入队列,这篇文章我们并不讲解实时信号

Linux 没有 3233 号信号,是因为它们被 glibc 保留给线程库使用,不对用户进程开放。


二,信号的产生

2-1,普通信号

  • 通过键盘发送信号
void reaction(int signals)
{
    cout<<"I am a process and I received a signal: "<<signals<<endl;
}
int main()
{
    while(true)
    {
        signal(SIGINT,reaction);
        cout<<"I am a process"<<endl;
        sleep(1);
    }
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process

当我们按Ctrl+c就是向前台进程发送一个SIGINT信号


  • 调用系统命令向进程发信号
kill -信号类型 进程PID
int main()
{
    while(true)
    {
        cout << "Running..." << endl;
        sleep(1);
    }
}
root@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2# ps axj | head -1 && ps axj | grep exe
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
   2149    2418    2417    2149 pts/2       2417 S+       0   0:00 grep --color=auto exe
root@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2# kill -SIGSEGV 2418

运行进程后,查找进程的PID并且用kill发送信号中止进程


  • 使用函数产生信号

使用函数产生信号一般要用到三个函数killraiseabort

  • kill
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

pid:指定要发送信号的目标进程或进程组

sig:要发送的信号编号

演示代码:

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        cout<<"argc need ==3";
        return 1;
    }
    int num=stoi(argv[1]+1);
    pid_t pd=stoi(argv[2]);
    kill(pd,num);
    return 0;
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ g++ -o exe code4.cc
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe -2 PID

这里我们仿写了一个kill指令,主要用到kill函数指定PID和传递信号,和stoi将字符串转化为int类型数据


  • raise

raise 用来 给调用它的进程自己发送一个信号。
函数原型

#include <signal.h>
int raise(int sig);

sig:要发送的信号编号

演示代码

void message(int sig)
{
    cout<<"receive signal "<<sig<<endl;
}
int main()
{
    signal(3,message);
    while(true)
    {
        raise(3);
        sleep(2);
    }
    return 0;
}

演示结果

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
receive signal 3
receive signal 3
...

这里raise每隔两秒会给自己进程发送一个3信号,每次进程捕捉到了3信号都会执行message函数


  • abort

函数原型

#include <stdlib.h>
void abort(void);

abort() 用于 异常终止当前进程,它会向自己发送 SIGABRT 信号,这个信号默认会让进程终止,并生成 core dump 文件,abort() 没有返回值,调用它后进程一定会结束

演示代码

void handler(int sig)
{
    cout<<"receive abort signal "<<sig<<endl;
}
int main()
{
    signal(SIGABRT,handler);
    while(true)
    {
        abort();
        sleep(2);
    }
}

演示结果

root@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2# ./exe
receive abort signal 6
Aborted (core dumped)

这里就是注意一下abort向进程发送的信号是6(SIGABRT)即可


  • alarm

alarm用于定时给自己的进程发送一个SIGALRM信号
函数原型

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

seconds:用于设置多少秒发送信号
演示代码:

int count=0;
void handler(int sig)
{
    cout<<"I am "<<sig<<endl;
    exit(0);
}
int main()
{
    signal(SIGALRM,handler);
    alarm(1);
    while(true)
    {
        count++;
        cout<<"count:"<<count<<endl;       
    }
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
...
count:42447
count:42448
count:42449
I am 14

alarm发送信号后,进程接收到信号,如果不定义信号,会默认终止我们的进程,演示代码:

int main()
{
    int count=0;
    alarm(1);
    while(true)
    {
        count++;
        cout<<"count:"<<count<<endl;
    }
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
...
count:136821
count:136822
count:136823
Alarm clock
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ 

这里我们设置了一个alarm,隔一秒发送一个SIGALRM信号给自己进程,进程收到信号后中止。


2-2 硬件异常产生信号

当程序运行时,如果硬件发现了某些错误,就会通过特殊的机制把这个问题告诉操作系统。操作系统接收到后,会把它翻译成一个信号,发给当前正在运行的进程。

如果程序里做了“除以 0”的操作,CPU 的运算单元会立刻报错,操作系统就会把这个错误转换成一个 SIGFPE 信号发给进程。

再比如,程序访问了一个不属于它的内存地址,内存管理单元(MMU)会触发异常,操作系统就会把这个错误变成一个 SIGSEGV 信号发给进程

模拟除以0
演示代码:

void handler(int sig)
{
    cout<<"I am "<<sig<<endl;
    exit(0);
}
int main()
{
    signal(SIGFPE,handler);
    int a=1,b=0;
    cout<<a/b<<endl;
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am 8

模拟野指针
演示代码:

void handler(int sig)
{
    cout<<"I am "<<sig<<endl;
    exit(0);
}
int main()
{
    signal(SIGFPE,handler);
    int* ptr=nullptr;
    *ptr=10;
    while(1)
    {

    }
    return 0;
}

演示结果:

gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
Segmentation fault (core dumped)

2-3 core dump

core dump(中文常叫“核心转储”)是操作系统在进程 异常终止 时,把该进程的内存内容、寄存器状态、调用栈等调试信息保存到一个文件里的过程

一般产生core dump的场景有除零错误(SIGFPE)访问非法内存(SIGSEGV)调用 abort()(SIGABRT)进程因为 未处理的致命信号 崩溃时

主要用于 程序调试和问题定位,通过 gdb 打开 core 文件,可以查看崩溃时进程在哪个函数,哪一行代码出错,变量、内存的值是多少,调用栈的情况。

生成core dump
Linux 默认可能限制 core dump 文件大小,需要先打开:

ulimit -c unlimited

运行一个会崩溃的程序

using namespace std;
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
    
    int a=1,b=0;
    cout<<a/b<<endl;
    return 0;
}

总结

Linux/系统中产生信号的方式主要有 三种:

  • 硬件产生的信号(硬件异常):CPU 或硬件检测到进程执行了非法操作。由硬件异常触发,内核捕获后发送给进程

    • 异常类型:除以 0(SIGFPE),指针/非法内存访问(SIGSEGV),非对齐访问/总线错误 (SIGBUS),非法指令 (SIGILL
  • 软件产生的信号(程序/系统调用触发):程序调用库函数或系统调用来发送信号

    • 常见例子:raisekillabortalarm
  • 用户交互产生的信号:用户在终端操作触发信号。

    • 常见例子:Ctrl+CCtrl+\Ctrl+Z
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值