用户态到内核态:Linux信号传递的九重门(一)

发布于:2025-05-12 ⋅ 阅读:(9) ⋅ 点赞:(0)

1. 信号的认识

1.1. 信号的特点

  • 异步通知:信号是异步的,发送信号的进程无需等待接收进程的响应。
  • 预定义事件:每个信号对应一个预定义的事件(如终止、中断、段错误等)。
  • 轻量级:信号不携带大量数据,仅通过编号(整数)标识不同事件。

1.2. 技术应用层面的信号

1.2.1. 一个样例

//test.cpp

#include<iostream>
#include<unistd.h>

int main()
{
    while(1)
    {
        std::cout<< "I am a signal!"<<std::endl;
        sleep(1);
    }
    return 0;
}

 ⽤户输⼊命令,在Shell下启动⼀个前台进程。⽤⼾按下 Ctrl+C ,这个键盘输⼊产⽣⼀个硬件中断,被OS获取,解释成信号,发送给⽬标前台进程。前台进程因为收到信号,进⽽引起进程退出。

1.2.2. 一个系统调用函数 

NAME
        signal - ANSI C signal handling
SYNOPSIS
        # include <signal.h>
        typedef void (* sighandler_t )( int );
        sighandler_t signal ( int signum, sighandler_t handler);
参数说明:
signum :信号编号 [ 后⾯解释,只需要知道是数字即可。
handler :函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏ handler方法。

⽽其实, Ctrl+C 的本质是向前台进程发送 SIGINT 2 号信号,我们证明⼀下,这⾥需要引⼊⼀

个系统调⽤函数。
Test1.cpp
#include<iostream>
#include<unistd.h>
#include<signal.h>

void handler(int signum)
{
    std::cout<<"我是"<<signum<<"号信号!"<<std::endl;
}

int main()
{
    signal(2, handler);
    while(1)
    {
        std::cout<< "I am a signal! My pid is:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

执行结果:

1.3. 信号概念

信号是进程之间事件异步通知的⼀种⽅式,属于软中断。

1.3.1. 查看信号

编号34以上的是实时信号,此博客不讨论实时信号。这些信号各⾃在什么条件下产⽣,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal 

 1.3.2. 信号处理

可选的处理动作有以下三种:忽略信号,自定义处理信号,默认处理。

  • 忽略信号

        Test2.cpp

#include<iostream>
#include<unistd.h>
#include<signal.h>

int main()
{
    //设置2号信号的处理方式时SIG_IGN(忽略指定信号)宏
    signal(2, SIG_IGN);
    while(1)
    {
        std::cout<< "I am a signal! My pid is:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

 运行结果:无论输入多少次的2号信号CTRL+C进程都没有终止。

  • 默认处理

        Test3.cpp

#include<iostream>
#include<unistd.h>
#include<signal.h>

int main()
{
    while(1)
    {
        std::cout<< "I am a signal! My pid is:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:进程直接终止

  • 自定义处理       

        Test4.cpp

#include<iostream>
#include<unistd.h>
#include<signal.h>

void handler(int signum)
{
    std::cout<<"我是"<<signum<<"号信号!"<<std::endl;
}

int main()
{
    signal(2, handler);
    while(1)
    {
        std::cout<< "I am a signal! My pid is:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

运行结果,进程接受2号信号之后执行自定义方法

2. 产生信号

2.1. 通过命令行输入产生信号

  • Ctrl+C (SIGINT) 已经验证过,这⾥不再重复。 
  • Ctrl+\(SIGQUIT)可以发送终⽌信号并⽣成core dump⽂件,⽤于事后调试。
  • Ctrl+Z(SIGTSTP)可以发送停⽌信号,将当前前台进程挂起到后台等。

2.2. 调⽤系统命令向进程发信号

测试代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>

int main()
{
    while(true)
    {
        std::cout<<"I am waiting signal, My pid is:"<<getpid()<<std::endl;
        sleep(1);
    }
    return 0;
}

 程序正常运行,我们在另外的终端输入kill -9 杀死正在运行的进程。

2.3. 使用函数产生信号

2.3.1. kill

kill 命令是调⽤ kill 函数实现的。 kill 函数可以给⼀个指定的进程发送指定的信号。

NAME
        kill - send signal to a process
SYNOPSIS
        # include <sys/types.h>
        # include <signal.h>
        int kill ( pid_t pid, int sig);
RETURN VALUE
        On success (at least one signal was sent), zero is returned. On error,  -1 is returned, and errno is set appropriately.

 自定义实现kill命令

#include<iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>

// mykill -signumber pid
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        std::cout<<"There is an error in your input format. Please re-enter it! "<<std::endl;
        exit(1);
    }

    int signum = std::stoi(argv[1]+1);
    int pid = std::stoi(argv[2]);

    int n = kill(pid, signum);
    return 0;
}

2.3.2 raise

raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。
NAME
        raise - send a signal to the caller
SYNOPSIS
        # include <signal.h>
        int raise ( int sig);
RETURN VALUE
        raise () returns 0 on success, and nonzero for failure.

测试代码:

#include <iostream>
#include <signal.h>
#include <unistd.h>

int main()
{
    int cnt = 0;
    while (true)
    {
        cnt++;
        std::cout << "I am waiting signals! My pid is:" << getpid() << std::endl;
        sleep(1);

        if (cnt == 6)
        {
            raise(2);
        }
    }
    return 0;
}

运行结果:程序执行6条cout语句之后自己给自己发送2号信号

 2.3.3. abort

abort 函数使当前进程接收到信号⽽异常终⽌。
NAME
        abort - cause abnormal process termination
SYNOPSIS
        # include <stdlib.h>
        void abort ( void );
RETURN VALUE
        The abort () function never returns.
// 就像 exit 函数⼀样 ,abort 函数总是会成功的 , 所以没有返回值

测试代码:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void handler(int signumber)
{

    std::cout << "获取了⼀个信号: " << signumber << std::endl;
}


int main()
{
    signal(SIGABRT, handler);
    while (true)
    {
        sleep(1);
        abort();
    }
}

运行结果:

2.4. 由软条件产生信号

SIGPIPE 是⼀种由软件条件产⽣的信号,在“管道”中已经介绍过了。本节主要介绍 alarm 函数和 SIGALRM 信号。

NAME
        alarm - set an alarm clock for delivery of a signal
SYNOPSIS
        # include <unistd.h>
        unsigned int alarm ( unsigned int seconds);
RETURN VALUE
        alarm () returns the number of seconds remaining until any previously
        scheduled alarm was due to be delivered, or zero if there was no previ
        ously scheduled alarm.

调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发 送SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。

2.4.1. 通过alarm验证体会IO效率

程序的作⽤是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终⽌。
必要的时候,对SIGALRM信号进⾏捕捉。
alarm1.cpp(IO多)
#include <iostream>
#include <unistd.h>
#include <signal.h>

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

alarm2.cpp(IO少)

#include <iostream>
#include <unistd.h>
#include <signal.h>

int count = 0;

void handler(int signumber)
{
    std::cout << "count : " << count << std::endl;
    exit(0);
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1);
    while (true)
    {
        count++;
    }
    return 0;
}

2.4.2. 如何理解软件条件

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进⾏相应的处理。简⽽⾔之,软件条件是因操作系统内部或外部软件操作⽽触发的信号产⽣。

 2.5. 硬件异常产生信号

硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。

2.5.1. 模拟除0

测试代码:

#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    // signal(SIGFPE, handler); // 8) SIGFPE
    sleep(1);
    int a = 10;
    a /= 0;
    while (1);
    return 0;
}

运行结果:

2.5.2. 模拟野指针

测试代码:

#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    // signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
    while (1)
        ;
    return 0;
}

运行结果:


网站公告

今日签到

点亮在社区的每一天
去签到