简 述: 此处指 Linux 中系统内核发出的信号;而不是之前 Qt 学习的信号。

[TOC]


本文初发于 “偕臧的小站“,同步转载于此。


编程环境:

  💻: uos20 📎 gcc/g++ 8.3 📎 gdb8.0

  💻: MacOS 10.14 📎 gcc/g++ 9.2 📎 gdb8.3


信号初识:

  • 特点:

    • 简单
    • 携带信息量很少
    • 用在某个特定的场景中
  • 型号的状态:

    • 产生原因:
      1. 键盘 Ctrl + C
      2. 命令:kill
      3. 系统函数:kill()
      4. 软条件:定时器
      5. 硬件:段错误,除 0 错误
    • 未决状态 – 没有被处理
    • 递达状态 – 信号被处理了
  • 处理方式:

    • 忽略
    • 捕捉,然后自定义动作
    • 执行了默认动作
  • 信号的四要素:

    • 信号名、信号编号、信号默认动作、事件描述

    • “动作(Action)”栏 的 字母 有 下列 含义:

      A 缺省动作是结束进程.
      B 缺省动作是忽略这个信号.
      C 缺省动作是结束进程, 并且核心转储.
      D 缺省动作是停止进程.
      E 信号不能被捕获.
      F 信号不能被忽略.

      译注: 这里 “结束” 指 进程 终止 并 释放资源, “停止” 指 进程 停止 运行, 但是 资源 没有 释放,有可能 继续 运行。

  • 通过 man 文档查看信号

    • 执行 man 7 signal
    • 注意(man 手册中有写):SIGKILLSIGSTOP 这两个信号不能够被捕捉,阻塞,忽略的

    The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

  • 概念:阻塞信号集,未决信号集

    • 是在 PCB 中
    • 阻塞信号集:让信号处于一个未决的状态
    • 未决信号集:如果信号被阻塞了,该信号集会对阻塞的信号做记录
  • 图解进程产生和处理:


kill() 函数:

作用: 发射信号给指定进程、或者同组的信号。

int kill(pid_t pid, int sig);
  • 参数:

    • pid:
      • pid > 0; 发送信号给指定的进程
      • pid = 0; 发送信号给 调用 kill 函数进程属于同一个组的所有进程
      • pid = -1;如果用户拥有超级用户权限,则信号将被发送到所有进程
      • pid < -1;取 |pid| 发给对应进程组
    • sig:
      • 推荐使用完整的宏名称而非数字,在少数发行版下,可能指定宏对应的数值有变化
  • 返回值:

    • 成功: 0
    • 失败:-1(ID 非法,信号非法,普通信号杀 init进程等权限级别问题,设置 errno)

写一个小的例子验证 kill 信号的使用,子进程在 5S 后,通知系统内核发送 SIGKILL 给它的父进程,然后系统将父进程杀死。

int main(int argc, char *argv[])
{
    pid_t pid = fork();

    if (pid > 0) {
        while (true) {
            printf("this is a parent process = %d\n", getpid());
            sleep(1);
        }
    } else if (pid == 0) {
        sleep(5);
        kill(getppid(), SIGKILL);
    }
    
    return 0;
}

运行效果:


raise() 函数:

作用: 自己给自己发射信号。在单线程程序中,它相当于 kill(getpid(), sig);

在一个多线程程序中,它等同于 pthread_kill(pthread_self(), sig)。

int raise(int sig);

比较简单,但依旧写一个例子使用一下:子进程给机子发射终止进程的信号。然后父进程在回收子进程的时候,答应其死亡的信号值

int main(int argc, char *argv[])
{
    pid_t pid = fork();

    if (pid > 0) {
        int s;
        pid_t wpid = wait(&s);
        printf("this is a child process dide pid = %d\n", wpid);
            
        if (WIFSIGNALED(s))
            printf("dide by signal: %d\n", WTERMSIG(s));
    } else if (pid == 0) {
        raise(SIGINT); //给自己发射信号
    }
    
    return 0;
}

代码中 19 行,选中的 SIGINT 信号,默认动作是终止进程。其对应的事件为 当用户按下<ctrl + c>组合键时候,用户终端向正在运行中的 (由该终端启动的)程序 发出此信号。

运行截图:


abort() 函数:

作用: 给自己发送异常终止的信号。该函数没有参数和返回值,也永远不会调用失败。

void abort(void);

定时器:

alarm() 函数:

unsigned int alarm(unsigned int seconds);
  • 设置定时器(每一个进程只有一个定时器)

  • 使用的是自然定时法则;不受进程状态的影响

  • 当时间到达之后,函数发出一个信号 SIGALRM

    • SIGALRM – 调用 abort() 函数时候产生该信号 – 终止进程并产生 core 文件
  • 返回值:始终是返回上一次调用此函数,还剩些的时间。

  • 写一个例子:

    int main(int argc, char *argv[])
    {
        int ret = alarm(5);
        printf("ret = %d\n", ret);
    
        sleep(2);
        ret = alarm(10); //重新设定定时器,返回值是返回之前闹钟的剩余的时间
        printf("ret = %d\n", ret);
    
        while (true) {
            printf("-------test-----\n");
            sleep(1);
        }
        
        return 0;
    }
  • 分析:

    这里 ret = 0 ,是因为第一次调用,它的上一次的剩余时间为 0;然后 ret = 3 是因为 5 - 2 s,然后定时器被充重置了新的 10 s。故后面会打印 10 次输出语句,然后收到系统的终止信号,杀死本进程。因为是执行的 alarm() 的默认动作,为终止进程,且没有捕捉该函数发射的对应的信号,也没有定义自定义动作,所以本进程会被终止。

  • 运行结果:

分析程序运行的损耗:

实际耗时 == 用户 + 系统 + 损耗

  • 写一个例子,检测一下计算机在 1s 内可以计算多少个数字?

  • 代码如下:

    int main(int argc, char *argv[])
    {
        alarm(1);
    
        int n = 0;
        while (true) {
            printf("%d\n", n++);
        }
    
        return 0;
    }
  • 运行演示:

    分析:其中损耗文件来自文件 IO 操作

    输出到终端: 1.012 = 0.31 + 0.34 + 损耗 (约 70w)

    输出到文件: 1.012 = 0.93 + 0.07 + 损耗 (约 800w)

    //执行 time ./myAlarmCount(输出到终端)
    0.31s user 
    0.34s system 
    64%   cpu 
    1.012 total  (输出到 692977)
      
    //time ./myAlarmCount > file(输出到文本)
    0.93s user 
    0.07s system 
    98%   cpu 
    1.012 total  (输出到 8686215)

setitimer() 函数:

  • 作用: 定时器,并且实现周期性 定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

struct itimerval {
  struct timeval it_interval; /* 定时器的循环周期 */
  struct timeval it_value;    /* 第一次触发定时器的时间 */
};

struct timeval {
  time_t      tv_sec;         /* 秒 */
  suseconds_t tv_usec;        /* 微秒 */
};
  • 参数:

    • which:
      • ITIMER_REAL: 自然定时法,会发出信号 SIGALRM
      • ITIMER_VIRTUAL:虚拟定时法,只会计算 用户时间 ,对应信号 SIGVTALRM
      • ITIMER_PROF: 只计算 用户时间 + 系统时间 ,对应发射 ITIMER_VIRTUAL 信号
    • new_value:我们需要设置的参数,总的定时时间是 tv_sec + tv_usec 之和
    • old_value:传出参数,传出上一次定时器的设置,一般用不到,用 NULL
  • 写一个例子:

    int main(int argc, char *argv[])
    {
        itimerval time;
        // time.it_interval = 3;  //每隔 3s 一次循环定时
    
        //第一次触发定时器的时间为 5s + 3ms
        time.it_value.tv_sec = 5;     //5s 
        time.it_value.tv_usec = 3;    //3 ms
    
        setitimer(ITIMER_REAL, &time, NULL);
    
        while (true) {
            printf("-----printf()-----\n");
            sleep(1);
        }
        
        return 0;
    }
  • 运行效果:


下载地址:

14_signal

欢迎 star 和 fork 这个系列的 linux 学习,附学习由浅入深的目录。