Linux信号详解

  Linux 信号是操作系统中的重要组成部分,可以用于进程间通信、处理异常等多种场景。本文将深入介绍 Linux 信号的相关知识,包括信号的定义、类型、发送和接收、处理等内容,帮助读者更好地理解和使用 Linux 信号

一、Linux信号

1. 信号的概念

生活角度的信号

  • 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
  • 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
  • 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种
      1. 执行默认动作(幸福的打开快递,使用商品)
      1. 执行自定义动作(快递是零食,你要送给你你的女朋友)
      1. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
  • 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

技术应用角度的信号

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


2. 信号的定义

信号是 Linux 操作系统中用于进程间通信、处理异常等情况的一种机制。它是由操作系统向一个进程或者线程发送的一种异步通知,用于通知该进程或线程某种事件已经发生,需要做出相应的处理。

信号的作用:

  • 进程间通信:进程可以通过向其他进程发送信号的方式进行通信,例如某个进程在完成了某项工作之后,可以向另一个进程发送 SIGUSR1 信号,通知其进行下一步的操作。

  • 处理异常:信号可以被用来处理程序中的异常情况,例如当一个进程尝试访问未分配的内存或者除以 0 时,系统会向该进程发送 SIGSEGV 或 SIGFPE 信号,用于处理这些异常情况。

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

3. 系统定义的信号

Linux 中,信号分为标准信号实时信号,每个信号都有一个唯一的编号。

  • 标准信号:最基本的信号类型,由整数编号表示,编号范围是 1 到 31。
  • 实时信号:Linux 中的扩展信号类型,由整数编号表示,编号范围是 32 到 64。

可以用kill -l命令可以察看系统定义的信号列表

kill -l

查询结果如下:
在这里插入图片描述

常见信号编号以及对应信号的名称:
在这里插入图片描述

注意不同的操作系统可能对信号的编号有所不同,因此在跨平台开发时应当注意信号编号的兼容性。

  • term 表示终止进程
  • core 表示生成核心转储文件,核心转储文件可用于调试
  • ignore 表示忽略信号
  • cont 表示继续运行进程
  • stop 表示停止进程(注意停止不等于终止,而是暂停)

相关信号的解读:

2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

4) SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

5) SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。

6) SIGABRT
调用abort函数生成的信号。

7) SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)8) SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

10) SIGUSR1
留给用户使用

11) SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

12) SIGUSR2
留给用户使用

13) SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

14) SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

15) SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

17) SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)18) SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

19) SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

20) SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

21) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

22) SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

23) SIGURG
有"紧急"数据或out-of-band数据到达socket时产生.

24) SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

25) SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。

26) SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

27) SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

28) SIGWINCH
窗口大小改变时发出.

29) SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作.

30) SIGPWR
Power failure

31) SIGSYS
非法的系统调用。
  • 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
  • 不能恢复至默认动作的信号有:SIGILL,SIGTRAP
  • 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
  • 默认会导致进程退出的信号有:
    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
  • 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
  • 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
  • 此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

二、信号产生的方式

1.通过键盘产生

Ctrl + C 

通过键盘敲入 Ctrl + C ,可以向进程发送 2 号信号使其终止。

2. 通过系统调用

① kill

#include<signal.h>
#include<sys/types.h>
 
int kill(int pid,int signal);  //通过函数向指定进程发送信号

② raise

raise接口可以向当前调用进程发送任意信号

可以给当前进程发送指定的信号,即自己给自己发信号

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

raise(11);   //向当前进程发送 11 号信号

③ abort

使当前进程接收到信号而异常终止

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

abort();

3. 软件条件

alarm

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数

打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

#include <stdio.h>
#include <unistd.h>

int main()
{
    int ret = alarm(20);

    while (1) {
        printf("I am a process, ret = %d\n", ret);
        sleep(5);

        int res = alarm(0); //取消闹钟
        printf("res = %d\n", res);
    }

    return 0;
}

4. 硬件异常

#include <stdio.h>
#include <signal.h>
 
void handler(int sig)
 {
    printf("catch a sig : %d\n", sig);
 }
 
int main()
 {
    signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
 
    while(1);
    return 0;
 }

这里出现野指针异常。系统本该直接清除掉该进程,但因为修改了野指针异常所对应的操作函数,导致系统只打印了一句话,而不正常的对该进程进行清除。但系统不断地检测到有野指针异常。但操作系统做的工作仅仅是打印一句话。那么这里就造成了循环打印catch a sig : xxx

在这里插入图片描述

三、信号处理函数

1. OS发送信号的实质

信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终一定都是通过OS向目标进程发送信号。产生信号的方式,其实都是OS发送信号数据给task_struct

struct task_struct 中有进程的各种属性,那么其中也一定有对应的数据变量,来保存是否收到了对应的信号,而信号的编号也是有规律的1~31

进程中采用 uint32_t sigs ;——位图结构来标识该进程是否收到信号

0000 0000 0000 0000 0000 0000 0000 0000

比特位的位置(第几个)代表的就是哪一个信号,比特位的内容(0或1),代表的就是是否收到了信号

故本质是OS向指定进程的task_struct中的信号位图写入比特1,即完成信号的发送,也可以说是信号的写入

struct task_struct
{
	//信号位图
	0000 0000 10...
	uint32_t sigmap;
}

2. 指令发送信号

  1. kill 命令

    kill 命令是 Linux 中最常用的发送信号的命令,语法如下:

     kill [-signal] PID
    

    其中,-signal 可选参数表示要发送的信号类型,如果省略该参数,则默认发送 SIGTERM 信号。PID 表示接收信号的进程 ID。

    例如,要向进程 ID 101080 发送 SIGINT 信号,可以执行以下命令:

    kill -SIGINT 101080
    kill -2 101080
    

    当然可以发送对应的信号编号

  2. kill 函数

    除了使用 kill 命令,程序中也可以通过 kill 函数来发送信号。kill 函数的原型如下:

    int kill(pid_t pid, int sig);
    

    其中,pid 表示接收信号的进程 ID,sig 表示要发送的信号类型。如果函数调用成功,则返回 0,否则返回 -1 并设置 errno。

    例如,要向进程 ID 123 发送 SIGINT 信号,可以执行以下代码:

    #include <signal.h>
    #include <unistd.h>
    
    int main() 
    {
        pid_t pid = 123;
        int sig = SIGINT;
        if (kill(pid, sig) == -1) 
        {
       		perror("kill");
       		return 1;
     	}
         return 0;
    }
    

3. signal()

signal 函数可以用来自定义信号捕捉函数

#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);


//示例
void handler(int signum)
{
    cout<<"get a signum: "<<signum<<endl;
}

int main()
{
    signal(14,handler);  //此时进程收到14号信号的默认处理动作就是执行handler函数
}

signal 函数的两个宏:

signal(2,SIG_DFL);   //default 使对应的2号信号恢复默认动作
signal(2,SIG_IGN);   //ignore 忽略二号信号
  1. 参数:

    • signum:此参数指定需要进行设置的信号,可使用信号名(宏)或信号的数字编号,建议使用信号名。
    • handler:sighandler_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设 置为 SIG_IGN 或 SIG_DFL,SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。
    • sighandler_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数 上,此时就可通过此参数来判断当前触发的是哪个信号。
  2. 返回值:此函数的返回值也是一个 sig_t 类型的函数指针

    • 成功:返回值则是指向在此之前的信号处理函数;
    • 出错:则返回 SIG_ERR,并会设置 errno。

​ 由此可知,signal()函数可以根据第二个参数 handler 的不同设置情况,可对信号进行不同的处理。

4. sigaction()

在Linux中,sigaction函数是用于设置和检索信号处理器的函数。

sigaction函数有以下语法:

#include <signal.h>
 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

其中,signum 表示要注册的信号编号,act 是一个指向 struct sigaction 结构体的指针,表示新的信号处理函数和信号处理选项,oldact 是一个指向 struct sigaction 结构体的指针,用于获取之前注册的信号处理函数和信号处理选项。

struct sigaction 结构体的定义如下:

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:指定一个信号处理器函数,这个函数包含三个参数:一个整数表示信号编号,一个指向siginfo_t结构体的指针,和一个指向void类型的指针。

  • sa_mask:指定了在执行信号处理函数期间要阻塞哪些信号。

  • sa_flags:是一个标志位,可以包括以下值:

    • SA_NOCLDSTOP:如果设置了该标志,则当子进程停止或恢复时不会生成SIGCHLD信号。
    • SA_RESTART:如果设置了该标志,则系统调用在接收到信号后将被自动重启。
    • SA_SIGINFO:如果设置了该标志,则使用sa_sigaction字段中指定的信号处理器。
  • sa_restorer:是一个指向恢复函数的指针,用于恢复某些机器状态。

返回值:如果成功,则返回0,否则返回-1,并设置errno错误号。可以使用以下代码来检查errno

注意:一般情况下,sa_handlersa_mask 使用较多,这里再次详细说明:

  1. sa_handler:

    • 赋值为常数SIG_IGN表示忽略信号

    • 赋值为常数SIG_DFL表示执行系统默认动作

    • 赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数

      该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信 号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

  2. sa_mask:

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

    注意:信号在被处理前,pendding位图就修改了对应的比特位

四、信号屏蔽机制

1. 信号处理方式

  1. 信号的处理方式有三种,称为信号的递达

    • 信号的忽略( SIG_IGN ),即不采取任何动作
    • 信号的默认,使用系统默认的处理方法
    • 信号的自定义捕捉,采用用户定义的方法处理
  2. 信号从产生到递达之间的状态,称为信号的未决( Pending )

    • 信号在位图中,未被处理(未决)
  3. 进程选择阻塞( Block )某个信号

    • 未决之后,暂时不递达,直到解除对信号的阻塞,期间对该信号为屏蔽

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

  1. block 信号阻塞表

    在该表中标记为 1 的信号将被阻塞

  2. pending 信号未决表

    信号被进程接收,但还未被处理的信号,就存放在该表中

    • 普通信号:只在pending表记录一次,如果产生多次,也只记录一次,之后系统对该信号的处理动作为 1 次
    • 实施信号:采用队列的形式,记录历史中产生的所有信号,后系统对该信号的处理动作为 1历史中产生的次数

    注意:pendding 表存在多个信号时,会全部处理完,才返回用户端

  3. bandler 信号对应处理方法表

    该表中记录了信号对应操作方法的函数指针

2.信号集操作函数

头文件:

#include <signal.h>  //头文件
  1. sigemptyset

    int sigemptyset(sigset_t *set);
    

    初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。

  2. sigemptyset

    int sigemptyset(sigset_t *set);
    

    函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

  3. sigfillset

    int sigfillset(sigset_t *set);
    

    函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。

  4. sigaddset

    int sigaddset (sigset_t *set, int signo); 
    

    添加屏蔽信号

  5. sigdelset

    int sigdelset(sigset_t *set, int signo);
    

    删除屏蔽信号

  6. sigismember

    int sigismember(const sigset_t *set, int signo); //查询是否有该屏蔽信号
    
    示例:
    sigset_t  set oset;
    sigismember(pending , int signo)
    

    查询是否有该屏蔽信号

  7. sigprocmask

    int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
    

    示例:

    sigset_t  set, oset;
    sigprocmask(SIG_BLOCK, &set, &oset);
    
    1. 参数:
    • how

      SIG_BLOCK    //添加屏蔽字
      SIG_UNBLOCK  //删除
      SIG_SETMASK  //直接用新的替换
      
    • const sigset_t

      当前需要修改的 sigset_t

    • oset

      传出之前的pending码

    使用示例:

    int main()
    {
    sigset_t set, oset;
    // 初始化,全置空
    //  sigfillset 全置1
    sigemptyset(&set);
    sigemptyset(&oset);
    
    // 设置屏蔽字
    //  sigdelset 清除
    sigaddset(&set, 2); // 对于set位图,对2号信号设置屏蔽字
    
    // 将 set 信号设置进入当前进程
    
    
    sigprocmask(SIG_BLOCK, &set, &oset); // oset为之前的屏蔽字  
    }
    
  8. sigpending

    int sigpending(sigset_t *set);
    
    示例:
    sigset_t pending;
    sigemptyset(&pending);
    
    • 功能:读取当前进程的未决信号集,通过set参数传出。调用成功则返回 0 ,出错则返回 -1

    示例:

     //通过此中方式读出
     void Printsignal (const sigset_t &pending)
    {
       for(int signo = 31; signo > 0; signo--)
      {
          if(sigismember(&pending, signo))
          {
              std::cout << "1";
          }
          else
          {
              std::cout << "0";
          }
      }
      std::cout << "\n";
     }
    

五、临界资源和临界区

  1. 临界资源

    • 概念:被保护起来的公共资源,一次仅允许一个进程使用的共享资源。其他都是非临界资源
  2. 临界区

    • 概念:每个进程中访问临界资源的那段程序(代码)称之为临界区

    • 临界区不是内核对象,而是系统提供的一种数据结构,程序中可以声明一个该类型的变量,之后用它来实现对资源的互斥访问。

      ​ 当欲访问某一临界资源时,先将该临界区加锁(若临界区不空闲则等待),用完该资源后,将临界区释放。

    • 补充(待定):分类:临界区也是代码的称呼,所以一个进程可能有多个临界区,分别用来访问不同的临界资源。

      • 内核程序临界资源:系统时钟

      • 普通临界资源:普通I/O设备,如打印机(进程访问这些资源的时候,很慢,会自动阻塞,等待资源使用完成)

  3. 信号量

    ​ 表示资源数目的计数器。每一个执行流想访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量。资源其实就是对信号量计数器进行自减操作,本质上只要自减成功。就完成了对资源的预定机制。如果申请不成功,执行流将被挂起阻塞。

  4. 进程进入临界区的调度原则

    • 如果有若干进程请求进入空闲的临界区(空闲即0进程访问),一次仅允许 一个进程进入。
    • 任何时候,处于临界区内的进程不可多于一个(0 或 1),若已有进程进入自己的临界区,则其它想进入自己临界区的进程必须等待。
    • 进行临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
    • 如果其它进程不能进入自己的临界区,则应让出 CPU,避免进程出现 “忙等” 现象。
    • 访问临界资源时先访问信号量资源(可以理解为信号量引用计数自减1,如果自减成功,说明还能访问,未达到上限,允许访问,访问结束后信号量自增1)
    • 临界资源的访问是原子的
  5. 细节问题

    • 每个进程都得先看到同一个信号量资源,就只能由OS提供IPC体系。

    • 信号量本身也是公共资源。

    • 单个信号量

      struct sem
      {
          int count;  //引用计数
          task_struct *wait_queue;  //进程等待队列
      }
      
      //如果当前引用计数不为零,那么新的进程将会直接被运行。
      //如果当前引用计数为零,那么新的进程将会加入等待队列。当别的进程结束时,引用计数自增,同时等待队列中的进程将会被执行,同时引用计数自减
      
  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ryan.Alaskan Malamute

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值