您现在的位置是:首页 >技术教程 >【Linux】进程信号网站首页技术教程

【Linux】进程信号

meow_yao 2024-06-14 17:18:23
简介【Linux】进程信号

目录

一、预备知识

二、信号产生

1.查看信号——kill -l

2.信号捕捉——signal()

1.函数定义

2.举例运用

3.信号产生

3.1键盘输入

3.2 系统调用

3.3 软件条件

3.4 硬件异常

补充学习:core_domp

三、信号保存

1.阻塞信号

2.信号的保存

四、信号的处理

1.sigset_t (信号集类型)

2.信号集操作函数

3. sigprocmask

4. sigpending

5.捕捉信号

6.处理信号的方式

6.1执行默认动作

6.2忽略信号

6.3执行自定义动作

7.可重入函数

7.1 定义

7.2 不可重入函数

8.volatile

9.SIGCHID信号


一、预备知识

1.信号是什么??

生活中,存在各种各样的信号,诸如,红绿灯、下课铃声、闹钟响等等。

信号发出后,我们接收到信号,我们会做出相应的动作,诸如“红灯停绿灯行”等。为什么?因为我们从小就被如此教育,这已经形成了我们的潜意识,即使没有看到红绿灯,我们也知道应该“红灯停绿灯行”!!

2.信号产生后,我们能够认识并处理它——我们能识别信号。

那么,进程信号和我们所理解的信号有什么不同吗??——答案是,没有区别!!进程就相当于我们自己,进程信号就是信号。进程能识别进程信号——程序员早就设置好了。

3.信号随时可能产生,在信号产生前,我们可能在做优先级更高的事情,即比起对接收到的信号做出相应的动作,我们更应该继续做原本正在做的动作。同理,进程信号也随时可能产生,在接收到信号之前,进程可能正在做优先级更高的事情,接收到信号时,不会立马对信号做出相应的动作,而是到一个合适的时机,才做出反应。

——如此,进程信号需要保存,进程必须有记录信号的能力

4.信号的产生对于进程而言是异步的。(即信号的产生与进程无关,进程不会等待信号的产生)

5.进程如何记录产生的信号,记录在哪里??

先描述再组织——>怎么描述一个信号,用什么数据结构管理信号??

信号只需要保存有无产生——>用位图进行管理——>task_struct中必须有一个位图结构,用int表示

6.发送信号的本质是写入信号,直接修改特定进程中信号位图中特定比特位的值0->1。

比特位的位置——信号的编号;比特位的值——是否收到信号。

7.task_struct是内核数据结构,只能由OS来修改——无论有多少种信号产生的方式,最终都是OS来完成

二、信号产生

1.查看信号——kill -l

2.信号捕捉——signal()

1.函数定义

1.1函数功能解释:

signal()将信号信号的处置设置为处理程序,该处理程序可以是SIG IGN、SIG DFL或程序员定义的函数("signalhandler")的地址。另外,信号SIGKILL 和SIGSTOP不能被捕捉或忽略,会立即强制执行。

1.2函数返回值

signal()返回信号处理程序的前一个值,或者在错误时SIG ERR。如果发生错误,则设置errno来指示原因。

2.举例运用

signal()函数:可以对进程信号进行自定义捕捉。

2.1如下,以2号信号(SIGINT)的自定义捕捉为例:

#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

//自定义方法
//signo:特定信号被发送给当前进程时,执行handler()方法时,要自动填充对应信号给handler方法
//我们甚至可以给所有的信号设置同一个方法
void handler(int signo)
{
    cout << "接收到信号:"<< signo <<endl;
    //exit(signo);
}

int main()
{
    //2号信号的默认动作是终止进程,可以通过signal()函数自定义2号信号对应的动作,即进程信号的自定义捕捉。
    signal(2,handler);//该函数执行时,更改了2号信号对应的动作(执行函数的映射关系),没有调用函数handler()
                      //后续过程中,当进程接收到2号信号时,进程会调用handler()函数

    while(true)
    {
        cout << "我是一个进程,我正在运行," << "我是:" <<getpid() << endl;
        sleep(1);
    }
}

注:./mysignal生成的是前台进程,可以被ctrl+C杀死;./mysignal & 产生的是后台进程,不可以被ctrl+C杀死。

2.2尝试自定义捕捉所有的普通信号

//尝试对所有普通信号进行自定义捕捉
for(int i = 1;i <= 31;i++)
{
    signal(i,handler);
}

经测试,几乎所有的普通信号都能被自定义捕捉,有两个例外——9号信号,即

以及19号信号,即

 

。这是操作系统设置的,防止用户自定义捕捉,导致进程无法被杀死或暂停。

3.信号产生

3.1键盘输入

平时在输入数据时,计算机如何知道键盘输入了数据呢??——硬件中断

简单来说,按键时,中断控制器会将键盘输入的行为传送给CPU(对应的针脚),然后通知系统,我们的键盘已经开始输入数据了。不同的硬件对应在CPU中不同的阵脚,对应不同的中断号,对应中断向量表中不同的区域。而后,操作系统从对应硬件中读取数据(硬盘什么位置被按下)。(之后要学习的网卡等其他外设的数据读取也是同理)。

例如:键盘输入ctrl+C时产生2号信号(SIGINT)。

补充:信号的概念:信号是进程之间事件异步通知的一种方式,属于软中断。(先不解释)

3.2 系统调用

3.2.1 kill()

函数功能:向进程pid为kill()函数的一个参数值的进程发送信号sig。

命令行进行命令输入kill命令的本质也是调用系统调用kill()

//mykill.cc
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <string>
#include <cstring>
#include <cerrno>

using namespace std;

void Usage(string proc)
{
    cout << "	Usage: 
	";
    cout << proc << "信号信号编号 目标进程
" << endl;
}

//./mykill signo target_id
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    int signo = atoi(argv[1]);
    int target_id = atoi(argv[2]);

    int n = kill(target_id,signo);
    if(n != 0)
    {
        cerr << errno << " : " << strerror(errno) << endl;
        exit(2);
    }

    return 0;
}

可以在命令行通过 ./mykill 指定信号 指定进程 给目标进程发送指定信号。(mykill是上面程序编译后得到的可执行程序名)

3.2.2 raise()

函数功能:向调用该函数的进程发送sig信号(自己给自己发信号)

//mykill.cc
int main(int argc,char* argv[])
{
    sleep(1);
    raise(2);//向自己发送2号信号

    return 0;
}

3.2.3 abort()

该函数是C语言接口。

函数功能:想自己发送6号信号(SIGABRT),该信号可以被捕捉,但是还是会终止进程。

//mykill.cc
void handler(int signo)
{
    cout << "进程" << getpid() << " 接收到信号:"<< signo <<endl;
    //exit(signo);
}

int main(int argc,char* argv[])
{
    signal(SIGABRT,handler);

    cout << "begin" << endl;
    sleep(1);
    abort();//自己给自己发信号SIGABRT,可以被捕捉,但仍会终止当前进程
    cout << "end" << endl;//进程运行时不会打印end

	return 0;
}

3.3 软件条件

SIGPIPE和SIGALRM都是由软件条件产生的信号。

3.3.1 SIGPIPE(学习管道时已经学习过)

3.3.2 alarm()

闹钟设置,一个进程只能设置一个闹钟,重复设置会产生覆盖的结果;但是多个进程都可以设置闹钟,同时可能存在多个闹钟,所以操作系统需要对闹钟进行管理。

函数功能:定闹钟,在seconds秒后终止进程,seconds =0 时取消前面所有的闹钟。

返回值:

返回先前闹钟的剩余时间,如果先前没定闹钟,则返回0。

闹钟结束时给自己发送14号信号SIGALRM,该信号可以被捕捉。闹钟重新设定后进程退出的时间也会发生改变。

int count = 0;
void handler(int signo)
{
    cout << "接收到信号: " << signo << ", count = " << count << endl;
    int n = alarm(10);//n的值恒为0,因为handler方法是在进程接收到信号时调用的函数,此时闹钟已经响了,
                      //但是进程不会结束,因为每次闹钟响,都会重新设定一个10s的闹钟
    cout << "重新设定闹钟," <<"alarm的返回值是: " << n << endl;
}

int main(int argc,char* argv[])
{
    //signal(SIGALRM, handler);//捕捉SIGALRM信号
    cout << "我是一个进程, 我的pid是 :" << getpid() << endl;
    //alarm(1);//时间到,闹钟响,执行handler方法
    //while(true) count++;//count的第一次打印的值为计算机的算力( 1S 我们的计算机会将一个整数累计到多少,算力)
    int n1 = alarm(5);
    cout << n1 << endl;//0
    sleep(3);
    int n2 = alarm(10);
    cout << n2 << endl;//2
    sleep(8);
    int n3 = alarm(2);
    cout << n3 << endl;//2
    //以上接连设定了三个闹钟,每次重新设定的闹钟都会覆盖前面的闹钟(一个进程在某一时刻至多存在一个自己设定的闹钟)
    //不过可以有多个进程为当前进程设定了闹钟(发送SIGALRM信号)

	return 0;
}

运行截图1(main()函数中被注释的部分取消注释,并将下面的代码注释掉):

运行截图2:

3.4 硬件异常

硬件异常有很多种,常见的有CPU硬件异常和MMU硬件异常。

3.4.1CPU硬件异常

例如:除数为0时程序崩溃(进程异常退出)。

该信号虽然可以被捕捉,但是如果捕捉后不退出进程的话,CPU会一直向进程发送8号信号。

3.4.2MMU硬件异常(MMU也在CPU内)

例如:野指针问题导致程序崩溃(进程异常退出)。

诸多信号在被进程接收到后,进程都会被杀死,结果都一样,为什么要有这么多种信号呢?——表征进程退出的原因(进程异常退出的原因)。

总结:信号产生后都需要借操作系统的手向进程PCB写信号位图(0 --> 1)

三、信号保存

1.阻塞信号

信号阻塞:信号写入后,不立马执行信号,进程做的就是信号阻塞(进程可以选择阻塞某个信号)。

信号未决:信号阻塞设置后,在进程解除阻塞之前,信号一直处于未决状态。

信号递达:信号的执行动作,忽略就是信号递达的一种表现形式。

2.信号的保存

block表:位图,记录某个信号是否被阻塞;对应二进制位为1则该信号被阻塞,反之没被阻塞。

pending表:位图,记录进程是否接收到某个信号,对应二进制位为1则接收到了,反之没收到。

handler表:函数指针数组,决定进程处理对应信号的动作。

信号在内核中的表示:

1.每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号 产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子 中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前 不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次 或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可 以依次放在一个队列里。(这里只讨论常规信号)

四、信号的处理

1.sigset_t (信号集类型)

block表和pending表均是位图, 信号的阻塞和未决标志都只有一个比特位来表示,非0即1。 因此,阻塞和未决标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。

注意: sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用系统调用函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

2.信号集操作函数

#include <signal.h>
int sigemptyset(sigset_t *set);
函数功能:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有
效信号。(成功返回0,出错返回-1)

int sigfillset(sigset_t *set);
函数功能:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系
统支持的所有信号。(成功返回0,出错返回-1)

int sigaddset (sigset_t *set, int signo);
函数功能:将signo信号添加set所指向的信号集。(成功返回0,出错返回-1)

int sigdelset(sigset_t *set, int signo);
函数功能:将signo信号从set所指向的信号集中移除。(成功返回0,出错返回-1)

int sigismember(const sigset_t *set, int signo); 
函数功能:用于判断set所指向的信号集的有效信号中是否包含signo信号。
         若包含则返回1,不包含则返回0,出错返回-1。
 

注意:在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

3. sigprocmask

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); //set   oldset(输出型参数)
函数功能:读取或更改进程的信号屏蔽字(阻塞信号集)
返回值:若成功则为0,若出错则为-1 

说明:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出;
如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改;
如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

how的可能传参及其功能:

SIG_BLOCK

set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set

SIG_UNBLOCK

set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set

SIG_SETMASK

设置当前信号屏蔽字为set所指向的值,相当于mask=set

4. sigpending

#include <signal.h>
int sigpending(sigset_t *set);//set:信号集输出型参数
函数功能:获取当前进程的未决信号集,通过set参数传出。
返回值:调用成功返回0,失败返回-1,并设置错误信息errno。

举例有助理解:

#include <iostream>
#include <signal.h>
#include <cassert>
#include <cstring>
#include <unistd.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
    for(int i = 1;i <= 31;i++)
    {
        //判断i号信号是否在pending表内
        if(sigismember(&pending,i)) cout << "1";
        else cout << "0";
    }
    cout<<endl;
}

int main()
{
    //1.设置block表
    //1.1定义信号集
    sigset_t set,oset;
    //1.2初始化信号集
    sigemptyset(&set);
    sigemptyset(&oset);
    //1.3将2号信号(SIGINT)添加到set信号集
    //sigaddset(&set,SIGINT);
    //将1~31号信号都添加到set信号集,以便测试有哪些信号不能被阻塞
    for(int i = 1;i <= 31;i++)
    {
        sigaddset(&set,i);
    }
    //1.4将新的信号屏蔽字设置进进程
    sigprocmask(SIG_BLOCK,&set,&oset);

    //2.查看pending表
    while(true)
    {
        //获取进程pending表
        sigset_t pending;
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n;

        cout << "当前进程的pending表内容为: ";
        PrintPending(pending);//打印pending表
        sleep(1);
    }
    return 0;
}

运行可执行程序,向进程发送1~31号信号,测试得到,普通信号的9号信号和19号信号无法被屏蔽(前面以及学过,9号和19号信号也不能被捕捉(可以定义自定义动作,但进程收到信号后仍会终止进程)

另外:在执行信号时,对应信号的block表bit位会被置为1,以防止信号递归递达;即正在执行信号动作时,如果又收到该信号,该信号的pending表bit位变1,但该信号不会被递达,此时block表bit位为1。动作完成后,block表bit位自动置为0,该信号被立即递达。

信号可以被立即处理吗?——可以

如果一个信号之前被阻塞(对应block表bit位为1),当解除阻塞时,信号会被立即递达。

5.捕捉信号

系统调用sigaction()函数实现信号的捕捉。

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
函数功能:重定义信号执行函数handler;基本功能同signal()函数.
返回值:成功返回0,失败返回-1,失败码设置errno.

The sigaction structure is defined as something like:
struct sigaction {
	void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
#include <iostream>
#include <signal.h>
#include <cassert>
#include <cstring>
#include <unistd.h>
using namespace std;

void PrintPending(const sigset_t& pending)
{
    for(int i = 1;i <= 31;i++)
    {
        //判断i号信号是否在pending表内
        if(sigismember(&pending,i)) cout << "1";
        else cout << "0";
    }
    cout<<endl;
}

void sigcb(int signo)
{
    cout << "接收到信号:"<< signo <<endl;
    sigset_t pending;
    int cnt = 10;
    while(true)
    {
        int n = sigpending(&pending);
        assert(n == 0);
        (void)n;

        cout << "当前进程的pending表内容为: ";
        PrintPending(pending);//打印pending表
        sleep(1);
        if(cnt-- == 0)
            break;
    }
   
    //exit(signo);
}

/*
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };
*/
int main()
{
    struct sigaction act,oldact;
    memset(&act, 0, sizeof(act));
    memset(&oldact, 0, sizeof(oldact));
    act.sa_handler = sigcb;
    act.sa_flags = 0;
    //设置act.sa_mask的值
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);
    sigaddset(&act.sa_mask,4);
    sigaddset(&act.sa_mask,5);

    sigaction(2,&act,&oldact);

    while(true)
    {
        cout << "我是一个进程,我正在运行," << "我是:" <<getpid() << endl;
        sleep(1);
    }
    
    return 0;
}

注意: ctrl + c 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

信号捕捉到底是如何做到的呢?——用户态和内核态。

什么是用户态和内核态??

1.用户态——执行用户写的代码的时候进程所处的状态;

2.内核态——执行OS所有代码的时候进程的状态。

例如:进程时间片到了,需要切换,就要执行进程切换逻辑(OS所有代码);

系统调用;

进程地址空间:(32位为例)

0~3G——用户代码和数据;3~4G——操作系统代码和数据

(用户级页表) (内核级页表)

<----虚拟内存

内核空间内存储的是OS的代码和数据的地址,具体的代码和数据存储在物理内存。内核空间是所有进程都有的,且所有进程虚拟空间的内核空间里的内容完全相同,通过内核级页表映射到物理内存-->内核级页表是所有进程共用的。

如何做到用户态和内核态的切换:CR3寄存器,修改执行级别(软硬件结合,CR3是CPU内的一个部件)

3 —— 用户态;0 —— 内核态

信号捕捉的原理:

6.处理信号的方式

6.1执行默认动作

不对handler表做任何修改,当进程接收到信号时,若信号对应block表的比特位为0,则执行默认动作。

6.2忽略信号

信号对应block表中比特位为0,pending为1,信号递达后,进程选择忽略对应的信号。SIG_DFL、SIG_IGN等。

6.3执行自定义动作

通过信号捕捉signal()或sigaction()自定义动作。

7.可重入函数

7.1 定义

被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称 为重入

有可能因为重入而造成错乱的函数称为不可重入函数;反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

7.2 不可重入函数

如果一个函数符合以下条件之一则是不可重入的:

1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。

2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

8.volatile

功能:保证内存的可见性。

若没有在flag全局变量前声明volatile关键字,则

1.未优化:向进程发送2号信号后退出while循环,执行完printf语句后结束进程;

2.优化后:向进程发送2号信号,flag的值变为1,但是由于while循环只需检测flag的值而不需要对flag的值做修改,CPU为了节省时间,不会去访问内存中所在的空间,而是直接访问寄存器中的内容;导致while循环的条件一直为真,进程不会自动结束。

声明volatile关键字后:

表示不允许优化,(优化是编译器做的,本质是修改代码,即编程语言代码转化为汇编语言时会被编译器进行修改),优化不一定是好的,必要的时候可以用volatile关键字进行声明,避免不需要的优化导致预想不到的后果。

//test.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int flag = 0;
void handler(int signo)
{
    printf("对2号信号进行捕捉, 并将flag的值从0置1
");
    flag = 1;
    sleep(5);
}

int main()
{
    signal(2,handler);
    
    while(!flag);//使得操作系统只取寄存器中读取数据(只需做数据的扫描)

    printf("进程退出
");
    return 0;
}

//makefile
//-O2是优化编译的一种选项(默认是-O0,没有优化)
test:test.c
	gcc -o test test.c -O2
.PHONY:clean
clean:
	rm -f test

优化后:

优化前:

9.SIGCHID信号

子进程退出时,会向父进程发送SIGCHID信号(17号信号),该信号的默认处理动作是忽略。

1.可以通过信号捕捉,执行自定义动作来等待子进程退出;

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

void handler(int signo)
{
    printf("执行自定义动作处理%d信号
",signo);

    pid_t id = waitpid(-1,NULL,0);
    printf("子进程%d等待成功
", id);
    sleep(5);
}

int main()
{
    //方法一:
    signal(SIGCHLD,handler);
    pid_t id = fork();
    //子进程
    if(id == 0)
    {
        printf("我是子进程, 我的进程pid是%d, 我的父进程是%d
",getpid(),getppid());
        sleep(5);
        
        exit(1);
    }

    while(1)
    {
        printf("我是父进程, 我的进程pid是%d
",getpid());
        sleep(1);
    }
    return 0;
}

2.事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例。

//方法二:
signal(SIGCHLD,SIG_IGN);//SIG_DFL为信号默认处理方式; SIG_IGN设置为忽略处理

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。