Skip to content

信号定义

操作系统内核区响应了某些条件而产生一个事件,操作系统可以把该事件发给进程。

对于这个进程来说,这个事件就是信号。进程在接收到信号后会采取相应的动作。

优缺点

信号优点:灵活,方便,快捷 信号缺点:只能传递简单的信息。

本质

就是软件层次对中断的模拟,signal机制可以被理解为进程的软中断,它是一种异步通信机制。

如果不理解软中断,异步通信等,也没关系,记住一句话就可以了。信号是linux内核给进程组发的简短信息。

举例

(1)假设用户启动了一个交互式的前台进程,然后通过ctrl+c结束它,系统通过键盘产生一个硬件中断。 (2)cpu 受硬件中断的影响暂停用户空间的代码, (3)操作系统将该硬件中断解释为一个 SIGINIT 信号,并将其记录在进程的 PCB 的信号位图上(信号位图就是一个 PCB 中表示信号的位图)。 (4)该进程收到信号后会返回内核区,在进程从内核区返回用户区继续执行之前,会检查 PCB 中的信号位图,就可以检测到 SIGINT 信号,SIGINT 信号默认处理动作为终止进程,所以就将进程直接终止了。

linux的所有信号

我们可以通过kill -l来查看linux的所有信号。

使用硬件发送信号

其实这一段已经说过了,大家多看几遍,和接下来的几种方法对比一下。

进程奔溃的本质

  • CPU内部有一个状态寄存器。如果CPU的计算过程出现了错误,错误信息就会被存储在这个状态寄存器中,CPU也会立马终止进程的代码。
  • 操作系统随时在监视CPU的状态,CPU的状态寄存器记录了异常,操作系统立马就会去给造成异常的进程发送信号,不同的信号包含了不同的错误信息,我们就看到了进程的报错信息。

调试方法

原理

信号之中都有一个core dump标志位。这个标志位的默认值为0,对于大多数进程终止信号,都会将该标志位设置为1,然后将进程的信息转存到core文件中,方便我们后期调试。

步骤

  • 查看core文件的大小上限,如果为0,表示不会生成core文件
bash
ulimit -c
  • core文件的大小设置为不限制
bash
ulimit -c unlimited
  • 生成一个可调试的执行文件
bash
g++ -g xxx.cpp -o xxx

或者 CMakeLists.txt 中则添加set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")

  • 执行该可执行文件,发生段错误,生成core文件
bash
./xxx
  • 使用gdb调试
bash
gdb ./xxx core.xx

没有core文件生成

core文件是由core文件的配置文件 (/proc/sys/kernel/core_pattern) 控制生成的。

该配置文件可以设置pid,uid等属性,有兴趣可以研究一下。

有时这个配置文件会设定为将内容重定向到某个文件中。该文件不能直接修改,标准的修改方法是:

bash
sudo bash -c "echo core > /proc/sys/kernel/core_pattern"

系统调用发送信号

cpp
int main(int argc, char* argv[]) {
    if(argc != c) {
        std::cout << "process needs a pid parament" << std::endl;
        return -1;
    }
    if (kill(atoi(argv[1]),SIGINT) == -1) {
        perror("kill func error");
        return -1;
    }
    return 0;
}

也可以直接使用kill命令发生信号,其实kill命令就是对kill函数的封装。

软条件中断

就是定时器或某些操作不满足而产生的中断。 注意区分一下软条件中断和进程异常的区别。软条件中断是软件需要我们中断,整个程序是没有错误的。而进程异常产生的信号就是完完全全的错误了。

(1) 最典型的软条件中断就是定时器了。也就是alarm函数 在现代C++程序中,我们的定时器一般都使用C++11的chrono库提供的计时方法

C++11标准的计时方法相比于传统的alarm函数优势还是很大的。无论是稳定性,还是可扩展性,灵活性,C++的chrono库都有优势。

这里就不演示alarm函数了,因为我们以后使用的都是chrono提供的计时功能。网络部分,就会讲chrono的。

(2) 还有就是一些操作条件不满足导致的信号了,比如管道读端已经退出了,还进行写操作,就会收到SIGPIPE的信号,因为这样的操作完全没有意义。

信号的接收处理

默认处理 每一种信号都有默认的处理方式。

cpp
// 这段代码相当于没写
int main() {
    if(signal(SIGINT,SIG_DFL) == SIG_ERR) {
        perror("signal func error");
        return -1;
    }
    return 0;
}

忽略处理 可以让进程忽略某些信号,注意SIGKILL和SIGSTOP信号不能被忽略,也就是9号(kill)和19号信号。

cpp
//该程序无法用 Ctrl+C 终止
int main() {
    if(signal(SIGINT,SIG_IGN) == SIG_ERR) {
        perror("signal func error");
        return -1;
    }
    while(1) {
        std::cout<< "hello world" <<std::endl;
        sleep(1);
    }
    return 0;
}

对信号进行捕捉 注意,SIGKILL和SIGSTOP信号不能被捕捉。

cpp
//按下Ctrl+C 输出catch SIGINT signal
void signal_handler_func(int signalNum) {
    std::cout<< "catch SIGINT signal" <<std::endl;
}

int main() {
    if(signal(SIGINT,signal_handler_func) == SIG_ERR) {
        perror("signal func error");
        return -1;
    }
    while(1) {
        std::cout<< "hello world" <<std::endl;
        sleep(1);
    }
    return 0;
}

总结 其实SIGKILL和SIGSTOP信号的特殊性很容易理解,如果这两个信号都可以被捕捉或忽略,那么进程就无法终止了。

在现代C++项目中,信号本身用的比较少了,所以用信号进行通信的方式也比较少了。在linux的c项目中,这些是绝对的核心。

因为我这一系列是以C++为项目核心的,所以信号的处理就到这里了,这些对于理解linux已经可以了。

信号集

用sigaction函数的 TODO