在linux/unix系统中,我们如果想杀死一个进程,可以使用 kill -9 PID 的方式来杀死一个进程,这种方式并不是调用了什么系统的API函数实现的,实际是给进程发送了一个 SIGKILL 信号。当进程收到这个信号后执行了一个默认的操作 Term,而这个 Term 代表的就是终止进程 (Terminate Process)。这就是一个信号最直观的应用。
而并非只有杀死进程用到了信号,在linux/unix中,很多场景都用到了信号机制,在说这些场景之前,我们先来看一下系统一共有多少个信号,在终端下使用命令 kill -l 可以查看所有信号和信号编号:
图中可以看出,一共有1~62个信号,前31个信号是我们讨论的重点,编号为34以后的信号是实时信号,一般在嵌入式开发中使用较多,我们本文中不做讨论。SIGKILL信号的编号就是9,所以我们在使用 kill -9 PID 的时候实际是给进程发送了一个编号为 9 的信号,而进程接收到这个信号以后,执行了系统设定的默认动作。那这个默认动作是什么呢?我们可以通过 man page 来查看一下 signal 中的详细解释,通过命令 man 7 signal 可以查看具体的信息:
在上图中,第一列的数据是信号,第二列是信号编号,第三列则是信号执行的默认动作,第四列代表系统发送这个信号给进程是代表出现了什么事件。我们看到 SIGKILL 对应的默认动作就是 Term,而且大家发现,除了 SIGKILL 信号的默认动作是 Term 以外,其他的很多信号也执行这个默认的 Term 动作。这个 Term 到底是什么意思?在 man 7 signal 中是如下解释:
“Default action is to treminate the process” 默认动作是杀死这个进程,除了 Term 我们还看到了 Ign、Core、Stop、Cont。他们的作用翻译为中文分别如下:
动作
作用
Term
默认动作是杀死这个进程
Ign
默认动作是忽略这个信号
Core
默认动作是杀死这个进程并转储核心文件,详见 man 5 core
Stop
默认动作是暂停这个进程
Cont
如果这个进程是暂停状态,那么默认动作则是继续(恢复)这个进程运行
了解了每种信号的默认动作,那我们就有必要来了解一下,一个进程在什么情况下会收到这些信号呢?下面的列表记录了每种信号的产生原因:
信号
Defalut
信号产生原因
- SIGHUP
Term
当用户退出shell时,由该shell启动的所有进程将收到这个信号
- SIGINT
Term
当用户按下了<Ctrl C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号
- SIGQUIT
Core
当按下<ctrl >组合键时产生该信号,终端向正在运行中的由该终端启动的程序发出些信号
- SIGILL
Core
CPU检测到某进程执行了非法指令
- SIGTRAP
Core
该信号由断点指令或其他 trap指令产生
- SIGABRT
Core
调用abort函数时产生该信号
- SIGBUS
Core
非法访问内存地址,包括内存对齐出错
- SIGFPE
Core
在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误
- SIGKILL
Term
无条件终止进程。本信号不能被忽略,处理和阻塞。
- SIGUSE1
Term
用户定义的信号,即程序员可以在程序中定义并使用该信号。
- SIGSEGV
Core
指示进程进行了无效内存访问(段错误会产生该信号)
- SIGUSR2
Term
这是另外一个用户自定义信号 ,程序员可以在程序中定义 并使用该信号
- SIGPIPE
Term
Broken pipe向一个没有读端的管道写数据
- SIGALRM
Term
定时器超时,超时的时间 由系统调用alarm设置
- SIGTERM
Term
程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出
- SIGSTKFLT
Term
协处理器堆栈错误
- SIGCHLD
Ign
fork() 子进程结束时,父进程会收到这个信号
- SIGCONT
Cont
在进程挂起时继续,否则是忽略,不能被忽略,处理和阻塞
- SIGSTOP
Stop
提供给管理员暂停进程的特权,不能被忽略,处理和阻塞
- SIGTSTP
Stop
停止进程的运行。按下<ctrl z>组合键时发出这个信号
- SIGTTIN
Stop
后台进程读终端控制台
- SIGTTOU
Stop
该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生
- SIGURG
Ign
套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达
- SIGXCPU
Term
进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程
- SIGXFSZ
Term
超过文件的最大长度设置
- SIGVTALRM
Term
虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间
- SIGPROF
Term
类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间
- SIGWINCH
Ign
窗口变化大小时发出
- SIGIO
Ign
此信号向进程指示发出了一个异步IO事件
- SIGPWR
Term
关机
- SIGSYS
Core
无效的系统调用
上面这些信号,我们可以在终端中使用 kill -信号 -PID 给某个进程发送,如果要通过程序实现,可以调用以下系统函数: int kill(pid_t pid, int sig);
- 第一个参数:要发送的进程PID
- 第二个参数:要发送的信号宏或者信号编号
- 返回值:成功返回0,失败返回-1并设置errno
- 当pid参数是正数时,它将发送信号到这个正数所对应的进程PID。
- 当pid参数等于0时,它将发送信号到所有进程的调用进程的进程组。
- 当pid参数等于-1时,它将发送信号到除init进程外所有有权限发送的进程中。
- 当pid参数小于-1时,它将发送信号到除了-号以外的这个进程PID的进程组中。
- 当sig等于0时,将不发送信号,但依然执行错误检查。
除了kill函数,还有其他几个发送信号的函数:
- **int raise(int sig)**:给当前调用进程或线程发送一个信号,如果只有一个线程就相当与 kill(getpid(), sig)
- **void abort(void)**:首先解除对 SIGABRT 的阻塞,然后给调用进程发送 SIGABRT 信号,会使调用进程异常终止
- **unsigned int alarm(unsigned int seconds)**:参数是设定一个以秒为单位的整数,当调用进程执行了该函数时,在等待了参数传递的秒数以后回给调用进程发送一个 SIGALRM 信号,该信号默认是 Term,也就是杀掉进程。如果参数设置为0,那么在未决信号集中的alarm信号位置0(后文会解释未决信号集),也就是取消了 SIGALRM 信号。这个信号可以用来做自己的延迟函数,代码如下(代码虽然实现了基本需求,但是存在严重bug,后文我们在讨论时序竞态的时候会讨论这个问题):
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void dosig(int n)
{
}
int mysleep(int sec)
{
int ret;
struct sigaction act, oldact;
// 捕获 SIGALRM 信号,交给 dosig 处理函数,oldact结构体保留原信号处理信息
act.sa_handler = dosig;
// 清空设定掩码
sigemptyset(&act.sa_mask);
// 设置标志位为0,代表使用 sa_handler 函数指针
act.sa_flags = 0;
// 捕获 SIGALRM 信号,第二个参数时上面设定好的结构体,第三个参数时备份
sigaction(SIGALRM, &act, &oldact);
// 根据传递进来的秒数发送一个 SIGALRM 信号
alarm(sec);
// 暂停程序运行,一直等待收到某信号并执行信号默认动作
// 由于我们捕获了 SIGALRM 信号,所以即使收到 SIGALRM 信号也不会终止进程
pause();
// 将 alarm 置零并记录返回值
ret = alarm(0);
// 恢复原有信号处理方式
sigaction(SIGALRM, &oldact, NULL);
return ret;
}
int main(int argc, char* argv[])
{
printf(“Hello World…n”);
// 延迟10秒
mysleep(10);
printf(“Hello World…n”);
return 0;
}
以上就是信号的基本概念和基本操作,后面的文章我们再详细介绍信号的更多操作。比如信号的阻塞、信号的捕获等等,这些我们都需要单独的文章篇幅来分析。