0
点赞
收藏
分享

微信扫一扫

【Linux】进程信号

静守幸福 2022-03-13 阅读 86

前言

我们早上去上学时,会遇到信号灯

那么当我们遇到信号灯时,下意识地就会根据信号灯的颜色,决定此时究竟过不过马路

但是

我们始终知道“红灯停,绿灯侠,黄灯亮了要注意”这个规则,不管我们在不在过马路,这个规则时刻在我们的脑海之中
在这里插入图片描述

我们将操作系统比作是一个社会
进程相当于社会中的人,形形色色
人收到红绿灯的信号,决定过不过马路,进程也会收到各种信号,决定接下来应该做什么

很显然,进程对于信号有以下3点认识:

  • 进程始终知道有哪些信号
  • 进程始终知道将来遇到这些信号时应该做什么
  • 进程遇到信号时,会根据自己的认识,在接下来合适的完成对应的动作

对于进程来讲是以上3点,对于信号本身来讲,又分为产生、保存、处理三个阶段

我们逐一来探索

目录

这是信号生命周期的三个阶段:
在这里插入图片描述

哦,让我们先对Linux的信号有个最基本的认识

这就是一种信号


1.信号的产生

信号的产生其实有4种方式:

  • 终端按键------键盘
  • 系统调用函数------kill
  • 软件条件
  • 硬件异常

1.1 终端按键------键盘

#include <iostream>
#include <unistd.h>
int main() {
  while(true) {
    std::cout << "I am a proc: " << getpid() << std::endl;
    sleep(1);
  }
  return 0;
}

在这里插入图片描述


1.2 系统调用函数------kill

在这里插入图片描述
其中 1-31号信号是普通信号,34-64号信号是实时信号

其中9号信号是权力最大的信号,不会被捕捉,能杀死后台进程

由于ctrl C 本质是给目标进程发送2号信号,所以我们使用kill -2也能终止进程:

kill -2 PID
kill -SIGINT PID

在这里插入图片描述

另外,对于相当一部分信号而言,进程受到信号的默认动作就是终止当前进程

但也并不全是,譬如:

在这里插入图片描述
我们可以通过调用kill接口,自己实现一个mykill程序

#include <iostream>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void Usage() {
  cout << "Usage:\n\t" << "./kill [SIG] [PID]" << endl;
  exit(1);
}
int main(int argc, char* argv[]) {


  if(argc != 3) {
    Usage();
  }
  else {
    kill(atoi(argv[2]), atoi(argv[1]));
  }
  return 0;
}

在这里插入图片描述


1.3 由软件条件产生信号

alarm函数可以产生对应的SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

我们写个程序看看一秒中之内while循环能打印多少次,由于cpu处理io很慢和云服务器网络传输的原因,打印次数很少:

#include <iostream>
#include <unistd.h>
using namespace std;
int main() {
  alarm(1); // 一秒之后,会给目标进程发送SIGALRM
  int cnt = 0;
  while(1) {
    cout << cnt << endl;
    cnt++;
  }

  cout <<"cnt: " <<cnt << endl;
  return 0;
}

在这里插入图片描述
当我们捕捉了14号信号,把IO移除后,我们看看:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int cnt = 0;
void handler(int signo) {
  cout << "signo: " << signo << endl;
  cout << "cnt: " << cnt << endl;
  exit(1);
}
int main() {

  signal(14, handler);
  alarm(1); // 一秒之后,会给目标进程发送SIGALRM
  while(1) {
    cnt++;
  }
  return 0;
}

在这里插入图片描述
有io和没io差了2000多倍


1.4 硬件异常产生信号

我们先来谈一下core-dump核心转储,通过core-dump核心转储我们才能知道硬件异常产生了什么信号

查看系统的core-dump的命令:

ulimit -a

因为线上不是调试环境,所以线上不需要核心转储,云服务器默认是0:

在这里插入图片描述
改成1024:

ulimit -c 1024

再补充一个概念,在学习进程等待时,我们waitpid的第二个参数status的第八个比特位叫做core-dump标志位,这个标志位标记了进程是否收到信号而结束,是的话就会被标记成1,同时会生成一个core文件,可供调试

  • 演示除零错误:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
int main() {
  pid_t id  = fork();
  if(id == 0) {
    //child
    int cnt = 5;
    while(cnt--) {
      cout << "I am child " << getpid() <<" count: " << cnt << endl;
      sleep(1);
    }
    int a = 10;
    int b = 0;
    a = a/b;
  }
  int status = 0;
  pid_t ret = waitpid(id, &status, 0);
  if(ret == id) {
    cout << "Succeed" << endl;
    cout << "exit code: " << ((status>>8)&0xff) << endl;
    cout << "sig: " << ((status)&0x7f) << endl;
    cout << "core dump: " << ((status>>7)&0x1) << endl;
  }
  return 0;
}

在这里插入图片描述

gdb xxx
core-file xxx

在这里插入图片描述
除零错误对应的正是8号信号

  • 野指针
  • 数组越界
  • ctrl \
  • 等等

大家可以通过设置core-dump核心转储来调试确定进程退出原因

所以说:

程序崩溃准确来说是进程的崩溃,是OS向进程发送信号,本质是OS向目标进程的pcb的位图中指定的比特位由0到1写信号

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

总结思考一下

  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
  • 信号的处理是否是立即处理的?在合适的时候
  • 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

2.信号的保存

我们终于来到了信号的第二阶段------保存阶段

先来介绍一下关于信号的其他一些知识:

进程的处理有三种形式:

  • 默认
  • 忽略
  • 捕捉(自定义handler函数)

在这里插入图片描述

  • block位图:比特位的位置代表信号的编号,比特位的内容(0 1)代表是否阻塞该信号

  • pending数组:比特位的位置代表信号的编号,比特位的内容(0 1)代表是否受到信号。如果没有受到对应的信号,照样可以阻塞特定的信号。阻塞更准确的理解,理解成一种状态

  • handler数组:用信号的编号,作为数组的索引,找到该信号对应的信号处理方式,然后指向对应的方法(递达)

我们稍后时候介绍一下什么是用户态和内核态

介绍一组pending/block位图的系统调用接口:

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

信号的系统调用接口:

  • 用来改变进程内核block位图的接口
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how

在这里插入图片描述

  • 返回内核中的pending位图
int sigpending(sigset_t *set);

实验:

  • 阻塞2号信号
  • 不断获取pending信号集,并打印
  • 发送2号信号给进程
  • 过一段时间取消对2号信号的block
  • 看到二号信号立刻被递达
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
  sigset_t in;
  sigset_t out;
  sigemptyset(&in);
  sigemptyset(&out);
  sigaddset(&in, SIGINT);
  sigprocmask(SIG_SETMASK, &in, &out);
  int cnt = 0;
  sigset_t pending;
  sigemptyset(&pending);
  while(true) {
    sigpending(&pending);
    for(int i = 1; i < 32; i++) {
      if(sigismember(&pending, i)) {
        std::cout << 1;
      }
      else {
        std::cout << 0;
      }
    }
    std::cout << std::endl;
    sleep(1);
    cnt++;
    if(cnt == 10) {
      sigdelset(&in, 2);
      sigprocmask(SIG_SETMASK, &in, &out);
    }
  }
  return 0;
}

在这里插入图片描述
程序运行时,每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会 使SIGINT信号处于未决状态,按Ctrl-\仍然可以终止程序,因为SIGQUIT信号没有阻塞。

3. 信号处理时

什么时候进行信号的递达?

你访问的是用户空间的代码数据,你的状态就是用户态
你访问的是内核空间的代码数据,你的状态就是内核态

那么用户想要通过系统调用访问内核的代码和数据

但是一个普通用户有权力访问操作系统的代码和数据吗?

没有

那么就是说,当用户妄图访问内核空间时,操作系统会对用户进行身份切换,从user切换到kernel

操作系统怎么知道我们当前的状态是什么呢

CPU中会存在一个状态寄存器,存储权限相关的数据,表示我们所处的状态;看你使用的是那个级别的页表

每个进程都有自己的用户级页表。但是内核只有一份,内核级页表也只需要维护一张

换句话说,内核级页表被所以进程所共享
在这里插入图片描述

上图回答了信号在何时被处理

内核如何进行信号的捕捉

4. 信号捕捉函数

1.signal函数

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

2.sigaction

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, 
struct sigaction *oldact);

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

我们来演示一下sigaction函数的用法吧

  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。

我们将2号信号捕捉,设置sa_mask使得执行2号信号的handler函数时,同时也将3号信号阻塞

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

int flag = 0;
void handler(int signo) {
  flag = 1;
  int cnt = 5;
  while(cnt) {
    cout << "cnt: " << cnt << endl;
    sleep(1);
    cnt--;
  }
  cout << "flag == 1, proc end by " << signo << endl;
}
int main() {
  sigset_t set;
  sigemptyset(&set);
  sigaddset(&set, 3);
  struct sigaction action;
  struct sigaction oldaction;
  action.sa_handler = handler;
  action.sa_mask = set;
  action.sa_flags = 0;
  sigaction(SIGINT, &action, &oldaction);
  while(!flag) {
    ;
  }
  return 0;
}

在这里插入图片描述
我们发现ctrl C发送信号后,pending位图中2号位置为1
开始执行handler函数,此时2号信号被阻塞同时pending位图中的2号位置0
在执行handler时,我们再发送2号信号,pending中的2号位又被置1;发送三号信号,但是3号信号始终是阻塞的。

5. SIGCHLD信号

子进程退出时会给父进程发SIGCHLD信号(17号信号) 如果子进程比父进程提前退出,那么子进程会僵尸
#include <iostream>    
#include <unistd.h>    
using namespace std;    
int main() {    
  if(fork() == 0) {    
    // child    
    int cnt = 5;    
    while(cnt) {    
      cout << "child: " << getpid() << " cnt: " << cnt << endl;      cnt--;
      sleep(1); 
    }              
    exit(0);
  }             
  //parent
  int cnt = 10;                                                  
  while(cnt) {
    cout << "parent: " << getpid() << " cnt: " << cnt << endl;
    cnt--;    sleep(1);
  }           
                 
  return 0;
} 

在这里插入图片描述
而我们将17号信号捕捉,handler方法改为SIG_IGN忽略时,则子进程直接退出,不会等待父进程将它回收:

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main() {
  signal(SIGCHLD, SIG_IGN);
  if(fork() == 0) {
    // child
    int cnt = 5;
    while(cnt) {
      cout << "child: " << getpid() << " cnt: " << cnt << endl;
      cnt--;
      sleep(1);
    }
    exit(0);
  }
  //parent
  int cnt = 10;
  while(cnt) {
    cout << "parent: " << getpid() << " cnt: " << cnt << endl;
    cnt--;
    sleep(1);
  }
  
  return 0;
}

在这里插入图片描述
值得注意的是,这样操作父进程便得不到子进程返回的PID了

我父进程让你子进程去办事情,你总得告诉我一声你有没有办完事情吧

举报

相关推荐

0 条评论