Linux进程信号

发布于:2024-05-13 ⋅ 阅读:(149) ⋅ 点赞:(0)


img

Linux进程信号

1、信号概念

信号是Linux提供给用户(进程)给其他进程发送异步(Asynchronous)信息的一种方式,属于软中断。

进程看待信号的方式

  1. 信号在没有发生的时候,我们已经知道当发生的时候怎么处理了。
  2. 信号到来的时候,我们正在处理更重要的事情,我们暂时不能处理到来的信号,我们必须暂时要将到来的信号进行临时保存。
  3. 信号到了,可以不立即处理,可以到合适的时候处理。
  4. 信号的产生是随时产生的,我们无法预料,所以信号是异步发送的。

2、信号原理

这里浅谈,为后面多线程作准备。

2.1、信号原理

  1. 对共享资源进行保护,是一个多执行流场景下,一个比较常见和重要的话题。
  2. 互斥和同步
  • 互斥:在访问任何一部分共享资源的时候,任何时刻只有我一个人访问,这就是互斥。
  • 同步:访问资源在安全的前提下,具有一定的顺序性。
  1. 被保护起来的,任何时刻只允许一个执行访问的公共资源叫做临界资源。
  2. 访问临界资源的代码,我们叫做临界区。
  3. 原子性:操作对象的时候,只有两种状态,要么还没开始,要么已经结束。

2.2、信号量理论

  • 信号量本质:对资源的预订机制,资源不一定被我持有,才是我的,只要我预订了,在未来的某个时间,就是我的。举例:看电影 – 提前买票 – 入场。
  • 信号量:信号量本身是一个计数器,描述临界资源的数量的计数器。
  • 所有的进程,访问临界资源,就必须申请信号量。那么信号量就是一个共享资源,得让所有进程看到同一个信号量!
  • 信号量的申请(++)和释放(–)必须得是原子性的。因为++和–本身是一行代码,但是其汇编代码一般有3个操作,那么在进行进程切换的时候,如果是在这3个操作之间修改了信号量,那么信号量的值就是不准确的,可能还会出错。 – PV操作。

3、系统信号列表

使用命令行查看系统信号列表:kill -l

每个信号都有一个编号和宏定义名称,这些宏定义可以在signal.h文件中找到。例如#define SIGQUIT 3

编号34以上的信号是实时信号(优先级最高,收到得立即处理)。这里不讨论实时信号。

这些信号各自在什么条件下产生,默认处理动作是什么,可以在signal(7)手册中查看:man 7 signal


4、信号的常见处理方式

  1. 忽略该信号。
  2. 默认处理方式。
  3. 自定义处理信号:提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(catch)一个信号。

5、信号的产生

5.1、kill命令

代码:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{

   while (true)
   {
       cout << "I am process,pid :" << getpid() << endl;
       sleep(1);
   }

   return 0;
}

命令:kill -信号编号 进程号,如kill -2 1406628

运行结果:


5.2、键盘产生信号

  1. 按键按下了
  2. 哪些按键按下了
  3. 字符输入(字符设备),组合键输入(输入的是命令)。

比如输入abcd和按下ctrl + c ,是通过键盘驱动和操作系统进行联合解释的。

操作系统怎么知道输入数据了?通过硬件中断的技术。

代码:

#include <iostream>
#include <unistd.h>

using namespace std;

int main()
{

while (true)
{
  cout << "I am process,pid :" << getpid() << endl;
  sleep(1);
}

return 0;
}

命令:键盘按下 ctrl + c

运行结果:


5.3、系统调用

kill函数系统调用:使用信号sig杀死进程pid

代码:

#include <iostream>
#include <csignal>
#include <assert.h>
#include <unistd.h>
using namespace std;

int main()
{
   int cnt = 0;
   while (true)
   {
       if (cnt == 5)
       {
           int n = kill(getpid(), SIGINT);
           assert(n == 0);
       }
       cout << "I am process,pid :" << getpid() << endl;
       cnt++;
       sleep(1);
   }

   return 0;
}

运行结果:

raise函数系统调用:相当于调用kill(getpid(), sig)。

代码:

#include <iostream>
#include <csignal>
#include <assert.h>
#include <unistd.h>
using namespace std;

int main()
{
   int cnt = 0;
   while (true)
   {
       if (cnt == 5)
       {
           int n = raise(SIGQUIT);
           assert(n == 0);
       }
       cout << "I am process,pid :" << getpid() << endl;
       cnt++;
       sleep(1);
   }

   return 0;
}

运行结果:

abort函数系统调用:相当于发送SIGABRT信号给当前进程。

代码:

#include <iostream>
#include <csignal>
#include <assert.h>
#include <unistd.h>
using namespace std;

int main()
{
   int cnt = 0;
   while (true)
   {
       if (cnt == 5)
       {
           abort();
       }
       cout << "I am process,pid :" << getpid() << endl;
       cnt++;
       sleep(1);
   }

   return 0;
}

运行结果:


5.3.1、自定义处理信号

使用signal函数完成对信号的自定义处理。

代码:

#include <iostream>
#include <csignal>
#include <unistd.h>
using namespace std;

void handler(int signo)
{
    cout << "get a signal,no: " << signo << endl;
}

int main()
{

    // handler调用完了,handler方法会被立即执行吗?不是,只是设置对应信号的处理方法
    // 为未来我们收到对应的信号才执行handler方法
    // ctrl + c 退出进程其实也是用的SIGINT信号 可以使用signal(SIGINT, handler);来测试,也就是运行时候ctrl + c看会不会退出
    signal(SIGINT, handler); // ctrl + c 是SIGINT信号,编号2
    signal(SIGQUIT, handler); // ctrt + \ 是SIGQUIT信号,编号3

    while (true)
    {
        cout << "I am process,pid :" << getpid() << endl;
        sleep(1);
    }

    return 0;
}

运行结果:


5.3.2、自己封装一个命令行完成kill指令

代码:

#include <iostream>
#include <csignal>
#include <assert.h>
#include <string.h>
#include <unistd.h>

using namespace std;

int main(int argc, char *argv[])
{
   if (argc != 3)
   {
       cout << "Usage:" << argv[0] << " -signo pid" << endl;
       exit(1);
   }

   int signo = stoi(argv[1] + 1);

   int pid = stoi(argv[2]);

   int n = kill(pid, signo);
   if (n < 0)
   {
       cerr << "kill error ,errno:" << errno << ", error string: " << strerror(errno) << endl;
   }
   return 0;
}


5.4、软件条件

SIGPIPE是一种由软件条件产生的信号,之前在管道中使用过(父进程读,子进程写,父进程关闭读端,子进程就会收到SIGPIPE信号退出)。这里我们要讲的是SIGARLM信号和alarm函数。

alarm函数:如果second不为0,那么返回值就是上一个闹钟的剩余时间,如果second为0,就会取消之前的所有闹钟。

代码:设置闹钟,并处理闹钟。

#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;

void handler(int sig)
{

   int n = alarm(2); // 每隔2秒响一次 , alarm的返回值是:如果之前有闹钟还没到时间,这个返回值是最近的闹钟还剩余的时间,如果之前没有闹钟,那么返回0Í

   cout << "还剩:" << n << " 秒" << endl;
   // exit(0);
}

int main()
{

   signal(SIGALRM, handler);

   alarm(50); // 响一次

   int cnt = 0;

   while (true)
   {
       sleep(1);
       cout << "cnt :" << cnt++ << ", pid: " << getpid() << endl; // IO很慢
   }
   return 0;
}

运行结果:

代码:设置闹钟,取消闹钟

#include <iostream>
#include <csignal>
#include <unistd.h>

using namespace std;


void handler(int sig)
{
   // cout << "get a sig, signo:" << sig << ", g_val:" << g_val << endl;

   // alarm(2);
   // cout << "闹钟到啦!" << endl;

   int n = alarm(2); // 每隔2秒响一次 , alarm的返回值是:如果之前有闹钟还没到时间,这个返回值是最近的闹钟还剩余的时间,如果之前没有闹钟,那么返回0Í

   cout << "还剩:" << n << " 秒" << endl;
   // exit(0);
}

int main()
{

   signal(SIGALRM, handler);

   alarm(50); // 响一次

   int cnt = 0;

   while (true)
   {
       sleep(1);
       cout << "cnt :" << cnt++ << ", pid: " << getpid() << endl; // IO很慢
       if (cnt == 20)
       {
           // 取消所有闹钟
           int ret = alarm(0);
           cout << "ret: " << ret << endl;

       }
   }
   return 0;
}

运行结果:


5.5、异常

  1. 代码除0了 – SIGFPE
  2. 野指针错误 – SIGSEGV

代码:

#include <iostream>
#include <csignal>
#include <assert.h>
#include <string.h>
#include <unistd.h>

using namespace std;

// 异常  --  异常没有终止,操作系统还有正常执行进程调度,那么上下文会进行保护,下一次调度到该进程的时候又会恢复,进程在运行的时候就会一直异常
void handler(int sig)
{
   cout << "get a sig, signo: " << sig << endl;
}

int main()
{
   signal(SIGFPE, handler);
   signal(SIGSEGV, handler);
   signal(SIGQUIT, handler);

   //除0
   int a = 10;
   a /= 0;

   // 空指针
   // int *p = nullptr;
   // *p = 1;

   while (true)
   {
       cout << "pid :" << getpid() << endl;
       sleep(1);
   }

   return 0;
}

运行结果:


总结:

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

答:OS是进程的管理者

信号的处理是否是立即处理的?

答:不是,是在合适的时候(有可能是当时是阻塞的)。

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

答:需要暂时被记录下来放在pending位图中。

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

答:知道,非实时信号总共就31个,处理动作都已经设置好了。

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

答:


6、core dump 核心转储

6.1、core dump 核心转储概念

core dump核心转储是什么?

答:将进程在内核中的核心数据转储到磁盘中形成core或者core.pid(centos 7下是这样的形式)文件。

为什么要使用core dump核心转储?

答:想通过core定位到进程为什么退出,以及执行到哪句代码退出的。

core dump核心转储有什么用?

答:协助我们调试代码。


6.2、core dump 核心转储的配置

使用core dump功能

在使用core dump功能之前,我们先确认我们的启用核心转储功能的配置是否正确。

在Linux系统中,/proc/sys/kernel/core_pattern 文件用于控制当程序崩溃并产生core dump文件时,这些文件的命名和存储位置。默认的 core_pattern 设置可能因不同的Linux发行版和系统配置而异。

不过,一个常见的默认设置可能是:

core

或者可能是:

|/usr/lib/systemd/systemd-coredump %e %u %g %s %t %c %h

这里的 % 后面的字符是格式化的占位符,用于在core dump文件名中包含各种信息。例如:

  • %e:可执行文件名
  • %u:用户ID
  • %g:组ID
  • %s:信号导致的core dump
  • %t:core dump的时间戳(以秒为单位)
  • %c:产生core dump的CPU
  • %h:主机名

core_pattern 被设置为一个文件名(如 core)时,core dump文件通常会被创建在程序崩溃时所在的目录下,并且名为 core(或者可能带有 .pid 后缀,其中 pid 是崩溃进程的进程ID)。

如果 core_pattern 被设置为一个管道(如 |/path/to/program),那么core dump的内容会被发送到指定的程序,而不是写入到文件中。在上面的例子中,systemd-coredump 是systemd提供的用于处理core dump的程序。

你可以通过以下命令查看当前的 core_pattern 设置:

cat /proc/sys/kernel/core_pattern

这个命令将输出当前系统上 core_pattern 的设置值。

如果/proc/sys/kernel/core_pattern文件默认的内容是:|/usr/lib/systemd/systemd-coredump %e %u %g %s %t %c %h。那么我们需要把这个内容修改为core,不然我们就算开启了核心转储功能,在当前目录下也生成不了core文件。

先切换到更高权限的root用户下(该文件权限较高),在命令行输入:echo "core" > /proc/sys/kernel/core_pattern


6.3、core dump 核心转储的使用

testSig.cc文件:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

using namespace std;

int main() {

   pid_t id = fork();
   if (id == 0) {
       int a = 10;
       a /= 0;
       exit(100);
   }

   int status = 0;
   pid_t rid = waitpid(id, &status, 0);
   if (rid > 0) {
       cout << "exit code : " << ((status >> 8) & 0xFF) << endl; // 没有意义,因为是异常退出,那么exit(100)不会执行
       cout << "exit core dump : " << ((status >> 7) & 0x1) << endl;
       cout << "exit sig : " << (status & 0x7f) << endl;
   }
   return 0;
}

Makefile文件:

testSig:testSig.cc
	g++ -o $@ $^  -std=c++11 -g

.PHONY:clean
clean:
	rm -f testSig

查看核心转储功能是否开启,命令行输入:ulimit -a

观察core file size,如果是0,则未开启。

这时候执行程序不会生成core文件。

开启方法,在命令行输入:ulimit -c (size大小,单位是KB)。如:ulimit -c 10240

这时候会生成core文件。

这时候,我们进入调试模式,并输入core-file core。就可以找到代码异常了。


7、信号的保存

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

7.1、信号在内核中的表示

信号在内核中的表示示意图:

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,因为当在处理一个信号的时候,会将该信号阻塞,直到处理完该信号,那么处理中相同信号到来的时候会被阻塞,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

7.2、阻塞信号

7.2.1、sigset_t 信号集

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。下面将详细介绍信号集的各种操作。

#ifndef __sigset_t_defined
#define __sigset_t_defined 1

#include <bits/types/__sigset_t.h>

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

#endif
#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

sigset_t其实就是位图类型。


7.2.2、信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
  • 函数sigismember是查看某个信号是否在信号集中。

这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1


7.2.3、sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。

返回值:成功返回0,错误返回-1。

如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oldset和set都是非空指针,则先将原来的信号屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

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


7.2.4、sigpending

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。


7.2.5、阻塞信号和解除阻塞信号实验

场景:

  1. 屏蔽2号信号
  2. 获取进程的pending位图
  3. 打印pending位图中的收到的信号
  4. 解除对2号信号的屏蔽

代码:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <assert.h>

using namespace std;


void PrintSigset(const sigset_t &block)
{
   cout << "pending bitmap : ";
   for (int signo = 31; signo >= 1; --signo)
   {
       if (sigismember(&block, signo))
           cout << "1";
       else
           cout << "0";
   }
}


int main()
{

   // 1. 屏蔽2号信号
   sigset_t block; // 用户在栈上开辟了空间
   sigemptyset(&block);
   sigaddset(&block, 2);

   sigset_t oblock;
   // 1.1 开始屏蔽2号信号,其实就是设置进入内核中
   int n = sigprocmask(SIG_SETMASK, &block, &oblock);
   assert(n == 0);

   cout << "block 2 sig success ..." << endl;
   cout << "pid : " << getpid() << endl;

   int cnt = 1;
   while (true)
   {
       // 2. 获取进程的pending位图
       sigset_t pending;
       sigemptyset(&pending);
       n = sigpending(&pending);
       assert(n == 0);

       // 3. 打印pending位图中的收到的信号
       PrintSigset(pending);
       cout << "  cnt : " << cnt;
       cout << endl;

       // 4. 解除对2号信号的屏蔽
       if (cnt == 20)
       {
           cout << "解除对2号信号的屏蔽...." << endl;
           n = sigprocmask(SIG_UNBLOCK, &block, &oblock);
           assert(n == 0);
       }
       cnt++;
       sleep(1);
   }

   return 0;
}

运行结果:

细节:信号递达的时候,一定会把对应的pending位图的位置清0。那么是先清0再递达还是先递达再清0?答案是先清0再递达。

我们用下面代码验证:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <assert.h>

using namespace std;


void PrintSigset(const sigset_t &block)
{
   cout << "pending bitmap : ";
   for (int signo = 31; signo >= 1; --signo)
   {
       if (sigismember(&block, signo))
           cout << "1";
       else
           cout << "0";
   }
}

void handler(int sig)
{
   sigset_t pending;
   sigemptyset(&pending);
   int n = sigpending(&pending);
   assert(n == 0);

   cout << "递达中..." << endl;
   PrintSigset(pending); // 0 ,递达之前pending位图2号信号已经清零
   cout << endl;
   cout << sig << " 号信号已递达..." << endl;
}

int main()
{

   // 捕捉2号信号
   signal(2, handler);

   // 1. 屏蔽2号信号
   sigset_t block; // 用户在栈上开辟了空间
   sigemptyset(&block);
   sigaddset(&block, 2);

   sigset_t oblock;
   // 1.1 开始屏蔽2号信号,其实就是设置进入内核中
   int n = sigprocmask(SIG_SETMASK, &block, &oblock);
   assert(n == 0);

   cout << "block 2 sig success ..." << endl;
   cout << "pid : " << getpid() << endl;

   int cnt = 1;
   while (true)
   {
       // 2. 获取进程的pending位图
       sigset_t pending;
       sigemptyset(&pending);
       n = sigpending(&pending);
       assert(n == 0);

       // 3. 打印pending位图中的收到的信号
       PrintSigset(pending);
       cout << "  cnt : " << cnt;
       cout << endl;

       // 4. 解除对2号信号的屏蔽
       if (cnt == 20)
       {
           cout << "解除对2号信号的屏蔽...." << endl;
           n = sigprocmask(SIG_UNBLOCK, &block, &oblock);
           assert(n == 0);
       }
       cnt++;
       sleep(1);
   }

   return 0;
}

运行结果:

我们可以屏蔽所有信号吗?

我们用下面代码来验证:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <assert.h>

using namespace std;

void PrintSigset(const sigset_t &block)
{
   cout << "pending bitmap : ";
   for (int signo = 31; signo >= 1; --signo)
   {
       if (sigismember(&block, signo))
           cout << "1";
       else
           cout << "0";
   }
}

int main()
{

   // 1. 屏蔽2号信号
   sigset_t block; // 用户在栈上开辟了空间
   sigemptyset(&block);

   // test:屏蔽所有信号? 9,19号信号不能被屏蔽,18号特殊信号这里也不能屏蔽(看编译器)
   for (int i = 1; i <= 31; ++i)
       sigaddset(&block, i);

   sigset_t oblock;
   // 1.1 开始屏蔽2号信号,其实就是设置进入内核中
   int n = sigprocmask(SIG_SETMASK, &block, &oblock);
   assert(n == 0);

   cout << "block 2 sig success ..." << endl;
   cout << "pid : " << getpid() << endl;

   int cnt = 1;
   while (true)
   {
       // 2. 获取进程的pending位图
       sigset_t pending;
       sigemptyset(&pending);
       n = sigpending(&pending);
       assert(n == 0);

       // 3. 打印pending位图中的收到的信号
       PrintSigset(pending);
       cout << "  cnt : " << cnt;
       cout << endl;

       cnt++;
       sleep(1);
   }

   return 0;
}

运行结果:我们发现,9号信号不能屏蔽

我们发现19号信号也不能屏蔽

其他信号都可以屏蔽:18号信号特殊,在不同编译器下运行效果不同。


8、信号的处理

8.1、信号的捕捉过程

在信号处理(捕捉)的过程中,一共会有4次的状态切换(内核态和用户态)。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂。举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数。这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。


8.2、信号的捕捉

前面我们有谈到过信号的捕捉(自定义处理信号),使用signal函数调用。

这里我们介绍一个新的信号捕捉函数sigaction。

  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signum是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数(和signal函数调用的handler函数指针一样),通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然。这也是一个回调函数,不是被main函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止(不想让信号嵌套式进行捕捉处理,防止栈溢出)。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段。

使用sigaction函数:

代码:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <assert.h>

using namespace std;

void PrintSigset(const sigset_t &block)
{
   cout << "pending bitmap : ";
   for (int signo = 31; signo >= 1; --signo)
   {
       if (sigismember(&block, signo))
           cout << "1";
       else
           cout << "0";
   }
}

void handler(int sig)
{
   sigset_t pending;
   sigemptyset(&pending);
   while (true)
   {
       int n = sigpending(&pending);
       assert(n == 0);

       PrintSigset(pending);
       cout << endl;
       sleep(1);
   }
}

int main()
{
   cout << "pid : " << getpid() << endl;
   struct sigaction act, oact;
   act.sa_handler = handler;
   act.sa_flags = 0;
   sigemptyset(&act.sa_mask);

   sigaddset(&act.sa_mask, 2);
   sigaddset(&act.sa_mask, 3);
   sigaction(2, &act, &oact);

   while (true)
       sleep(1);
   return 0;
}

运行结果:


9、可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个结点而最后只有一个结点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数。反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?因为每个函数调用都有其独立的栈帧来存储局部变量和参数,这些栈帧是隔离的,因此不同的控制流程在调用同一个函数时不会相互干扰。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

10、volatile

volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。

我们来看以下代码:

#include <stdio.h>
#include <signal.h>


int flag = 0;
void handler(int sig)
{
   printf("chage flag 0 to 1\n");
   flag = 1;
}
int main()
{
   signal(2, handler);
   while (!flag);

   printf("process quit normal\n");
   return 0;
}

命令行输入:gcc sig.c

运行结果:ctrl + c 后进程退出了。因为捕捉2号信号后flag变为1,那么在while(!flag);就直接跳过循环。

命令行输入:gcc sig.c -O1。注意这个是大写的O,不是数字0。

运行结果:进程并没有退出!原因是:捕捉2号信号后flag变为1。但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile,不让flag放到寄存器中。

修改后的代码:flag加上volatile关键字。

#include <stdio.h>
#include <signal.h>

volatile int flag = 0;
void handler(int sig)
{
   printf("chage flag 0 to 1\n");
   flag = 1;
}
int main()
{
   signal(2, handler);
   while (!flag);

   printf("process quit normal\n");
   return 0;
}

运行结果:ctrl + c 后进程退出了。因为捕捉2号信号后flag变为1,那么在while(!flag);就直接跳过循环。


11、SIGCHLD信号

  • 进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
  • 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

代码:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

using namespace std;

void CleanupChild(int signo)
{

   if (signo == SIGCHLD)
   {
       while (true)
       {
           // 50个退出,50个没有 的情况 --  非阻塞等待
           pid_t rid = waitpid(-1, nullptr, WNOHANG); // -1 : 回收任意一个子进程
           if (rid > 0)
           {
               cout << "wait child success: " << rid << endl;
           }
           else if (rid <= 0)
               break;
       }
   }
   cout << "wait sub process done" << endl;
}

int main()
{
   signal(SIGCHLD, CleanupChild);

   for (int i = 0; i < 100; i++)
   {
       pid_t id = fork();
       if (id == 0)
       {
           // child
           int cnt = 5;
           while (cnt--)
           {
               cout << "I am child process: " << getpid() << endl;
               sleep(1);
           }
           cout << "child process died" << endl;
           exit(0);
       }
   }

   // father
   while (true)
       sleep(1);

   return 0}

运行结果:我们可以看到,最后只剩一个父进程,子进程没有僵尸。

  • 事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction或者signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。

代码:

#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

using namespace std;

int main()
{
   signal(SIGCHLD, SIG_IGN);

   for (int i = 0; i < 100; i++)
   {
       pid_t id = fork();
       if (id == 0)
       {
           // child
           int cnt = 5;
           while (cnt--)
           {
               cout << "I am child process: " << getpid() << endl;
               sleep(1);
           }
           cout << "child process died" << endl;
           exit(0);
       }
   }

   // father
   while (true)
       sleep(1);

   return 0;
}

运行结果:我们可以看到,最后只剩一个父进程,子进程没有僵尸。


OKOK,Linux进程信号就到这里,如果你对Linux和C++也感兴趣的话,可以看看我的主页哦。下面是我的github主页,里面记录了我的学习代码和leetcode的一些题的题解,有兴趣的可以看看。

Xpccccc的github主页