三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

时间:2023-12-04 20:03:14

34.1 信号特点

  • 信号的发生是随机的,但信号在何种条件下发生是可预测的
  • 进程杠开始启动时,所有信号的处理方式要么默认,要么忽略;忽略是 SIGUSR1 和 SIGUSR2 两个信号,其他都采取默认方式(大多数是终止进程)。
  • 进程在调用 exec 函数后,原有信号的捕捉函数失效
  • 子进程的诞生总是继承父进程的信号处理方式
  • 在系统层面上信号的发生是可靠的
    • 在Linux 中的可靠性只保证一次,进程在处理信号期间,若发生同类型的信号不会丢失(内核会保留),会被延迟处理
    • 但同类型信号的多次发生只会保留一次,即被处理一次。
    • 若不同类型的信号发生也会被内核保留直接会被处理,处理完后再处理原有信号
  • 用户层面可靠性依赖于信号而执行的用户代码放置在信号处理程序内部执行是可靠的,否则不一定可靠
  • 在信号发生时,慢系统调用以被中断并在信号处理后系统调用会被重启
  • 在信号发生时,用户函数可以被中断,但不能被重启,沿着中断点继续执行
    • 在用户函数中要保证数据一致性,即可重入性,不要去访问全局变量和静态变量,堆中的变量若在函数内部分配没有关系,否则会出现不可重入性

34.2 信号集和信号屏蔽函数

34.2.1 信号集

  • 信号集为一个或多个信号的集合,主要用在信号屏蔽函数中
 #include <signal.h>
int sigemptyset(sigset_t *set);
  • 函数功能:将信号集清空,对应将所有信号屏蔽字置 0
  • 函数参数:
    • set        信号集合
#include <signal.h>
int sigfillset(sigset_t *set);
  • 函数功能:将所有信号加入到信号集中,对应将所有信号屏蔽字置 1
#include <signal.h>
int sigaddset(sigset_t *set, int signo);
  • 函数功能:将某个信号加入到信号集中,对应将信号屏蔽字某位置 1
#include <signal.h>
int sigdelset(sigset_t *set, int signo);
  • 函数功能:将某个信号从信号集中删除,对应将信号屏蔽字某位置 0

  上述函数返回,若成功返回0,出错返回 -1

 #include <signal.h>
int sigismember(sigset_t *set, int signo);
  • 函数功能:测试信号集中是否包含某个信号,对应判断信号屏蔽字某位是否置 1
  • 返回值:真,返回1,假,返回0;出错返回 -1

34.2.2 信号屏蔽函数

 #include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrct oset);
  • 函数功能:利用 set 去覆盖内核中信号屏蔽字, oset 存放原有的信号屏蔽字
  • 函数参数:
    • @ how
      • SIG_BLOCK:利用 set 中信号设置信号屏蔽字
      • 三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数
      • SIG_UNBLOCK:利用 set 中信号不设置信号屏蔽字
      • 三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数
      • SIG_SETMASK:利用 set 中信号去替换内核信号屏蔽字
      • 三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数
  • 返回:成功,返回0;失败,返回 -1
  • 说明:
    • 进程可以暂时屏蔽信号,使得进程在执行过程中发生的相应信号暂时被阻塞,等待进程解除信号屏蔽后,再由内核或驱动将信号传递给进程
    • 信号屏蔽可屏蔽程序执行过程中的中断
 #include <signal.h>
int sigpending(sigset_t *set);
  • 函数功能:获取信号未决字的内容
  • 返回值:成功返回0;出错返回 -1

34.2.3 信号屏蔽设置

  • 信号在处理过程中是被屏蔽的(置 1),处理完毕解除屏蔽(被置 0),可在函数可重入性中使用信号屏蔽技术
  • 内核中的 task_struct 中包含两个 32 位字(记录相关的信号信息),分别为信号屏蔽字 mask 和信号未决字 pending。
    • mask:
      • 共有 31 位(代表 1~31 号信号,0 号没有意义),每一位代表一个信号,初始为 0,若这位上发生信号,会被立即处理;若为 1(设置 1 则信号被屏蔽,设置 0 则信号不被屏蔽),则在该位上发生信号不会被处理,会延迟处理
    • pengding
      • 初始为 0,若 mask 中某一位为 1,但又发生了同样的信号,则在 pending 同样的位置会被置为 1,以便让进程知道该信号又发生过而进行延迟处理
  • 若干个信号一起设置为 0 或 1 称为信号集
  • 子进程继承父进程中的信号屏蔽字,而不继承信号未决字

34.2.4 获取信号屏蔽字

 #include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h> void out_set(sigset_t set)
{
int i;
for(i = ; i < ; i++){
if(sigismember(&set, i)) {
printf("%d\n", i);
}
}
} void sig_handler(int signo)
{
printf("begin process the %d\n", signo); /** 获得正在处理信号时内核中的信号屏蔽字的内容 */
sigset_t oset;///< 放置内核屏蔽字的内容
sigemptyset(&oset);
if((sigprocmask(SIG_BLOCK, NULL, &oset)) < ) {
perror("sigprocmask error");
}
out_set(oset);
printf("finish process the %d\n", signo);
} int main(void)
{
if(signal(SIGUSR1, sig_handler) == SIG_ERR) {
perror("signal sigusr1 error");
} if(signal(SIGUSR2, sig_handler) == SIG_ERR) {
perror("signal sigusr2 error");
} sigset_t oset;///< 放置内核屏蔽字的内容
printf("before signal occured mask:\n");
/** 清空信号集 oset */
sigemptyset(&oset);
/** 在信号发生前,获得信号屏蔽字的内容 */
if((sigprocmask(SIG_BLOCK, NULL, &oset)) < ) {
perror("sigprocmask error");
}
out_set(oset);
printf("process %d wait signal...\n", getpid());
pause();///< 进程暂停等待信号 printf("after signal occured mask:\n");
sigemptyset(&oset);
/** 在信号发生后,获得信号屏蔽字的内容 */
if((sigprocmask(SIG_BLOCK, NULL, &oset)) < ) {
perror("sigprocmask error");
}
out_set(oset);
return ;
}

测试:

  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

  在信号发生前,信号屏蔽字都为 0,然后发送 SIGUSR1 信号,信号集被设置为 10。在结束后,信号又被重新设置回 0.

34.2.5 解决函数不可重入性

 #include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> int g_v[];
int *h_v; ///< 堆中变量 void set(int val)
{
int a_v[]; int i = ;
for(; i < ; i++) {
a_v[i] = val;
g_v[i] = val;
h_v[i] = val;
sleep();
} printf("g_v:");
for(i = ; i < ; i++){
if(i != ) {
printf(", %d", g_v[i]);
}
else {
printf(", %d", g_v[i]);
}
}
printf("\n"); printf("h_v:");
for(i = ; i < ; i++){
if(i != ) {
printf(", %d", h_v[i]);
}
else {
printf(", %d", h_v[i]);
}
} printf("\n");
printf("a_v:");
for(i = ; i < ; i++){
if(i != ) {
printf(", %d", a_v[i]);
}
else {
printf(", %d", a_v[i]);
}
}
} void sig_handler(int signo)
{
if(signo == SIGTSTP){
printf("SIGTSTP occured\n");
set();
printf("\nend SIGTSTP\n");
}
} int main(void)
{
if(signal(SIGTSTP, sig_handler) == SIG_ERR){
perror("signal sigtstp error");
} h_v = (int *)calloc(, sizeof(int)); printf("begin running main\n");
//屏蔽信号(1~31)
sigset_t sigset;
sigemptyset(&sigset);
sigfillset(&sigset); ///<要屏蔽所有的信号
if(sigprocmask(SIG_SETMASK, &sigset, NULL) < 0){
perror("sigprocmask error");
}
set();
//解除信号屏蔽
if(sigprocmask(SIG_UNBLOCK, &sigset, NULL) < ){
perror("sigprocmask error");
}
printf("\nend running main\n");
return ;
}

  未加屏蔽之前的测试结果,开始运行后,按 CTRL+Z 进行中断:

  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

  加入上述信号屏蔽函数后:

  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

  可以看出,数值都可以正常输出。

34.2.6 查看信号未决字的内容

  在不断发同类型的信号的时候,查看信号未决字

  

 #include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> void out_set(sigset_t set)
{
int i = ;
for(; i < ; i++){
if(sigismember(&set, i)){
printf("%d,", i);
}
} printf("\n");
} void sig_handler(int signo)
{
printf("begin the signal handler\n");
int i = ;
sigset_t sigset;
for(; i < ; i++) {
sigemptyset(&sigset);
if(sigpending(&sigset) < ) {
perror("sigpending error");
}
else {
printf("pending signal:");
out_set(sigset);
sigemptyset(&sigset);
} printf("i is %d\n", i);
sleep();
} printf("end the signal handler\n");
} int main(void)
{
if(signal(SIGTSTP, sig_handler) == SIG_ERR){
perror("signal sigtstp error");
} printf("process %d wait signal...\n", getpid());
pause();///< 进程暂停等待信号
printf("process finished\n");
}

  只发送一次中断信号的结果:

  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

  信号未决字在发送一个信号的时候,再发送同类信号的时候,信号未决字才置1,

  连续发送两次信号:

  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数  三十四、Linux 进程与信号——信号特点、信号集和信号屏蔽函数

  可以看出信号被处理了两次,未决字发生了变化。

  同样可以发送超过两次同类信号,会发现也只会处理两次。