目录

1.面试题回顾

2.复习与信号相关的三张表

3.与信号集相关的操作函数

sigemptyset

sigfillset

★特别提醒

sigismember

sigprocmask

sigpending

sigaddset和sigdelset

★总结修改3个表的方法

修改handler表

修改block表

修改pending表

4.代码示范

按键测试2号信号是否被屏蔽

打印block表说明2号信号被屏蔽

打印pending表说明Ctrl+C被按下

解除屏蔽,打印pending表

5.能不能屏蔽所有信号?

反思: 为什么9号和19号信号无法被屏蔽?

结论

6.回答问题: 0号信号虽然没有,但具体需不需要在sigset_t位图中占位?

glibc验证

Linux内核验证

结论


1.面试题回顾

参见OS62.【Linux】面试题: 信号集是什么? 在Linux中是如何实现的?文章

2.复习与信号相关的三张表

参见OS61.【Linux】理解信号: 信号的发送和保存文章

3.与信号集相关的操作函数

与信号相关的三张表分别是handler表、block表、pending表,其中handler表、block表的底层含有sigset_t位图

该位图对于每种信号用一个比特位表示“有效”或“无效”状态,至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的

使用者只能调用系统提供的函数来操作sigset_ t实例化后的对象,而不应该手动操作内部数据(因为不同的类Unix系统的底层实现不一样,例如对sigset赋值是没有意义的)

sigemptyset

int sigemptyset(sigset_t *set);

作用: 顾名思义,将信号集的位图设为全0

sigfillset

int sigfillset(sigset_t *set);

作用: 顾名思义,将信号集的位图设为全1

★特别提醒

在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的
状态

sigismember

int sigismember(const sigset_t *set, int signum);

作用: 判断某个信号是否在信号集中

sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

作用: 读取或更改进程的block表,即信号屏蔽字,又称阻塞信号集

注: sigprocmask的proc是process的简称

参数how: 从下面的3个宏选一个,表示处理方法:

返回值: 若成功则为0,若出错则为-1

参数set: 输入型参数,是让sigprocmask操作的信号集

参数oset: 输出型参数(oset的o是output的首字母),作用是保存旧的信号集,保存的目的是为了之后恢复

如果调用sigprocmask解除了对当前若干个未决信号的屏蔽,则在sigprocmask返回前,至少将其中一个信号递达

注: 如果set为NULL,block表不会发生任何变化,但是oset存储当前进程的block表的内容

sigpending

int sigpending(sigset_t *set);

作用: 读取当前进程的未决信号集,通过set参数传出,即set是输出型参数

返回值: 若成功则为0,若出错则为-1

sigaddset和sigdelset

int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

作用: sigaddset向设置某个信号在信号集中对应的比特位为1,sigdelset向设置某个信号在信号集中对应的比特位为0

★总结修改3个表的方法

修改handler表

使用signal函数,之前在OS57.【Linux】理解信号: 信号的产生(1) 键盘输入和kill命令文章讲过

修改block表

使用sigprocmask函数

修改pending表

使用sigpending函数

4.代码示范

设置图中的SIGINT信号

从图中得知: 2号信号SIGINT: 被屏蔽(block[1]==1),进程当前收到了(pending[1]==1),处理函数为SIG_IGN

2号信号在被屏蔽的情况下,按下Ctrl+C,penging表的2号信号的对应的比特位值应该从0变1,写代码演示

新建如下文件:

test_signal/
├── makefile
└── test_signal.cpp

makefile写入:

test_signal.out:test_signal.cpp
	g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
	rm -f test_signal.out

定义sigset_t set对象,注意: 一定要初始化!

sigset_t set;
//必须初始化
if (sigemptyset(&set))
{
    std::cout<<"无法初始化信号集"<<std::endl;
    exit(1);
}

屏蔽2号信号前,先设置set中2号信号对应的比特位为1:

if (sigaddset(&set,2))
{
    std::cout<<"无法设置信号集的掩码"<<std::endl;
    exit(2);
}

此时set还没有写入进程,set在当前进程的栈区上,必须借助sigprocmask系统调用才能写入内核真正生效

屏蔽2号信号:

//修改block表,用sigprocmask
if (sigprocmask(SIG_SETMASK,&set,&oset))
{
    std::cout<<"无法设置当前进程的信号集"<<std::endl;
    exit(2);
}

按键测试2号信号是否被屏蔽

#include <signal.h>
#include <iostream>
int main()
{
    sigset_t set,oset;
    //必须初始化
    if (sigemptyset(&set))
    {
        std::cout<<"无法初始化信号集"<<std::endl;
        exit(1);
    }
    if (sigaddset(&set,2))
    {
        std::cout<<"无法设置信号集的掩码"<<std::endl;
        exit(2);
    }

    //修改block表,用sigprocmask
    if (sigprocmask(SIG_SETMASK,&set,&oset))
    {
        std::cout<<"无法设置当前进程的信号集"<<std::endl;
        exit(2);
    }
    std::cout<<"现在可以按下Ctrl+C测试了"<<std::endl;
    for (;;);
    return 0;
}

运行结果:无论怎么按Ctrl+C都无法退出进程

打印block表说明2号信号被屏蔽

利用sigprocmask的特殊情况: 如果set为NULL,block表不会发生任何变化,但是oset存储当前进程的block表的内容

得到oset后,再调用sigismember逐个判断每个信号即可

这里只判断1~31号信号:

void print_block_bitmap()
{
    sigset_t oset;
    //必须初始化
    if (sigemptyset(&oset))
    {
        std::cout<<"无法初始化信号集"<<std::endl;
        exit(1);
    }
    if (sigprocmask(SIG_SETMASK,NULL,&oset))
    {
        std::cout<<"无法获取当前进程的block信号集"<<std::endl;
        exit(2);
    }
    for (int i=1;i<=31;i++)
        if (sigismember(&oset,i))
            std::cout<<i<<"号信号被屏蔽"<<std::endl;
        else
            std::cout<<i<<"号信号未屏蔽"<<std::endl;
}

运行结果:

打印pending表说明Ctrl+C被按下

Ctrl+C被按下操作系统向进程发送2号信号,pending表中2号信号对应的比特位为1,说明该信号已经产生(操作系统已经发送给进程),但当前被屏蔽,处于"等待处理"状态

但这里不能对pending进行位操作,必须借助sigpending系统调用:

void print_pending_bitmap()
{
    sigset_t oset;
    std::cout<<std::endl;
    if (sigemptyset(&oset))
    {
        std::cout<<"无法初始化信号集"<<std::endl;
        exit(1);
    }
    if (sigpending(&oset))
    {
        std::cout<<"无法获取当前进程的pending信号集"<<std::endl;
        exit(2);
    }
    for (int i=1;i<=31;i++)
    if (sigismember(&oset,i))
        std::cout<<i<<"号信号被阻塞,处于等待处理状态"<<std::endl;
     else
        std::cout<<i<<"号信号未产生"<<std::endl;
}

运行结果:

解除屏蔽,打印pending表

为了防止2号信号导致进程终止,这里捕获2号信号

这里使用之前保存的oset来解除屏蔽 (注: 是否屏蔽不影响信号的处理函数)

void myhandler(int){} 
signal(2,myhandler);

//解除阻塞
//修改block表,用sigprocmask
//使用之前保存的oset来恢复
if (sigprocmask(SIG_SETMASK,&oset,nullptr))
{
    std::cout<<"无法设置当前进程的信号集"<<std::endl;
    exit(2);
}
sleep(2);
print_pending_bitmap();

运行结果:

5.能不能屏蔽所有信号?

做个实验:

#include <signal.h>
#include <iostream>
//省略print_block_bitmap()定义
int main()
{
    //必须初始化
    sigset_t set;
    if (sigfillset(&set))
    {
        std::cout<<"无法初始化信号集"<<std::endl;
        exit(1);
    }

    //修改block表,用sigprocmask
    if (sigprocmask(SIG_SETMASK,&set,nullptr))
    {
        std::cout<<"无法设置当前进程的信号集"<<std::endl;
        exit(2);
    }
    print_block_bitmap();
    return 0;
}

运行结果: 1~31号信号中,9号和19号信号无法被屏蔽

反思: 为什么9号和19号信号无法被屏蔽?

理由和OS57.【Linux】理解信号: 信号的产生(1) 键盘输入和kill命令文章提到的"反思: 为什么进程无法捕获9和19号信号?"一样

结论

结论: 9号和19号无法被屏蔽,无法被捕获,无法被忽略

6.回答问题: 0号信号虽然没有,但具体需不需要在sigset_t位图中占位?

这是OS61.【Linux】理解信号: 信号的发送和保存遗留的问题:0号信号虽然没有,但具体需不需要在sigset_t位图中占位?

换句话说,sigset_t位图的最低位表示1号信号? 还是位图的最低位用于占位,没有实际意义?

glibc验证

在glibc-2.42的/signal/sigismem.c中定义了sigismember函数:

/* Return 1 if SIGNO is in SET, 0 if not.  */
int
sigismember (const sigset_t *set, int signo)
{
  if (set == NULL || signo <= 0 || signo >= NSIG)
    {
      __set_errno (EINVAL);
      return -1;
    }

  return __sigismember (set, signo);
}
libc_hidden_def (sigismember)

当signo==0时,被if (set == NULL || signo <= 0 || signo >= NSIG)拦截了,无法返回__sigismember (set, signo)

当signo==1时,执行__sigismember (set, signo)

/sysdeps/unix/sysv/linux/sigsetops.h中定义了__sigismember:

/* Return a mask that includes the bit for SIG only.  */
#define __sigmask(sig) \
  (1UL << (((sig) - 1) % ULONG_WIDTH))

/* Return the word index for SIG.  */
static inline int
__sigword (int sig)
{
  return (sig - 1) / ULONG_WIDTH;
}


static inline int
__sigismember (const sigset_t *set, int sig)
{
  unsigned long int mask = __sigmask (sig);
  int word = __sigword (sig);
  return set->__val[word] & mask ? 1 : 0;
}

其中sigset_t的定义:

/signal/bits/types/sigset_t.h中:

/* A set of signals to be blocked, unblocked, or waited for.  */
typedef __sigset_t sigset_t;

x86下,__sigset_t 定义在/sysdeps/unix/sysv/linux/bits/types/__sigset_t.h中:

#ifndef ____sigset_t_defined
#define ____sigset_t_defined

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

调用过程:

__sigismember (set, 1) 

→  unsigned long int mask = __sigmask (1); 

        →  unsigned long int mask = (1UL << (((1) - 1) % ULONG_WIDTH))

        → unsigned long int mask = 1UL 

→ int word = __sigword (1);

        → __sigword (1)

        → return (sig - 1) / ULONG_WIDTH

        → return (1- 1) / ULONG_WIDTH

        → int word = 0;

→ return set->__val[word] & mask ? 1 : 0;

        → return set->__val[0] & 1UL ? 1 : 0;

发现1号信号找的是__val[0]!

Linux内核验证

/arch/x86/include/asm/signal.h中定义了sigaddset宏:

#define sigaddset(set,sig)		    \
	(__builtin_constant_p(sig)	    \
	 ? __const_sigaddset((set), (sig))  \
	 : __gen_sigaddset((set), (sig)))

static inline void __gen_sigaddset(sigset_t *set, int _sig)
{
	asm("btsl %1,%0" : "+m"(*set) : "Ir"(_sig - 1) : "cc");
}

static inline void __const_sigaddset(sigset_t *set, int _sig)
{
	unsigned long sig = _sig - 1;
	set->sig[sig / _NSIG_BPW] |= 1 << (sig % _NSIG_BPW);
}

其中__gen_sigaddset和__const_sigaddset的作用是等价的,__const_sigaddset实现有点复杂,但__gen_sigaddset函数内只有一个指令:

asm("btsl %1,%0" : "+m"(*set) : "Ir"(_sig - 1) : "cc");

提示已经非常明显了: "Ir"(_sig - 1),当_sig为1时, "Ir"(_sig - 1)为 "Ir"(0),发现设置1号信号,内部设置的是位图的最低位!

结论

结论: 底层上,位图的最低位表示1号信号

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐