【Linux】进程信号

目录

  • 一、概念
  • 二、信号产生的方式
    • 1.键盘产生
    • 2.指令产生
    • 3.系统调用产生信号
    • 4.软件条件
    • 5.硬件异常
    • 6.总结
  • 三、waitpid接收信号
    • 1.core dump
    • 2.status获取信号
  • 四、信号存储
    • 1.信号其他相关常见概念
    • 2.信号的内核结构
    • 3.sigset_t
    • 4.sigprocmask
    • 5.sigpending
  • 五、信号捕获
    • 1.用户态和内核态
    • 2.信号捕捉
    • 3.sigaction
  • 六、volatile
  • 七、SIGCHLD信号

一、概念

信号是进程之间事件异步通知的一种方式,属于软中断,本质也是数据 。

关于信号:
信号的产生对于进程来说是异步的
因为信号随时可能产生,所以信号产生前,进程可能在做优先级更高的事情,不能立马处理这个信号,所以进程需要将这个信号记录下来,在合适的时候去处理这个信号,所以进程具有记录信号的能力

进程是如何记录对应的信号,如何去处理?

我们可以先看系统定义的信号列表,使用指令 kill -l
【Linux】进程信号_第1张图片

linux中共有62个信号,前31个为普通信号,34到64为实时信号

进程只需要管理1-31这些信号,通过哪种数据结构进行管理呢?
位图 一个整数有32个比特位,足够对这些信号进行管理,所以进程对这些信号的管理可以转换为对位图进行操作

回顾以前的学习,我们一共见过三次信号

1.在以前学习的过程中,我们使用过kill -9 进程pid 结束进程(可以终止后台进程)
【Linux】进程信号_第2张图片
我们也使用过 键盘ctrl + c 终止一个进程(只能终止前台进程),实际上是给进程一个信号
【Linux】进程信号_第3张图片

2.在waitpid时父进程在等待子进程退出时,如果子进程异常退出,父进程的错误码会被设置,错误码中的比特位中有进程退出信号

3.我们在使用管道时,如果写端关闭,读端的进程会受到13号信号,异常终止。

二、信号产生的方式

1.键盘产生

上述讲过,在运行一个进程时,可以通过键盘 ctrl + c 或者ctrl + \ 来进行对进程发送信号。

2.指令产生

【Linux】进程信号_第4张图片

3.系统调用产生信号

① kill 发送一个信号给其他进程

#include 
#include 
int kill(pid_t pid, int sig);

【Linux】进程信号_第5张图片

② raise 哪个进程调用这个接口就给哪个进程发送任意信号

 #include 
 int raise(int sig);

【Linux】进程信号_第6张图片

③ abort 使调用进程接受信号而异常终止

include <stdlib.h>
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值。

这里有一点需要注意的是,我们在使用signal捕获信号后使用自定义处理信号行为,系统还是会强制结束进程【Linux】进程信号_第7张图片

4.软件条件

过某种软件(OS),来触发信号发送,系统层面设置定时器,或者某种操作而导致条件不就绪的场景。

例如在进程间通信中:当读端不读,而且关闭了读端fd,但是写端一直在写,最终写进程会收到sigpipe(13)信号。

alarm: 一定时间后向进程发送14号信号

 #include 
 unsigned int alarm(unsigned int seconds);
	// 返回值为0或还剩多长时间闹钟结束

【Linux】进程信号_第8张图片

5.硬件异常

使用下面这段代码,显然可以看出这里对空指针进行了解引用操作,这里肯定会发生程序错误。

#include
#include
using namespace std;

void handler(int signo)
{
    cout<<"进程收到了:"<<signo<<"信号导致的"<<endl;
    exit(1);
}


int main()
{
    //signal(SIGSEGV,handler); //捕捉信号
    int* p=nullptr;
    *p=10;

    cout<<"div zero..."<<endl;

    return 0;
}

运行结果: 发生段错误
【Linux】进程信号_第9张图片
使用signal接口对信号进行捕捉,是11号信号
【Linux】进程信号_第10张图片

这里的硬件异常的本质是:程序中对空指针或者野指针的解引用,会去访问这块内存,而在页表中没有映射关系,相对应的硬件MMU就会出现异常。

6.总结

1.信号产生的方式虽然不同,但是最终一定都是通过OS向目标进程发送的信号!
2,由于收到信号后可能不会立即执行对应操作,在Linux内核中使用变量会保存信号。
3.进程中,采用位图来标识进程是否收到信号。
4.所以OS发送信号的本质是向指定进程的task_struct中的信号位图写入比特为1,所以信号的发送也可称为信号的写入。

三、waitpid接收信号

1.core dump

如果必要,操作系统会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘中,还会记录程序在哪里异常,方便后期调试。

在云服务器上,将数据转储到磁盘上的功能默认是被关掉的。
ulimit -a 查看
【Linux】进程信号_第11张图片
我们自己打开 使用命令 ulimit -c 10240
【Linux】进程信号_第12张图片

如果现在运行,上面会导致程序崩溃的代码,core dump 就会起作用了,并在当前目录下形成core文件
【Linux】进程信号_第13张图片

而在gdb调试时,通过使用dump保存的文件,就会给出崩溃原因和在哪里崩溃:这种调试方式称为事后调试。
【Linux】进程信号_第14张图片

我们可以看到这些信号有的会让进程直接退出,有的会发生 核心转储,OS可以将该进程在异常的时候,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中
【Linux】进程信号_第15张图片

2.status获取信号

#include
#include
#include
#include
#include
 
int main()
{
    if(fork() == 0)
    {
        while(1)
        {
            printf("I am child  hahaha\n");
            int a = 10;
            a /= 0;
        }
    }
 
    int status = 0;
    waitpid(-1, &status, 0);
    printf("exit code: %d, exit sig: %d, core dump: %d\n", (status>>8)&0xFF, status&0x7F, (status>>7)&1);
    return 0;
}

【Linux】进程信号_第16张图片

core dump 表示有没有形成核心转储文件,因为liunx操作系统默认关闭,所以这里显示为0
【Linux】进程信号_第17张图片

四、信号存储

1.信号其他相关常见概念

1.实际执行信号的处理动作称为信号递达(Delivery)
2.信号从产生到递达之间的状态,称为信号未决(Pending)。
3.进程可以选择阻塞 (Block )某个信号。
4.被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
5.注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

2.信号的内核结构

【Linux】进程信号_第18张图片
在这里插入图片描述

3.sigset_t

这是操作系统设置的类型,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态, 。
sigset_t类型的变量不能单独使用,必须要配合特定的系统调用接口使用

#include 
int sigemptyset(sigset_t *set); // 将信号集所有的位清0
int sigfillset(sigset_t *set);  // 初始化位图,将信号集所有的位置为1
int sigaddset (sigset_t *set, int signo); // 添加信号到信号集
int sigdelset(sigset_t *set, int signo);  // 从信号集中删除信号
int sigismember(const sigset_t *set, int signo); // 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

4.sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集) 。

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1
// set:输入型参数
// oset:输出型参数

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。

如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。

如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。

how参数的可选值 :

SIG_BLOCK:将set中的信号添加到信号屏蔽字中
SIG_UNBLOCK:将set中的信号从信号屏蔽字中解除阻塞
SIG_SETMASK:将信号屏蔽字设置为set

【Linux】进程信号_第19张图片

5.sigpending

该系统调用不对pending表修改,而仅仅是获取进程的pending位图

#include 
int sigpending(sigset_t *set); // 参数为输出型参数

使用:

#include
#include
#include
#include

using namespace std;

void show_pending(sigset_t *set)
{
    for(int i=1;i<31;i++)
    {
        if(sigismember(set,i))cout<<1;
        else cout<<0;
    }
    cout<<endl;
}

void hander(int signo)
{
    cout<<"2号信号已被递达,处理完成"<<endl;
}

int main()
{
    signal(2,hander); //自定义捕获2号信号
    sigset_t set,oset;
    sigemptyset(&set);
    sigemptyset(&oset);

    sigaddset(&set,2);//将2号信号加入

    sigprocmask(SIG_BLOCK,&set,&oset);//阻塞2号信号
    sigset_t peding;
    int count=0;

    while(true)
    {
        sigemptyset(&peding);
        sigpending(&peding);//获取pending位图
        show_pending(&peding);//打印pending位图
        sleep(1);
        count++;
        if(count==10) //10秒后解除对2号信号的阻塞
        {
            sigprocmask(SIG_SETMASK,&oset,NULL);//恢复2号信号
            cout<<"2号信号恢复,可以被递达"<<endl;
        }
    }
    return 0;
}

【Linux】进程信号_第20张图片

五、信号捕获

信号什么时候被处理?

① 一个信号之前被block,他解除block后会被立即递达。
【Linux】进程信号_第21张图片

从内核态转回到用户态,进行信号检测并处理信号

1.用户态和内核态

用户态:用户代码和数据被访问或者执行的时候,所处的状态。自己写的代码全部都是在用户态执行。
内核态:执行OS的代码和数据时,进程所处的状态。OS的代码的执行全部都是在内核态执行(例如系统调用)。

主要区别:权限大小,内核态权限远远大于用户态。

用户态使用的是用户级页表,只能访问用户数据和代码;内核态使用的是内核级页表,只能访问内核数据和代码。

CPU内有寄存器保存了当前进程的状态。

所谓系统调用:就是进程的身份转化成为内核,然后根据内核页表找到对应函数执行。

需要重新认识以下地址空间
【Linux】进程信号_第22张图片

内核级页表是一个全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容。

理解进程切换
1.在当前进程的进程地址空间中的内核空间,找到操作系统的代码和数据。
2.执行操作系统的代码,将当前进程的代码和数据剥离下来,并换上另一个进程的代码和数据。

2.信号捕捉

这是系统正常处理信号的方式,如果是使用自定义的处理方式,更为复杂
【Linux】进程信号_第23张图片

自定义捕获信号内核的处理方式
【Linux】进程信号_第24张图片

3.sigaction

sigaction:类似signal方法,捕捉信号,自定义信号

#include 
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// act:新的处理动作
// oldact:原来的处理动作
 
// act 结构体
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_mask字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 
 
//sa_flags字段包含一些选项, 本章的代码都把sa_flags设为0, sa_sigaction是实时信号的处理函数, 本章不详细解释这两个字段

【Linux】进程信号_第25张图片

六、volatile

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

例子:

#include
#include

int flag=0;  //全局变量

void hander(int signo)
{
    printf("change flag 0 to 1\n");
    flag=1;
}

int main()
{
    signal(SIGINT,hander); //自定义捕获处理2号信号
    while(!flag); //不做任何处理,等待信号
    printf("main quie 正常\n");
    return 0;
}

编译器没有优化 接收到2号信号正常结束
【Linux】进程信号_第26张图片
编译器优化后 接收到2号信号不会结束
【Linux】进程信号_第27张图片
原因:
【Linux】进程信号_第28张图片

解决方法

在flag变量前加上volatile就可防止这种情况的发生。

在这里插入图片描述

【Linux】进程信号_第29张图片

七、SIGCHLD信号

子进程在退出时其实会向父进程发送SGCHLD信号,表示自己退出了
【Linux】进程信号_第30张图片

为了避免出现僵尸进程,父进程需要使用wait或waitpid函数等待子进程结束,父进程可以阻塞等待子进程结束,也可以非阻塞地查询的是否有子进程结束等待清理,即轮询的方式。采用第一种方式,父进程阻塞就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实,子进程在终止时会给父进程发生SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理动作,这样父进程就只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait或waitpid函数清理子进程即可。

我们可以写一个自定义信号处理方式,来实现父进程对子进程的回收,并且不需要轮询等待子进程退出。不过需要考虑的是,如果同时创建多个子进程,在子进程退出时候会向父进程发送信号,在处理SIGCHLD的时候,可能会收到另一个子进程发来的退出信号,此时,我们正在处理上一个子进程退出的自定义信号处理,可能会导致多个子进程一起退出时,父进程只能处理少数几个子进程的退出信号。

#include
#include
#include
#include
#include
#include

volatile int flag=0; int id;

void handler(int signo) {
    printf("捕捉到一个信号:%d, who: %d\n",signo,getpid());
    sleep(5);

    //可以同时处理多个进程退出
    while(1)
    {
        pid_t ret=waitpid(-1,NULL,WNOHANG);
        if(ret>0)
        {
            printf("wait success ,ret: %d, id: %d\n",ret,id);
        }else break; //没有子进程了
    }
    printf("hander done...\n"); }

int main() {
    signal(SIGCHLD,handler);
    int i=1;
    for(;i<=10;i++)
    {
        id=fork();
        if(id==0)
        {
            //child
            int cnt=5;
            while(cnt)
            {
                printf("我是子进程,我的pid:%d,ppid: %d\n",getpid(),getppid());
                sleep(1);
                cnt--;
            }
            exit(0);
        }
    }

    while(1)sleep(1);
    return 0; }

【Linux】进程信号_第31张图片

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

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

【Linux】进程信号_第32张图片
【Linux】进程信号_第33张图片

你可能感兴趣的:(Linux,linux,运维,服务器)