嵌入式Hacker (es-hacker)

Embedded bsp developer enjoys thinking and hacking opensource and develop boards(NanoPi, LicheePi, RPi...)

0%

Linux系统编程-信号机制入门1

早,继续记录我的学习心得。

每日的微进步

人们很容易高估某个决定性时刻的重要性,也很容易低估每天进行微小改进的价值

以前我也以为大规模的成功需要大规模的行动,现在我不这么认为了。长期来看,由于复利效果,一点小小的改进就能产生惊人的变化。

还有一点值得注意的情况,大多数人有了家庭和子女后,并且现在国内盛行加班文化,很难载集中精力能抽出大块的时间进行学习了,部分还能坚持学习的人几乎都是以牺牲睡眠时间为代价的,我个人不太认为这种做法,我始终认为有更合理健康的方法能形成一个工作、生活、学习、娱乐的有效循环,或许 认识到微进步的重要性就是一个很好的开始 吧。

本文就是我的微进步,欢迎阅读。

以下是正文

写作目的:

  • 初步了解 Linux 信号机制

目录

1
一、快速入门
2
  1. 基本概念
3
  2. 入门实验
4
5
二、发送信号
6
  1. 如何发送信号
7
  2. 闹钟功能
8
  3. 入门实验
9
10
三、相关参考
11
12
四、欢迎关注我的公众号(嵌入式Hacker)

一、快速入门

信号有时被称为提供处理异步事件机制的软件中断,与硬件中断的相似之处在于打断了程序执行的正常流程,很多比较重要的应用程序都需处理信号。事件可以来自于系统外部,例如用户按下 Ctrl+C,或者来自程序或者内核的某些操作。作为一种进程间通信 (IPC) 的基本形式,进行可以给另一个进程发送信号。

信号很早就是 Unix 的一部分。随着时间的推移,信号有了很大的改进。比如在可靠性方面,之前的信号可能会出现丢失的情况。在功能方面,现在信号可以携带用户定义的附加信息。最初,不同的 Unix 系统对信号的修改,后来,POSIX 标准的到来挽救并且标准化了信号机制。

  • 用术语 raise 表示一个信号的产生,catch 表示接收到一个信号。

  • 事件的发生是异步的,程序对信号的处理也是异步的。

  • 信号可以被生成、捕获、响应或忽略。有两种信号不能被忽略: SIGKILL 和 SIGSTOP。不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。

1. 基础概念

信号类型:

1
$ man 7 signal
2
DESCRIPTION
3
   Standard signals
4
          First the signals described in the original POSIX.1-1990 standard.
5
6
       Signal     Value     Action   Comment
7
       ──────────────────────────────────────────────────────────────────────
8
       SIGHUP        1       Term    Hangup detected on controlling terminal
9
                                     or death of controlling process
10
       SIGINT        2       Term    Interrupt from keyboard
11
       SIGQUIT       3       Core    Quit from keyboard
12
       SIGILL        4       Core    Illegal Instruction
13
       SIGABRT       6       Core    Abort signal from abort(3)
14
       SIGFPE        8       Core    Floating point exception
15
       SIGKILL       9       Term    Kill signal
16
       SIGSEGV      11       Core    Invalid memory reference
17
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
18
                                     readers
19
       SIGALRM      14       Term    Timer signal from alarm(2)
20
       SIGTERM      15       Term    Termination signal
21
       SIGUSR1   30,10,16    Term    User-defined signal 1
22
       SIGUSR2   31,12,17    Term    User-defined signal 2
23
       SIGCHLD   20,17,18    Ign     Child stopped or terminated
24
25
       SIGCONT   19,18,25    Cont    Continue if stopped
26
       SIGSTOP   17,19,23    Stop    Stop process
27
       SIGTSTP   18,20,24    Stop    Stop typed at terminal
28
       SIGTTIN   21,21,26    Stop    Terminal input for background process
29
       SIGTTOU   22,22,27    Stop    Terminal output for background process
30
31
       The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
32
33
       Next the signals not in the POSIX.1-1990 standard but described in SUSv2 and POSIX.1-2001.
34
35
       Signal       Value     Action   Comment
36
       ────────────────────────────────────────────────────────────────────
37
       SIGBUS      10,7,10     Core    Bus error (bad memory access)
38
       SIGPOLL                 Term    Pollable event (Sys V).
39
                                       Synonym for SIGIO
40
       SIGPROF     27,27,29    Term    Profiling timer expired
41
       SIGSYS      12,31,12    Core    Bad argument to routine (SVr4)
42
       SIGTRAP        5        Core    Trace/breakpoint trap
43
       SIGURG      16,23,21    Ign     Urgent condition on socket (4.2BSD)
44
       SIGVTALRM   26,26,28    Term    Virtual alarm clock (4.2BSD)
45
       SIGXCPU     24,24,30    Core    CPU time limit exceeded (4.2BSD)
46
       SIGXFSZ     25,25,31    Core    File size limit exceeded (4.2BSD)
47
48
        ...
49
50
       Next various other signals.
51
52
       Signal       Value     Action   Comment
53
       ────────────────────────────────────────────────────────────────────
54
       SIGIOT         6        Core    IOT trap. A synonym for SIGABRT
55
       SIGEMT       7,-,7      Term
56
       SIGSTKFLT    -,16,-     Term    Stack fault on coprocessor (unused)
57
       SIGIO       23,29,22    Term    I/O now possible (4.2BSD)
58
       SIGCLD       -,-,18     Ign     A synonym for SIGCHLD
59
       SIGPWR      29,30,19    Term    Power failure (System V)
60
       SIGINFO      29,-,-             A synonym for SIGPWR
61
       SIGLOST      -,-,-      Term    File lock lost (unused)
62
       SIGWINCH    28,28,20    Ign     Window resize signal (4.3BSD, Sun)
63
       SIGUNUSED    -,31,-     Core    Synonymous with SIGSYS
64
65
       (Signal 29 is SIGINFO / SIGPWR on an alpha but SIGLOST on a sparc.)

发送信号:

  • 如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要使用kill 命令。

  • kill 命令有一个有用的变体叫 killall,它可以给运行着某一命令的所有进程发送信号。

处理信号:
Unix 系统提供了两种方法来改变信号处置:signal() 和 sigaction()。signal()系统调用是设置信号处置的原始 API,所提供的接口比sigaction() 简单。另一方面,sigaction() 提供了 signal() 所不具备的功能。进一步而言,signal() 的行为在不同 Unix 实现间存在差异,这意味着对可移植性有所追求的程序绝不能使用此调用来建立信号处理函数 (signal handler)。故此,sigaction()是建立信号处理器的首选API。

由于可能会在许多老程序中看到 signal() 的应用,我们先了解如何用 signal() 函数来处理信号。

signal() 的定义:

1
$ man 2 signal
2
#include <signal.h>
3
typedef void (*sighandler_t)(int);
4
sighandler_t signal(int signum, sighandler_t handler);
  • 参数1 signum 指定希望修改 handler 的信号编号,参数2 handler,则指定信号抵达时所调用的 signal handler 函数的地址。

  • 成功,返回以前的信号处理函数;出错,返回 SIG_ERR;

2. 入门实验

简单试用 signal()。

分解代码:

1
static void ouch(int sig) {
2
    printf("OUCH! - I got signal %d\n", sig);
3
    (void) signal(SIGINT, SIG_DFL);
4
}
1
int main() {
2
    (void) signal(SIGINT, ouch);
3
4
    while(1) {
5
        printf("Hello World!\n");
6
        sleep(1);
7
    }
8
}

运行效果:

1
$ ./ctrlc1 
2
Hello World!
3
Hello World!
4
^COUCH! - I got signal 2
5
Hello World!
6
Hello World!

相关要点:

  • 在信号处理函数中,调用如 printf 这样的函数是不安全的。一般的做法是:在信号处理函数中设置一个标志,然后在主程序中检查该标志,如需要就打印一条消息。

  • 如果想保留信号处理函数,让它继续响应用户的 Ctrl+C 组合键,我们就需要再次调用 signal 函数来重新建立它。这会使信号在一段时间内无法得到处理,这段时间从调用中断函数开始,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行。

  • 不推荐使用 signal 接口。之所以介绍它,是因为可能会在许多老程序中看到它的应用。更清晰、执行更可靠的函数: sigaction(),在所有的新程序中都应该使用这个函数,暂不做深入介绍。

二、发送信号

1. 如何发送信号

进程可以通过调用 kill 函数向包括它本身在内的其他进程发送一个信号。

kill():

1
$ man 2 kill
2
#include <sys/types.h>
3
#include <signal.h>
4
int kill(pid_t pid, int sig);

把参数 sig 给指定的信号发送给由参数 pid 指定的进程号所指定的进程。

kill 调用会在失败时返回 -1 并设置 errno 变量,失败的原因:

  • 给定的信号无效(errno设置为EINVAL);

  • 发送进程权限不够(errno设置为EPERM);

  • 目标进程不存在(errno设置为ESRCH);

关于权限:
要想发送一个信号,发送进程必须拥有相应的权限,包括2种情况:

  • 两个进程必须拥有相同的用户 ID,即你只能发送信号给属于自己的进程;

  • 超级用户可以发送信号给任何进程;

2. 闹钟功能

进程可以通过调用 alarm() 函数在经过预定时间后发送一个 SIGALRM 信号。

alarm():

1
$ man 2 alarm
2
#include <unistd.h>
3
unsigned int alarm(unsigned int seconds);
  • 在 seconds 秒之后发送一个 SIGALRM 信号。

  • 返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回 -1。

相关要点:

  • 由于处理的延时和时间调度的不确定性,实际闹钟时间将比预先安排的要稍微拖后一点儿。

  • 把参数 seconds 设置为 0 将取消所有已设置的闹钟请求。

  • 如果在接收到 SIGALRM 信号之前再次调用 alarm() 函数,则闹钟重新开始计时

  • 每个进程只能有一个闹钟时间。

3. 入门实验

用 kill() 模拟闹钟。

分解代码:
设置 signal handler:

1
int main()
2
{
3
    pid_t pid;
4
5
    printf("alarm application starting\n");
6
7
    pid = fork();
8
    switch(pid) {
9
    case -1:
10
      /* Failure */
11
      perror("fork failed");
12
      exit(1);
13
    case 0:
14
      /* child */
15
        sleep(5);
16
        kill(getppid(), SIGALRM);
17
        exit(0);
18
    }
19
20
    /* parent */
21
    printf("waiting for alarm to go off\n");
22
    (void) signal(SIGALRM, ding);
23
24
    pause();
25
    if (alarm_fired)
26
        printf("Ding!\n");
27
28
    printf("done\n");
29
    exit(0);
30
}

定义 signal handler:

1
static int alarm_fired = 0;
2
static void ding(int sig)
3
{
4
    alarm_fired = 1;
5
}

通过 fork 调用启动新的进程:子进程休眠 5 秒后向其父进程发送一个 SIGALRM 信号。父进程在安排好捕获 SIGALRM 信号后暂停运行,直到接收到一个信号为止。

运行效果:

1
$ ./alarm 
2
alarm application starting
3
waiting for alarm to go off
4
<等待5 秒钟>
5
Ding!
6
done

相关要点:

  • pause() 把程序的执行挂起直到有一个信号出现为止。使用信号并挂起程序的执行是 Unix 程序设计中的一个重要部分。

    1
    $ man 2 pause
    2
    #include <unistd.h>
    3
    int pause(void);
  • 当它被一个信号中断时,将返回 -1(如果下一个接收到的信号没有导致程序终止的话)并把 errno 设置为 EINTR。

  • 更常见的方法是使用 sigsuspend() 函数,暂不做介绍。

  • 在信号处理函数中没有调用 printf,而是通过设置标志,然后在main函数中检查该标志来完成消息的输出。

  • 如果信号出现在系统调用的执行过程中会怎么样?

    • 一般只需要考虑“慢”系统调用,例如从终端读数据,如果在这个系统调用等待数据时出现一个信号,它就会返回错误 EINTR。
      1
      $ man 3 errno
      2
      EINTR
      3
      Interrupted function call (POSIX.1); see signal(7).
  • 如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败。

  • 我们需要更健壮的信号接口:

    • 在编写程序中处理信号部分的代码时必须非常小心,因为在使用信号的程序中会出现各种各样的“竞态条件”。例如,如果想调用pause等待一个信号,可信号却出现在调用 pause() 之前,就会使程序无限期地等待一个不会发生的事件。

    • POSIX 标准推荐了一个更新和更健壮的信号编程接口:sigaction。

更多值得学习的知识点:

  • 信号的不可靠性;
  • 发送信号给进程组;
  • 信号的继承;
  • 信号与重入;
  • sigcation 接口;
  • 信号集;
  • 信号阻塞;
  • 发送带附加信息的信号和信号排队;
  • Linux 内核里信号处理和信号发送的实现;

三、相关参考

  • 《Unix 环境高级编程-第10章 信号》
  • 《Linux/Unix 系统编程手册-第20章 信号:基本概念》
  • 《Linux系统编程-第10章 信号》
  • 《Linux程序设计-第11章 进程和信号》
  • 《深入Linux内核架构 5.4.1信号》

四、欢迎关注我的公众号 (嵌入式Hacker)

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式Hacker,一起来学习吧,无论是 关注或转发 , 还是赏赐,都是对作者莫大的支持,谢谢 各位的大拇指,祝工作顺利,家庭和睦~

这是一篇有趣的文章吗?

欢迎关注我的其它发布渠道