Linux中的信号
信号的概念及机制
每个进程收到的所有信号,都是由内核负责发送并由内核进行处理
与信号相关的事件和状态
产生信号:
- 按键产生:如:Ctrl+C、 Ctrl+z、 Ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill命令
递达:
递送并且到达进程
未决:
信号产生与递达之间的状态,主要是由于阻塞(或屏蔽)而导致
信号的处理方式:
- 执行默认动作,信号的处置为SIG_DFL(default);
- 忽略(丢弃),把信号的处置设置为SIG_IGN(ignore)来忽略,但有两个信号不能被忽略:SIGKILL和SIGSTOP;
- 捕获(调用用户的处理函数signal handler来捕获catching),但有两个信号不能被捕获:SIGKILL和SIGSTOP。
Linux 内核的进程控制块 PCB 是一个结构体,task_struct, 除了包含进程 id,状态,工作目录,用户 id,组 id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
阻塞信号集(信号屏蔽字):
将某些信号加入集合,对它们设置屏蔽,例如屏蔽x信号后,再接收到该信号,则不对该信号进行处理,直至该信号解除屏蔽。
未决信号集:
- 信号产生时,未决信号集中该信号对应的描述位立刻翻转为1,表明该信号处于未决状态;信号被处理后,该描述位则立刻翻转为0。通常情况下,未决状态十分短暂;
- 信号产生后由于某些原因(主要是阻塞)而不能递达,此类信号组成的集合为未决信号集。在屏蔽解除前,信号将一直处于未决状态。
示意图:
信号的编号
可使用kill-l 查看当前系统可使用的信号:
信号4要素
- 编号;
- 名称;
- 事件;
- 默认处理动作。
可通过man 7 signal 查看帮助文档获取。 也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h
部分signal有三个value,,第一个值通常对 alpha 和 sparc 架构有效,中间值针对 x86、arm和其他架构,最后一个应用于 mips 架构。一个‘-’表示在对应架构上尚未定义该信号。
默认动作:
- Term:终止进程;
- Ign:忽略信号(默认及时对该种信号进行忽略操作);
- Core:终止进程,生成Core文件。(查验进程死亡原因,用于gdb调试);
- Stop:停止(暂停)进程;
- Cont:继续运行进程。
注意:9)SIGKILL和19)SIGSTOP信号不允许被忽略和捕捉。
Linux常用信号一览表
1) SIGHUP: 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号,默认动作为终止进程
2) SIGINT:当用户按下了<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
3) SIGQUIT:当用户按下<ctrl+\>组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
4) SIGILL:CPU 检测到某进程执行了非法指令。默认动作为终止进程并产生 core 文件;
5) SIGTRAP:该信号由断点指令或其他 trap 指令产生。默认动作为终止里程 并产生 core 文件。
6) SIGABRT: 调用 abort 函数时产生该信号。默认动作为终止进程并产生 core 文件。
7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生 core 文件。
8) SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误。
默认动作为终止进程并产生 core 文件。
9) SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了
可以杀死任何进程的方法。
10) SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
11) SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生 core 文件。
12) SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
13) SIGPIPE:Broken pipe 向一个没有读端的管道写数据。默认动作为终止进程。
14) SIGALRM: 定时器超时,超时的时间 由系统调用 alarm 设置。默认动作为终止进程。
15) SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。
执行 shell 命令 Kill 时,缺省产生这个信号。默认动作为终止进程。
16) SIGSTKFLT:Linux 早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
17) SIGCHLD:子进程状态发生变化时,父进程会收到这个信号。默认动作为 忽略这个信号。
18) SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
19) SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
信号的产生
终端按键产生
硬件异常产生
kill函数/命令产生
int kill(pid_t pid, int sig);
举例:循环创建子进程,父进程用kill终止任一子进程
软件条件产生
alarm函数
每个进程都有且仅有一个定时器
alarm函数会利用内核给当前进程发送14)SIGALRM信号,进程收到信号后,默认终止
unsigned int alarm(unsigned int seconds);
alarm(0)会取消定时器
举例:编写程序测试1秒钟能数多少个数
#include <unistd.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
alarm(1);
int i=0;
while(1){
i++;
printf("%d\n", i);
}
return 0;
}
使用time命令查看程序执行时间
time ./alarm
实际执行时间real = 系统时间sys + 用户时间user + 等待时间
setitimer函数
同样可设置定时器,但精度可至微秒,且可循环执行。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
举例:setitimer实现alarm
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/time.h>
#include <signal.h>
void sys_err(const char* str) {
perror(str);
exit(1);
}
void func() {
printf("hello world!\n");
}
int main(int argc, char* argv[]) {
struct itimerval new_value;
struct itimerval old_value;
signal(SIGALRM, func);
new_value.it_interval.tv_sec=1;
new_value.it_interval.tv_usec=0;
new_value.it_value.tv_sec=2;
new_value.it_value.tv_usec=0;
if(setitimer(ITIMER_REAL, &new_value, &old_value) == -1 ){
sys_err("setitimer error");
}
while(1);
return 0;
}
信号集操作函数
内核通过读取未决信号集来判断信号是否被处理。
信号屏蔽字mask可以影响未决信号集。
我们可以在程序中自定义集合set来改变mask,以达到屏蔽指定信号的目的。
信号集设定函数
sigset_t set;
int sigemptyset(sigset* set);
int sigfillset(sigset* set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
sigprocmask函数
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
sigpending函数
读取当前进程的未决信号集
int sigpending(sigset_t* set);//set为传出参数 成功:0 失败:-1 设置errno
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char* str) {
perror(str);
exit(1);
}
void print_set(sigset_t* set){
int i;
for(i=1;i<32;i++){
if(sigismember(set,i)){
putchar('1');
}else{
putchar('0');
}
}
printf("\n");
}
int main(int argc, char* argv[]) {
sigset_t set, oldset, pedset;
int ret=0;
sigemptyset(&set);
sigaddset(&set, SIGINT);
ret=sigprocmask(SIG_BLOCK, &set, &oldset);
if(ret==-1){
sys_err("sigprocmask error");
}
while(1){
ret=sigpending(&pedset);
if(ret==-1){
sys_err("sigpending error");
}
print_set(&pedset);
}
return 0;
}
信号捕捉
signal函数
该函数是注册一个信号捕捉函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
==void (*signal(int signum, void (*sighandler_t)(int))) (int);
上述函数较老 应尽量避免使用
sigaction函数
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
void sys_err(const char* str) {
perror(str);
exit(1);
}
void sig_catch(int signo) {
printf("catch you, %d \n", signo);
}
int main(int argc, char* argv[]) {
struct sigaction act,oldact;
act.sa_handler = sig_catch;
sigemptyset(&(act.sa_mask));
act.sa_flags = 0;
int ret = sigaction(SIGINT, &act, &oldact);
if( ret == -1) {
sys_err("sigaction error");
}
while(1);
return 0;
}
信号捕捉特性
1. 捕捉函数执行期间,信号屏蔽字 由 mask --> sa_mak, 捕捉函数执行结束后,恢复为mask;
2. 捕捉函数执行期间,本信号自动被屏蔽(sa_flag=0);
3. 捕捉函数执行期间,被屏蔽信号多次发送,解除屏蔽后只处理一次。
内核实现信号捕捉过程:
SIGCHLD信号
产生条件
- 子进程终止时;
- 子进程接收到SIGSTOP信号而停止时;
- 子进程处于停止态,接收到SIGCONT后唤醒时。
借助SIGCHLD信号回收子进程
代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include <pthread.h>
void sys_err(char* str) {
perror(str);
exit(1);
}
void catch_child(int signo) {
int status;
pid_t wpid;
while((wpid = waitpid(0, &status, WNOHANG)) > 0) {
if(WIFEXITED(status)) {
printf("child %d exit %d \n",wpid, WEXITSTATUS(status));
} else if(WIFSIGNALED(status)) {
printf("child %d cancel signal %d \n", wpid, WTERMSIG(status));
}
}
return;
}
int main() {
pid_t pid;
int i;
for(i =0; i<5; i++) {
if((pid=fork())==0) {
break;
} else if(pid < 0) {
sys_err("fork error");
}
}
if(pid == 0) {
int n = 1;
while(n--) {
printf("child id %d\n", getpid());
sleep(1);
}
return i*i;
} else if(pid > 0) {
struct sigaction act,oldact;
act.sa_handler=catch_child;
sigemptyset(&(act.sa_mask));
act.sa_flags=0;
sigaction(SIGCHLD,&act,&oldact);
while(1) {
printf("I'm parent: %d\n",getpid());
sleep(1);
}
}
return 0;
}
SIGCHLD信号注意问题
- 子进程继承了父进程的信号屏蔽字和信号处理动作,但并未继承未决信号集;
- 须注意注册信号捕捉函数的位置应在父进程中;
- 应在fork之前阻塞SIGCHLD信号,注册完捕捉函数之后解除阻塞,避免父进程来不及捕捉子进程的SIGCHLD信号。
中断系统调用
- 慢速系统调用:可能会使进程永久调阻塞的调用,如:read,write,wait……
- 其他系统调用:如:getpid,fork……
慢速系统调用被中断时,可利用修改sa_flags参数来设置被信号中断后系统调用是否重启。设置为SA_INTERRUPT表示不重启,SA_RESET表示重启。
扩展:sa_flags还有其他参数,在捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEER,除非sa_mask中包含该信号(将其阻塞)。
,&act,&oldact); //解除阻塞 while(1) { printf(“I’m parent: %d\n”,getpid()); sleep(1); }
}
return 0;
}
#### SIGCHLD信号注意问题
1. 子进程继承了父进程的信号屏蔽字和信号处理动作,但并未继承未决信号集;
2. 须注意注册信号捕捉函数的位置应在父进程中;
3. 应在fork之前阻塞SIGCHLD信号,注册完捕捉函数之后解除阻塞,避免父进程来不及捕捉子进程的SIGCHLD信号。
### 中断系统调用
1. 慢速系统调用:可能会使进程永久调阻塞的调用,如:read,write,wait……
2. 其他系统调用:如:getpid,fork……
慢速系统调用被中断时,可利用修改sa_flags参数来设置被信号中断后系统调用是否重启。设置为SA_INTERRUPT表示不重启,SA_RESET表示重启。
扩展:sa_flags还有其他参数,在捕捉到信号后,在执行捕捉函数期间,不希望自动阻塞该信号,可将sa_flags设置为SA_NODEER,除非sa_mask中包含该信号(将其阻塞)。
|