信号,用来处理异步事件(用来同步和通信)
一、基本概念
事件发生对进程的通知机制,也可以叫做软件中断。
和硬件中断相似之处在于可以打断程序正在执行的正常流程,就是在软件层次上对中断进行模拟
信号的目的是用来通知通信的
一个具有合适权限的进程可以向另一个进程发送信号,信号可作为一种同步技术,甚至是进程间通信(IPC)的原始形式
产生信号的场合:
-
硬件异常。硬件检测到错误条件并通知内核,随即由内核发送相应信号给相关进程。
-
终端中输入了能够产生信号的特殊字符。CTRL+C产生中断信号(SIGINT),CTRL+Z产生暂停信号(SIGCOUNT)
-
进程调用kill()系统调用可以将任意信号发送给另一个进程或者进程组。限制为收发信号的进程的所有者为同一人,或root为发送信号的所有者
-
用户通过kill命令将信号发送给其他进程
-
发生软件事件。如定时器超时,CPU时间超限,进程的某个子进程退出等
进程可以给自己发送信号,发送给进程的大多数信号都来自内核
信号由接收到信号的进程处理,处理方式有:
-
忽略信号。大多数信号都可以这样处理。但SIGKILL和SIGSTOP不能被忽略。不能被忽略的原因是,它们向内核和超级用户提供了可靠的进程终止和停止的方法。如忽略某些由硬件异常产生的信号,进程的运行行为是未定义的。
-
捕获信号。信号到达进程后,执行预先绑定好的信号处理函数。Linux提供了signal系统调用用于注册信号的处理函数。
-
执行系统默认操作。进程不处理,交给系统处理,每一个信号系统都有默认的处理方式。(大多数默认处理方式就是终止该进程)
信号是异步的
产生信号的事件是随机出现的,进程无法预测该事件产生的准确时间。
进程不能通过判断一个变量或者系统调用来判断是否产生了一个信号。
信号的本质是int类型的数字编号
内核针对每个信号,都给定义了一个唯一的整数编号,从数字1开始顺序展开。每个信号都有对应的名字(宏)
一般不同系统中,编号和名字的对应关系不一样,一般使用信号的名字(宏)
信号定义在<signum.h>中
/* Signals. */ #define SIGHUP 1 /* Hangup (POSIX). */ #define SIGINT 2 /* Interrupt (ANSI). */ #define SIGQUIT 3 /* Quit (POSIX). */ #define SIGILL 4 /* Illegal instruction (ANSI). */ #define SIGTRAP 5 /* Trace trap (POSIX). */ #define SIGABRT 6 /* Abort (ANSI). */ #define SIGIOT 6 /* IOT trap (4.2 BSD). */ #define SIGBUS 7 /* BUS error (4.2 BSD). */ #define SIGFPE 8 /* Floating-point exception (ANSI). */ #define SIGKILL 9 /* Kill, unblockable (POSIX). */ #define SIGUSR1 10 /* User-defined signal 1 (POSIX). */ #define SIGSEGV 11 /* Segmentation violation (ANSI). */ #define SIGUSR2 12 /* User-defined signal 2 (POSIX). */ #define SIGPIPE 13 /* Broken pipe (POSIX). */ #define SIGALRM 14 /* Alarm clock (POSIX). */ #define SIGTERM 15 /* Termination (ANSI). */ #define SIGSTKFLT 16 /* Stack fault. */ #define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */ #define SIGCHLD 17 /* Child status has changed (POSIX). */ #define SIGCONT 18 /* Continue (POSIX). */ #define SIGSTOP 19 /* Stop, unblockable (POSIX). */ #define SIGTSTP 20 /* Keyboard stop (POSIX). */ #define SIGTTIN 21 /* Background read from tty (POSIX). */ #define SIGTTOU 22 /* Background write to tty (POSIX). */ #define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */ #define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */ #define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */ #define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */ #define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */ #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */ #define SIGPOLL SIGIO /* Pollable event occurred (System V). #define SIGIO 29 /* I/O now possible (4.2 BSD). */ #define SIGPWR 30 /* Power failure restart (System V). */ #define SIGSYS 31 /* Bad system call. */ #define SIGUNUSED 31
不存在编号为0的信号,事实上kill函数对编号0自有用处
二、信号的分类
从可靠性分:可靠信号和不可靠信号
从实时性分:实时信号和非实时信号
可靠信号和不可靠信号
UNIX下不可靠信号:原始信号集在每次处理信号后,对信号先执行默认的系统操作,可能导致错误的处理。如果用户不希望这样操作,需要在处理函数结尾再一次调用signal,重新为该信号绑定相应的处理函数。(类似于do... while,上来先执行一遍)
从而导致错误的处理或信号可能丢失(处理信号时,来了新的信号,导致信号丢失)
Linux对Unix下的不可靠信号进行改进:调用完信号处理函数后,不必重新调用signal。
linux下的不可靠信号:指的是信号可能会丢失。信号值小于(SIGRTMIN,34)的都是不可靠信号
可靠信号:信号值在(SIGRTMIN(34),SOIGRTMAX (64))的信号
使用kill -l可以查看所有信号
可靠信号并没有一个具体对应的名字,而是使用了 SIGRTMIN+N 或 SIGRTMAXN 的方式来表示。
可靠信号支持排队,不会丢失
新版本的信号发送函数sigqueue()和信号绑定函数sigaction()
实时信号和非实时信号
实时信号支持排队,都是可靠信号,保证了发送的多个信号都能被接收
非实时信号不支持排队,都是不可靠信号,也称为标准信号
三、常见信号和默认行为
四、进程对信号的处理
进程接收到内核或者用户发来的信号后,可以根据情况采用不同的处理方式,如忽略信号,捕获信号或者执行系统默认操作。
设置信号处理返回时的系统调用signal()和sigaction()
1、signal函数
将信号的处理方式设置为捕获信号、 忽略信号以及系统默认操作
#include <signal.h>
typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
signum:需要设置的参数,建议使用宏定义
handler:SIG_IGN表示此进程需要忽略该信号,SIG_DFL表示系统默认的操作,也可以绑定信号处理函数
可以将多个信号绑定到同一个信号处理函数
Tips: SIG_IGN、 SIG_DFL 分别取值如下: /* Fake signal functions. */ #define SIG_ERR ((sig_t) -1) /* Error return. */ #define SIG_DFL ((sig_t) 0) /* 执行默认操作 */ #define SIG_IGN ((sig_t) 1) /* 忽略信号 */
返回值:成功返回指向信号处理函数,失败返回SIG_ERR(-1)
信号处理函数原型:
原型: static void sig_handler(int sig) { printf("Received signal: %d\n", sig); } 使用: signal(SIGINT, (sig_t)sig_handler);
如使用CTRL+C发送信号给前台进程,进程会收到信号,并执行对应的进程处理函数
如果进程放在后台:可以使用:kill -信号 PID 来发送信号
两个注意点:
-
如果进程没有设置信号处理方式,会默认为执行系统默认操作,如,SIGINT的默认操作是终止进程
-
调用fork()创建子进程,子进程会继承父进程的信号处理方式
2、sigaction函数
signal()函数简单好用,而 sigaction()更为复杂,但作为回报, sigaction()也更具灵活性以及移植性
sigaction()允许单独获取信号的处理函数而不是设置,并且还可以设置各种属性对调用信号处理函数时的行为施以更加精准的控制
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
act:为NULL,不改变信号的处理方式,struct act为新的处理方式
struct sigaction { void (*sa_handler)(int); *****************信号处理函数**************************************** void (*sa_sigaction)(int, siginfo_t *, void *); *********不能和信号处理函数一起设置********* sigset_t sa_mask;*******执行由 sa_handler 所定义的信号处理函数之前,会 先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号 从信号掩码字段中删除。 ***** int sa_flags;**********指定了一组标志,这些标志用于控制信号的处理过程****************************************** void (*sa_restorer)(void); *********过时了********* };
flag:
siginfo_t:
siginfo_t { int si_signo; /* Signal number */ int si_errno; /* An errno value */ int si_code; /* Signal code */ int si_trapno; /* Trap number that caused hardware-generated signal(unused on most architectures) */ pid_t si_pid; /* Sending process ID */ uid_t si_uid; /* Real user ID of sending process */ int si_status; /* Exit value or signal */ clock_t si_utime; /* User time consumed */ clock_t si_stime; /* System time consumed */ sigval_t si_value; /* Signal value */ int si_int; /* POSIX.1b signal */ void *si_ptr; /* POSIX.1b signal */ int si_overrun; /* Timer overrun count; POSIX.1b timers */ int si_timerid; /* Timer ID; POSIX.1b timers */ void *si_addr; /* Memory location which caused fault */ long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */ int si_fd; /* File descriptor */ short si_addr_lsb; /* Least significant bit of address(since Linux 2.6.32) */ void *si_call_addr; /* Address of system call instruction(since Linux 3.5) */ int si_syscall; /* Number of attempted system call(since Linux 3.5) */ unsigned int si_arch; /* Architecture of attempted system call(since Linux 3.5) */ }
oldact:设置可以得到原来的信号处理返回时,设置为NULL不理会
信号处理函数和中断函数一样,越简单越好。越简单越不容易引发竞争。
五、向进程发送信号
1、kill函数
将信号发送给指定的进程或进程组中的每一个进程
int kill(pid_t pid, int sig);
pid:进程号
参数 pid 不同取值含义:
⚫ pid 为正,信号 sig 将发送到 pid 指定的进程。
⚫ pid 等于 0,sig 发送到当前进程的进程组中的每个进程。
⚫ pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
⚫ pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程
sig:需要发送的信号,0表示为不发送信号,但执行错误检查(检查上一个参数PID是否存在)
这里的权限指的是:发送者进程的实际用户ID或有效用户ID和接收者一致
2、raise函数
向自己发送信号
int raise(int sig);
返回值:成功返回0,失败返回非0值
六、alarm和pause
1、alarm函数
设置一个定时器(闹钟) ,当定时器定时时间到时,内核会向进程发送 SIGALRM信号
unsigned int alarm(unsigned int seconds);
seconds:设置为0,表示取消之前的alarm,否则设置为对应的时间(单位为秒)
返回值:之前有alarm还没超时,将上一个alarm的剩余时间返回,并重新设置为当前闹钟。其它返回0
只能触发一次,若想要多次触发,需要在SIGALRM信号的处理函数中再次调用alarm函数设定定时值
2、pause函数
使进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时, pause()才返回,在这种情况下, pause()返回-1,并且将 errno 设置为 EINTR。
int pause(void);
七、信号集
一个能表示多个信号的数据类型。
很多系统调用都使用信号集这种数据类型来作为参数传递,譬如 sigaction()函数、 sigprocmask()函数、 sigpending()函数等。
信号集就是sigset_t类型的一个结构体
define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } sigset_t
可以将多个信号添加到这个结构体中,linux封装了一些API方便操作,
譬如 sigemptyset()、 sigfillset()、 sigaddset()、 sigdelset()、 sigismember(),
1、初始化信号集
不包含任何信号的初始化:sigemptyset
包含所有信号的初始化:sigfillset
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
2、向信号集中添加删除信号
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
3、测试信号是否在信号集中
int sigismember(const sigset_t *set, int signum);
返回值:在1,不在0,失败-1
获取信号的描述信息
每个信号都有一个描述信息,存放在sys_siglist这个char*数组中
1、sys_siglist数组
sys_siglist[SIGINT]
2、strsignal函数
char *strsignal(int sig);
返回一个指针,%s打印即可
3、psignal函数
可以在标准错误(stderr)上输出描述信息(标准输出和标准错误都对应着屏幕)
void psignal(int sig, const char *s);
s是添加的备注信息
八、信号掩码(阻塞信号传递)
内核为每个进程维护一个信号掩码(信号集),接收到属于信号掩码中的信号会被阻塞,直到信号移出去,进程才会得到信号,开始处理
向信号掩码中添加信号:
-
应用程序调用 signal()或 sigaction()函数设置处理方式时,进程会自动将该信号添加到信号掩码中
sigaction()要看是否设置了SA_NODEFER标志,设置表示不添加,不设置默认添加
-
通过sigaction的sa_mask参数添加
-
sigprocmask系统调用
sigprocmask()函数
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how: 调用函数时的一些行为
参数 how 可以设置为以下宏:
⚫ SIG_BLOCK:将参数 set 所指向的信号集内的所有信号添加到进程的信号掩码中。(原基础来添加)
⚫ SIG_UNBLOCK:将参数 set 指向的信号集内的所有信号从进程信号掩码中移除。
⚫ SIG_SETMASK:进程信号掩码直接设置为参数 set 指向的信号集。 (只添加set)
set:要添加或移除信号掩码的信号,NULL表示不改动
oldset:为为NULL的话,在添加信号掩码前,将当前信号掩码存到oldset
九、阻塞等待信号sigsuspend
阻塞掩码可以保护不希望由信号中断的关键代码段
为解决这个问题,引入sigsuspend,它将信号恢复和pause封装成一个原子操作
sigsuspend函数
int sigsuspend(const sigset_t *mask);
返回值:始终返回-1,通过errno判断,errno为EINTR表示信号被打断,errno为EFAULT,调用失败
注意:这里的mask信号集是被传入的被阻塞的信号,比如受保护代码前我们不希望被SIGINT打断,执行完后,我们希望恢复SIGINT信号,这里就不要把SIGINT信号传给mask,要传的是新的要添加到信号掩码的信号。
执行完受保护的代码段之后,调用 sigsuspend()挂起进程,等待被信号唤醒,被唤醒之后再解除 SIGINT 信号的阻塞状态。
十、实时信号
在执行信号处理函数期间接收到了新的信号,如果新信号在信号掩码中,信号会被添加到等待信号集。
1、sigpending函数
查看进程中的等待信号
int sigpending(sigset_t *set);
发送实时信号
等待信号集表明一个信号是否会发生,不能表明其发生的次数,实时信号能够解决这一问题。
-
实时信号范围大,可用于应用程序自定义,非实时信号只提供了SIGUSR1和SIGUSR2两个信号
-
队列化管理。实时信号多次发送给某一进程,信号会被多次传递。而标准信号只被传递一次。
-
指定伴随数据(整形或者指针)。接收进程可以在信号处理函数中得到这个附加数据
-
实时信号的编号越小优先级越高,同信号多次传递,先来后到。
使用实时信号的条件:
-
发送进程使用 sigqueue()系统调用向另一个进程发送实时信号以及伴随数据。
-
接收实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样就不能获取到实时信号的伴随数据了。
int sigqueue(pid_t pid, int sig, const union sigval value);
value: 指定信号的伴随数据, union sigval 数据类型。
typedef union sigval { int sival_int; void *sival_ptr; } sigval_t;
十一、异常退出abort
使用 exit()、 exit()或Exit()用于正常退出应用程序,
对于异常退出程序,一般使用 abort()库函数,它会生成核心转储文件,可用于判断程序调用 abort()时的程序状态。
void abort(void);
abort()通常产生 SIGABRT 信号来终止调用该函数的进程, SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;
当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。