【Linux】Ⅴ进程信号

2021/4/17 7:25:19

本文主要是介绍【Linux】Ⅴ进程信号,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

同步与异步

  • 同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)。
  • 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
  • 换句话说,就是由调用者主动等待这个调用的结果。
  • 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞与非阻塞

  • 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

信号的概念

信号是一个软件中断。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。

信号的种类:一共有62

  • 1~31:非可靠信号,信号有可能会丢失。

  • 34~64:可靠信号,信号不会丢失。

Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。

信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在 支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送 函数kill()。

信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

 

信号的产生

硬件产生

  • Ctrl+c:SIGINT(2号信号),发送给前台进程

如何把一个进程放到后台去运行,在启动命令之后加&符号

fg:就将刚刚放到后台的进程,在放到前台来运行

  • Ctrl+z:SIGTSTP(20号信号)除非有特定的场景,否则不要用
  • Ctrl+|: SIGQUIT(3号信号)

软件产生

按照原因分类:

  1. 与进程终止相关的信号。当进程退出,或者子进程终止时,发出这类信号。
  2. 与进程例外事件相关的信号。如进程越界,或企图写一个只读的内存区域(如程序正文区),或执行一个特权指令及其他各种硬件错误。
  3. 与在系统调用期间遇到不可恢复条件相关的信号。如执行系统调用exec时,原有资源已经释放,而目前系统资源又已经耗尽。
  4. 与执行系统调用时遇到非预测错误条件相关的信号。如执行一个并不存在的系统调用。
  5. 在用户态下的进程发出的信号。如进程调用系统调用kill向其他进程发送信号。
  6. 与终端交互相关的信号。如用户关闭一个终端,或按下break键等情况。
  7. 跟踪进程执行的信号。

相关函数和命令

kill函数

#include <signal.h> 
int kill(pidt pid,int sig); 
//pid:接受该信号的进程的pid 
//sig:要发送的具体信号值

abort函数

谁调用该函数,谁收到6号信号封装了的ki11

#include <stdlib.h>
void abort(void);
//相当于kill(getpid(),6);
6号信号(SIGABRT)==>(double free)
解引用空指针+内存访问越界==》进程收到11号信号(SIGSEGV)

kill命令

kill -l
//列出当前所有可用信号,可见上图
kill -[信号名称/编号] 对应进程的pid
eg: kill -1 2246  对pid为2246的进程发出暂停信号
    kill -SIGKILL  2243  杀死进程2243

信号的注册与注销——位图+signqueue队列

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。

在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。如果发送给一个处于可运行状态的进程,则只置相应的域即可。

注意:当前操作系统中没有0号信号 

非可靠信号的注册——在sigqueue中最多只占用一个sigqueue节点

当进程收到一个非可靠信号

第一件事情:将非可靠信号对应的比特位更改为1;

第二件事情:添加sigqueue节点到sigqueue队列当中。但是!!!如果在添加sigqueue节点的时候,队列当中已然有了该信号的sigqueue节点,则不添加。

可靠信号的注册——可能在sigqueue中占用多个sigqueue结点

如果进程收到一个可靠信号

第一件事情:在sig位图当中更改该信号对应的比特位为1;

第二件事情:不论之前sigqueue队列当中是否存在该信号的sigqueue节点,都再次添加sigqueue节点到sigqueue队列当中去。

非可靠信号的注销

1.将该信号的sigqueue节点从sigqueue队列当中进行出队操作

2.信号在sig位图当中对应的比特位从1置位0

可靠信号的注销

1.将该信号的sigqueue节点从sigqueue队列当中进行出队操作

2.需要在判断sigqueue队列当中是还有相同的sigqueue节点

2.1没有相同的节点

信号在sig位图当中对应的比特位从1置位0

2.2还存在相同的节点

不会更改sig位图当中对应的比特位(保持为1)

信号的处理

1.SIG_DFL:默认处理方式

2.SIG_IGN:忽略处理

SIGCHLD信号——僵尸进程的产生实际上是因为子进程在退出的时候,给父进程发送一个SIGSHILD信号,但是父进程在收到这个信号之后,但是父进程对SIGSHILD信号默认是忽略处理,所以才导致子进程的退出资源未能得以回收,才变成了僵尸进程。

不能对子进程发送9号信号(因为不能杀一个已经死去的人)

3.自定义信号处理方式

①signal函数

#include <signal.h>
typedef void (*sighandler_t) (int);
sighandler_t signal(int signum,sighandler_t handler);
//该函数可以更把signum信号的处理动作更换成 handler所指向的函数
//也就是说函数收到signum信号的时候,不会去执行signum信号,
//而是会执行handle所指向的函数

man手册说明:

signal()的行为在UNIX版本之间有所不同,并且在历史上在Linux的不同版本中也有所不同。

避免使用它:改为使用sigaction(2)。请参阅下面的可移植性。

signal()将信号符号的设置为处理程序,该处理程序可以是SIG_IGN,SIG_DFL或程序员定义的函数的地址(“信号处理程序”)。

如果信号值传递给该进程,则发生以下情况之一:

*如果设置为SIG_IGN,则忽略该信号。

*如果设置为SIG_DFL,则发生与信号关联的默认操作(请参见signal(7))。

*如果设置为函数,则首先将处置重置为SIG_DFL,或者阻止信号(请参见下面的可移植性),然后使用参数signum调用处理程序。如果调用处理程序导致信号被阻塞,则从处理程序返回后,信号将被解除阻塞。

信号SIGKILL和SIGSTOP不能够被捕获或忽略。

第一个参数指定信号的值,第二个参数是一个函数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。

如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigcallback(int signo)
{
    printf("signo : %d\n", signo);
}
int main()
{
    signal(2, sigcallback);
    signal(20, sigcallback);
    while(1)
    {
        printf("linux so easy\n");
        sleep(1);
    }

    return 0;
}

②sigaction函数

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

man手册说明:

sigaction()系统调用用于更改进程在收到特定信号后采取的操作。 (有关信号的概述,请参见signal(7))

signum指定信号,并且可以是除SIGKILL和SIGSTOP之外的任何有效信号。

如果act为非NULL,则从act安装信号信号的新动作。 如果oldact为非NULL,则先前的操作将保存在oldact中。

siginfo_t*是指向siginfo_t结构的指针,结构中包含信号携带的数据值。

signal函数底层也是调用sigaction函数。

在使用 struct sigaciton之前,要先进行初始化——用sigempty函数。

int sigemptyset(sigset_t*set);/将位图的所有比特位设置为0
//第一次按下ctrl+|之后执行sigcallback
//第二次按下ctrl+|之后执行三号信号,程序退出
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigcallback(int signo)
{
    printf("signo : %d\n", signo);
}
int main()
{
    //act --》 入参
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    act.sa_handler = sigcallback;
    //oldact --》 出参
    struct sigaction oldact;
    sigaction(3, &act, &oldact);
    getchar();
    sigaction(3, &oldact, NULL);//复原
    while(1)
    {
        printf("linux so easy\n");
        sleep(1);
    }
    return 0;
}

信号的捕捉

什么时候进入到内核空间:调用系统调用函数的时候,或者调用库函数的时候。(库函数底层大多数都是封装系统调用函数的)

信号的阻塞

  1. 信号的阻塞,并不会干扰信号的注册!信号该注册还是注册的,只不过当前的进程不能立即处理了

2.1 当我们将block位图当中对应信号的bite为置为1,表示当前进程阻塞该信号

2.2 当进程收到一个该信号的时候,进程还是一如既往的对该信号进行注册

2.3 当进程进入到内核空间,准备返回用户空间的时候,调用do_signal函数,不会立即去处理该信号

3. 这里面的不会立即处理,一定不是之后不处理(解除阻塞的时候就会对其进行处理)。

sigprocmask函数——设定对信号屏蔽集内的信号的处理方式(阻塞或不阻塞)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how:告诉sigprocmask函数,应该做什么操作
  • SIG_BLOCK:设置某个信号为阻塞
  • SIG_UNBLOCK:解除对某个信号的阻塞
  • SIG_SETMASK:替换阻塞位图set:用来设置阻塞位图
  • SIG_BLOCK:设置某个信号为阻塞

block(new)=block(old)| set (按位或)

0010 = 0000 | 0010

  • SIG_UNBLOCK:解除对某个信号的阻塞

block(new)=block(old)&(~set)(取反之后按位与)

0000 = 0010 & (~0010)=0010 & 1101

  • SIG_SETMASK:替换阻塞位图

block(new)=seto1dset:原来的阻塞位图

  • oldset:原来的阻塞位图

注意:9号信号不能被阻塞,也不可以对9号信号进行自定义函数

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sigcallback(int signo)
{
    printf("signo : %d\n", signo);
}
int main()
{
    signal(2, sigcallback);
    signal(40, sigcallback);
    sigset_t set;
    sigfillset(&set);
    //将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志
    //位置为1,阻塞所有的信号
    sigset_t oldset;//保存之前的信号集
    sigprocmask(SIG_SETMASK, &set, &oldset);
    getchar();
    sigprocmask(SIG_SETMASK, &oldset, NULL);//信号集复原,解除上面的阻塞
    while(1)
    {
       printf("linux so easy\n");
       sleep(1);
    }
    return 0;
}

 



这篇关于【Linux】Ⅴ进程信号的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程