1、signal函数与sigaction函数
最近主要用到1-31号信号,也就是标准信号(不可靠),具体每一个信号的含义和作用,可以查看我的另一篇文章:标准信号详解。
1.1 signal函数
作用:对特定信号进行相应处理。
函数原型:
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
handler是回调函数,此函数返回值必须是void,参数是一个int。系统为我们事先提供好的两个宏,分别是 SIG_DFL (default) 和 SIG_IGN (ignore)。如果 handler 被指定为 SIG_DFL,系统将为该信号指定默认的信号处理函数,如果 handler 被指定为 SIG_IGN,系统将忽略该信号。实际上在程序启动时,所有信号的处理函数都被指定为默认或者忽略。
signum需要捕捉的信号。
signal 的返回值表示旧的信号处理函数。如果返回值等于 SIG_ERR 说明注册失败。
1.2 sigaction函数
不同于signal函数,sigaction函数可以使用带附加参数的信号处理函数,在处理信号的时候可以阻塞其他信号等。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数:
signum:要捕获的信号。
act:struct sigaction 结构体,它保存了信号处理函数指针等等,后面具体讲解。
oldact:返回旧的 struct sigaction 结构体
返回 0 成功,-1 失败
struct sigaction:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
a_handler : 不带附加参数的信号处理函数指针
sa_sigaction: 带有附加参数的信号处理函数指针(两个信号处理函数指针只能二选一)
sa_mask: 在执行信号处理函数时,应该屏蔽掉哪些信号
sa_flags: 用于控制信号行为,它的值可以是下面选项的组合。
SA_NOCLDSTOP : 当捕获 SIGCHLD 时,不接收子进程停止的通知。
SA_NOCLDWAIT:当捕获 SIGCHLD 时,收子进程在退出时不变成僵尸进程。
SA_NODEFER:当该信号处理函数执行时,不阻塞该信号。
SA_ONESTACK:在指定的栈(signaltstack 函数指定)上执行信号处理函数。
SA_RESETHAND:在进入信号处理函数入口点处恢复该信号的处理函数为默认函数。
SA_RESTART:由此信号中断的系统调用是否要再启动
SA_SIGINFO:如果指定该选项,则向信号处理函数传递参数(这时应该使用 sa_sigaction 成员而不是 sa_handler).
sa_restorer:该成员在早期是用来清理函数栈的,如今已被废弃不用。
1.3 使用带附加参数的信号处理函数
如果要使用带附加参数(即sigaction结构体中的siginfo_t结构体)的信号处理函数,此处理函数的原型必须是这样:
void fun(int sig, siginfo_t *siginfo, void *context); // 函数是什么名字无所谓
而且,应该在成员sa_flags上加上选项SA_SIGINFO. sa_flags 加上选项 SA_SIGINFO 的含义仅仅是表明:在处理信号的时候,会附带一个 siginfo_t* 类型的参数。
siginfo_t结构体:
完整版:
struct 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 */
int si_band; /* Band event */
int si_fd; /* File descriptor */
}
此处我们用不到如此多的参数,因此可以参考简化版:
struct siginfo_t {
pid_t si_pid; /* 发送信号的进程 ID */
uid_t si_uid; /* 发送信号的进程实际用户 ID */
sigval_t si_value; /* 附加参数(联合体) */
int si_int; /* 实际上这个参数的值就是 si_value,他们相等 */
void *si_ptr; /* 同上 */
}
union sigval_t {
int sival_int;
void *sival_ptr;
};
值得注意的是 si_value 成员、si_int 和 si_ptr 这几个成员,这些成员实际上由用户在发送信号的时候传递的。而且,si_value 的值,和 si_int,si_ptr 的值是完全一致的。
1.4带附加参数的信号发送函数
int sigqueue(pid_t pid, int sig, const union sigval value);
其中,value就是附加的参数。
2、标准信号的不可靠性
1-31号信号被称为标准信号,32-64号信号被称为实时信号。
标准信号是不可靠的,如果同时来了很多相同的信号,而且没来得及处理,这些信号就会被合并成一个信号。
实时信号就没有这个问题,信号来一次就会处理一次。
例:当信号处理函数是wait函数,有子进程变成僵尸进程后会对其进行回收。如果在信号处理程序中调用wait函数并sleep 1秒,此时有多个子进程变成僵尸进程,那么只会回收一个子进程。
3、可重入函数
当一个程序内引用了全局变量或者静态变量,那么如果在程序执行过程中由于信号打断而去执行另一个调用同样全局变量和静态变量的函数,并在这个函数中对此变量的值改变,那么重新返回时,全局或静态变量已经改变,程序执行结果会出现非期望值。
我们把所有引用了全局变量或静态变量的函数称为不可重入函数。不可重入函数都不是信号安全的,也不是线程安全的。(线程安全的函数不一定是异步信号安全的。)
如果一个函数使用了不可重入函数,那么该函数也会变成不可重入的。这意味着,你不能在信号处理函数中使用不可重入函数。
4、发送信号
4.1 kill函数
kill函数与kill命令相同,用来发送信号。
int kill(pid_t pid, int sig);
参数:
pid:
pid > 0,表示向进程号为 pid 的进程发信号
pid = 0,表示向同组进程发信号(有权限才行)
pid < -1,向进程组 |pid|
发信号(有权限才行)
pid = -1,向所有进程发信号(有权限才行,早期的 POSIX 并未定义此种情况)
sig:
表示向进程发送什么信号。如果 sig 为 0 ,通常用来测试是否有权限向进程发信号。
4.2 alarm函数
alarm函数与kill函数不同,它只能给自己发信号,而且只能发送SIGALRM信号。它的特点是可以设置发送的时间。
unsigned int alarm(unsigned int seconds);
seconds:单位 秒,seconds秒后向本进程发送SIGALRM信号。
返回值:上一个定时alarm剩余时间。如果没有,或者上一个alarm已经完成,则为0。同时,会取消上一个alarm。
5、信号容器sigset_t
从名字可以看出来,这是一个信号集合,既然是set,那么每种信号只会出现一次。
它实际上是一个64位的整数,信号一共有1-64共64种,所以sigset_t每一位代表一种信号,因此,sigset_t也被称为信号位图。
操作sigset_t的函数:
sigemptyset (清空集合,所有比特位置 0)
sigfillset (填充集合,所有比特位置 1)
sigaddset (添加信号到集合,将某一比特位置 1)
sigdelset (删除某个信号,将某一比特位置 0)
sigismember (是否存在某个信号,判断某一比特位是否为 1)
6、阻塞信号和未决信号
在进程的PCB中保存了两个信号集:阻塞信号集和未决信号集。(阻塞是个动词,未决是个名词,你明白我意思的)
6.1阻塞信号
函数sigprocmask用来修改阻塞信号集。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how
SIG_BLOCK 该选项表示将 set 参数指示的信号集中的信号添加到进程阻塞集中
SIG_UNBLOCK 该选项与功能 SIG_BLOCK 相反,表示将进程阻塞信号集中指定的信号删除
SIG_SETMASK 该选项表示将进程阻塞信号集直接设定为你指定的 set
set
指定的信号集合
oldset
旧的阻塞信号集(返回)
返回值
0 成功
-1 失败
6.2未决信号
顾名思义,未决信号就是收到但还没有处理的信号。实际上就是被阻塞的信号集(一个sigset_t)。
可以使用sigpending函数来获取未决信号集
int sigpending(sigset_t *set);
set参数即获取到的未决信号集。
7、中断系统调用
谁中断了系统调用?
信号
哪些系统调用会被中断?
低速系统调用 会被中断。所谓的低速系统调用是指有可能导致I/O永远阻塞的系统调用。例如:pause(使进程睡眠直到有信号到来)、read、write、wait、ioctl等。
注意:从本地磁盘读写不是低速系统调用,虽然读写磁盘文件可能会暂时阻塞调用者(磁盘将驱动程序将请求保存到队列,最后会在适当的时期执行该请求),除非发生硬件错误,否则 I/O 操作总是很快返回,并使调用者不在处于阻塞状态。
什么是自动重启?
被中断的系统调用并不会返回错误,而是重新执行。
如何设置自动重启?
sigaction函数捕获某种信号时,可在struct sigaction结构体种设置sa_flags的SA_RESTART选项。如果你使用的是signal函数,那么SA_RESTART 选项默认就是开启的(大多数时候,我们并不希望开启此选项)。
8、sigsetjmp和siglongjmp
8.1 setjmp和longjmp
我们都知道,goto可以实现函数内的跳转,但是并不能跨函数跳转。那么如何实现跨函数跳转呢?setjmp和longjmp就是用来做这件事的!
例子:
jmp_buf hello;
void func1() {
setjmp(hello);
printf("hello world\n");
func2();
}
void func2() {
longjmp(hello);
}
setjmp用来设置跳转入口,当程序执行到longjmp时,就会跳转到相应的入口。
函数原型:
int setjmp(jmp_buf env)
env必须时全局变量。jmp_buf是一个固定大小的数组。
void longjmp(jmp_buf env, int val);
env是要跳转的位置,第二个参数会通过setjmp返回(我认为这样的话,在不同的地方跳转到同一个地方,可以根据setjmp返回值来判断是从哪个地方跳转过来的,或者可以传递一些必要的整型参数)。
8.2 sigsetjmp和siglongjmp
有一件事要明确一下:
在信号处理函数执行时,会阻塞当前信号。
当信号处理函数返回时,系统会帮我们把刚刚阻塞的信号再从阻塞集中移除。
那么如果我们在信号处理函数中使用lonjmp,在其返回之前跳出,当前处理的信号岂不是会被一直阻塞?是这样的,所以在信号处理函数中,我们应当使用另一组工具,sigsetjmp和siglongjmp。
int sigsetjmp(sigjmp_buf env, int savesigs);
使用sigsetjmp时,将savesigs设为非0值,那么每次跳至sigsetjmp处时,会自动将阻塞信号集恢复成它记住的阻塞信号集(信号处理函数之前)。这样就没有上面说的bug啦!
信号这一节知识很多,一个整理总结非常必要。本来想自己整理,但是Allen大佬已经整理的非常好,在此直接贴上链接,供自己以后学习,也供大家一起学习!https://blog.csdn.net/q1007729991/article/details/53939194