【Linux】学习记录_9_信号

9 信号

信号(signal),又称为软中断信号,用于通知进程发生了异步事件, 它是Linux系统响应某些条件而产生的一个事件,它是在软件层次上对中断机制的一种模拟, 是一种异步通信方式,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。

62种信号分为2大类:信号值为1~31的信号属性非实时信号(也称为不可靠信号), 它们是从UNIX系统中继承下来的信号, 信号值为34~64的信号为实时信号(也被称为可靠信号)。

  • 信号的"值"在 x86、PowerPC 和 ARM平台下是有效的,别的平台的信号值也许不一致。

  • "描述"中一些情况发生时会产生相应的信号,任何进程都可用kill()函数来产生任何信号。

  • SIGKILL和SIGSTOP是特殊的信号,不能被忽略、阻塞或捕捉,只能按缺省动作来响应。

  • 信号的响应处理过程如下:

    • 若信号被阻塞,那么将信号挂起, 不对其做任何处理,等到解除对其阻塞为止。

    • 若信号被捕获,那么进一步判断捕获类型,

      • 若设置响应函数,那么执行响应函数;

      • 若设置为忽略,那么直接丢弃该信号。 最后才执行信号的默认处理。

Linux 系统中有许多信号,其中前面 31 个信号都有一个特殊的名字,对应一个特殊的事件。

信号值为1~31的信号属性非实时信号,这类信号不支持排队, 信号可能会丢失。发送多次相同的信号,进程只能收到一次, 也只会处理一次,因此剩下的信号将被丢弃。而实时信号(信号值为34~64的信号)支持排队,发送了多少个信号给进程,进程就会处理多少次。

为什么说信号还有可靠与不可靠呢,这得从信号的处理过程来介绍了: 一般来说,一个进程收到一个信号后不会被立即处理,而是在恰当时机进行处理! 一般是在中断返回的时候,或者内核态返回用户态的时候(这种情况是比较常见的处理方式)。

也就是说,即使这些信号到来了,进程也不一定会立即去处理它, 因为系统不会为了处理一个信号而把当前正在运行的进程挂起,这样的话系统的资源消耗太大了, 如果不是紧急信号,是不会立即处理的,所以系统一般都会选择在内核态切换回用户态的时候处理信号。 比如有时候进程处于休眠状态,但是又收到了一个信号,于是系统就得把信号储存在进程唯一的PCB(进程控制块)当中。

而非实时信号是不支持排队的,假如此时又有一个信号到来,那么它将被丢弃,这样进程就无法处理这个信号, 所以它是不可靠的。对于实时信号则没有这种顾虑,因为它支持排队,信号是不会被丢弃的, 这样子每个到来的信号都能得到有效处理。

9.1 信号的处理

生成信号的事件一般可以归为3大类:程序错误、外部事件以及显式请求。

  • 程序错误:零作除数、非法存储访问等,通常是由硬件检测到的, 但由内核向发生此错误的那个进程发送相应的信号;

  • 外部事件:用户在终端按下某些键时产生终端生成的信号,当进程超越了CPU或文件大小的限制时, 内核会生成一个信号通知进程;

  • 显式请求如:使用kill()函数允许进程发送任何信号给其他进程或进程组。

信号的生成既可以是同步的,也可以是异步的。

  • 同步信号大多数是程序执行过程中出现了某个错误而产生的, 由进程显式请求生成的给自己的信号也是同步的。

  • 异步信号是接收进程可控制之外的事件所生成的信号,一般进程无法控制,只能被动接收,因为进程也不知道这个信号会何时发生,只能在发生的时候去处理它。 一般外部事件总是异步地生成信号,异步信号可在进程运行中的任意时刻产生, 进程无法预期信号到达的时刻,它所能做的只是告诉Linux内核假如有信号生成时应当采取什么行动(这相当于注册信号对应的处理)。

无论是同步还是异步,信号发生时,可以告诉Linux内核采取如下3种动作中的任意一种:

  • 忽略信号。大部分信号都可以被忽略,两个除外:SIGSTOP和SIGKILL绝不会被忽略。

  • 捕获信号。当信号出现时调用信号处理函数,它专门对产生信号的事件作出处理。

  • 让信号默认动作起作用。系统为每种信号规定了一个默认动作,这个动作由Linux内核来完成, 有以下几种可能的默认动作:

    • 终止进程并且生成内存转储文件,即写出进程的地址空间内容和寄存器上下文至进程当前目录下名为cone的文件中;

    • 终止终止进程但不生成core文件。

    • 忽略信号。

    • 暂停进程。

    • 若进程为暂停状态,恢复进程,否则将忽略信号。

9.2 捕获信号

9.2.1 signal()

用于捕获信号,可改变进程中对信号的默认行为,捕获这个信号后, 也可以自定义对信号的处理行为。

使用signal()时,需提前设置回调函数,即响应函数, 或者设置忽略某个信号,这个过程称为"信号的捕获"。 对一个信号的"捕获"可以重复进行,不过signal()函数将会返回前一次设置的信号响应函数指针。

c 复制代码
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal是一个带有signum和handler两个参数的函数。 signum指出准备捕获或忽略的信号,handler指出接收到指定的信号后将要调用的函数。

signum是指定捕获的信号,如果指定的是一个无效的信号, 或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为EINVAL。

handler是一个函数指针,它的类型是 void(*sighandler_t)(int) 类型,拥有一个int类型的参数, 这个参数的作用就是传递收到的信号值,返回类型为void。

signal()函数会返回一个sighandler_t类型的函数指针,这是因为调用signal()函数修改了信号的行为, 需要返回之前的信号处理行为是哪个,以便让应用层知悉, 如果修改信号的默认行为识别则返回对应的错误代码SIG_ERR。

handler需要用户自定义处理信号的方式,当然还可以使用以下宏定义:

  • SIG_IGN:忽略该信号。

  • SIG_DFL:采用系统默认方式处理信号。

c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/** 信号处理函数 */
void signal_handler(int sig)            //(3)
{
    printf("\nthis signal number is %d \n",sig);

    if (sig == SIGINT)
    {
        printf("I have get SIGINT!\n\n");
        printf("The signal has been restored to the default processing mode!\n\n");
        /** 恢复信号为默认情况 */
        signal(SIGINT, SIG_DFL);        //(4)
    }
}

int main(void)
{
    printf("\nthis is an singal test function\n\n");

    /** 设置信号处理的回调函数 */
    signal(SIGINT, signal_handler);         //(1)

    while (1)
    {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n");
        sleep(1);                           //(2)
    }
    exit(0);
}

9.2.2 sigaction()

c 复制代码
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
{
    void     (*sa_handler)(int);/* 是捕获信号后的处理函数,int类型的参数传入信号的值 */
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
c 复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/** 信号处理函数 */
void signal_handler(int sig)                    //(1)
{
    printf("\nthis signal number is %d \n",sig);

    if (sig == SIGINT) {
        printf("I have get SIGINT!\n\n");
        printf("The signal is automatically restored to the default handler!\n\n");
        /** 信号自动恢复为默认处理函数 */
    }
}

int main(void)
{
    struct sigaction act;
    printf("this is sigaction function test demo!\n\n");
    /** 设置信号处理的回调函数 */
    act.sa_handler = signal_handler;            //(2)
    /* 清空屏蔽信号集 */
    sigemptyset(&act.sa_mask);                  //(3)
    /** 在处理完信号后恢复默认信号处理 */
    act.sa_flags = SA_RESETHAND;                //(4)
    sigaction(SIGINT, &act, NULL);              //(5)
    while (1)
    {
        printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n\n");
        sleep(1);
    }
    exit(0);
}
  1. 这里没有在函数中让信号恢复默认处理,因为设置了sa_flags成员变量, 在处理完信号后自动恢复默认的处理。

  2. 实验使用sa_handler作为信号处理成员变量而不是sa_sigaction。

  3. 调用sigemptyset()函数清空进程屏蔽的信号集,在信号处理的时候不会屏蔽任何信号。

  4. 设置sa_flags成员变量为SA_RESETHAND,在处理完信号后恢复默认信号处理。

  5. 调用sigaction()函数捕获SIGINT信号。

9.3 发送信号相关API函数

9.3.1 kill()
c 复制代码
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

kill()函数的参数有两个,分别是pid与sig,还返回一个int类型的错误码。

  • pid的取值如下:

    • pid > 1:将信号sig发送到进程ID值为pid指定的进程。

    • pid = 0:信号被发送到所有和当前进程在同一个进程组的进程。

    • pid = -1:将sig发送到系统中所有的进程,但进程1(init)除外。

    • pid < -1:将信号sig发送给进程组号为-pid (pid绝对值)的每一个进程。

  • sig:要发送的信号值。

  • 函数返回值:

    • 0:发送成功。

    • -1:发送失败。

进程可调用kill()函数向包括它本身在内的其他进程发送一个信号。 如果程序没有发送该信号的权限,对kill函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。 因此要想发送一个信号,发送进程必须拥有相应的权限,这通常意味着两个进程必须拥有相同的用户ID(即你只能发送信号给属于自己的进程, 但超级用户可以发送信号给任何进程)。

Kill()函数会在失败时返回-1并设置errno变量。失败的原因可能是:

  • 给定的信号无效(errno设置为INVAL)

  • 发送进程权限不够(errno设置为EPERM)

  • 目标进程不存在(errno设置为ESRCH)。

9.3.2 raise()

raise()函数也是发送信号函数, raise()函数只是进程向自身发送信号的,而没有向其他进程发送信号, 可以说kill(getpid(),sig)等同于raise(sig)。

9.3.3 alarm()

alarm()也称为闹钟函数,可在进程中设置定时器,当定时器指定的时间seconds到时, 它就向进程发送SIGALARM信号。其函数原型如下:

unsigned int alarm(unsigned int seconds);

如果在seconds秒内再次调用了alarm()函数设置了新的闹钟,则新的设置将覆盖前面的设置, 即之前设置的秒数被新的闹钟时间取代。它的返回值是之前闹钟的剩余秒数,如果之前未设闹钟则返回0。 特别地,如果新的seconds为0,则之前设置的闹钟会被取消,并将剩下的时间返回。

相关推荐
我真的太难了啊15 分钟前
学习QT第二天
开发语言·qt·学习
Mr. bigworth25 分钟前
Linux安装RabbitMQ
linux·运维·rabbitmq
稻草人ZZ34 分钟前
Keepalived部署
linux·服务器·网络·keepalived
醉陌离1 小时前
渗透测试学习笔记——shodan(3)
笔记·学习
叫我龙翔1 小时前
【项目日记】仿mudou的高并发服务器 --- 实现缓冲区模块,通用类型Any模块,套接字模块
linux·运维·服务器·网络·c++
流着口水看上帝2 小时前
JavaScript学习路线
学习
Ting丶丶2 小时前
安卓应用安装过程学习
android·学习·安全·web安全·网络安全
被猫枕的咸鱼2 小时前
项目学习:仿b站的视频网站项目03-注册功能
学习
荼靡6032 小时前
shell(三)
linux·服务器·数据库