信号的阻塞与递达

news2024/9/30 11:38:59

目录

阻塞信号

信号和相关概念

sigset_t 信号集

信号的保存与阻塞

第一个问题

第二个问题

第三个问题

信号的递达

信号递达

内核态与用户态

为什么可以从用户态到内核态

我们怎么知道现在是内核态还是用户态?

信号的处理

sigaction

可重入函数

volatile 关键字

SIGCHLD


阻塞信号

信号和相关概念

当我们一个进程不想要关心某个信号的时候,我们应该怎么做呢?

那么有没有办法就是可以完全不用管某一个信号呢?

其实时有办法的——阻塞信号!

我们知道,信号是不一定会立马被处理的,所以信号就有一个保存的过程!

信号产生————————>信号保存————————>信号处理

信号未决 信号递达

  • 我们把处理信号的过程叫做信号递达

  • 当信号从产生到信号处理的过程叫做信号未决

  • 进程可以选择阻塞某个信号

  • 当一个信号被阻塞时,即使信号产生了(信号未决),信号也不会被递达,知道进程解除了信号阻塞,信号才会被递达。

  • 虽然说阻塞和忽略看起来都没有处理信号,但实际上忽略也是处理递达的一种,而阻塞时没有处理。

前面我们说的信号阻塞,那么究竟什么是阻塞呢?

阻塞实际上就是进程不需要去处理这个信号,即使该信号已经被OS写入到PCB中和信号有关的变量中了。

实际上,在PCB中关于信号的不只是有一张信号是否产生的表!!!

在PCB中关于信号的实际上并不是只有一张表,而是有三张。

其中有 block 表,表示该信号是否被阻塞,这张表和上一次说关于记录信号是否未决的表是一样的。

还有就是 pending 表,该表就是我们说的用来记录信号是否未决的表。

还有一个是 handler 表,该表是用来记录遇到对应信号的处理方法的,可能是 SIG_IGN 表示忽略, 还可以是 SIG_DFL 表示默认, 还有就是自定义,我们自己的方法,然后通过 signal 函数设置到 handler 表中。

这三张表的对应是意义对应的,这三张表并不是分别来看的,而是一行一行来看的。

所以当系统检查是否有信号的时候,就会遍历 pending 表,如果 pending 有二进制位为 1 那么就需要检查 block 表,如果没有被阻塞,也就是 block 表对应的二进制位为 0,就可以调用里面的方法了,但是调用的话是下面这样调用吗?

handler[i]();// 是这样吗?

并不是这样,实际上还需要检查一下是否为 SIG_DFL / SIG_IGN,而这两个实际上就是 0 和 1, 由 0,1强转成一个函数指针,也就是 。

typedef void(*sighandler_t)(int);// 函数指针
(sighandler_t)0;
(sighandler_t)1;

就是这样,所以在调用 handler 之前,需要检查。

if((int)handler[i] == 0)
{
    // 调用默认
}
else if((int)handler[i])
{
    // 忽略
}
else
{
    // 调用
    handler[i]();
}

sigset_t 信号集

sigset_t 是一个OS 给我们提供的一个类型,这个类型可以获取到信号里面的 pending 表,还可以设置 block 表,这个 sigset_t 和 pending 表是一模一样的,而这个就叫做信号集。

在介绍下面的内容前,先介绍一下关于信号的操作。

前面,我们只学习了一个关于捕捉的一个函数 signal 函数,对 handler 表进行处理,所以当然我们还需要对 block 表也进行处理。

首先,我们先看一下对于信号集处理的一些函数:

NAME
       sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations.
​
SYNOPSIS
       #include <signal.h>
​
       int sigemptyset(sigset_t *set);
​
       int sigfillset(sigset_t *set);
​
       int sigaddset(sigset_t *set, int signum);
​
       int sigdelset(sigset_t *set, int signum);
​
       int sigismember(const sigset_t *set, int signum);
  • 第一个函数就是将一个信号集变量清空,便于我们添加想要阻塞的信号。

  • 第二个函数就是将信号集都置为 1。

  • 第三个函数就是添加信号,为一个信号集。

  • 第四个函数就是删除一个信号集中的一个信号。

  • 第五个信号就是检测一个信号是否在信号集中。

上面是对信号集的操作,下面看一下关于获取pending 信号集和,对 block 表的操作函数:

NAME
       sigpending - examine pending signals
​
SYNOPSIS
       #include <signal.h>
​
       int sigpending(sigset_t *set);
  • 该函数作用就是获取当前 pending 表

NAME
       sigprocmask - examine and change blocked signals
​
SYNOPSIS
       #include <signal.h>
​
       int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • 这个函数就是对 block 表的操作的函数。

  • 其中第一个参数表示想要进行哪种操作

    1. SIG_BLOCK: 表示将一个信号设置到当前的信号集中。

    2. SIG_UNBLOCK: 表示将一个信号从当前信号集中去掉。

    3. SIG_SETMASK: : 表示将一个信号集直接给另一个信号集。

  • 第二个参数就是想要设置的信号集。

    1. 如果是 SIG_BLOCK的话,那么就是需要把想要设置的信号添加到该信号集中。

    2. 如果是 SIG_UNBLOCK 的话,那么就是将想要去掉的信号添加到该信号集中。

    3. 如果是SIG_SETMASK的话,那么就是将新的信号集添加到该信号集中。

  • 第三个参数是一个返回值参数,就是将旧的信号集返回,如果不需要旧的信号集的话,那么可以设置为空。

信号的保存与阻塞

既然信号可以保存,也可以阻塞,还有就是我们上一次的那个问题(如果对所有的信号都进行捕捉,那么这个进程是不是不会被信号杀死了?)

所以这次我们来回答几个问题:

1.如果对所有的信号进行捕捉,那么该进程是不是不会被信号杀死了?

2.如果我们对一个信号进行阻塞,那么它是不是旧不会被递达,既然不会被递达,那么是不是我们就可以看到它的 pending 表里面的对于的信号由 0->1 了, 如果我们在解除阻塞没那么是不是又可以看到由 1->0 了?

3.如果我们对所有的信号进程阻塞,那么是不是所有的信号都不会被递达了,既然不会被递达,那么是不是意味着该信号是不是不会被信号杀死了?

下面我们来回答这三个问题,其中我们可以通过做实验的方式,通过现象看本质来回答:

第一个问题

下面我们将写一个代码,对1~31 所有的信号都进行捕捉,然后我们看一下是否该进程真的不会被信号所杀死?

void catchSig(int signu)
{
  cout << "捕捉到了一个信号: "<< signu << endl;
}
​
void test1()
{
  // 对信号进行捕捉
  for(int sig = 1; sig <= 31; ++sig)
    signal(sig, catchSig);
  while(1) sleep(1);
}

这里我们对所有的信号都进行捕捉,然后我们的处理方法都是讲捕捉到的信号打印出来!

结果:

// 向该程序发送的信号
[lxy@hecs-348468 blockSig]$ ps axj | grep signal 
15205 16474 16474 15205 pts/0    16474 S+    1000   0:00 ./signal
15009 16478 16477 15009 pts/1    16477 R+    1000   0:00 grep --color=auto signal
[lxy@hecs-348468 blockSig]$ kill -1 16474
[lxy@hecs-348468 blockSig]$ kill -2 16474
[lxy@hecs-348468 blockSig]$ kill -3 16474
[lxy@hecs-348468 blockSig]$ kill -5 16474
[lxy@hecs-348468 blockSig]$ kill -8 16474
[lxy@hecs-348468 blockSig]$ kill -11 16474
[lxy@hecs-348468 blockSig]$ kill -16 16474
[lxy@hecs-348468 blockSig]$ kill -22 16474
​
// 该程序处理信号
[lxy@hecs-348468 blockSig]$ ./signal 
捕捉到了一个信号: 1
捕捉到了一个信号: 2
捕捉到了一个信号: 3
捕捉到了一个信号: 5
捕捉到了一个信号: 8
捕捉到了一个信号: 11
捕捉到了一个信号: 16
捕捉到了一个信号: 22
捕捉到了一个信号: 30

那么该程序真的就不会被信号杀死了吗?

并不是的,实际上,信号中有一个管理员信号,这个信号并不能被捕获,就是 9 号信号!

下面我们在看一下结果:

[lxy@hecs-348468 blockSig]$ ./signal 
Killed
​
[lxy@hecs-348468 blockSig]$ ps axj | grep signal 
15205 16474 16474 15205 pts/0    16474 S+    1000   0:00 ./signal
15009 16478 16477 15009 pts/1    16477 R+    1000   0:00 grep --color=auto signal
[lxy@hecs-348468 blockSig]$ kill -9 16474

这里当我们发送 9 号信号的时候,该进程就被 kill 掉了!

实际上不光 9 号信号不能被捕获,还有 19 号信号也是不能被捕获的,而 19 号信号就是停止一个进程。

第二个问题

当我们阻塞一个信号的时候,该信号不会被递达,我们向该进程发送对于的信号,我们可以看到对于的二进制位由0 变 1。

我们这里阻塞 2 号信号:

sigset_t blockSig(int signu)
{
  // 对 signu 号信号进行阻塞
  sigset_t bset, obset;
  // 对两个 sigset_t 类型的变量进行清空
  sigemptyset(&bset);
  sigemptyset(&obset);
  // 添加 signu 信号
  sigaddset(&bset, signu);
  // 添加对 signu 信号的阻塞
  sigprocmask(SIG_BLOCK, &bset, &obset);
  return obset;
}
​
void printPending(sigset_t& pending)
{
  for(int sig = 1; sig <= 31; ++sig)
  {
    if(sigismember(&pending, sig)) cout << '1';
    else cout << '0';
  }
  cout << endl;
}
​
void test2()
{
  // 对 2 号信号进程阻塞
  sigset_t obset = blockSig(2);
  // 下面我们就打印 pending 查看是否会递达
  sigset_t pending;
  while(true)
  {
    // 获取 pending 表
    sigpending(&pending);
    // 打印 pending 表
    printPending(pending);
    sleep(1);
  }
}

这里我们上来就是对 2 号信号进程阻塞,然后我们向该一直打印进程的 pending 表,如果 pending 表中有信号了,那么对于的二进制位就会由0 变 1。

结果:

// 结果
[lxy@hecs-348468 blockSig]$ ./signal 
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
^\Quit
​
// 发送信号
[lxy@hecs-348468 blockSig]$ ps axj | grep signal 
15009 18096 18096 15009 pts/1    18096 S+    1000   0:00 ./signal
15205 18098 18097 15205 pts/0    18097 R+    1000   0:00 grep --color=auto signal
[lxy@hecs-348468 blockSig]$ kill -2 18096

这里看到 2 号二进制位确实由 0 变 1 了,那么我们还想再看到由 1 变 0 可以吗?

void test2()
{
  // 对 2 号信号进程阻塞
  sigset_t obset = blockSig(2);
  // 下面我们就打印 pending 查看是否会递达
  sigset_t pending;
  int count = 20;
  while(true)
  {
    // 获取 pending 表
    sigpending(&pending);
    // 打印 pending 表
    printPending(pending);
    if(count-- == 0)
    {
      sigset_t ubset;
      sigemptyset(&ubset);
      sigaddset(&ubset, 2);
      sigprocmask(SIG_UNBLOCK, &ubset, nullptr);
      cout << "对 2 号信号取消阻塞成功" << endl;
    }
    sleep(1);
  }
}

上面的打印 pending 和 blockSig 函数和前面都是一样的,只是再 blockSig 函数中添加了一句打印信息。

这里我们再 20 秒后,我们进行对 2 号信号进行解除阻塞。

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
对 2 号信号阻塞成功
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
​
[lxy@hecs-348468 blockSig]$ 

为什么这里没有看到由 1 变 0 呢? 因为解除阻塞后,2 号信号就会递达,但是 2 号信号的默认动作就是终止进程,我们并没有捕获2 号进程,所以我们就看到进程终止了,我们也可以对 2 号进程进行捕获一下:

void test2()
{
  signal(2, catchSig);
  // 对 2 号信号进程阻塞
  sigset_t obset = blockSig(2);
  // 下面我们就打印 pending 查看是否会递达
  sigset_t pending;
  int count = 20;
  while(true)
  {
    // 获取 pending 表
    sigpending(&pending);
    // 打印 pending 表
    printPending(pending);
    if(count-- == 0)
    {
      sigset_t ubset;
      sigemptyset(&ubset);
      sigaddset(&ubset, 2);
      sigprocmask(SIG_UNBLOCK, &ubset, nullptr);
      cout << "对 2 号信号取消阻塞成功" << endl;
    }
    sleep(1);
  }
}

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
对 2 号信号阻塞成功
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
捕捉到了一个信号: 2
对 2 号信号取消阻塞成功
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^\Quit

这里就看到 pending 表里面的 2 号信号由 1 变 0 了。

第三个问题

那么如果我们将所有的进程都全部阻塞,那么该进程会被会被信号杀死呢?

void test3()
{
  // 阻塞所有信号
  for(int sig = 1; sig <= 31; ++sig)
    blockSig(sig);
  // 打印 pending 表
  sigset_t pending;
  while(true)
  {
    sigpending(&pending);
    printPending(pending);
    sleep(1);
  }
}

这里我们对所有的信号都进行阻塞,然后我们打印 pending 表。

然后我们向该进程发送信号,我们看能否杀掉:

0000000000000000000000000000000
0000000000000000000000000000000
1000000000000000000000000000000
1000000000000000000000000000000
1100000000000000000000000000000
1110000000000000000000000000000
1110000000000000000000000000000
1111000000000000000000000000000
1111000000000000000000000000000
1111100000000000000000000000000
1111100000000000000000000000000
1111100000000000000000000000000
1111110000000000000000000000000
1111110000000000000000000000000
1111111000000000000000000000000
1111111000000000000000000000000
​
[lxy@hecs-348468 blockSig]$ ps axj | grep signal 
15205 19751 19751 15205 pts/0    19751 S+    1000   0:00 ./signal
15009 19753 19752 15009 pts/1    19752 R+    1000   0:00 grep --color=auto signal
[lxy@hecs-348468 blockSig]$ kill -1 19751
[lxy@hecs-348468 blockSig]$ kill -2 19751
[lxy@hecs-348468 blockSig]$ kill -3 19751
[lxy@hecs-348468 blockSig]$ kill -4 19751
[lxy@hecs-348468 blockSig]$ kill -5 19751
[lxy@hecs-348468 blockSig]$ kill -6 19751
[lxy@hecs-348468 blockSig]$ kill -7 19751

这里看到,我们前面发的信号都已经被阻塞了,都没由递达,那么真的是这样吗?

其实我们前面再第一个问题就已经说了, 9 号信号是管理员信号既然无法被捕捉,那么也当然不可被阻塞,所以我们一定是可以使用 9 号信号来进行杀掉该进程的。

0000000000000000000000000000000
0000000000000000000000000000000
Killed
​
[lxy@hecs-348468 blockSig]$ ps axj | grep signal 
15205 19751 19751 15205 pts/0    19751 S+    1000   0:00 ./signal
15009 19753 19752 15009 pts/1    19752 R+    1000   0:00 grep --color=auto signal
[lxy@hecs-348468 blockSig]$ kill -9 19751

这里就被 kill 掉了。

信号的递达

前面我们一直再说信号不一定立即会处理,一般会在合适的时候处理信号,那么什么是合适的时候呢?

信号递达

内核态与用户态

不知道还记不记得骂我们再进程地址空间的时候说了,每一个进程都有属于自己的进程地址空间,而再32位计算机下进程地址空间的下面3G是属于用户的进程地址空间,而到最上面的1G是内核地址空间。

而信号是什么时候被处理的呢?

合适的时候就是,由内核态回到用户态的时候!!!

这里扯到了内核态和用户态。

那么什么又是内核态和用户态呢?

由于操作系统中是提供了一些系统调用,还有一些属于操作系统的代码,这些操作系统的代码时不能让普通用户来执行的没所以此时执行这部分代码就需要让内核来执行,而内核来执行的话,就是内核态,而用户态只能执行用户自己地址空间上的代码,也就是下面的 3G 地址空间上的内容。

那么我们又时如何从用户态到内核态的呢?

实际上代码再汇编之后会有一句 int 80 这句代码就是让用户态变为内核态,那么这句代码再那里呢?

因为我们有时候会调用系统调用,这句代码就在系统调用里面。

而当我们执行完系统调用之后,会从内核态返回用户态,而返回的时候,系统就会检测 pending 表,然后处理对应的信号,而合适的时候就是当进程从内核态回到用户态的时候!!

为什么可以从用户态到内核态

那么我们为什么可以从用户到内核呢?

还是因为进程地址空间

实际上,用户进程地址空间时每一个进程独立的,而内核进程地址空间时操作系统的代码和数据。

那么既然是操作系统的代码和数据,如果每一个进程都有一份,那么是不是就会造成数据冗余?

所以,实际上内核进程地址空间里面的数据可以理解为所有进程共享的,因为是操作系统的代码,所以每一个进程所共享!

而我们知道,再进程访问数据的时候,是需要通过页表映射的,而之前说的页表其实是用户级页表,是用来映射用户进程地址空间的代码和数据的,而还有内核级页表,内核级页表是用来映射内核的代码和数据的。

所以当我们访问操作系统的代码和数据的时候,同样是再进程地址空间上,就像我们再调用别人的库的时候,动态库会加载到共享区,所以我们调用相应的代码的时候就会跳到共享区执行对应的代码,而调用系统调用也同样的道理,当我们需要调用系统调用的时候,也是跳到内核的进程地址空间,然后执行操作系统代码。

我们怎么知道现在是内核态还是用户态?

那么再我们执行代码的时候,我们怎么知道当前是内核态还是用户态?

再CPU中有一个寄存器里面保存的就是当前的状态是用户还是内核态。

而且我们知道内核态的权限一定是很高的!

既然我们知道当前是内核态还是用户态,我们也知道如何从内核态转移到用户态,那么我们就可以理解信号的处理是从内核态返回用户态的时候处理的了。

信号的处理

前面我们一句将用户态和内核态简单的介绍了一下。

那么假设我们现在有一段代码,这段代码里面可能会有系统调用,发送中断,或者其他情况进入内核态。

那么如果我们不进行系统调用就不会进入内核态吗?

并不是这样的,我们的操作系统基本都是分时操作系统,时间片轮转,所以当一个操作系统检测到一个进程的时间片到了的时候,自动会将该进剥离下来,换下一个进程运行,当我们把下一个进程的上下文恢复后,我们就需要从内核态切换到用户态,此时也是内核态切换到用户态,所以即使没有系统调用,我们也会进行内核态到用户态的变换。

下面先看一下信号处理的步骤:

  1. 当前正在执行一段代码。

  2. 进行系统调用/中断/其他情况进入内核态。

  3. 当处理完系统调用等操作,返回用户态的时候,就会顺便检测 pending 表。

  4. 检测 pending 表发现里面有 1 ,并且 block 表对应的二进制位不为 1,此时处理该信号。

    1. 如果此时的 handler 为 SIG_DFL 那么就会执行默认的信号处理动作, 处理前将对应的进制位改为0。

    2. 如果此时的 handler 为 S_G_IGN 那么就会直接将 pending 表里面的对应的二进制位改为0.

    3. 第三种情况就是自定义,但是自定义是用户的代码(这里先说结果),自定义的话,那么此时就会再一次从内核态切换到用户态,然后执行自定义方法,执行结束后再一次从用户态切换到内核态,然后返回到用户态,此时返回的时候还需要返回到上一次调用系统调用的是代码的下面。

  5. 上面就是信号处理的整个流程!

上面谈到了当信号的处理方法是自定义的时候(用户代码),那么就需要从内核态到用户态,为什么?

首先关于上面的这问题,我们提出一个疑问,内核态可以执行用户态的代码吗?

内核态可以执行用户态的代码,因为内核态的权限很高,所以一定可以执行。

那么为什么需要从内核态转移到用户态?

因为当我们执行自定义方法的时候,这方法是用户的代码,如果我们使用内核态执行代码,那么如果这段代码里面有对内核进行”破坏“的行为呢?

所以并不是内核态执行不了用户的代码,而是由于安全问题,内核态并不想执行用户的代码。

下面我们可以画一张图片来理解一下这个信号处理的过程:

实际上信号处理的整个流程就是这样。

其中我们的线段与用户到内核的那根横线有几个焦点,说明就有几次的状态改变。

sigaction

之前我们学习了关于信号的注册的一个函数 signal。

而我们还有一个函数也是关于信号注册的,sigaction,但是相比之下,我们还是比较常用的是 signal 函数,因为比较简单一点。

但是下面我们还是会介绍一下 sigaction 函数:

NAME
       sigaction - examine and change a signal action
​
SYNOPSIS
       #include <signal.h>
​
       int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • 该函数作用就是注册一个信号的处理方法。

  • 其中第一个参数就是想要注册处理方法的信号编号。

  • 第二个参数是一个OS系统提供的数据结构。

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • 上面的这个结构体中第一个变量就是想要拍注册的方法。

  • 其中二个参数,第四个,第五个,第六个参数我们并不关心。

  • 而第三个参数我们一会介绍。

我们先使用这个函数看一下:

void test4()
{
  struct sigaction act, oldact;
  act.sa_handler = catchSig;
​
  sigaction(2, &act, &oldact);// 对 2 号信号进行注册
  printf("%d\n", oldact.sa_handler);
  while(true) sleep(1);
}

这里首先我们需要两个 struct sigaction 的变量,用作 sigaction 的参数。

实际上不传入oldact 也可以,但是不传入的话,就获取不到之前的信号的各种数据了。

然后有了 act 变量后,需要对该变量进行初始化,需要将自定义方法设置到该变量中。

这里我们还打印了 oldact 中的 sa_handler变量,这个变量是什么呢?

实际上这个变量就是之前方法的处理动作,如果是 SIG_DFL 的话,那么就是0,如果是 SIG_IGN 的话,那么就是 1

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
flags: 0
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^\Quit

下面我们可以稍微修改一下代码,看一下 sa_handler是否与我们所说的相同:

void test4()
{
  signal(2, SIG_IGN);
  struct sigaction act, oldact;
  act.sa_handler = catchSig;
​
  sigaction(2, &act, &oldact);// 对 2 号信号进行注册
  printf("%d\n", oldact.sa_handler);
  while(true) sleep(1);
}

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
1
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^C捕捉到了一个信号: 2
^\Quit

现在我们有几个问题,由于信号的产生是异步的,所以当我们再处理一个信号的同时,这个信号又到了怎么办?而且我们这个信号的处理方法中又有一个系统调用,那么这样不就会陷入递归吗?

实际上,再信号的处理中,假设我们在处理i一个信号的同时,这个信号又到了,那么我们并不会处理这个信号,我们再处理这个信号的时候,这个信号的 block 表就会被设置,然后即使这个信号再一次到了,那么也不会处理这个信号了,只有当这个信号处理结束了,才会将 block 表中这个信号的阻塞给关闭。

下面我们可以看一下:

我们打算写一个关于 2 号信号的处理,让 处理 2 号信号的时候,2 号信号的处理动作里面sleep 一段时间,然后此时我们继续发送 2 号信号给该进程。

void handler(int signum)
{
  cout << "捕捉到了一个信号:" << signum << endl;
  sigset_t pending;
  for(int i = 0; i < 20; ++i)
  {
    sleep(1);
    sigpending(&pending);
    for(int sig = 1; sig <= 31; ++sig)
      if(sigismember(&pending, sig))cout  << "1";
      else cout << "0";
    cout << endl;
  }
​
}
​
void test5()
{
  signal(2, handler);
  while(true)
  {
    sleep(1);
  }
}

这里我们在处理 2 号信号的时候,我们可以打印 pending 表,然后再处理 2 号信号的时候,我们继续发送 2 号信号,我们看一下 pending 表里面的 2 号信号会不会未决。

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
捕捉到了一个信号:2
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
Quit
​
[lxy@hecs-348468 blockSig]$ pidof signal 
27126
[lxy@hecs-348468 blockSig]$ kill -2 27126
[lxy@hecs-348468 blockSig]$ kill -2 27126
[lxy@hecs-348468 blockSig]$ kill -3 27126

这里第一次发送 2 号信号然后程序捕捉到 2 号信号,第二次发送 2 号信号,然后就检测到 2 号信号未决。

经过我们这个理解,我们可以看一下 struct sigaction 结构体中的第三个对象了,假设我们现在再处理一个信号的时候,我们同时也想阻塞其他的信号,那么我们就可以添加到 sa_mask 中。

void test6()
{
  struct sigaction act, oldact;
  act.sa_handler = handler;// 添加处理方法
  sigemptyset(&act.sa_mask);
  sigfillset(&act.sa_mask);
  sigaction(2, &act, &oldact);
  while(true) sleep(1);
}

这里使用的 handler 返回发和上面的相同,所以这里就不写了。

这里我们将 sa_mask 全部填满,就是为了再处理 2 号信号的时候将其他的信号全部阻塞。

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
捕捉到了一个信号:2
0000000000000000000000000000000
0000000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
1100000000000000000000000000000
1100000000000000000000000000000
1100000000000000000000000000000
1100000000000000000000000000000
1100000000000000000000000000000
1110000000000000000000000000000
1110000000000000000000000000000
1110000000000000000000000000000
1111000000000000000000000000000
1111000000000000000000000000000
1111000000000000000000000000000
1111000000000000000000000000000
1111000000000000000000000000000
1111100000000000000000000000000
Illegal instruction
​
[lxy@hecs-348468 blockSig]$ pidof signal 
28195
[lxy@hecs-348468 blockSig]$ kill -2 28195
[lxy@hecs-348468 blockSig]$ kill -2 28195
[lxy@hecs-348468 blockSig]$ kill -1 28195
[lxy@hecs-348468 blockSig]$ kill -3 28195
[lxy@hecs-348468 blockSig]$ kill -4 28195
[lxy@hecs-348468 blockSig]$ kill -5 28195
[lxy@hecs-348468 blockSig]$ kill -6 28195

这里是因为 2 号信号处理完了,所以又处理了一个信号,然后程序退出。

可重入函数

这里我们直接看一个函数,我们就知道什么是可重入函数的了,什么是不可重入函数。

我们现在有一个链表的头插函数,当我们这个链表头插的时候了一半,然后突然到了一个函数,这个信号的处理方法里面也有一句头插的代码,插入之后然后由内核态返回到用户态继续执行之前代码,然后此时完成用户态头插的最后一步,那么现在信号中头插入的节点就内存泄露了。

这个就是不可重入函数。

实际上可重入函数和不可重入函数只是函数的特性,并没有好坏之分。

其实这里的话,我们的代码并没有出现问题。只是不可重入函数再做特定的事情的时候,有时候需要限制一下。

那么一般什么是不可重入函数呢?

一般我们见到的函数里面80%都是不可重入函数,包括前面C++中的STL中的也是不可重入函数。

不可重入函数中一般有对全局变量的是修改,或者是 malloc/new 或者 free/delete 一般有这些的都是不可重入函数。

volatile 关键字

下面我们介绍一个关键字, volatile。

我们先看一个场景:

void test7()
{
  const int n = 10;
  int* p = (int*)&n;
  *p = 20;
  cout << "n: " << n << " *p: " << *p << endl;
}

这个代码会编译不通过吗?还是会运行奔溃,又或者是会输出?那么会输出的话又会输出什么?都是10 还是不一样呢?还是都是20?

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
n: 10 *p: 20

为什么会这样呢?

实际上,n 的值已经被修改了,但是由于编译器认为 const 的值是不可以被修改的,所以这里编译器做了一定的优化,再看到输入 n 的时候直接就是输出 10,但是实际上已经被修改了。

那么我们怎么知道已经被修改了呢?

volatile 关键字加到变量前面就是每一次取这个变量的值都需要到内存中去取,所以我们现在把 vloatile 关键字加到 n 前面,看一下是否被修改了:

void test7()
{
  volatile const int n = 10;
  int* p = (int*)&n;
  *p = 20;
  cout << "n: " << n << " *p: " << *p << endl;
}

结果:

[lxy@hecs-348468 blockSig]$ ./signal 
n: 20 *p: 20

SIGCHLD

不知道还记不记得我们再进程等待那里,父进程需要等待子进程。

该信号实际就是当子进程退出的时候,子进程然后系统就会发SIGCHLD信号给父进程。

一方面是父进程为了获取子进程的退出状态,还有一方面是为了解决如果不等待的话,就会有资源泄露。

但是SIGCHLD这个信号默认是忽略的。

SIGCHLD   20,17,18    Ign     Child stopped or terminated

那么默认忽略是什么呢?

我们可以创建子进程看一下默认忽略是什么:

void test9()
{
  if(fork() == 0)
  {
    sleep(5);
    exit(0);
  }
  sleep(10);
  wait(nullptr);
  sleep(10);
}

这里现象我就不呈现出来了,这里说一下就好了。

首先这里运行该程序,然后就会有两个进程,过了5秒后,子进程退出,然后此时子进程就是僵尸进程,又过了5秒后,子进程被回收,然后父进程又继续跑十秒。

这个就是我们之前进程等待时的现象,那么如果我们不wait呢?

void test9()
{
  if(fork() == 0)
  {
    sleep(5);
    exit(0);
  }
  sleep(10);
  //wait(nullptr);
  sleep(10);
}

这里我们没有进行等待,会又什么现象呢?

如果没有进行进程等待的话,就是当程序运行5秒之后,然后子进程僵尸,知道父进程运行完毕,子进程也一直都是僵尸。

那么我们看到 SIGCHLD 信号上面显示忽略,那么这个忽略有什么作用呢?

下面我们自己设置 SIGCHLD 信号忽略,我们看一下有什么不一样吗?

void test9()
{
  signal(SIGCHLD, SIG_IGN);
  if(fork() == 0)
  {
    sleep(3);
    exit(0);
  }
  sleep(6);
  wait(nullptr);
  sleep(10);
}

这里我们对SIGCHLD进行了忽略,我们看一下结果:

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
30882 30883 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
30882 30883 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
30882 30883 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal

过了 3 秒后,子进程没有进入僵尸直接退出了,那么我们不等待的话会怎么样?

void test9()
{
  signal(SIGCHLD, SIG_IGN);
  if(fork() == 0)
  {
    sleep(3);
    exit(0);
  }
  sleep(6);
  //wait(nullptr);
  sleep(10);
}

实际上,我们通过上面结果我们也就知道了,我们本意是想要子进程僵尸3秒,但是子进程推出后就直接退出了。所以我们也一定知道,即使我们没有等待,子进程也会退出。

结果:

PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
30882 30883 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
30882 30883 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
30882 30883 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal
-------------------
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
25931 30882 30882 25931 pts/0    30882 S+    1000   0:00 ./signal

结果和上面一样。

这就是信号里面的所有内容~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1180970.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Stable Diffusion源码调试(一)

Stable Diffusion源码调试&#xff08;一&#xff09; 个人模型主页&#xff1a;https://liblib.ai/userpage/369b11c9952245e28ea8d107ed9c2746/model Stable Diffusion版本&#xff1a;https://github.com/AUTOMATIC1111/stable-diffusion-webui/releases/tag/v1.4.1 调试t…

使用 CountDownLatch 实现多线程协作

目录 前言 在多线程编程中&#xff0c;经常需要实现一种机制来协调多个线程的执行&#xff0c;以确保某些操作在所有线程完成后再进行。CountDownLatch 就是 Java 并发包中提供的一种同步工具&#xff0c;它能够让一个或多个线程等待其他线程完成操作。 了解 CountDownLatch …

嵌入式软件工程师面试题——2025校招社招通用(十)

说明&#xff1a; 面试题来源于网络书籍&#xff0c;公司题目以及博主原创或修改&#xff08;题目大部分来源于各种公司&#xff09;&#xff1b;文中很多题目&#xff0c;或许大家直接编译器写完&#xff0c;1分钟就出结果了。但在这里博主希望每一个题目&#xff0c;大家都要…

芯片无线升级,给产品和芯片买个保险

例如&#xff0c;想让卧室灯过于刺眼&#xff0c;需要稍微暗一个度。 目前来说常见的只能重新买了重新安装&#xff1f;&#xff01; 可都已经安装的好的电灯&#xff0c;实在是食之无味&#xff0c;弃之可惜。 这时候产品不拆换&#xff0c;还可以升级就显得尤为重要了。 为了…

React 其他常用Hooks

1. useImperativeHandle 在react中父组件可以通过forwardRef将ref转发到子组件&#xff1b;子组件拿到父组件创建的ref&#xff0c;绑定到自己的某个元素&#xff1b; forwardRef的做法本身没有什么问题&#xff0c;但是我们是将子组件的DOM直接暴露给了父组件&#xff0c;某下…

C++ http协议POST body raw 字段向服务器发送请求

环境&#xff1a;ubuntu系统c使用http协议不是很方便&#xff0c;通过curl库我们可以很方便使用http协议&#xff0c;由于我的请求方式比较特殊&#xff0c;在网上没有找到相关的资料&#xff0c;之前使用python实现过一版&#xff0c;但是当设备数量超过100台时&#xff0c;程…

FPGA时序分析与约束(10)——生成时钟

一、概述 最复杂的设计往往需要多个时钟来完成相应的功能。当设计中存在多个时钟的时候&#xff0c;它们需要相互协作或各司其职。异步时钟是不能共享确定相位关系的时钟信号&#xff0c;当多个时钟域交互时&#xff0c;设计中只有异步时钟很难满足建立和保持要求。我们将在后面…

软件性能测试指标分享,第三方检测机构进行性能测试的好处

在现代科技发展迅猛的时代背景下&#xff0c;软件的性能表现对于用户体验和企业竞争力至关重要。软件性能测试是通过对软件系统进行一系列的测试&#xff0c;以评估其在各种工作条件下的性能表现。这些工作条件可以包括并发用户数、数据量、网络传输速度等。软件性能测试的目的…

[动态规划] (十一) 简单多状态 LeetCode 面试题17.16.按摩师 和 198.打家劫舍

[动态规划] (十一) 简单多状态: LeetCode 面试题17.16.按摩师 和 198.打家劫舍 文章目录 [动态规划] (十一) 简单多状态: LeetCode 面试题17.16.按摩师 和 198.打家劫舍题目分析题目解析状态表示状态转移方程初始化和填表顺序 代码实现按摩师打家劫舍 总结 注&#xff1a;本题与…

python 之 列表推导式

文章目录 基本结构示例 1&#xff1a;将列表中的元素乘以 2 添加条件判断示例 2&#xff1a;筛选出偶数并加倍 嵌套列表推导式示例 3&#xff1a;生成九九乘法表 使用条件表达式示例 4&#xff1a;根据条件返回不同的值 镶嵌使用详细介绍基本结构示例生成二维数组多重筛选和操作…

软件测试需求分析是什么?为什么需要进行测试需求分析?

在软件开发中&#xff0c;软件测试是确保软件质量的重要环节之一。而软件测试需求分析作为软件测试的前置工作&#xff0c;对于保证软件测试的顺利进行具有重要意义。软件测试需求分析是指对软件测试的需求进行细致的分析和规划&#xff0c;以明确测试的目标、任务和范围&#…

vuecli3 批量打印二维码

安装以个命令: npm install qrcode --save npm install print-js --save 页面使用: import qrcode from qrcode import printJS from print-js <el-button type"primary" click"handleBulkPrint">批量打印</el-button>methods: {// 批量打印…

发布成绩看这里

你是否曾经在成绩发布时手忙脚乱&#xff0c;为处理大量的成绩数据而感到烦恼&#xff1f;现在&#xff0c;让我们一起探讨如何利用代码和Excel实现学生自助查询成绩的功能。 一、使用Excel处理成绩数据 收集成绩数据首先需要将学生的成绩数据收集起来。最方便的方法是使用Exce…

明星和KOL的影响力是医美产品推广的加速器

在当今时代&#xff0c;越来越多的人开始关注自身外貌和健康。医美类产品应运而生&#xff0c;为人们的美丽和自信带来了无限可能。然而&#xff0c;面临激烈的市场竞争&#xff0c;医美类产品在营销推广方面必须做出差异化和创新化的努力&#xff0c;才能取得成功。 一、打造独…

OpenAI 首届开发者大会-亮点多多

正如 Sam Altman 此前所言&#xff0c;OpenAI 首届开发者大会为人们带来了一些非常棒的新东西。 继今年春天发布 GPT-4 之后&#xff0c;OpenAI 又创造了一个不眠夜。 过去一年&#xff0c;ChatGPT 绝对是整个科技领域最热的词汇。OpenAI 也依靠 ChatGPT 取得了惊人的成绩&…

鸿蒙原生应用开发-DevEco Studio本地模拟器的使用

使用Local Emulator运行应用/服务 DevEco Studio提供的Local Emulator可以运行和调试Phone、TV和Wearable设备的HarmonyOS应用/服务。在Local Emulator上运行应用/服务兼容签名与不签名两种类型的HAP。 Local Emulator相比于Remote Emulator的区别&#xff1a;Local Emulator是…

一文掌握 Apache SkyWalking

Apache SkyWalking SkyWalking是一个开源可观测平台&#xff0c;用于收集、分析、聚合和可视化来自服务和云原生基础设施的数据。SkyWalking 提供了一种简单的方法来保持分布式系统的清晰视图&#xff0c;甚至跨云。它是一种现代APM&#xff0c;专为云原生、基于容器的分布式系…

idea Error: java: OutOfMemoryError: insufficient memory处理

IDEA设置里&#xff0c;修改heap size更大一点&#xff0c;可以解决问题

Solidity快速入门之函数输出

返回值return和returns Solidity有两个关键字与函数输出相关&#xff1a;return和returns&#xff0c;他们的区别在于&#xff1a; returns加在函数名后面&#xff0c;用于声明返回的变量类型及变量名&#xff1b;return用于函数主体中&#xff0c;返回想要返回的变量&#x…

1560分钟一节课VUE项目从入门到精通

在职场&#xff0c;流传着这样一句话&#xff1a;跳槽加薪是现实&#xff0c;原地加薪是梦想。工作跳一跳&#xff0c;工资翻一番。 事实好像确实如此&#xff0c;相关机构调研发现&#xff0c;跳槽换工作后的平均加薪幅度能达到36%&#xff01; ▲ 图源网络&#xff0c;如侵删…