Linux之信号

个人主页点我进入主页

专栏分类:C语言初阶 C语言进阶 数据结构初阶 Linux C++初阶算法 C++进阶

欢迎大家点赞,评论,收藏。

一起努力,一起奔赴大厂

一. 信号的引入

1.1提炼基本概念

  • 信号在生活中随时可以产生,信号的产生和我是异步的;
  • 信号的产生后你能认识这个信号吗?
  • 知道信号产生了,信号该如何处理 ?
  • 当我们正在做其他重要的事,把来到的信号暂时不处理,做完事情后我得记得这个事,什么时候处理这个事?合适的时候。

1.2信号概念的基础准备

​ 信号:Linux系统提供的一种,向指定进程发送特定事件的方式,对其进行识别和处理,信号的产生是异步的(异步就是各干各的)。信号有哪些呢??可以输入指令:

cpp 复制代码
kill -l

目前有64个信号,每一个信号都是位图的一位,我们经常使用的

cpp 复制代码
kill -9 pid

运行代码:

cpp 复制代码
#include <iostream>

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

int main()

{

  while (true)

  {

​    std::cout << "hello signal,my pid :" << getpid() << std::endl;

​    sleep(1);

  }

  return 0;

}

9号信号发送结果如图:

1.3信号处理

信号的处理有三种方式

  • 忽略
  • 默认
  • 自定义捕捉

在自定义捕捉中有一个系统调用

cpp 复制代码
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

它的第一个参数是哪一个信号,第二个参数是一个函数指针类型,它代表接受到signum信号就会执行handler。

1.3.1 ctrl+c和2号命令

先看代码:

cpp 复制代码
\#include <iostream>

\#include <signal.h>

\#include <sys/types.h>

\#include <unistd.h>

void handler(int sig)

{

   std::cout << "my sig is : " << sig << std::endl;

}

int main()

{

  signal(2, handler);

  while (true)

  {

​    std::cout << "my pid is : " << getpid() << std::endl;

​    sleep(1);

  }

  return 0;

}

运行结果如图:

其实2号命令还可以通过键盘来产生,输入

cpp 复制代码
ctrl+c

也是同样的效果

1.3.2 ctrl+\和3号命令

3号信号是

cpp 复制代码
ctrl+\

不过不在这里进行演示。

二 . 信号的产生

信号从产生到处理共有三个阶段,第一个阶段就是信号的产生,其中信号的产生有五种

2.1通过kill命令向指定进程发送信号

就是1.2的kill命令的使用,这里不做解释

2.2通过键盘产生信号

就是1.3的ctrl+c和ctrl+\这两个命令,他们分别对应2号和3号命令,这里不做解释。

2.3系统调用

2.3.1 kill

cpp 复制代码
int kill(pid_t pid, int sig);

第一个参数是进程的pid,第二个参数是给进程pid发送的信号。代码如下:

cpp 复制代码
//main.cc

#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>

int main(int argc,char*argv[])
{
    if(argc!=3)
    {
        std::cout<<"Usage:"<<argv[0]<<", you need give signal and pid"<<std::endl;
        exit(1);
    }
    int signal=std::stoi(argv[1]);
    int pid=std::stoi(argv[2]);
    std::cout<<"signal: "<<signal<<" pid: "<<pid<<std::endl;
    kill(pid,signal);
    return 0;
}


//test.cc

#include <iostream>
#include <sys/types.h>
#include <unistd.h>

int main()
{
    while (true)
    {
        std::cout << "my pid is : " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果如下:

2.3.2 raise

cpp 复制代码
int raise(int sig);

这个系统调用就是谁使用就给谁发送sig信号。看代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int sig)
{
    std::cout << "my sig is : " << sig << std::endl;
}
int main()
{
    signal(2,handler);
    while (true)
    {
        raise(2);
        sleep(1);
    }
    return 0;
}	

运行结果如下:

2.3.3 abort

cpp 复制代码
void abort(void);

这个系统调用就是给使用的进程发送6号信号。看代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int sig)
{
    std::cout << "my sig is : " << sig << std::endl;
}
int main()
{
    signal(6,handler);
    for(int i=0;i<20;i++)
    {
        if(i==7)
        {
            abort();
        }
        std::cout<<"hello abort"<<std::endl;
        sleep(1);
    }
    
    return 0;
}

运行结果如下:

2.4 软件条件(闹钟)

例如在进行进程间通信时的***管道***,它的读端关闭就会发送13号信号。还有一种就是一个**闹钟**

cpp 复制代码
 unsigned int alarm(unsigned int seconds);

有这个闹钟,我们需要知道三件事:

2.4.1 IO很慢

​ 根据闹钟,我们可以设定为1秒,当时间到后闹钟会发送14号信号,所以闹钟响后我们退出程序,输出一下count,另一个代码是一直输出count,1秒后停止,看代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int count=0;

void handler(int x)
{
    std::cout<<"count: "<<count<<std::endl;
    exit(1);
}
int main()
{
    signal(14, handler);
    alarm(1);
    while(true)
    {
        count++;
    }
    return 0;
}

运行结果为

另外一个代码:

cpp 复制代码
int main()
{
    //signal(14, handler);
    alarm(1);
    while(true)
    {
        count++;
        std::cout<<count<<std::endl;
    }
    return 0;
}

运行结果为:

可以看到当进行输出时只有14万,但是直接加却有5亿多,所以可以看出IO真的很慢。

2.4.2 理解闹钟

​ 闹钟可以在操作系统中存在多个,所以操作系统需要对闹钟进行管理,所以是**先描述在组织**,闹钟是一个结构体,里面有未来的超时时间,进程pid等,它是如何组织呢?通过最大堆最小堆,只要看看堆顶元素是否超时就可知道其余的是否超时。在硬件上,计算机内部会有一个时间戳,这样就让计算机即使没有电也能知道时间。看下面代码是对闹钟的一些操作:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main()
{
    alarm(5);
    sleep(2);
    int n=alarm(0);
    std::cout<<"n: "<<n<<std::endl;
    return 0;
}

看运行结果:

其中alarm(0)就是说让闹钟立刻响,返回上一个闹钟的剩余时间,闹钟只默认触发一次。

再例如第一个闹钟设为10,sleep(4)秒,n=alarm(2),此时的n为6。

2.5异常

这里的异常和硬件有关,主要包括除0野指针的非法访问

2.5.1 除0

先看代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main()
{
    int a=1/0;
    while(true)
    {
        std::cout<<"hello bit ,pid: "<<getpid()<<std::endl;
    }
    return 0;
}

代码运行如下:

cpp 复制代码
Floating point exception (core dumped) //8号信号

对8号信号进行捕捉,然后看效果,代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void handler(int sig)
{
    std::cout<<"hello, my pid :"<<getpid()<<" sig: "<<sig<<std::endl;
    sleep(1);
}
int main()
{
    signal(8,handler);
    int a=1/0;
    while(true)
    {
        std::cout<<"hello bit ,pid: "<<getpid()<<std::endl;
    }
    return 0;
}

运行结果如下:

我们只有一次除0错误,操作系统为什么会一直发送8号信号呢???

在cpu中会有一个eflag寄存器来记录是否溢出,和是否出现异常,当出现异常时操作系统作为硬件的管理者,就会对目标进程发送信号,由于我们捕捉了8号信号,会导致进程不能退出,而由于寄存器只有一套,当时间片到的时候会出现进程切换,此时就会对进程的上下文进行保存,当再次调度这个进程时就会再次发这个信号,就会造成上面的情况。

2.5.2 野指针的非法访问

和除0一样,代码如下:

cpp 复制代码
void handler(int sig)
{
    std::cout<<"hello, my pid :"<<getpid()<<" sig: "<<sig<<std::endl;
    sleep(1);
}
int main()
{
    signal(11,handler);
    int *p=nullptr;
    *p=1;
    while(true)
    {
        std::cout<<"hello bit ,pid: "<<getpid()<<std::endl;
    }
    return 0;
}

运行结果如下:

出错和除0相似,不过是由于虚拟地址出错,就会向进程发送11号信号。

三.Core和Term

3.1 Core和Term区别

​ 当时Term时,出现异常,进程就会异常终止,但是是Core时,出现异常,进程会异常终止,而且会给 我们生成一个debug文件,这个debug文件就是进程退出时的镜像文件,我们称为**核心转储**。那我们出现异常时怎么没有见到过核心转储?这是由于核心转储一般都是默认关闭的,如何查看呢?

cpp 复制代码
ulimit -a

3.2 Core如何设置

其中第一个就是,如何打开呢?先设置一下输入指令:

ulimit -c 10240

然后运行一下有除0错误的代码,运行:

此时可以看到一个core文件

利用gdb进行调试

3.3 进程退出遗留问题

低7位是退出信号,次低8位是退出码,这个第8位就是我们是否进行了核心转储。

四.信号的保存

4.1 基本性质

  • 实际执行信号的处理动作称为信号的递达(Deliverv)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号
  • 发送信号后,被阻塞的信号时刻保存在未决的状态,直到进程解除对信号的阻塞,才能执行递达的动作
  • 阻塞和忽略是不同的,只要信号被阻塞就不会递达,忽略是信号递达后的一种状态

信号在内核中右这样一个图:

其中block就是信号是否阻塞,它是一个位图结构,阻塞是1,没有阻塞是0,pending表表示信号是否出处于未决状态,同样是一个位图结构,handler表是表示信号递达后的动作,这是一个函数指针数组。

4.2 sigset_t和它的操作函数

​ sigset_t是操作系统提供的位图。sigset_t的操作函数有:

cpp 复制代码
int sigemptyset(sigset_t* set);//将set的位图全部置为0
int sigfillset(sigset_t* set);//将set的位图全部置为1
int sigaddset(sigset_t* set, int signo);//将set的signo位置为0
int sigdelset(sigset_t* set, int signo);//将set的signo位置为0
int sigismember(const sigset_t* set, int signo);//将set的signo位置返回

4.3 sigprocmask

cpp 复制代码
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

它的第一个参数是:

第二个参数是操作数,第三个参数是原来的sigset_t。这个函数是对block表进行修改

4.4sigpending

cpp 复制代码
int sigpending(sigset_t *set);

这个函数是将当前进程的pending表返回。

4.5 应用

​ 根据上面的几个函数,那么我们就可以先将2号信号进行阻塞,然后发送2号信号就可以看到pending表由0变1,然后取消阻塞pending表由1变0(需要对2号进行捕捉)。下面看代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>


void handler(int sig)
{
    std::cout<<"sig: "<<sig<<std::endl;
}
void Print( sigset_t s)
{
    std::cout<<"my pid is: "<<getpid()<<" pending is: ";
    for(int i=31;i>=0;i--)
    {
        if(sigismember(&s,i))
        {
            std::cout<<"1";
        }
        else 
        {
            std::cout<<"0";
        }
    }
    std::cout<<std::endl;
}
int main()
{
    signal(2,handler);//捕2号信号

    sigset_t set,oldset;
    sigemptyset(&set);
    sigemptyset(&oldset);
    sigaddset(&set,2);

    //修改当前进程的block表
    sigprocmask(SIG_BLOCK,&set,&oldset);
    int cur=20;
    while(cur--)
    {
        sigset_t s;
        sigpending(&s);
        Print(s);
        sleep(1);
        if(cur==5)
        {
            sigprocmask(SIG_SETMASK,&oldset,&set);
        }
    }
    return 0;
}

运行结果如图:

可以发现输入多少次2号信号都是处于阻塞,看到了从0到1再到0的过程。

五.信号的处理

5.1信号的捕捉

​ 信号递达有三种处理方式:默认(signal(signo, SIG_DFL)),忽略 (signal(signo, SIG_IGN)),自定义捕捉(signal(signo,handler));信号肯恩那个不会立即处理而是选择在合适的时候,这个合适的时候就是进程从内核态返回到用户态的时候

信号产生后看它是怎末样子的?就先看它的pending表,如果时0就没有产生,如果是1就看它的block表看他是不是被阻塞,是1就是被阻塞无法继续,是0就看它的handler表,其中忽略和默认很好理解,但是自定义捕捉的方法是用户自己写的啊,操作系统不相信任何用户,既然是用户写的操作系统当然不会相信,防止这个做出超越自己权限的操作,所以需要内核态到用户态的转换

上面图片还可以入如下理解(信号的捕捉需要4次切换)

5.2再谈地址空间

​ 在地址空间中前面谈的都是0-3的地址空间,3-4是操作系统的,里面有操作系统的系统调用,它也有对应的页表,这意味着操作系统也在我们的地址空间中,所以我们无论如何进行进程切换都可以找到操作系统,我们访问操作系统其实还是在我们的地址空间中进行的,和我们访问库函数没有区别,由于操作系统不相信任何用户,所以需要访问3-4地址空间时需要一些约束。而且这个内核级页表只有一份!!

5.3键盘输入数据的过程

​ 根据冯诺依曼体系可以直到键盘和cpu不能直接相连。但是键盘可以给cpu发送硬件中断,通过寄存器接受,例内存被分成一块一块的,里面都是函数指针,假如读键盘操作对应的下标是4,寄存器接受到的硬件中断就是4,然后调用读键盘的操作。

5.4如捕捉信号

cpp 复制代码
sighandler_t signal(int signum, sighandler_t handler);
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signal在前面谈论过了,这里不解释了。struct sigaction结构体的内容是:

cpp 复制代码
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

今天只使用void (*sa_handler)(int), sigset_t sa_mask, int sa_flags;这三个参数,第一个是一个函数指针和signal的handler一样,第二个参数是sigset_t类型,在这里设为0,sa_flags设为0.其余操作和signal一样。

六.可重入函数

​ 在insert时会有一个信号,这个信号也有一个insert,这样会造成node2的丢失,insert函数被执行流执行也被信号捕捉流执行,这个现象被称为被重入,函数称为不可重入函数。

七.volatile

看代码:

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>

int flag=0;
int num=0;
void handler(int sig)
{
    std::cout<<"get a sig: "<<sig<<" flag 0->1"<<std::endl;
    flag=1;
}
int main()
{
    signal(2,handler);
    while(!flag);
    std::cout<<flag<<std::endl;
    return 0;
}

输入

cpp 复制代码
g++ main.cc -o testsignal -O1

运行结果:

可以看到既然发送了2号信号,那么代码应该会可以结束,但是它一直运行,这就是因为代码优化的问题.

解决就是在flag前加上volatile关键字:

八.SIGCHLD

​ 在父子进程中,父进程需要等待子进程,这是为了防止子进程出现僵尸问题,当子进程退出时会给父进程发送SIGCHLD信号,下面有两种情况:

8.1 当我们有多个子进程时,这些进程同时退出

​ 当同一种信号产生时,如果这个信号正处于未决状态时,再次受到这个信号他都不会再次做出反应,所以多个子进程同时退出时会出现问题。所以我们可以采用下面解决方案:

cpp 复制代码
  while(true)
    {
        pid_t rid=waitpid(-1,nullptr,0);
        if(rid>0)
        {
            std::cout<<"wait child success,rid: "<<rid<<std::endl;
        }
        else if(rid<=0)
        {
            std::cout<<"wait chaild success done"<<std::endkl
            break;
        }
    }

采用循环的方式当没有子进程时就结束,

8.2 多个子进程部分退

​ 当只有部分退时采用上面显然有些浪费,在Linux中对SIGCHLD信号忽略就不用对子进程回收,操作系统会自动回收。所以只需要加入

cpp 复制代码
signal(SIGCHLD,SIG_IGN);

存中...(img-GbpnIXT6-1723811396570)]

八.SIGCHLD

​ 在父子进程中,父进程需要等待子进程,这是为了防止子进程出现僵尸问题,当子进程退出时会给父进程发送SIGCHLD信号,下面有两种情况:

8.1 当我们有多个子进程时,这些进程同时退出

​ 当同一种信号产生时,如果这个信号正处于未决状态时,再次受到这个信号他都不会再次做出反应,所以多个子进程同时退出时会出现问题。所以我们可以采用下面解决方案:

cpp 复制代码
  while(true)
    {
        pid_t rid=waitpid(-1,nullptr,0);
        if(rid>0)
        {
            std::cout<<"wait child success,rid: "<<rid<<std::endl;
        }
        else if(rid<=0)
        {
            std::cout<<"wait chaild success done"<<std::endkl
            break;
        }
    }

采用循环的方式当没有子进程时就结束,

8.2 多个子进程部分退

​ 当只有部分退时采用上面显然有些浪费,在Linux中对SIGCHLD信号忽略就不用对子进程回收,操作系统会自动回收。所以只需要加入

cpp 复制代码
signal(SIGCHLD,SIG_IGN);

就不需要对子进程进行等待,这种情况适用于不需要子进程退出信息的情况。

相关推荐
legend_jz1 分钟前
【Linux】线程控制
linux·服务器·开发语言·c++·笔记·学习·学习方法
Komorebi.py2 分钟前
【Linux】-学习笔记04
linux·笔记·学习
黑牛先生4 分钟前
【Linux】进程-PCB
linux·运维·服务器
友友马22 分钟前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
猿java1 小时前
Linux Shell和Shell脚本详解!
java·linux·shell
A.A呐2 小时前
【Linux第一章】Linux介绍与指令
linux
Gui林2 小时前
【GL004】Linux
linux
ö Constancy2 小时前
Linux 使用gdb调试core文件
linux·c语言·vim
tang_vincent2 小时前
linux下的spi开发与框架源码分析
linux
xiaozhiwise2 小时前
Linux ASLR
linux