[Linux]——信号的一生

信号

对于信号这个词大家一定都不陌生,信号产生在我们生活的各个角落,比如你过十字路口时头顶的红灯亮起你就知道要停下脚步等待绿灯的切换,再比如下课铃响了你就可以收拾书包回家…
信号的真正的意义就在于当一个对象收到信号的时候,他知道针对这个信号该做出什么样的反应。值得注意的是,做出反应的前提是就算你没有收到这个信号,你也知道收到这个信号时你应该怎么做,同样说明,你并不知道你什么时候会收到信号。

现在我们来谈谈Linux下的信号机制,既然是Linux下的信号,那么毋庸置疑,他一定是一种进程间的通信机制,和我们生活中的信号相同,Linux下的信号也是异步的,异步就是不可预见的意思。信号可以导致一个正在执行的进程被异步打断,转而处理一个突发事件。

似曾相识的信号

如果你在你的代码中写了一行执行除0操作的代码,那么程序立马就会奔溃并且提示你就行了除0的操作。实际上这是cpu在执行运算操作时发现了除0错误并报错,操作系统检测到了硬件错误,所以向进程发送进程终止掉了程序。

下图中的第二行也就是报错的那一行实际上就是我们进程接收到的信号所打印的报错信息,在进程控制一节中我们也详细讲过这个案例。更确切的说,是我们的进程收到了8号信号,系统的默认行为是终止程序 (进程收到信号会执行默认行为)
在这里插入图片描述
现在我们不妨带大家见识见识Linux下所有的信号,你只需要在命令行输入kill -l 即可,下图是显示结果:这是我们Linux下的62个信号,有的同学会大吃一惊,这不是64么?擦亮的你的眼睛小伙子,没有32,33号信号,至于他们去哪里了,我也不知道

首先你清楚的看到了用方框框住的那个8号信号,FPE刚好就是上面错误信息的缩写,这说明我们没有欺骗你,进程确实收到了8号信号,如果你还是不信,不妨fork一个子进程,让父进程waitpid子进程,看看status的状态码的低7位(具体操作戳上面进程控制的链接)

你也清楚的看到我使用线将这些新信号分成了上下两个区域,上半区域叫做普通信号,下半区域叫做实时信号。我们这篇博客重点以普通信号展开讲解。
在这里插入图片描述
上面信号的作用你可以查一查资料,不过很多信号我们在下面的讲解中提到,接着往下看就行。

信号的相关术语

在学习信号处理流之前,我们先介绍一些相关术语,能让你加快理解有些概念。

  • 发送信号:一个进程可以向另外一个进程发送信号,也可以向自己发送信号,但是关键在于,等你看完这篇博客你会发现,信号其实并不是是发送的,而是操作系统向进程写信号的行为。
  • 安装中断:设置信号来时不执行默认操作,而是执行自己的代码,即期望某个信号来到时让进程执行相应的中断服务程序
  • 捕获(递达)信号:实际执行信号的处理动作被称为捕获
  • 屏蔽(阻塞)信号:进程暂时不接受某些信号。如果在屏蔽期间向进程发送了被屏蔽的信号,该信号不会被进程捕获;一段时间后如果进程解除该信号的屏蔽,该信号将被捕获到
  • 忽略信号:进程已经被递送给目标进程,但是目标进程不处理,直接丢弃。
  • 未决信号:信号已经产生,但因为目标进程暂时屏蔽该信号而不能被目标进程捕获信号。

信号的生命周期

说了上面一大推繁琐的概念,这里我们终于要进入我们关键的部分了,要谈一个信号是如何产生,如何被处理,最后被销毁,等等细节就一定要谈到这个信号的生命周期,那么Linux下的信号生命周期是怎么样的,看看下面的流程图。

在这里插入图片描述
有了这副流程图,我们就以流程图的每一步由粗到细步步展开,这样能更好地让大家记住。

信号被产生并发送给目标进程

不要惊讶,这是流程图中的第二个流程,这里我们只是考虑了先有鸡还是现有蛋的问题。我们必须参生信号后再让目标进程安装信号来接受,但从逻辑来讲只有安装过信号后才能发送给进程。不要纠结这个,我们来看看这一部分需要做什么。

这一部分就如我们的标题一样,我们需要产生信号并发送给目标进程,关键在于我们有哪些方式来实现我们的行为。要了解的是,发送信号是指一个进程向另一个进程发送某个信号,但是实际上并不是直接发送的,而是由操作系统转发的。
在这里插入图片描述

  • 键盘命令

你一定向某个前台程序,使用过Ctrl + c 的操作,现象就是这个前台进程收到你发送的信号并且终止。这里需要强调的是,你一定要明白前台进程的概念。

  • Shell命名

我们之前已经接触过这一行为,我们只需要找到进程相对的pid并且向他发送信号就能产生进程捕获信号后默认的行为

在这里插入图片描述

  • 系统调用

Kill函数向指定进程发送一个信号,其函数声名如下:

在这里插入图片描述
此函数的第一个参数是要传递信号的进程pid,第二个参数为要发送的信号值,pid由下面的几种取值:
1.pid > 0 将信号发送给进程PID为pid的进程
2.pid = -1 将信号发送给系统内调用者可以发送信号的所有进程
3.pid = 0 将信号发送给和当前进程在同一进程组的所有进程

Raise函数自举一个信号,也就是raise函数用来给当前进程发送一个信号,即唤醒一个进程

在这里插入图片描述
此函数只有一个参数就是要发送的信号值。成功则返回0,否则返回-1.

  • 软件条件

如果你学习过管道你应该知道管道的读写规则,子进程进行写操作,如果父进程不光不读并且关闭了读文件操作符。那么由软件条件就会产生SIG_PIPE的信号终止子进程。这里我们再介绍一种alarm函数和SIGALRM信号。

在这里插入图片描述
参数很简单,唯一的参数指的是时间秒,即在多少秒内发送SIGALRM信号。默认情况下,进程收到此信号终止进行。这里的返回值注意一下,这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数 。

函数具体就不演示了,我们的重点不在于函数的使用,而是明白不同种产生并向进程发送信号的方法。

  • 异常

异常退出程序我们见到的很多,像上面除0的操作就是由于硬件异常导致操作系统杀死了进程。这里介绍下abort函数:

在这里插入图片描述
abort函数使当前进程收到信号而异常终止。

这一部分比较简单,大家只需要知道这些产生信号的多种情况,信号通常被某个进程产生,同时设置此信号的目的进程,然后由操作系统管理。

在目的进程中安装信号

我们在谈安装信号之前必须谈一谈操作系统处理信号的办法,一般操作系统处理信号有下面的几种办法。

  • 忽略此信号。大多数信号都可以使用这种方式处理,但是有两种信号不能被忽略。SIGKILL和SIGSTOP是操作系统向用户提供的一种使进程终止或者停止的可靠方式。
  • 自定义信号捕捉方式。当某种信号到来时,执行用户自定义的操作,这要求该进程首先安装该信号(后面谈安装),然后通知内核在某种信号发生时调用一个特殊函数。
  • 执行系统默认操作。Linux系统对每一个信号都规定一个默认的操作,详细可查表。

现在我们来谈谈什么叫做信号的安装:在目的进程安装该信号,即设置如果目标进程捕获该信号时执行的操作代码。Linux采用signal和sigaction系统调用来完成。因为信号是异步事件的典型应用,产生信号对进程而言是随机出现的,因此,进程不能预先知道信号会不会发送到当前的进程,也不能预知信号什么时候发送到当前进程。因此只能在信号到来前告诉内核“在此信号发生时,请执行下列操作”,这也就是所谓的信号安装。

signal函数安装信号

在这里插入图片描述
此函数有两个参数,第一个参数sig为要设置的信号,第二个参数为接收到此信号后你所要执行的函数的函数指针,第二个参数还给我们了三个宏的选择

SIG_ERR   //返回错误
SIG_DFL   //执行信号默认操作
SIG_IGN   //忽略信号

如果执行成功,此函数返回上一次对此信号的设置,如果设置多次,最终者为最近设置的一次,如果设置失败,将返回SIG_ERR的错误。话不多说,我们一起来用用这个函数。

在这里插入图片描述
代码非常简单,相信你仔细看一定看的懂,handler的参数就是我们捕获到的信号码,这里要说的是,2号信号实际上就是我们按ctrl + c 给进程发送的信号,原来默认会让进程终止,现在我们让他执行我们自定义的行为打印handler函数中的句子。(无法终止进程的同学按ctrl + \ 即可)

在这里插入图片描述
如果你使用while循环将31个信号全部捕获,那么你会发现无论使用键盘上的操作也无法结束进程,这里就提到了我们之前说过的9号信号,这个信号十分强势,不受自定义行为的影响,所以你只需要在另一个终端下找到当前进程的pid发送9号信号即可。同样不受自定义行为控制的还有18和19号信号,一个让进程暂停,另一个则唤醒进程,他们是相对的俩个信号。

signal信号安装函数很简单,他只能提供简单的信号安装操作,所以逐步被淘汰,因此Linux提供了功能更强大的sigaction函数

sigaction函数安装信号

在这里插入图片描述
此函数的第一个参数为要安装的信号,第二三个参数都为信号结构体sigaction(用于描述要采取的操作及相关信息)变量。第二个参数用来指定欲设置的信号处理方式,第三个参数将存储执行此函数前针对此信号的安装信息,说白了就是一个备份。
现在我们来看看struct sigaction中有什么东西:

struct sigaction {

	void (*sa_handler)(int);//类似于调用signal函数
	
	void (*sa_sigaction)(int, siginfo_t *, void *);//信号捕获函数,可以获取其他信息
	
	sigset_t sa_mask;//执行信号捕获函数期间要屏蔽的其他信号集合
	
	int sa_flags;//影响信号行为的特殊标志
	
	void (*sa_restorer)(void);//没有使用,作为了解

}
  • 结构体中sa_mask是一个信号集合(位图),用于标识在执行信号捕获函数时,添加到进程屏蔽信号集合中的信号集。但是不会屏蔽9,18,19号信号。
  • 上面的sa_handler和sa_sigaction函数指针只需要二取一即可,如果使用前者那么行为和signal函数相同,而后者可以获取更多的信息,这看起来像时signal的升级版。

我们先来简单的使用一下这个函数,用他模仿一下signal的行为,之后我们再进一步的研究其他作用:

#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void handler()
{
	//...
}
int main()
{
	struct sigaction act, oact;
	act.sa_handler = handler;
	sigempty(&act.sa_mask);//这个函数后面介绍,用来初始化sa_mask的
	act.sa_flags = 0;//不使用后者函数指针设置为0即可
	sigaction(2, &act, &oact);
	while (1)
	{
		printf("...");
		sleep(1);
	}
	return 0;
}

哦吼,看起来使用函数指针1没有那么吃力,那么现在不妨看看函数指针2怎么样:
如果我们将结构体中的sa_flags设置为某个值,并且使用成员sa_sigaction。他的功能将会更强大,在中断处理中,除了可以捕获信号值外,我们还能获得其他的信息,下面看看我们能获得其他的什么东西呢。

void (*sa_sigaction)(int, siginfo_t *, void *)

参数一肯定还是捕获信号的信号码,参数二是一个结构体指针,参数三是可以赋值给ucontext_t类型的一个对象的指针,以引用在传递信号时被中断的接收进程或者线程的上下文。现在我们需要更细化的看看参数二结构体中的成员

typedef struct
  {
    int si_signo;                /* Signal number.  */
    int si_errno;                /* If non-zero, an errno value associated with
                                   this signal, as defined in <errno.h>.  */
    int si_code;                /* Signal code.  */

    union
      {
        int _pad[__SI_PAD_SIZE];

         /* kill().  */
        struct
          {
            __pid_t si_pid;        /* Sending process ID.  */
            __uid_t si_uid;        /* Real user ID of sending process.  */
          } _kill;

        /* POSIX.1b timers.  */
        struct
          {
            int si_tid;                /* Timer ID.  */
            int si_overrun;        /* Overrun count.  */
            sigval_t si_sigval;        /* Signal value.  */
          } _timer;

        /* POSIX.1b signals.  */
        struct
          {
            __pid_t si_pid;        /* Sending process ID.  */
            __uid_t si_uid;        /* Real user ID of sending process.  */
            sigval_t si_sigval;        /* Signal value.  */
          } _rt;

        /* SIGCHLD.  */
        struct
          {
            __pid_t si_pid;        /* Which child.  */
            __uid_t si_uid;        /* Real user ID of sending process.  */
            int si_status;        /* Exit value or signal.  */
            __sigchld_clock_t si_utime;
            __sigchld_clock_t si_stime;
          } _sigchld;

        /* SIGILL, SIGFPE, SIGSEGV, SIGBUS.  */
        struct
          {
            void *si_addr;        /* Faulting insn/memory ref.  */
            short int si_addr_lsb;        /* Valid LSB of the reported address.  */
          } _sigfault;

        /* SIGPOLL.  */
        struct
          {
            long int si_band;        /* Band event for SIGPOLL.  */
            int si_fd;
          } _sigpoll;

        /* SIGSYS.  */
        struct
          {
            void *_call_addr;        /* Calling user insn.  */
            int _syscall;        /* Triggering system call number.  */
            unsigned int _arch; /* AUDIT_ARCH_* of syscall.  */
          } _sigsys;
      } _sifields;
  } siginfo_t __SI_ALIGNMENT;

着重看几个关键的:

  • si_signo:系统生成的信号编号
  • si_errno:包含与实现相关的其他错误信息
  • si_code:包含一个标识该信号生成原因的代码
  • 后续联合成员在应用中会让不同的信号填充不同的部分,这里就不在仔细分析

对了,别忘了我们要调用sa_sigaction时与他配合的sa_flags,这里笔者摘录了几个常用的选项:

  • SA_NOCLDSTOP:子进程退出时不生成SIGCHLD信号给父进程
  • SA_RESTART:影响可中断函数的行为,即指定的可中断函数会调用失败,并且设置erron全局变量
  • SA_SIGINFO:如果未能捕捉到该信号,那么函数必须调用sa_handler成员来捕捉信号,且不得修改sa_sigaction成员,相反的情况是只能调用sa_sigaction成员而不可修改sa_handler成员

现在了解了更多,不妨这次我们使用sa_sigaction成员试试看:

#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
void handler(int signo, siginfo_t* info, void* p)
{
	printf("signo = : %d\n", signo);
	printf("sender pid = : %d\n", info->si_code);//使用第二个参数打印更多信息
}
int main()
{
	struct sigaction act, oact;
	act.sa_sigaction = handler;//更换成员
	sigempty(&act.sa_mask);//初始化sa_mask的
	act.sa_flags = SA_SIGINFO;
	sigaction(2, &act, &oact);
	while (1)
	{
		printf("...");
		sleep(1);
	}
	return 0;
}

到这里,我们两个安装函数就介绍完了,如果你仔细的理解两个函数,相信你一定明白了原理。

信号在目的进程被注册

说到这里,我们的硬菜终于来了,信号在进程中如何被注册即捕获处理绝对是信号生命周期里最重要的一部分。在讲解这一部分之前要问大家一个问题,信号已经产生并且处于未决状态被递达给进程,此时进程一定是立即处理么?

上课的时候,老师的布置作业之后你一定不会立马收拾书包走人,而是先把作业是什么记下来,等到下课后才回家做作业。对于操作系统来说是同样的道理,一个信号处于未决状态此时进程并不一定立即处理,而是先把他记下来,所以我们此时的问题就是这些信号是存在到哪里的呢。

在这里插入图片描述
希望你看到这张图想起些什么,如果没想到也没关系,还记的我之前告诉大家普通信号的数量么?没错,是31,那么一个整型有多少个位呢?是32,所以现在你应该明白了,如果标识某个信号是否未决,那么就将其中对应的一位修改为1。我相信每个进程的pcb中一定会有这么一张位图,也就是我们的未决信号集。

信号在目的进程被注册。操作系统将信号添加到目的进程PCB相关的数据结构中。每个进程的PCB中都有一个未决信号的数据成员,该成员如下:

struct sigpending pending;
struct sigpending
{
	struct sigqueue* head , * tail;//成员1和2
	sigset_t signal;//未决信号集
}

成员1,2分别指向一个sigqueue类型的未决信号队列的队尾和队头,第三个成员就是我们的未决信号集,sigqueue结构体描述一个特定信号所携带的信息,并且指向下一个sigqueue:

struct sigqueue
{
	struct sigqueue* next;
	siginfo_t info;
};

因此简单的来说,信号在进程中注册指的就是将相应的信号值加入到进程的未决信号集中,并且将信号所携带的其他信息保留到信号队列的某个sigqueue结构中。只要信号在进程的未决信号集中,表明该进程已经知道了这些信号的存在,但是还没有来的及处理或者信号当前被屏蔽。函数sigpending可以取当前进程的未决信号。

利用sigpending函数产看未决信号集

sigset_t这个类型可以表示每个信号 的“有效”或“无效”状态,在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。这是操作系统为我们提供的一种类型,我们直接拿来用就好,稍后讲解对他的操作函数。
在这里插入图片描述
这个函数的参数是输出型参数,你需要给他传一个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置位,表示 该信号集的有效信号包括系统支持的所有信号。 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
  • 初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
  • sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回1,不包含则返回0,出错返回-1

对于信号集合的操作非常简单,但是事实上未决信号集是操作系统管理的,所以我们肯定不可能使用这些函数对未决信号集进行操作。你还记得我们之前提到屏蔽的概念么,这里我们又要将话题转移到信号屏蔽

信号屏蔽即传递信号给进程,但是该进程不捕获信号,而是使信号处于未决状态。当屏蔽信号集发生变化不在屏蔽信号时才可以捕获该信号

画重点,信号屏蔽集,也叫阻塞信号集合,他是用来标识哪些信号在当前进程被屏蔽的集合,既然他也是集合,那么意味着他和未决信号集同样也是使用了sigset_t这个类型的变量标识被屏蔽信号。那么如何设置一个进程要屏蔽的信号呢,来看看系统调用函数。
在这里插入图片描述
第一个参数系统默认给了三个选项:

  • SIG_BLOCK:将第二个参数所描述的集合添加到当前进程屏蔽的信号集中
  • SIG_UNBLOCK:将第二个参数所描述的集合从当前进程屏蔽的信号集中删除
  • SIG_SETMASK:无论之前屏蔽了哪些信号,设置当前进程屏蔽的集合为第二个参数描述的对象

第二个参数和第三个参数都是sigset_t类型的指针,第二个参数传入一个你要哦屏蔽的信号的集合,第三个参数如前面说的是对上一次信号屏蔽集的备份,以便你之后还想恢复回来。下面我们来实际用一用我们上面提到的函数把。

在这里插入图片描述
show函数:

在这里插入图片描述
现在由于我们屏蔽了2号信号,所以我们对这个跑起来的进程使用ctrl+c无法终止程序,并且我们能看到show打印的字符集2号信号被添加:
在这里插入图片描述
相信现在你对这些函数的使用有了进一步的了解,我们现在除了知道了未决信号集,信号屏蔽集,我们又要告诉你一个新的东西,这次不在是sigset_t类型的集合,而是一个handler函数指针数组,为了便于理解,我们直接用一幅图来讲解。
在这里插入图片描述

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针数组表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
  • 当我们调用signal使用自定义函数捕捉信号时,其实我们就修改了图中的第三张表,第三张表中原本放的是默认或者忽略信号的处理方式,如果使用自定义方式那么数组中的内容就会被修改为该自定义函数的地址。
  • 值得注意的一点是,阻塞(屏蔽)信号并不意味着信号不能处于未决状态,只是此时信号不能被捕获,就拿表中数据来说,2号信号被屏蔽,但是他此时处于未决状态,所以进程不对此信号进行处理。3号信号被添加到阻塞信号集中,如果有一个3号信号发送给此进程,那么未决信号集中的3位置就会被修改为1,但是同样进程不做处理,知道阻塞信号集中将阻塞取消。
  • Linux中的常规信号在递达之前最多只计一次,而实时信号在递达之前产生多次以依次放在一个队列里。

信号在进程中被注销

进程在执行相应的处理函数之前,首先要把信号在进程中注销。在目标进程在执行的过程中,会检测是否有信号等待处理。进程每次从内核区返回到用户时都做这样的检测。如果存在未决信号等待处理且没有被该进程屏蔽,则在运行相应的信号处理函数之前,进程会把信号在未决信号链中的结构卸掉。对于非实时信号来说,由于在未决信号链中最多只有一个sigqueue结构体,因此该结构被释放后,应该把信号在未决信号集中删除;对于实时信号而言,可能在信号链中占有多个sigqueue结构,因此如果只占用一个sigqueue结构则删除未决信号集的标志,反之则不删除。

信号生命终止

进程注销信号后,目的进程根据当前进程对此信号设置的处理方式,暂时终止当前代码的执行,保护上下文,转而执行信号处理函数,即捕获该信号,执行完毕后在恢复到被中断的位置接着执行。
聊了这么久,我们前面一直提信号的捕捉或者递达这一概念,那么在操作系统中到底是什么时候进行信号的捕捉呢?我们依旧使用一张图来表达我们的流程:
在这里插入图片描述
图看不懂没关系,现在先请记住一句话:信号是从内核区返回用户区时被检测处理。我们都知道操作系统是计算机中权限最高的管理者,操作系统不相信任何人,所以处理信号这种行为就是执行内核区的代码。有的同学会问什么是执行内核区的代码?操作系统给我提供的系统调用api就是执行内核区代码,而我们执行自己写的代码就是执行用户区的代码。之所以需要在内核区中捕获处理信号是因为,操作系统在捕获信号时需要检测未决信号集合,屏蔽信号集等,并需要在处理信号后将他们由1置为0,这是用户无法做到的,我们将上图再高度抽象化一次。
在这里插入图片描述
可以看出,图中下半部分相交的部分就是信号检测并捕获的部分,而上面与横线相交的部分就是状态改变的位置。到此我们信号的一生从产生到被捕获也就结束了。

可重入函数

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

还记的我们上面讲的signal函数吗,他就是一个可重入函数,也就代表他是一个不可靠的信号函数。当调用sigprocmask函数解除信号的屏蔽后,系统会按顺序检测未决信号,但是这个顺序不是信号到来的顺序,而是信号值的顺序。当检测到1号信号发现他需要被处理时,进入处理函数,因此内核为这个函数在用户创建了函数栈,而此时操作系统发现2号信号也需要被处理,所以打断一号信号的处理跑去处理2号信号,处理完2号信号后发现1号信号仍然未决未注销,因此再次执行处理函数,执行完毕后注销,回到用户空间,按正常的函数情况执行,发现有函数栈没有执行,因此再执行一次…

因此这也就是为什么signal函数逐渐被淘汰的原因,而sigaction则不会出现这种情况。一般的,调用了malloc或者free或者调用的标准I/O库函数的都是不可重入函数。

总结

信号的一生到这里就讲解完毕了,每一部分都需要用心细细的体会,信号的重点就在于你需要知道他从产生到被销毁的具体过程是怎么样的,而如果你脑海中有这一步步的流程,那么相信你对信号会有更深的理解。本文如有错误或者不懂的地方,欢迎广大读者提出。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值