操作系统——Linux信号概念,信号的产生、处理、阻塞和捕捉,可重入函数的概念

        从生活角度出发,人们在日常的工作生活中,为了提高工作效率,会经常接收信号也会产生信号。在操作系统体系原理中,也有类似的信号概念,其目的也是为了让计算机能够以信号为媒介,优化工作方式,提高工作效率。因此信号相关知识的学习,是深入了解操作系统,深入认识计算机工作原理过程中尤为重要的一环。


一、信号的引入与概念

        在Linux中,信号是进程中事件的一种异步通知,是软中断。在日常生活,假设我们接受到了一个信号(如QQ的消息提示音),而此时我们正在做其他的工作(如写作业),于是我们并不会立刻去处理这一信号,但我们记得这一信号产生过,因此在做完当前的工作后,才会处理该信号(写完作业才打开手机看消息)。

        同样的道理适用于操作系统,当一个进程正在运行时,突然接收到一个信号,操作系统并不会立刻暂停当前进程去处理信号,而是会等当前进程运行完一个小“事务”,或运行到某个标记位,再停下来处理之前产生过的信号。

Linux中的信号(长什么样)

        我们可以用 kill -l 指令查看Linux中的各个信号:

        我们所看到的,红色框框里的是每个信号它们自己的编号,黄色框框是信号名。每个信号都是通过 " #define 信号名 数字 " 的 宏定义的方式定义的,可以在 signal.h 头文件中找到。

还可以发现,Linux中一共有62个信号,是没有32/33号信号的。

        1~31信号:标准信号(非实时信号),也就是我们上面介绍的信号类型,与进程事件是异步的。

        34~64 :实时信号,是Linux中的扩展信号类型,被接受后会立即执行。

重点是标准信号!

信号的作用与几个常见信号

        进程间通信:进程之间可以通过发送和接收信号的方式实现通信,例如当子进程退出时向父进程发送SIGCHLD信号。

        处理任务/异常:当程序出现异常,OS会向进程发送信号处理异常;或用户通过修改信号处理方法,让系统执行自定义任务。

        系统调试:用于程序的调试,例如在程序运行时,向该进程发送 SIGUSR2 信号,可以打印程序的状态信息等。

信号处理的常见动作

        1.忽略此信号。

        2.执行默认操作。

        3.进入用户态,处理用户提供的信号处理函数,这种方式称为信号捕捉

常见的几个信号

2)SIGINT:中断信号,可以由 Ctrl+C 发送

3)SIGQUIT:退出信号,可由Ctrl+ \ 发送

9)SIGKILL:强制退出信号,无法被捕捉,无法阻塞

13)SIGPIPE:管道破裂信号,当进程向已经关闭写端的管道或socket写数据时产生该信号

14)SIGALRM:闹钟信号,可以用 alarm(int seconds) 函数设定倒计时间

        (全部信号的具体内容,可以通过signal手册 [ man 7 signal ] 查看)


二、信号的产生

终端按键产生

        当前台进程正在运行时,我们在键盘上按下Ctrl+C(对应SIGINT)或Ctrl+\(对应SIGQUIT),就会令当前进程退出。其中,按下键盘可以产生一个硬件中断,所产生的信号被OS获取并解析,传递给前台目标进程,进程接收到并处理该信号。

系统调用函数

        kill指令可以向指定pid的进程发送指定的信号

kill -9 "PID" --向指定"PID"的进程发送9号信号,即SIGKILL信号,等价于=>

kill -SIGKILL "PID"

        kill命令是调用kill函数实现的,kill函数可以给指定pid的进程发信号

        raise函数可以给调用自己的进程发信号,自己给自己发

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。

        abort函数,使调用它的进程接收到信号就异常终止

#include <stdlib.h>
void abort(void);

和exit一样,abort总会成功,因此没有返回值

软件条件产生

        概念:当软件层面达到某种条件所产生的信号就是软件条件信号。

        实现方法:OS先识别进程的某种软件的触发条件满足或不满足,当满足后由OS构建信号,并发送给对应的进程。

        如前文所提到的SIGPIPE -- 管道破裂信号。当一个进程试图向一个已经关闭写端的进程写数据,或者读端先关闭,当再次写入时OS会自动终止写端进程,并产生SIGPIPE信号。

除此之外还有其他软件条件信号:

  1. SIGSEGV(段错误):当程序访问无效的内存地址或者试图执行无效的内存操作时,会导致段错误信号的触发。

  2. SIGFPE(浮点异常):当程序执行了除以零、溢出或无效的浮点运算等引起浮点异常的操作时,会触发该信号。

  3. SIGILL(非法指令):当程序试图执行未定义或非法的指令时,例如硬件不支持的指令或者内存中的数据被解释为指令,会导致非法指令信号的触发。

  4. SIGBUS(总线错误):当程序试图对无效的内存地址进行访问,或者在总线传输期间发生了错误时,会触发总线错误信号。

  5. SIGALRM(定时器到期):当设置的定时器超时时,会触发定时器到期信号,通常用于实现定时功能。

SIGALRM 与 alarm 函数

        想使用定时器信号,需要先在程序中定用alarm函数定义倒计时时间:

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

void handler(int signum){
    printf("捕捉到闹钟信号:%d\n",signum);
    alarm(1);    //在信号处理函数中要重置倒计时
}

int main(){

    signal(SIGALRM,handler);//signal函数用于捕捉信号,指定信号和其对应的处理函数
    alarm(1);

    while(1);    //使进程不能结束退出

    return 0;
}

#需要在信号处理函数中需要再次设定alarm时间,相当于alarm设定一次只会执行一次倒计时,并不会自动重装。可以联想到单片机编程中的非自动重装定时器中断,在进入中断函数后需要再设定计时时间。

硬件条件产生

        硬件条件产生的信号,一般是硬件工作出现异常所产生的。

        如SIGFPE(浮点错误信号),当CPU的运算单元出现异常(如程序进行了除0操作时),OS内核会解析该异常并向产生该异常的进程发送SIGFPE信号。

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

void handler(int signum){
    printf("捕捉到浮点异常信号:%d\n",signum);
    exit(0);
}

int main(){

    signal(SIGFPE,handler);

    int err = 1/0;

    return 0;
}

        除此之外还有段错误信号,当进程访问了非法的内存空间地址(如访问0地址),MMU(内存管理单元)会产生异常,OS将异常解析为 11) SIGSEGV(段错误)信号并发送给指定进程:

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

void handler(int signum){
    printf("捕捉到段错误信号:%d\n",signum);
    exit(1);
}

int main(){

    signal(SIGSEGV,handler);

    int *p = NULL;
    *p = 1;    //非法访问了0x0地址

    return 0;
}


三、信号的捕捉

        前文提到,信号是可以被捕捉的,通过捕捉信号,可以用 用户自定义的信号处理函数 替换掉 系统默认的信号处理动作。

1. signal

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

void (*sighandler_t)(int):

        是一个函数指针类型,指向 参数类型为 int,返回值类型为 void 的函数。

参数:

        signum:要注册的信号类型,可以是信号名,也可以是对应的编号。

        handler:函数指针,指向要注册的信号处理函数。

返回值:

        函数指针,指向注册的信号处理函数。如果注册失败,则返回SIG_ERR。

        前文展示了很多使用 signal 方法的案例,此处不再作演示。

2. sigaction

        sigaction相比于signal拥有更强大的功能,可以更细致地设置信号的处理方式。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

struct sigaction:

        用于设置和获取信号处理函数的相关信息的结构体。

参数:

        signum:要注册的信号类型。

        act:输入型参数,填入指向新的信号处理函数的sigaction结构体的指针,表示新的信号处理函数和其选项。

        oldact:输出型参数,获取指向之前信号处理函数的sigaction结构体的指针。

        sigaction函数和sigaction结构体,两者同名却是完全不同的两者。

struct sigaction 结构体定义在 <signal.h> 文件中,其定义如下:

struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

sa_handler

        函数指针,指定信号处理函数。若设置为SIG_IGN,则表示忽略该信号。若设置为SIG_DFL,则表示使用默认处理。

sa_sigaction

        指定信号的扩展处理函数,与 sa_handler 二选一。若设置了 sa_sigaction ,则会在调用 sa_handler 之前调用 sa_sigaction 。

sa_mask

        指定在执行信号处理函数期间要阻塞的信号集。即在信号处理函数运行期间,会将 sa_mask 中的信号添加到进程的信号屏蔽字中,防止这些信号再次中断当前的信号处理函数。

sa_flags

        附加选项标志位,可以(按位或)设置以下选项:

                SA_RESTART:指定在信号处理函数返回后,被中断的系统调用会重启。

                SA_NOCLDSTOP:当子进程退出时,不发送 SIGCHLD 信号。

                SA_NODEFER:不阻塞当前处理信号。

                SA_SIGINFO:指定使用 sa_sigaction 方法,而不是 sa_handler 。

sa_restorer

        指向恢复函数的指针,用于恢复机器的某些状态。已弃用

案例演示

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

void handler(int signum){
    printf("捕捉到信号:%d\n", signum);
    exit(1);
}

int main(){

    struct sigaction newact = {
        newact.sa_handler = handler
    };

    struct sigaction oldact;

    sigaction(2, &newact, &oldact);
    while(1);    //死循环维持程序不退出

    return 0;
}


四、阻塞信号与信号集

相关概念

        信号递达(Delivery):实际开始执行 信号处理(函数) 的动作,称为递达。

        信号未决(Pending):信号 产生之后递达之前 所处的状态,称为未决。

        阻塞(Block):被阻塞的信号产生后,会一直处于未决状态,直到解除阻塞,才会执行递达动作。(不同于忽略的是,被阻塞信号不会递达,被忽略信号是递达后的一种处理方式。)

信号系统在内核中的示意图

如图中的例子:

        SIGHUP 信号没有被阻塞,也没有产生,该信号对应的信号处理方式是 SIG_DFL 默认处理方式;

        SIGINT 信号没有被阻塞,且已经产生但还未递达,等到它递达后,对应执行 SIG_IGN 忽略处理;

        SIGQUIT 信号已经产生还未递达,但由于其被阻塞,所以会一直处于未决状态,直到阻塞被解除,才会在递达后执行与其对应的 handler 用户自定义信号处理函数。

        当信号被阻塞时,其对应的 block 标记位将变成 1 ,若信号此时正处于未决状态,则其对应的 pending 标记位将变成 1 。

        当一种信号的 block 被设为 1 时,该信号在产生后,会一直处于未决状态无法被递达,其 pending 位会一直为 1 。直到该信号的阻塞被解除,该信号才会递达,递达后其 pending 将被置为 0 。

        若一种信号在被阻塞时,在递达之前,产生了很多次,操作系统如何处理?答:常规信号在递达前产生多次只记一次,实时信号递达前产生多次将被依次放入队列中。

信号集及其操作

sigset_t

        前文中的每个信号的 block 和 pending 都是由一个 bit 位标记的,且不记录产生了多少次,因此可以使用一种数据类型来存储。sigset_t 就是这种数据类型,用于表示信号集。信号集可以表示每种信号的“有效”或“无效”的状态,可以一分为二的看做 “ 阻塞信号集 + 未决信号集 ” ,其中阻塞信号集又可以称为当前进程的 “ 信号屏蔽(阻塞)字(signal mask)”。

信号集的操作

        初始化:sigemptyset(sigset_t *set) 函数将信号集初始化为空集,或 sigfillset(sigset_t *set) 函数将信号集初始化为包含所有信号。

        添加信号:sigaddset(sigset_t *set, int signo)

        删除信号:sigdelset(sigset_t *set, int signo)

        查看信号是否在信号集中:sigismember(const sigset_t *set, int signo)

        操作整个信号集:sigprocmask(int how, const sigset_t *set, sigset_t *oset) 函数可以读取或修改当前进程的阻塞信号集。sigpending(sigset_t *set) 函数读取当前进程的未决信号集,通过 set 参数传出。

        值得注意的是,在修改进程的信号屏蔽字时,需要保证进程的原子性(要么执行完,要么不执行,不能执行到一半不执行)。使用 sigprocmask() 可以保证修改信号屏蔽字的操作是原子的。

        


五、信号处理详解 -- 内核处理信号的步骤

        前文我们讲到,信号的处理主要分为几个步骤:

        

而在信号的整个处理过程中,依旧是由操作系统进行调度和管理的。

信号捕捉处理的过程

操作系统处理信号的流程示意图

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

        如果信号处理动作是用户自定义函数,则在信号递达时就调用这个函数,这称为信号捕捉。由于OS在执行信号处理相关操作的程序需要在内核级中跑,而用户自定义函数在用户级空间,因此整个信号捕捉的过程就会比较复杂:

        1.当一个信号产生后,进程并不会立刻停下来执行信号处理函数,而是会先将信号的未决标记位置 1 。

        2.当进程运行因出现中断、异常或系统调用而进入内核后,内核会处理相关的异常,处理完异常后,内核就会着手处理可以递达的信号(有未决且未被阻塞的信号)。

        3.内核着手处理信号时,先进入内核中的 do_signal() 函数,如果没有用户注册的信号处理函数,则直接在内核中运行默认的信号处理方法。如果有注册信号捕捉方法,OS则会由内核级进入用户级,进而处理用户自定义的信号处理函数。

        4.此时由内核级回到用户级,并不是回到主程序main函数的上下文,而是执行 sighandler 函数,sighandler 函数和 main 函数使用不同的堆栈空间,它们之间不存在调用或被调用的关系,是两个独立的控制流程。确切地说,它们都由OS调度。

        5.运行完信号处理的程序后,如果当前在用户级,则会先调用特殊的系统接口 sigreturn() 进入内核,再执行 sys_sigreturn() 方法回到进程的主程序中。若上一步没有信号捕捉,也就是说当前在内核级,则会直接执行 sys_sigreturn() 。


六、相关 -- 核心转储 & 可重入函数

核心转储

        将进程的用户空间内存数据获取出并存储到磁盘中的动作,称为核心转储。

        核心转储的具体操作:Core Dump :当一个进程要异常终止时,可以选择让进程进行核心转储。获取该进程用户空间内存数据,写入到名为 core 的文件中,并保存到磁盘中。这个操作就成为 Core Dump 。其产生的core文件可以用于事后调试(post-mortem debug)。一个进程可以产生多大的 core 文件,可以通过 ulimit 指令修改进程PCB中的Resource Limit信息而设置,其最大可以设置为 1024 KB :$ ulimit -c 1024

        信号的核心转储功能默认是关闭的,因为 core 文件中含有用户信息,存在信息泄密的问题,而且在企业中经常会发生进程异常终止的情况,产生过多的 core 文件会占用磁盘空间,因此核心转储一般只在开发环境中需要调试时才打开。

        并不是所有信号都可以执行核心转储的功能,具体哪些信号可以有核心转储,可以在 $ man 7 signal 中查看:

其中带有 C 动作的信号可以执行核心转储

可重入函数

        可重入函数(reentrant function)是一种可以被多个进程或线程同时访问而不会产生冲突的函数。与其相反的是不可重入函数,不可重入函数可能会导致并发问题,即当多个线程或进程调用该函数时,会出现竞争条件,从而产生意外结果。

        具体来说,可重入函数满足以下两个条件:

        1.不使用全局变量或静态变量,或者使用时通过加锁等方式对其进行同步保护;

        2.所有数据都存储在传递给函数的参数或者栈上,而不是存储在全局数据区或堆区。

        由于可重入函数不依赖于全局状态,因此多个进程或线程可以同时调用该函数,而不需要担心数据竞争或其他并发问题。这使得可重入函数非常适合用于多线程应用程序或操作系统内核中。

        信号的可重入性:是指当信号处理函数被中断时,可以安全地再次调用该函数,而不会引起死锁或其他问题。

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值