[Linux semaphores]signal&core dump&产生信号&阻塞信号

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第1张图片

[Linux semaphores]signal&core dump&产生信号&阻塞信号

    • 信号的特点
      • 人和生活中的信号
      • 进程处理信号
    • 信号的应用
      • signal
    • 信号的记录
    • 产生信号
      • 键盘按键产生信号
        • Ctrl+c
        • 其他的键盘信号
        • 按Ctrl+C终止进程和按Ctrl+\终止进程,有什么区别?
      • 异常产生信号
        • Core Dump核心转储
      • 使用核心存储
        • status
      • 系统调用
        • kill
        • raise
        • abort
      • 由软件条件产生信号
        • alarm
      • 产生信号小结
    • 阻塞信号
      • 信号其他概念
      • signal in kernel
      • sigset_t
      • 信号集操作函数
      • sigprocmask
      • sigpending
    • 处理信号
      • 内核态与用户态切换
        • 内核态与用户态
        • 用户空间和内核空间
        • 内核态和用户态之间是进行如何切换的?
      • 信号捕捉
        • 如何切换
      • sigaction
    • 可重入函数
    • volatile关键字
    • SIGCHLD(拓展)

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第2张图片

信号的特点

信号是进程之间事件异步通知的一种方式,属于软中断。

为什么要有信号?为了让我们的进程能有处理突发事件的能力

下面思考一下一个信号的产生周期

人和生活中的信号

信号产生前:烽火狼烟

信号还没有产生的时候,人们常常是知道信号产生之后,应该怎么做

信号产生的时候和人正常生活之间的运行关系:异步关系(你干你的我干我的)

为什么我们能够识别?将信号特征和识别方法及其对应的处理过程记住

信号产生时:废寝忘食

信号产生的时候,不一定立马处理信号,有可能正在处理优先级更高的信号,所以我们会在合适的时候处理,说明我们一定要有某种方式记下来这个信号已经产生

信号产生后:捕捉信号

  • 默认行为(条件反射)
  • 自定义行为(手头要紧,先处理手头,本来的条件反射用自定义行为解决)
  • 忽略信号(特殊情况,注意也是处理了信号)

进程处理信号

信号产生前

进程内部能够识别信号

进程知道收到信号之后该怎么做

信号属于进程内部特有的特征

信号产生时

信号到来的时候,信号暂时因为优先级,无法处理,该信号需要被进程保存起来

信号产生后

  • 默认行为(终止进程,暂停进程,继续运行)
  • 自定义行为
  • 忽略信号

信号的应用

信号是如何记录和发送的?

记录:信号的记录是在task_struct中的,本质是为了更多的记录信号是否产生,应该使用位图来保存信号数据。比特位的位置代表的是信号编号(1-31),比特位的内容(0还是1)用来检验是否收到信号

发送:进程发送信号,本质上是进程内信号的位图被修改了,只有OS是有资格修改进程数据的,所以说本质上是OS修改目标进程task_struct中的信号位图,信号的发送只有操作系统是有资格的

不过信号发送的方式可以有很多

信号一共有多少种?

命令行输入

kill -l

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第3张图片

一共有62个信号,其中普通信号1-31,用的较多

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义#define SIGINT 2

signal

NAME
       signal - ANSI C signal handling
SYNOPSIS
       #include 
       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);
参数:
第一个参数signum:
    指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。  
第二个参数handler:
    描述了与信号关联的动作,它可以取以下三种值:
    1)SIG_IGN   这个符号表示忽略该信号。
    2)SIG_DFL   这个符号表示恢复对信号的系统默认处理。不写此处理函数默认也是执行系统默认操作。
返回值:
    返回先前的信号处理函数指针,如果有错误则返回SIG_ERR(-1)。   

信号的记录

实际上,当一个进程接收到某种信号后,该信号是被记录在该进程的进程控制块当中的。我们都知道进程控制块本质上就是一个结构体变量,而对于信号来说我们主要就是记录某种信号是否产生,因此,我们可以用一个32位的位图来记录信号是否产生。

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第4张图片

其中比特位的位置代表信号的编号,而比特位的内容就代表是否收到对应信号,比如第6个比特位是1就表明收到了6号信号。

产生信号

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第5张图片

键盘按键产生信号

Ctrl+c

用户输入命令,在Shell下启动一个前台进程。用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出

  1. Ctrl-C 产生的信号(键盘)只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C 这种控制键产生的信号。
  3. 前台进程在运行过程中用户随时可能按下Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
  4. 信号是进程之间事件异步通知的一种方式,属于软中断。
其他的键盘信号

其他的信号我们可以一个个尝试,打出来

void handler(int signo)
{
    printf("got a singal! signo: %d\n", signo);
    signal(signo, handler);
}

int main()
{
    for(int signo=1; signo<32; signo++){
        signal(signo, handler);
    }

    while(1){
        sleep(1);
    }
}

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第6张图片

有些信号是不可以被捕捉的

比如说信号9,我们可以kill -9 杀掉进程,无敌大法

按Ctrl+C终止进程和按Ctrl+\终止进程,有什么区别?

按Ctrl+C实际上是向进程发送2号信号SIGINT,而按Ctrl+\实际上是向进程发送3号信号SIGQUIT。查看这两个信号的默认处理动作,可以看到这两个信号的Action是不一样的,2号信号是Term,而3号信号是Core。

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第7张图片

Term和Core都代表着终止进程,但是Core在终止进程的时候会进行一个动作,那就是核心转储。

异常产生信号

SIGQUIT的默认处理动作是终止进程并且Core Dump

Core Dump核心转储

如果我们有一个程序,运行时直接崩掉,这时候我们会调试,还有一种方案是我们通过核心转储,把进程在内存上的核心信息,转储到磁盘上,叫做core.[pid],核心转储文件,目的是为了调试和定位问题。

我们可以认为 core dump 是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump 下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core dump 对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump 文件可以再现程序出错时的情景。

一般云服务器,默认是关闭的,也就是ulimit之后发现size为0

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第8张图片

我们的打开方式是ulimit -c [size]

使用核心存储

当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的。如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的。

当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因。而在某些特殊情况下,我们会用到核心转储,核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件,一般命名为core.pid。

而核心转储的目的就是为了在调试时,方便问题的定位。

下面我们就可以通过gdb调试。利用core文件进行调试:

(gdb) core-file core.[pid]

gdb会直接帮我们定位到出错的位置

为什么C/C++进程会崩溃?

本质就是因为收到了信号,什么错误对应什么信号,信号都是有OS发出的

有如除零错误的,问题往往处在硬件上,当除零的时候,状态寄存器可能出问题,于是操作系统监视识别到硬件出错问题,定位错误,解释为信号,向目标进程的信号记录位修改

status

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第9张图片

之前在进程控制的时候,我们通过status拿到了退出码和退出信号,现在我们提出的code dump标志也在status中,如果进程被信号所杀的话

if(fork() == 0){
    //child
    printf("I am child, pid: %d\n", getpid());
    sleep(3);
    //int a = 1/0;
    int *p = NULL;
    *p = 100;
    exit(0);
}
int status = 0;
waitpid(-1, &status, 0);
//0000 0000 0000 0000
//0000 0000 1000 0000
printf("exit code: %d, coredump: %d,signal %d\n",
       (status>>8)&0xff, (status>>7)&1 ,status&0x7f);

所以说这个位表示的就是是否被core dump了

系统调用

kill

我们要使用kill命令向一个进程发送信号时,我们可以以kill -信号名 进程ID的形式进行发送。

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第10张图片

from https://twitter.com/b0rk/status/994203063194963969

实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号,kill函数的函数原型如下:

NAME
    kill - send signal to a process

SYNOPSIS
    #include 
    #include 

    int kill(pid_t pid, int sig);

函数说明:kill()可以用来送参数sig 指定的信号给参数pid 指定的进程。参数pid 有几种情况:
1、pid>0 将信号传给进程识别码为pid 的进程
2、pid=0 将信号传给和目前进程相同进程组的所有进程
3、pid=-1 将信号广播传送给系统内所有的进程
4、pid<0 将信号传给进程组识别码为pid 绝对值的所有进程参数 sig 代表的信号编号可参考附录D

返回值:执行成功则返回0, 如果有错误则返回-1

错误代码:
1、EINVAL 参数sig 不合法
2、ESRCH 参数pid 所指定的进程或进程组不存在
3、EPERM 权限不够无法传送信号给指定进程

测试用例

我们可以自己写一个mykill命令,仿照着使用有如

./mykill [pid] [signal id]

具体实现如下

#include 
#include 
#include 
#include 

void Usage(const char *proc)
{
    printf("Usage: %s pid signo\n", proc);
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    pid_t pid = atoi(argv[1]);
    int signo = atoi(argv[2]);

    kill(pid, signo);

    return 0;
}
raise

可以使用函数 raise() 生成信号,该函数带有一个整数信号编号作为参数,语法如下:

int raise (signal sig);

功 能: 向正在执行的程序发送一个信号

参数:要发送信号的名称

返回值:成功返回0 ,失败返回非0值

测试用例:自己给自己发信号

void handler(int signo)
{
    printf("get a sig: %d\n", signo);
}

int main()
{
    signal(2, handler);
    while (1)
    {
        printf("I am a process,pid: %d\n", getpid());
        sleep(1);
        raise(2);//传入2号信号
    }
}

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第11张图片

abort

abort函数使当前进程接收到信号而异常终止。

exit常常是正常的终止,且是有返回值的

#include 
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。

⚙️ 测试用例:自己终止自己

void handler(int signo)
{
    printf("get a sig: %d\n", signo);
}

int main()
{
    signal(2, handler);
    while (1)
    {
        printf("I am a process,pid: %d\n", getpid());
        abort();
    }
}

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第12张图片

由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号。下面主要介绍alarm函数和SIGALRM信号。

alarm
alarm()函数说明
#include ;

unsigned int alarm(unsigned int seconds);

功能与作用:alarm()函数的主要功能是设置信号传送闹钟,即用来设置信号SIGALRM在经过参数seconds秒数后发送给目前的进程。如果未设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。

函数返回值:如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置,即之前设置的秒数被新的闹钟时间取代;当参数seconds为0时,之前设置的定时器闹钟将被取消,并将剩下的时间返回。

测试用例:while一直++,signal只要收到alarm信号就开始捕捉

int count = 0;

void handler(int signo)
{
    //alarm(1);
    printf("get a signal: %d\n", signo);
    printf("count: %d\n", count);
    exit(0);
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1);

    while (1)
    {
        count++;
    }
}

image-20220711165836570

产生信号小结

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

OS是进程的管理者

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

在合适的时候处理

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

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

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

阻塞信号

信号其他概念

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

signal in kernel

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第13张图片

pending表

用来保存是否收到信号,收到几号信号。

实际上它就是个位图结构。

handler

对特定信号的处理方法。

handler表也是一个位图结构。

handler的类型是:void (*handler[31])(int) ,一个函数指针数组。对应的下标里面是对应特定信号的处理动作。

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第14张图片

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第15张图片

block

表示信号是否被阻塞。阻塞位图,也叫信号屏蔽字。
本质上也是位图结构。

下面把三张表合在一起看

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第16张图片

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。

  • Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

小结:

  1. 在block位图中,比特位的位置代表某一个信号,比特位的内容代表该信号是否被阻塞。
  2. 在pending位图中,比特位的位置代表某一个信号,比特位的内容代表是否收到该信号。
  3. handler表本质上是一个函数指针数组,数组的下标代表某一个信号,数组的内容代表该信号递达时的处理动作,处理动作包括默认、忽略以及自定义。
  4. block、pending和handler这三张表的每一个位置是一一对应的。

sigset_t

想要设置修改上述位图,一定需要通过操作系统的接口来修改,这里就需要学习一个新的类型sigset_t

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

typedef __sigset_t sigset_t;

从之前的图片来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。

sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。

  • 在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞。
  • 在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现

从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的

#include 
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置位,表示该信号集的有效信号包括系统支持的所有信号。

**注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。**初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

#include 
#include 

int main()
{
	sigset_t s; //用户空间定义的变量

	sigemptyset(&s);

	sigfillset(&s);

	sigaddset(&s, SIGINT);

	sigdelset(&s, SIGINT);

	sigismember(&s, SIGINT);
	return 0;
}

这四个函数都是成功返回0,出错返回-1。

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

然而只是这些函数,是无法改变task_struct中的这些位图的,我们还需要一个系统接口告诉操作系统和进程

sigprocmask

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

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
    //oset可以理解为oldset,也就是一个典型的输出型参数

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。

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

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第17张图片

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第18张图片

sigpending

#include 
int sigpending(sigset_t *set);
set:是输出型参数,将进程的pending位图输出返回。
成功返回0,失败返回-1.

测试用例

  1. 先把2号信号屏蔽了
  2. kil1或者是键盘发送2号信号,可以预见,2号信号不会被递达
  3. 2号将会一直被阻塞,一定一直在pending中
  4. 使用sigpending获取当前进程的pending信号集
void printPending(sigset_t *pending)
{
    int i = 1;
    for(; i <= 31; i++){
        if(sigismember(pending, i)){
            printf("1 ");
        }
        else{
            printf("0 ");
        }
    }
    printf("\n");
}

int main()
{
    sigset_t set, oset; //用户空间定义的变量
    sigemptyset(&set);
    sigemptyset(&oset);
    sigaddset(&set, 2); //SIGINT
    sigprocmask(SIG_SETMASK, &set, &oset);//阻塞了2号信号
    sigset_t pending;
    int count = 0;
    while(1){
        sigemptyset(&pending);
        sigpending(&pending);
        printPending(&pending);
        sleep(1);
    }
    return 0;
}

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第19张图片

被阻塞之后恢复

void printPending(sigset_t *pending)
{
    int i = 1;
    for(; i <= 31; i++){
        if(sigismember(pending, i)){//判断指定信号是否在目标集合中
            printf("1 ");
        }
        else{
            printf("0 ");
        }
    }
    printf("\n");
}

void handler(int signo)
{
    printf("handler signo: %d\n", signo);
}

int main()
{
    signal(2, handler);//捕捉二号,免得被2号恢复之后终止

    sigset_t set, oset; //用户空间定义的变量
    sigemptyset(&set);//清空初始化
    sigemptyset(&oset);

    sigaddset(&set, 2); //SIGINT

    sigprocmask(SIG_SETMASK, &set, &oset);//设置阻塞信号集阻塞了2号信号

    sigset_t pending;
    int count = 0;
    while(1){
        //获取未决信号集
        sigemptyset(&pending);
        sigpending(&pending);

        printPending(&pending);
        sleep(1);
        count++;
        if(count == 10){
            sigprocmask(SIG_SETMASK, &oset, NULL); //恢复曾经的信号屏蔽字
            printf("恢复信号屏蔽字\n");
        }
    }

    return 0;
}

处理信号

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

进程在收到信号后在递达要处理信号时不是立即处理,而是在内核态切换到用户态时进行信号的处理

内核态与用户态切换

内核态与用户态

用户态:执行用户自己的代码时系统的状态称为用户态,是一种受监管的普通状态。

内核态:执行系统调用的代码,本质是调用内核的代码,必须要内核级别的权限,是一种权限非常高的状态。

用户空间和内核空间

每一个进程不仅拥有用户级页表,还有一张内核页表,用来映射内存中的操作系统代码和进程的进程地址空间,实现进程切换等操作。

对于每一个进程来说,用户级页表不同,内核级页表相同

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第20张图片

CPU上有一个寄存器可以表示进程当前所处的状态,进程所对应的页表,会被保存到寄存器中,方便快速寻找

状态切换的本质是页表进行切换,状态发生变化

内核态和用户态之间是进行如何切换的?

**什么时候会从用户态切换到内核态?什么时候会从内核态转换到用户态? **

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第21张图片

如何理解进程切换?

  1. 在当前进程的进程地址空间中的内核空间,找到操作系统的代码和数据。
  2. 执行操作系统的代码,将当前进程的代码和数据剥离下来,并换上另一个进程的代码和数据。

信号捕捉

前面简单说了切换的概念和内核态用户态的区别,下面看看信号捕捉是怎么在用户态和内核态之间切换的

如何切换

图片总结:

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第22张图片

注意: sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。

p.s. 识别到信号可以被捕捉(自定义),当前的身份是内核态,可以直接访问用户态的代码吗??理论上是可以的,但是绝对不能这样设计

简图总结:

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第23张图片

其中,该图形与直线有几个交点就代表在这期间有几次状态切换,而箭头的方向就代表着此次状态切换的方向,图形中间的圆点就代表着检查pending表。

Mindmap总结:

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第24张图片

文字总结:

内核如何实现信号的捕捉?

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。

当前正在执行main函数,这时发生中断或异常(进程切换)切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。

sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

当识别到信号的处理动作是自定义时,能直接在内核态执行用户空间的代码吗?

理论上来说是可以的,因为内核态是一种权限非常高的状态,但是绝对不能这样设计。

如果允许在内核态直接执行用户空间的代码,那么用户就可以在代码中设计一些非法操作,比如清空数据库等,虽然在用户态时没有足够的权限做到清空数据库,但是如果是在内核态时执行了这种非法代码,那么数据库就真的被清空了,因为内核态是有足够权限清空数据库的。

也就是说,不能让操作系统直接去执行用户的代码,因为操作系统无法保证用户的代码是合法代码,即操作系统不信任任何用户。

sigaction

捕捉信号除了用前面用过的signal函数之外,我们还可以使用sigaction函数对信号进行捕捉,sigaction函数的函数原型如下:

#include 
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
DESCRIPTION
       The sigaction() system call is used to change the action taken by a process on receipt of a specific signal.  The sigaction structure is defined as something like:
struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

参数说明:

  • signum代表指定信号的编号。
  • 若act指针非空,则根据act修改该信号的处理动作。
  • 若oldact指针非空,则通过oldact传出该信号原来的处理动作。

结构体的第一个成员sa_handler:

将sa_handler赋值为常数SIG_IGN传给sigaction函数,表示忽略信号。
将sa_handler赋值为常数SIG_DFL传给sigaction函数,表示执行系统默认动作。
将sa_handler赋值为一个函数指针,表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数。

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

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

说白了就是sa_mask这个字段设置了屏蔽字,在我处理信号2的时候,不可以有其他的另一个有关信号2的处理请求被递达

**测试用例 **:处理信号,如果接收到信号2就输出并返回原状态

struct sigaction act, oact;

void handler(int signo)
{
       printf("get a signal: %d\n", signo);
       sigaction(SIGINT, &oact, NULL);
}

int main()
{
       memset(&act, 0, sizeof(act));
       memset(&oact, 0, sizeof(oact));

       act.sa_handler = handler;
       act.sa_flags = 0;
       sigemptyset(&act.sa_mask);

       sigaction(SIGINT, &act, &oact);
       while(1){
           printf("I am a process!\n");
           sleep(1);
       }
       return 0;
}

运行代码后,第一次向进程发送2号信号,执行我们自定义的打印动作,当我们再次向进程发送2号信号,就执行该信号的默认处理动作了,即终止进程。

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第25张图片

可重入函数

下面主函数中调用insert函数向链表中插入结点node1,某信号处理函数中也调用了insert函数向链表中插入结点node2,乍眼一看好像没什么问题。

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第26张图片

1、首先,main函数中调用了insert函数,想将结点node1插入链表,但插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回到用户态之前检查到有信号待处理,于是切换到sighandler函数。

2、而sighandler函数中也调用了insert函数,将结点node2插入到了链表中

3、当结点node2插入的两步操作都做完之后从sighandler返回内核态

4、再次回到用户态就从main函数调用的insert函数中继续往下执行,即继续进行结点node1的插入操作。

最终结果是,main函数和sighandler函数先后向链表中插入了两个结点,但最后只有node1结点真正插入到了链表中,而node2结点就再也找不到了,造成了内存泄漏。

像上例这样,insert函数被不同的控制流调用(main函数和sighandler函数使用不同的堆栈空间,它们之间不存在调用与被调用的关系,是两个独立的控制流程),有可能在第一次调用还没返回时就再次进入该函数,我们将这种现象称之为重入。

而insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数我们称之为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称之为可重入(Reentrant)函数。

我们自己写的函数,之前所调用的函数,还有STL各种接口,容器等等,是否是可重入函数?

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

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

volatile关键字

站在信号的角度重新理解一下volatile,volatile是保存内存可见

**测试用例:**当我们不使用volatile关键字的时候,我们的死循环只要接收到信号2就会中断循环并输出一行

volatile int flag = 0;
//int flag=0;
void handler(int signo)
{
   printf("get %d signo!\n", signo);

   flag = 1;
}

int main()
{
   signal(2, handler);
   while(!flag);
   printf("Proc Normal Quit!\n");
   return 0;
}

标准情况下,键入CTRL-C ,2号信号被捕捉,执行自定义动作,修改flag=1 , while 条件不满足,退出循环,进程退出

优化情况下,键入CTRL-C ,2号信号被捕捉,执行自定义动作,修改flag=1 ,但是while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while循环检查的flag,并不是内存中最新的flag,这就存在了数据二义性的问题。while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要volatile

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第27张图片

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第28张图片

如果优化级别较高的时候,此时程序可能一直死循环,并没有正常退出,示意图

[Linux semaphores]signal&core dump&产生信号&阻塞信号_第29张图片

但是如果我把它写成volatile的话,就会没有这问题,因此每次都会从内存读取数据,保存了内存的可见性

volatile int flag = 0;

image-20220712173839632

SIGCHLD(拓展)

wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。

  • 采用第一种方式,父进程阻塞了就不能处理自己的工作了;

  • 采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

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

**测试用例:**父进程在这里非阻塞等待接收子进程结束时返回的一个SIGCHLD,然后用handler处理,或者是方法二:在Linux中让子进程设置忽略,则此时进程会自动删除,而不是僵尸

可以这么写,但是意思不大,只是显示函数用法

#include 
#include 
#include 
#include 
#include 
#include 
void handler(int signo)
{
   printf("get a : %d signo\n", signo);
   int ret = 0;
   while( (ret = waitpid(-1, NULL, WNOHANG)) > 0){
       printf("watit child %d success\n", ret);
   }
}

int main()
{
    signal(17, handler); 
	//signal(17,SIG_IGN);
    if(fork() == 0){
        printf("child is running, begin dead!: %d\n", getpid());
        sleep(3);
        exit(1);
    }
    while(1);
}

参考资料:https://blog.csdn.net/chenlong_cxy/article/details/121315266

你可能感兴趣的:(请回答Linux,linux,服务器,运维)