问题
三种注册信号与处理函数的方法有什么区别???
信号的 OneShot 特性
System V 风格的 signal 函数,注册的信号处理是一次性的
进程收到信号后,调用由 signal 注册的处理函数
处理函数一旦执行,进程通过默认的方式处理后续相同信号
如果想要重复触发,那么必须再次调用 signal 注册处理函数
信号自身的屏蔽特性
在信号处理函数执行期间,很可能再次收到当前信号
- 即:处理 A 信号的时候,再次收到 A 信号
对于 System V 风格的 signal 函数,会引起信号处理函数的重入
- 即:调用处理函数的过程中,再次触发处理函数的调用
在注册信号处理函数时:
- System V 风格的 signal 不屏蔽任何信号
- BSD 风格的 signal 会屏蔽当前注册的信号
会打断当前 A 信号的信号处理函数,去执行 B 信号的信号处理函数,等 B 信号的信号处理函数执行完成返回后,再继续去执行 A 信号的信号处理函数
系统调用重启特性
系统调用期间,可能收到信号,此时进程必须从系统调用中返回
对于执行时间较长的系统调用 (write / read),被信号中断的可能性很大
如果希望信号处理之后,被中断的系统调用能够重启,则:可以通过 errno == EINTR 判断重启系统调用
系统调用重启示例代码
系统调用重启特性
System V 风格的 signal 函数
- 系统调用被信号中断后,直接返回 -1,并且 errno == EINTR
BSD 风格的 signal 函数
- 系统调用被中断,内核在信号处理函数结束后,自动重启系统调用
注意事项
并非所有的系统调用对信号中断都表现同样的行为
- 一些系统调用支持信号中断后自动重启
- read(),write(),wait(),waitpid(),ioctl(),...
- 一些系统调用完全不支持信号中断后自动重启
- poll(),select(),usleep(),...
三种方法的区别
现代信号处理注册函数
sigaction() 函数比 signal() 函数更加灵活,sigaction() 函数可以指定信号的一些特性,比如:ONTSHOT、屏蔽自身和其他信号、重启系统调用等
由于 signal() 函数在不同的 Linux 发行版中可能存在不同的特性,所以不推荐使用 signal() 来处理信号,推荐使用 sigaction() 函数来处理信号
现代信号处理语义分析
信号屏蔽与标记
sigset_t sa_mask
- 信号屏蔽:sa_mask = SIGHUP | SIGINT | SIGUSR1;
- 注意:并不是所有信号都可以被屏蔽,如:SIGKILL,SIGSTOP
int sa_flags
- 信号特性:sa_flags = SA_ONESHOT | SA_RESTART;
- 特殊特性 (SA_SIGINFO),信号处理时能够收到额外的附加信息
sa_flags 默认参数是不具备 ONESHOT 并且屏蔽自身、不会重启系统调用
信号状态小知识
信号产生
- 信号来源,如:SI_KERNEL,SI_USER,SI_TIMER,...
信号未决
- 从信号产生到信号被进程接收的状态 (处于未决状态的信号必然已经存在)
信号递达
- 信号送达进程,被进程接收 (忽略,默认处理,自定义处理)
信号屏蔽 vs 信号阻塞
信号屏蔽
- 信号处理函数执行期间,被屏蔽的信号不会被递送给进程 (针对多个信号)
- sa_mask = SIGHUP | SIGINT | SIGUSR1;
信号阻塞
- 信号处理函数执行期间,当前信号不会递送给进程 (当前信号)
- act.sa_flags = SA_RESTART | SA_NODEFER;
main.c
// #define _GNU_SOURCE
// #define _XOPEN_SOURCE 600
// #define _POSIX_C_SOURCE 200800L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void delay_handler(int sig)
{
int i = 0;
// sysv_signal(SIGINT, delay_handler);
printf("begin delay handler...\n");
for(i=0; i<5; i++)
{
printf("sleep %d ...\n", i);
sleep(1);
}
printf("end delay handler...\n");
}
void signal_handler(int sig)
{
printf("handler : sig = %d\n", sig);
}
int r_read(char* data, int len)
{
int ret = -1;
while( data &&
((ret = read(STDIN_FILENO, data, len-1)) == -1) &&
(errno == EINTR) )
{
printf("restart syscall mannually...\n");
}
if( ret != -1 )
{
data[ret] = 0;
}
return ret;
}
int main(void)
{
char buf[32] = {0};
signal(SIGINT, signal_handler);
// bsd_signal(40, signal_handler);
// sysv_signal(SIGINT, signal_handler);
// bsd_signal(SIGINT, delay_handler);
r_read(buf, 32);
printf("input: %s\n", buf);
return 0;
}
第 57 行,我们为用 signal() 函数为 SIGINT 信号注册了信号处理函数
我们使用 strace 来跟踪这个进程的系统调用
可以看出 signal() 函数就是调用 sigaction() 函数来实现的
现代信号处理注册示例
现代信号发送
sigqueue(...) 的黄金搭档是 sigaction(...)
sa_flags 设置 SA_SIGINFO 标志位,可使用三参数信号处理函数
现代信号处理函数的关键参数
其中有几个常用的参数,si_signo: 信号值;si_code: 信号是怎么产生的;si_pid: 信号是由哪个进程发送过来的;si_value: 发送方发送信号传递过来的参数
现代信号发送处理示例
test2.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main(int argc, char* argv[])
{
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
union sigval sv = {1234567};
printf("current pid(%d) ...\n", getpid());
printf("send sig(%d) to process(%d)...\n", sig, pid);
// kill(pid, sig);
sigqueue(pid, sig, sv);
raise(SIGINT);
while( 1 )
{
printf("while...\n");
sleep(1);
}
return 0;
}
第 18 行,使用 sigqueue() 函数给指定进程发送指定信号,并且携带了一个 int 类型参数
main2.c
// #define _GNU_SOURCE
//#define _XOPEN_SOURCE 600
//#define _POSIX_C_SOURCE 200800L
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void delay_handler(int sig)
{
int i = 0;
printf("begin delay handler...\n");
for(i=0; i<5; i++)
{
printf("sig(%d) - sleep %d ...\n", sig, i);
sleep(1);
}
printf("end delay handler...\n");
}
void signal_handler(int sig, siginfo_t* info, void* ucontext)
{
printf("handler : sig = %d\n", sig);
printf("handler : info->si_signo = %d\n", info->si_signo);
printf("handler : info->si_code = %d\n", info->si_code);
printf("handler : info->si_pid = %d\n", info->si_pid);
printf("handler : info->si_value = %d\n", info->si_value.sival_int);
}
int r_read(char* data, int len)
{
int ret = -1;
while( data &&
((ret = read(STDIN_FILENO, data, len-1)) == -1) &&
(errno == EINTR) )
{
printf("restart syscall mannually...\n");
}
if( ret != -1 )
{
data[ret] = 0;
}
return ret;
}
int main(void)
{
char buf[32] = {0};
struct sigaction act = {0};
act.sa_sigaction = signal_handler;
act.sa_flags = SA_RESTART | SA_SIGINFO;
sigaddset(&act.sa_mask, 40);
sigaddset(&act.sa_mask, SIGINT);
sigaction(40, &act, NULL);
sigaction(SIGINT, &act, NULL);
r_read(buf, 32);
printf("input: %s\n", buf);
return 0;
}
我们想要收到发送方发送信号携带的 4 个字节的数据时,必须使用 sigaction() 函数来注册信号,并且 sa_flags 要指定 SA_SIGINFO 参数
程序运行结果如下图所示:
main.out 进程在接收到信号值为 40 的信号后,成功获取到信号所携带的数据
思考
利用信号搞进程间通信靠谱吗???