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);

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

相关推荐
神秘人X7073 分钟前
Linux高效备份:rsync + inotify实时同步
linux·服务器·rsync
轻松Ai享生活9 分钟前
一步步学习Linux initrd/initramfs
linux
轻松Ai享生活13 分钟前
一步步深入学习Linux Process Scheduling
linux
绵绵细雨中的乡音2 小时前
网络基础知识
linux·网络
Peter·Pan爱编程2 小时前
Docker在Linux中安装与使用教程
linux·docker·eureka
kunge20133 小时前
Ubuntu22.04 安装virtualbox7.1
linux·virtualbox
清溪5493 小时前
DVWA中级
linux
Sadsvit4 小时前
源码编译安装LAMP架构并部署WordPress(CentOS 7)
linux·运维·服务器·架构·centos
xiaok4 小时前
为什么 lsof 显示多个 nginx 都在 “使用 443”?
linux
苦学编程的谢5 小时前
Linux
linux·运维·服务器