系列文章:
操作系统详解(1)——操作系统的作用
操作系统详解(2)——异常处理(Exception)
操作系统详解(3)——进程、并发和并行
操作系统详解(4)——进程控制(fork, waitpid, sleep, execve)
文章目录
- 概述
- 信号的种类
- Hardware Events
- Software Events
- 信号的原理
- 信号发送
- 信号接收
- Pendning Signal(等待的信号)
- Block a Signal
- 信号的 Internal Data Structures
- 信号的实现机制
- 进程组
- process group
- 实例应用: kill Program
- 实例应用: shell
- 发送信号
- kill Function
- alarm Function
- 接收信号
- signal handler
- 更改默认行为
- Block & Unblock Signals
- 总结
概述
与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函数的定义:
#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)
默认情况下, 子进程和父进程属于同一个进程组.
#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 里的一个程序, 能够将任意的信号发送到其它进程.
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. 这是一个抽象的概念, 下面是一个例子:
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, 在我们已经掌握进程组的概念后再来回顾一下:
#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函数的代码片段:
#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
#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 除外)
#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是用户自定义的一个函数
下面是一个例子:
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再一次被相同类型的信号打断.
上面所说的, 是隐式的阻塞机制.
用户也可以显式地设置信号的阻塞与否.
#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的理解.