操作系统详解(5)——信号(Signal)

系列文章:
操作系统详解(1)------操作系统的作用
操作系统详解(2)------异常处理(Exception)
操作系统详解(3)------进程、并发和并行
操作系统详解(4)------进程控制(fork, waitpid, sleep, execve)

文章目录

概述

与Exception(异常处理)相比,signal是软件层面的,更高级的处理机制。signal能使当前的进程和kernel打断其它的进程。

低等级的 hardware exceptions:

  • processed by kernel's exception handlers
  • not normally be visible to user processes
  • Signals 提供了机制,能把exceptions的出现暴露给用户进程

高等级的 software events:

  • in the kernel
  • in other user processes

信号的种类

Hardware Events

  • SIGFPE signal (number 8)
    • process 尝试除0
    • kernel向其发送SIGFPR signal
  • SIGILL signal (number 4)
    • process 执行非法指令
    • kernel向其发送
  • SIGSEGV signal (number 11)
    • process 访问非法内存

Software Events

  • SIGINT signal (number 2)
    • 按ctrl-c,kernel对在foreground(bash前台)执行的进程,发送SIGINT signal
  • SIGKILL signal (number 9)
    • 一个进程可以向另一个进程发送SIGKILL signal
    • 来强制终止另一个进程
  • SIGCHLD signal (number 17)
    • 当子进程终止(terminate)或阻塞(stop)时
    • kernel向父进程发送SIGCHLD siganl

以下是linux系统中的一些信号:(可以通过>man signal查询)

信号的原理

信号发送

信号发送的两种方式:

  • kernel检测到的 system event
    • Divide by zero
    • Termination of a child process
    • ...
  • 一个进程调用kill function
    • 显式请求kernel向 destination process 发送signal
    • 进程可以向自身发送信号

以下是kill函数的定义:

c 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//returns: 0 if OK, -1 on error

发送signal后, kernel会更新destination process的上下文状态. 具体一点地来说, 每一个进程都有其维护的上下文.

![[进程#上下文内容]]

kernel会为每一个process维护:

  • pending bit vector
  • blocked bit vector
    (意思将在下面介绍)
    发送信号后, kernel会更新目标进程的pending bit vector, 并把相应的 信号位 置为1, 表示信号已经发送.

信号接收

目标进程收到信号,指的是kernel迫使它对向它发送的信号作出反应:

  • Ignore the signal
  • Terminate
  • Catch the signal
    • by executing a user-level function called a signal handler

Pendning Signal(等待的信号)

Pending signal 是已经发送但还没有接收到的信号

Attention:

  • Only one
    • 对一个进程而言, 每一种类型的 pending siganl 若存在仅有一个
  • Not queued
    • 如果一个进程已经有了编号为k的pending signal,则后续发送到该进程的signal k不会被排序
    • 意思是它们会直接被丢弃(忽视)
    • 一个pending signal 一次最多只会接收到一个(在处理信号之前)

Block a Signal

一个进程可以选择性地对特定的信号进行阻塞.

如果信号被阻塞, 意味着可以向它发送信号, 并把pending bit置为1, 但是pending signal不会被接收(不会执行相应的action), 直到process unblock 该信号.

信号的 Internal Data Structures

kernel维护每一个进程的:

  • the set of pending signals in the pending bit vector
  • the set of blocked signals in the blocked bit vector

The kernel sets bit k in pending

whenever a signal of type k is delivered

The kernel clears bit k in pending

whenever a signal of type k is received.

(当接收到该信号后清除pending bit)

信号的实现机制

进程组

process group

每一个进程都只属于一个process group

以一个正整数标识(process group ID, 即pgid)

默认情况下, 子进程和父进程属于同一个进程组.

c 复制代码
#include <unist.h>
pid_t getpgrp(void);
// returns: process group ID of calling process

#include <unistd.h>
pid_t setpgid(pid_t pid, pid_t pgid);
// returns: 0 on success, -1 on error
// change the process group of process pid to pgid
//If pid == zero, 则使用当前进程的PID
//If pgid == zero, 则将被pid指定的进程的PID作为PGID 

举个栗子: setpgid(0, 0)

如果当前进程ID是15213, 则这个函数调用将:

  • 新创建一个进程组, 且process group ID is 15213
  • 将进程15213添加到该组

实例应用: kill Program

kill是 linux 里的一个程序, 能够将任意的信号发送到其它进程.

bash 复制代码
unix> kill -9 15213
- sends signal 9 (SIGKILL) to process 15213
unix> kill -9 -15213
- a negative PID 指代的则是group ID
- 这里的作用是: sends a SIGKILL signal to every process in process group 15213.

实例应用: shell

我们平时使用的命令行就使用了进程相关的知识.

在shell中, 每当我们键入一个合法的命令行, 就会创建一个Job. 这是一个抽象的概念, 下面是一个例子:

bash 复制代码
unix> ls | sort
-a foreground job consisting of two processes connected by a pipe

在shell中, 最多只有一个foreground job在运行, 但可能有零个或者更多(没有限制)background job

对于每一个job, shell都为他们创建了各自的process group

一般来说, process group ID 就来自于job中的parent process

下图是一个shell里process group的结构:

当我们在键盘按下ctrl-c时, 将会导致SIGINT signal被发送到shell.

Shell 首先捕捉(catch )到这个信号,然后执行相应的handler, 将SIGINT signal 发送到foreground process group里的每一个进程(包括父进程和子进程)

默认情况下这将终止foreground job

发送信号

kill Function

上面有提到过kill Function, 在我们已经掌握进程组的概念后再来回顾一下:

c 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//returns: 0 if OK, -1 on error
  • pid > 0:
    • 向number=sig的信号发送给process pid
  • pid < 0:
    • 发送给每一个在进程组 abs(pid) 里的进程

下面是个使用kill函数的代码片段:

c 复制代码
#include <stdio.h>
#include <stdlib,h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
	pid_t pid;

	// Child sleeps until SIGKILL signal received
	if ((pid = Fork()) == 0) {
	    pause(); // Wait for a signal to arrive
	    printf("Control should never reach here!\n");
	    exit(0);
	}

	// Parent sends a SIGKILL signal to a child
	Kill(pid, SIGKILL);
	exit(0);
}

很有意思的是, printf语句将永远不会被执行. 子进程在pause()之后陷入沉睡, 当收到SIGINT信号后, 会直接终止.

alarm Function

c 复制代码
#include <unist.h>
unsigned int alarm(unsigned int secs);
// returns: 前一个alarm调用剩余的秒数, 如果没有previous alarm则返回0

alarm的意思是计时器. 这个系统调用让kernel在secs 秒后向该进程发送 SIGALRM 信号

如果secs为0, 则会取消当前的alarm

alarm函数会取消之前的pending alarms

接收信号

signal handler

当kernel结束一次异常处理, 即将将控制流交还给进程p的用户态时, 会检查 set of unblocked pending signals(pending & blocked )

这里指的异常处理, 广义上应指Exception Handler, 包含 异步中断同步中断 , 关于Exception Handling 的相关内容可回顾[[异常处理]]

如果有pending signal, 且没有被blocked(即pending bit = 1, blocked bit = 0 ) , 那么该信号就属于unblocked pending signals 的集合.

如果该集合为空(usual), 则用户态执行控制流 (被中断前执行)的下一条语句

但如果集合非空, 那么kernel就会选择set中的一个信号k(通常是最小的sig数), 并迫使进程p receive signal k.

信号的接收会引发相关的操作, 上面提到, 是Ignore, Terminate or catch.

当执行完后, 程序执行回到逻辑控制流原来的下一条语句 I n e x t I_{next} Inext

每一种信号原来都有默认的action, 为以下之一:

  • The process terminates.
  • The process terminates and dumps core.
  • The process stops until restarted by a SIGCONT signal.
  • The process ignores the signal.

注: dump core 指的是核心转储. 程序异常终止或崩溃后会生成核心转储文件, 保存程序的相关数据, 用于定位错误.

一般情况下, 信号的默认action都可以更改, 但以下两种除外:

  • SIGKILL
  • SIGSTOP

关于各种信号的默认行为, 可以在文章开始部分的表里查询.

更改默认行为

用户可以使用 signal 函数来更改特定信号的行为(当然, SIGKILL 和 SIGSTOP 除外)

c 复制代码
#include <signal.h>
typrdef void handler_t(int)
handler_t *signal(int signum, handler_t *handler)
//returns: ptr to previous handler if OK,
//SIGERR on error (does not set errno(全局错误变量))
  • handler is SIG_IGN: ignore signal of type signum
  • handler is SIG_DFL: 恢复默认行为
  • 另外的情况, call signal handler
    这里的handler是用户自定义的一个函数

下面是一个例子:

c 复制代码
void handler(int sig) /* SIGINT handler */
{
    printf("Caught SIGINT\n");
    exit(0);
}

int main()
{
    /* Install the SIGINT handler */
    if (signal(SIGINT, handler) == SIG_ERR)
        unix_error("signal error");

    pause(); /* wait for the receipt of a signal */

    exit(0);
}

当handler返回时, 会触发一个 sigreturn 系统调用. kernel这时会再次check pending and blocked vector,如果有

unblocked pending signals,则重复以上步骤;没有,则返回原先执行语句的下一条。

PS: 相同的 handler function 可以用来catch不同种类的信号

Block & Unblock Signals

Signal Handlers 也可以被打断,如下图:

Attention: Signal Handler 不会被与自身绑定的信号再次打断。在上例中,如果handler S在执行时收到了signal s, 那么handler 不会从头再开始执行。

这是由于当kernel强迫process catch 信号后,将该信号的pending bit设为0,blocked bit设为1, 来表示没有延迟的信号, 并防止 handler再一次被相同类型的信号打断.

上面所说的, 是隐式 的阻塞机制.

用户也可以显式地设置信号的阻塞与否.

c 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
//清空set(置为全0)
int sigfillset(sigset_t *set);
//把当前blocked的状态赋给set
int sigaddset(sigset_t *set, int signum);
//往set里增加编号为signum的信号
int sigdelset(sigset_t *set, int signum);
//从set里删除编号为signum的信号
//Above: Return: 0 if OK, -1 on error

int sigismember(const sigset_t *set, int signum);
//查找编号为signum的信号是否在set中
//Returns: 1 if member, 0 if not, -1 on error

sigprocmask 函数: 设置blocked signals
set相当于一个遮罩, 存储的其实就是一个vector, 里面为1的值就是被阻塞的信号

blocked 是实际上的blocked vector

(set 是辅助用的容器,而blocked是实际上进程信号被blocked的情况)

how:

  • SIG_BLOCK: 将set中的信号全部添加到blocked
    • blocked = blocked | set
  • SIG_UNBLOCK: 将set中的信号全部从blocked移除
    • blocked = blocked & ~set
  • SIG_SETMASK: 将set中信号的状态赋给blocked
    • blocked = set

oldset: 如果不是NULL, 则传入的指针存储blocked bit vector先前的状态, 以便恢复

其它的函数在注释里给予了说明.

总结

本文介绍了signal的基本原理与实现机制, 并给予了signal发送,接收,处理的使用用例. 但信号的知识点非常复杂, 接下来的文章将辅以实际的案例, 通过题目以及手搓一个简单的shell来加深对signal的理解.

相关推荐
pd_linux2 分钟前
【无标题】arm v8 速记
linux
墨黎芜5 分钟前
遥感数字图像处理:从入门到精通——作物旱情遥感监测
学习·信息可视化
毕设源码-朱学姐5 分钟前
【开题答辩全过程】以 基于uniapp的云笔记系统的设计与实现为例,包含答辩的问题和答案
笔记·uni-app
sagima_sdu7 分钟前
主流开源大模型架构全景
大数据·linux·人工智能
Wyawsl9 分钟前
nginx安全笔记
笔记·nginx·安全
Darth Nihilus10 分钟前
Raspberry Pi Compute Module Zero Development Board开发板(四)
linux·嵌入式硬件
芯跳加速10 分钟前
Obsidian智能体学习(二)
大数据·人工智能·学习
Xzq21050914 分钟前
Reactor模式
linux·网络
小鸡吃米…18 分钟前
Python 中的并发 —— 进程池
linux·服务器·开发语言·python
星辰引路-Lefan18 分钟前
全平台 Docker 部署 CPA(CLIProxyAPI Plus) 灵活定制指南 (Linux/Windows)——接入Codex
linux·windows·docker·ai·ai编程