什么是信号机制
信号(signal)机制是Linux系统中最为古老的进程之间的通信机制。Linux信号也可以称为软中断,是在软件层次上对中断机制的一种模拟。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达, 信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程发生了什么。
信号的发生有两个来源:
(1)硬件来源,比如我们按下了键盘或者其他硬件信号触发,硬件异常如除以零运算,内存非法访问。
(2)软件来源,最常见发送信号的系统函数 kill() raise() alarm() 和 setitimer() 等函数以及ctl+c发出SIGINT、ctl+z SIGTSTP、ctl+\ SIGQUIT。
Linux系统中定义了一系列的信号,可以使用 kill -l 命令列出所有的信号。
Linux的信号机制是从Unix继承下来的,早期Unix系统只定义了32种信号,现在Linux支持64种信号。
信号集
信号集顾名思义就是信号的集合,信号集的类型为sigset_t,每一种信号用1bit来表示,前面我们提到信号有64种,那么这个sigset_t类型至少占64bit,可以通过sizeof(sigset_t)来查看。
每个进程的PCB进程控制块中都有两个信号集,一个叫作未决信号集,一个叫作信号屏蔽字,信号集的每一位不是0就是1,初始状态下,两个信号集的值都为0。
当有信号传递到该进程的时候,未决信号集的对应位设置为1,其他位不变,这个时候信号只是传递到进程,并未被处理,叫作未决状态。常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。用户只能获取未决信号集值,无法改变其值。
未决信号想要递达程序的信号处理函数(默认、忽略、自定义),还要经过信号屏蔽字的过滤,一旦该信号对应bit为1,则该信号将阻塞,不能传递到信号的处理函数。用户可以设置获取信号屏蔽字的值。
信号集处理函数
sigprocmask,调用函数sigprocmask可以读取或更改进程的信号屏蔽字。
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前, 至少将其中一个信号递达。
读取当前进程的未决信号集,sigpending:
sigpending获取的是未决信号集,不能获取信号关键字
综合示例
#include
#include
void printsigset(const sigset_t *set)
{
int i;
for (i = 1; i < 32; i++)
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
puts("");
}
int main(void)
{
sigset_t s, p;
sigemptyset(&s);
sigaddset(&s, SIGINT); //将信号集s中的SIGINT置为1
sigprocmask(SIG_BLOCK, &s, NULL); //设置信号屏蔽字
while (1)
{
sigpending(&p); //获取未决信号集
printsigset(&p); //打印未决信号集
sleep(1);
}
return 0;
}
信号传递过程
sigaction函数
#include
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
#include
#include
void sig_handle(int sig)
{
puts("recv SIGINT");
sleep(5);
puts("end");
}
int main(int argc, char** argv)
{
struct sigaction act;
act.sa_handler = sig_handle;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT); //当进入信号处理函数的时候,屏蔽掉SIGQUIT的递达
sigaction(SIGINT, &act, NULL);
while(1)
sleep(1);
return 0;
}
进程对信号的响应和处理
进程可以通过三种方式来响应和处理一个信号:
(1)忽略信号:即对信号不做任何处理,但是两个信号不能忽略: SIGKILL 以及 SIGSTOP .
(2)捕捉信号:当信号发生时,执行用户定义的信号处理函数。
(3)执行默认操作: Linux对每种信号都规定了默认操作,man 7 signal查看。
进程对收到的信号有对应的缺省操作,如果进程要自定义处理某个信号,那么就要在进程中安装该信号。
信号之间不存在相对的优先权。信号在产生时也并不马上送给进程,信号必须等待直到进程再一次被调度运行。
read函数的EINTR错误
#include
#include
#include
#include
#include
void sig_handle(int sig)
{
printf("SIGINT\n");
}
i
nt main(int argc, char** argv)
{
char buf[10];
struct sigaction act;
act.sa_handler = sig_handle;
act.sa_flags = 0;
//act.sa_flags = SA_RESTART; //先试一试sa_flags为0时,然后再试一试SA_RESTART的情况
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, NULL);
puts("read stdio:");
int ret = read(STDIN_FILENO, buf, 9);
if(ret == -1)
{
if(errno == EINTR)
perror("read:");
}
else
{
buf[ret] = '\0';
printf("read %d : %s", ret, buf);
}
return 0;
}
以上的运行结果的是,act.sa_flags = 0时一旦发送了一个信号,read将被打断,返回-1,并且设置errno为
EINTR。如果想打断后继续运行read,可以设置一下act.sa_flags = SA_RESTART。或者也可以使用signal函数来处理信号,signal相当于默认设置了SA_RESTART标志。
可重入函数
不含全局变量和静态变量是可重入函数的一个要素, 可重入函数见man 7 signal, 在信号捕捉函数里应使用可重入函数,在信号捕捉函数里禁止调用不可重入函数。
例如:strtok就是一个不可重入函数,因为strtok内部维护了一个内部静态指针,保存上一次切割到的位置,如果信号的捕捉函数中也去调用strtok函数,则会造成切割字符串混乱,应用strtok_r版本,r表示可重入。
#include
#include
#include
#include
static char buf[] = "hello world good book";
void sig_handle(int sig)
{
strtok(NULL, " ");
}
int main(int argc, char** argv)
{
signal(SIGINT, sig_handle);
printf("%s\n", strtok(buf, " "));
printf("%s\n", strtok(NULL, " "));
sleep(5); //可以被信号打断,返回剩余的时间,想想看这个函数应该怎么调用
printf("%s\n", strtok(NULL, " "));
return 0;
}
运行的时候,发现通过Ctrl+c发射信号与没发射信号的结果不一样,可以改用strtok_r函数。