学习笔记9

知识点归纳

第6章 信号和信号处理

信号和中断

  • “中断”是从I/O设备或协处理器发送到CPU的外部请求,它将CPU从正常执行转移 到中断处理。与发送给CPU的中断请求一样,“信号”是发送给进程的请求,将进程从正常执行转移到中断处理。

  • “中断”是发送给“进程”的事件,它将“进程”从正常活动转移到其他活动,称为“中断处理”。“进程”可在完成“中断”处理后恢复正常活动。根据来源,中断可分为三类:

    1.来自硬件的中断:终端、间隔定时器的“Ctrl+C”组合键等。

    2.来自其他人的中断:kill(pid,SIG#), death_of_child等。

    3.自己造成的中断:除以0、无效地址等。

  • 每个进程中断都被转换为一个唯一ID号,发送给进程。与多种类的人员中断不同,我们始终可限制在一个进程中的中断的数量。Unix/Linux中的进程中断称为信号,编号为1到31。进程的PROC结构体中有对应每个信号的动作函数,进程可在收到信号后执行该动作函数。与人员类似,进程也可屏蔽某些类型的信号,以推迟处理。必要时,进程还可能会修改信号动作函数。

Unix/Linux信号示例

  • 按“Ctrl+C”组合键通常会导致当前运行的进程终止。原因如下:Ctrl+C组合键会生成一个键盘硬件中断。键盘中断处理程序将Ctrl+C组合键转换为SIGINT(2)信号,发送给终端上的所有进程,并唤醒等待键盘输入的进程。在内核模式下,每个进程都要检查和处理未完成的信号。进程对大多数信号的默认操作是调用内核的kexit(exitValue)函数来终止。在Linux中,exitValue的低位字节是导致进程终止的信号编号。
  • 用户可使用nohup a.out &命令在后台运行一个程序。即使在用户退出后,进程仍将继续运行。nobup命令会使sh像往常一样复刻子进程来执行程序,但是子进程会忽略SIGHuP(1)信号。当用户退出时,sh会向与终端有关的所有进程发送一个SIGHUP信号。后台进程在接收到这一信号后,会忽略它并继续运行。为防止后台进程使用终端进行I/O,后台进程通常会断开与终端的连接(通过将其文件描述符0、1、2重定向到/dev/null),使其完全不受任何面向终端信号的影响。
  • 用户再次登录时也许会发现(通过ps-u LTD)后台进程仍在运行。用户可以使用sh命令kill pid (or kill -s 9 pid)杀死该进程。之所以是9个信号是因为在最初的Unix中,只有9个信号。9号信号被保留为终止进程的终极手段。虽然后来的Unix/Linux系统将信号编号扩展到了31,但是信号编号9的含义仍然保留了下来。

Unix/Linux中的信号处理

  • Unix/Linux支持的31种信号,在signal.h文件中均有定义,每种信号都有一个符号名。
  • 来自硬件中断的信号:在进程执行过程中,一些硬件中断被转换为信号发送给进程。硬件信号示例是中断键(CtrI+C),它产生一个SIGINT(2)信号。
    间隔定时器,当它的时间到期时,会生成一个SIGALRM( 14),SIGVTALRM(26)或SIGPROF (27)信号。
  • 其他硬件错误,如总线错误、IO陷阱等。
  • 来自异常的信号:当用户模式下的进程遇到异常时,会陷入内核模式,生成一个信号,并发送给自己。常见的陷阱信号有SIGFPE(8),表示浮点异常(除以0),最常见也是最可怕的是SIGSEGV(11),表示段错误,等等。
  • 来自其他进程的信号:进程可使用kill(pid, sig)系统调用向pid标识的目标进程发送信号。读者可以尝试以下实验。在 Linux 下,运行简单的C程序main(){ while(1); }使进程无限循环。从另一个(X-window)终端,使用ps -u查找循环进程pid。然后输入sh命令kill -s 11 pid循环进程会因为段错误而死亡。当某进程被某个信号终止时,它的exitValue就包含这个信号编号。父进程sh只是将死亡子进程的信号编号转换为一个错误字符串。
  • Unix/Linux支持31种不同的信号,每种信号在 signal.h文件中都有定义:

信号处理步骤

  • 当某进程处于内核模式时,会检查信号并处理未完成的信号。如果某信号有用户安装的捕捉函数,该进程会先清除信号,获取捕捉函数地址,对于大多数陷阱信号,则将已安装的捕捉函数重置为 DEFault。然后,它会在用户模式下返回,以执行捕捉函数,以这种方式篡改返回路径。当捕捉函数结束时,它会返回到最初的中断点,即它最后进入内核模式的地方。
  • 重置用户安装的信号捕捉函数:用户安装的陷阱相关信号捕捉函数用于处理用户代码中的陷阱错误。由于捕捉函数也在用户模式下执行,因此可能会再次出现同样的错误。如果是这样,该进程最终会陷入无限循环,一直在用户模式和内核模式之间跳跃。为了防止这种情况,Unix 内核通常会在允许进程执行捕捉函数之前先将处理函数重置为 DEFault。这意味着用户安装的捕捉函数只对首次出现的信号有效。
  • 信号和唤醒:在Unix/Linux,内核中有两种 SLEEP进程;深度休眠进程和浅度休眠进程。前一种进程不可中断,而后一种进程可由信号中断。如果某进程处于不可中断的SLEEP 状态,到达的信号(必须来自硬件中断或其他进程)不会唤醒进程。如果它处于可中断的SLEEP状态,到达的信号将会唤醒它。

苏格拉底挑战

遇到的问题与解决方案

1. 信号竞态条件

问题: 信号是异步的,可能在任何时候发生。在处理信号时可能会引发竞态条件,导致不可预测的行为。

解决方案: 使用信号安全的函数,如sigaction,来注册信号处理函数。避免在信号处理函数中使用非可重入函数。

2. 信号丢失

问题: 如果进程频繁地接收信号,而且信号的发送速度比进程处理的速度还快,就可能发生信号丢失。

解决方案: 使用可靠信号,如SIGRTMINSIGRTMAX 范围内的信号,这些信号不会排队,而是排他性地发送。

3. 中断系统调用

问题: 有些系统调用是可中断的,即它们在接收到信号时可能会提前返回。

解决方案: 在系统调用返回时检查errno,如果为EINTR,则重新调用系统调用。

4. 全局变量问题

问题: 信号处理函数与主程序共享相同的地址空间,可能会访问相同的全局变量,导致竞态条件。

解决方案: 使用volatile sig_atomic_t类型的变量,确保在信号处理函数和主程序之间对该变量的访问是原子的。

5. 阻塞信号导致死锁

问题: 如果信号被阻塞,而信号处理函数又尝试执行一个被阻塞的操作,可能导致死锁。

解决方案: 避免在信号处理函数中执行可能被当前阻塞的操作。考虑在信号处理函数中仅设置标志,由主程序来处理。

6. 可重入性问题

问题: 信号处理函数必须是可重入的,因为它可能在任何时候中断正在进行的代码。

解决方案: 尽量避免在信号处理函数中使用全局变量。如果必须使用,确保对它们的访问是原子的。

实践过程



posted @ 2023-11-11 23:16  LLLZTTT  阅读(23)  评论(0)    收藏  举报
$