文章目录
- 信号捕捉操作sigaction
- pending表的清理
- 可重入函数
- 总结
信号捕捉操作sigaction
sigaction和signal差不多,但是比signal的功能要强大的多。
第一个参数是信号编号,表示我们要捕捉的信号编号,第二个参数和第三个参数是内置的结构体类型,我们看看:
第一个结构体成员表示捕捉信号时,捕捉到信号之后调用的方法,第二个与实时信号有关不用管,第三个成员是 sa_mask,稍后着重讲解,第四个成员默认设置为0,第五个成员这个字段通常被设置为nullptr。
sa_mask:
我们写一段代码方便理解这个字段:
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;//打印block表
void PrintBlock()
{sigset_t set,oset;sigemptyset(&set);sigemptyset(&oset);sigprocmask(SIG_BLOCK,&set,&oset);for(int i = 31;i > 0;i--){if(sigismember(&oset,i)){cout<<1;}else cout<<0;}
}void handler(int signo)
{while(true){cout<<"get a new signal:"<<signo<<endl;PrintBlock();sleep(1);}
}int main()
{struct sigaction act,oact;//创建结构体对象act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaddset(&act.sa_mask,2);sigaddset(&act.sa_mask,3);sigaddset(&act.sa_mask,4);sigaction(2,&act,&oact);while(true){//等待信号pause();}return 0;
}
我们将block表打印出来可以看见,block表上屏蔽了三个信号
所以得出结论:我们自定义屏蔽信号的list可以直接放入sa_mask中。
pending表的清理
pending表是在处理信号后清除的,还是在处理信号前清除的?
代码验证:
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;
void PrintPending()
{sigset_t pending;sigpending(&pending);for(int i = 31;i > 0;i--){if(sigismember(&pending,i)) cout<<1;else cout<<0;}
}void handler(int signo)
{while(true){cout<<"get a new signal:"<<signo<<endl;PrintPending();sleep(1);}
}int main()
{struct sigaction act,oact;//创建结构体对象act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaction(2,&act,&oact);while(true){//等待信号pause();}return 0;
}
我们将自定义方法死循环,如果在执行自定义方法的过程中,pending已经置零了,那么可以下结论,pending表是在信号处理之前清零的,正如下图。
现在我们再来验证一个问题,就是自定义执行方法会不会递归调用,比如当我们在信号到来时,我们调用自定义方法,然后我们将自定义执行方法改为死循环,让他一直执行自定义执行方法,然后我们再次发送信号,当信号到达时是否又要调用函数?如果我们一直发送信号是不是会一直调用自定义执行方法函数,如果会一直调用,那么存在栈溢出问题,因为一直调用,每个函数死循环栈迟早会满的,我们来验证一下这个问题:
代码:
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;//打印block表
void PrintBlock()
{sigset_t set,oset;sigemptyset(&set);sigemptyset(&oset);sigprocmask(SIG_BLOCK,&set,&oset);for(int i = 31;i > 0;i--){if(sigismember(&oset,i)){cout<<1;}else cout<<0;}
}void PrintPending()
{sigset_t pending;sigpending(&pending);for(int i = 31;i > 0;i--){if(sigismember(&pending,i)) cout<<1;else cout<<0;}
}int cnt = 0;void handler(int signo)
{cnt++;while(true){cout<<"get a new signal:"<<signo<<" cnt:"<<cnt<<endl;PrintPending();sleep(1);}
}int main()
{struct sigaction act,oact;//创建结构体对象act.sa_handler = handler;sigemptyset(&act.sa_mask);sigaction(2,&act,&oact);while(true){pause();}return 0;
}
我们定义一个全局变量,当信号触发,多次调用函数时,全局变量会++,如果全局变量没有++,则说明,操作系统是防止了自定义方法递归调用的,说明信号只能串行,不能并行,当一个信号执行完才能执行下一个信号。
操作系统时如何实现这种机制的?—操作系统在收到信号时,会屏蔽对应信号,保证信号是串行的,可以尝试打印block表观察。
可重入函数
可重入函数是指 可以被多个线程或多个信号处理程序同时调用,而不会导致未定义行为 的函数。它们不依赖共享的可变状态,也不会引起竞态条件(Race Condition)。
我们假定一个进程中有两个执行流,在main函数中有一个执行流,在信号的自定义执行方法中又是一个执行流,当我们执行主执行流时,我们定义一个链表,这个链表是全局的,主执行流头插节点1,当节点1指向头节点时,产生信号,立马跳到了第二个执行流,执行自定义方法,但是自定义方法也是头插节点,node2指向头节点,最后head指向node2,自定义方法执行完了之后,回到主执行流之后,head会改变指向,指向node1,执行完流程之后,我们会发现node2没有释放,所以我们可以得出一个结论就是当有信号介入的时候可能会出现内存泄露,所以上面举的例子叫做不可重入函数,当执行可重入函数的时候是不会出问题的,我们以前见到的百分之90都是不可重入函数,只有很少一部分是可重入函数。
常见的可重入函数
以下函数在多线程或信号处理中是安全的,它们不依赖全局状态,也不会修改共享资源。
函数 | 说明 |
---|---|
strlen() | 计算字符串长度,仅读取内存,不修改数据 |
memcpy() | 复制内存区域,使用局部变量,不依赖全局状态 |
strcpy() | 复制字符串,但调用者需保证目标缓冲区足够大 |
gettimeofday() | 获取当前时间,数据存放在用户提供的结构体中 |
memcmp() | 比较两个内存块的内容,仅进行只读操作 |
strchr() | 查找字符串中的字符,仅进行只读操作 |
strncpy() | 复制字符串,使用固定长度,避免缓冲区溢出 |
isdigit() | 判断字符是否为数字,仅查询静态数据,不修改全局状态 |
常见的不可重入函数
这些函数内部使用了全局或静态变量,或者依赖非线程安全的操作,因此在多线程环境下可能导致竞态条件。
函数 | 问题 |
---|---|
malloc() / free() | 使用全局堆管理结构,多个线程同时调用可能导致数据竞争 |
printf() / scanf() | 使用全局缓冲区,多个线程同时输出可能导致数据混乱 |
ctime() | 返回指向静态缓冲区的指针,多线程调用会导致数据覆盖 |
rand() | 使用全局种子 seed ,多个线程调用会影响随机数结果 |
strtok() | 使用静态变量保存状态,导致多线程同时解析字符串时出现问题 |
asctime() | 返回静态缓冲区中的字符串,多次调用会覆盖上次结果 |
gethostbyname() | 使用静态数据存储 DNS 解析结果,导致数据竞争 |
setenv() / getenv() | 修改或访问全局环境变量,可能导致竞态条件 |
总结
本篇文章介绍了 sigaction
进行信号捕捉的基本用法,详细分析了 pending
表的清理机制,并探讨了可重入函数的特性及其在多线程和信号处理中的重要性。理解这些概念有助于编写更健壮的 Linux 应用程序,避免由于信号处理或多线程环境中的竞态条件导致的不确定行为。在实际开发中,合理使用 sigaction
及线程安全的函数,可以有效提升程序的稳定性和可靠性。