【Linux】进程信号

目录

1. 进程信号概念

2. 信号的产生方式

2.1. 键盘产生

2.2. 调用系统函数向进程发信号

2.3. 由软件条件产生信号

2.4. 程序崩溃产生信号

3. waitpid接收信号

3.1. core dump

3.2. status获取信号

4.信号产生

4.1. 信号产生

4.2. sigset_t

4.3. sigprocmask

4.4. sigpending

5. 信号发送后

5.1. 用户态与内核态

5.2. 信号捕捉

5.2.1 signal、sigaction

5.3. volatile

5.4. SIGCHLD信号


1. 进程信号概念

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

信号是给进程发的,进程在收到信号后,会在合适的时候执行对应的命令。

进程具有识别信号并处理信号的能力。进程收到信号,不一定会立即处理信号,在合适的时候处理,信号保存在进程PCB中。

信号是操作系统发送给进程的。

查看信号

kill -l

linux中共有62个信号,前31个为普通信号,34到64为实时信号(不学习)。

以前,我们在使用ctrl c 结束进程时,本质是向指定进程发送2号信号。

验证:

使用接口

signal:捕捉信号,修改进程对信号的默认处理动作(具体原理后面文章后面讲)

#include <signal.h>

typedef void (*sighandler_t)(int); // 函数指针

sighandler_t signal(int signum, sighandler_t handler);

代码:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>


void handler(int signo)
{
    printf("get a signal:signal no:%d, pid: %d\n", signo, getpid());
}


int main()
{
    signal(2, handler); // 通过signal捕捉信号,对2号信号的处理动作改成自定义的方式,且只有信号到来的时候这个信号才会被调用

    while(1)
    {
        printf("hello world!, pid:%d\n", getpid());
        sleep(1);
    }
    return 0;
}

运行代码,然后一直按键盘ctrl+c,尝试终止进程,但是由于我们改变了2号信号的处理方式,这里不会终止进程,只能通过其他信号终止。

键盘可以产生信号,键盘产生的信号只能用来终止前台进程,后台进程可以使用命令 kill -9 pid 杀掉。

9号信号不可被捕捉(自定义)。

一般而言,进程收到信号的处理方式有三种:

默认动作:一部分是终止自己,暂停等。

忽略动作:是一种处理方式,只不过动作就是什么也不干。

自定义动作(捕捉信号):即修改信号的默认处理动作。

2. 信号的产生方式

2.1. 键盘产生

键盘产生信号上面已经验证了这里就不再验证。

2.2. 调用系统函数向进程发信号

系统调用接口:

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

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

raise:给自己发送信号

 #include <signal.h>
 int raise(int sig);

例如:

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


static void Usage(const char* proc)
{
    printf("./test signo who\n");
}

int main(int argc, char* argv[])
{
    if(argc != 3) // 判断命令行参数
    {
        Usage(argv[0]);
        return 1;
    }
    int signo = atoi(argv[1]);
    int who = atoi(argv[2]);
    kill(who, signo); // 向指定进程发送信号

    return 0;
}

然后在命令行上产生一个sleep进程,然后运行test程序,向该进程发送9号信号结束该进程。

2.3. 由软件条件产生信号

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

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

例如:

alarm:一定秒数后向进程发送14号信号

 #include <unistd.h>
 unsigned int alarm(unsigned int seconds);
	// 返回值为0或还剩多长时间闹钟结束
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>

void handler(int signo)
{
    printf("get a signal:signal no:%d\n", signo);
    exit(0);
}

int main()
{
    int i = 1;
    for(i = 1; i <= 31; ++i)
    {
        signal(i, handler);
    }
    alarm(3); // 3秒后向进程发送14号信号

    while(1)
    {
        printf("I am a proccess\n");
        sleep(1);
    }
    return 0;
}

2.4. 程序崩溃产生信号

在Windows和Linux下进程崩溃的本质,是进程收到了对应的信号,然后进程执行信号的默认处理动作(杀死进程)

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

#include<stdio.h>
int main()
{
    while(1)
    {
        int *p = NULL;
        *p = 10;
        sleep(1);
    }
    return 0;
}

确实从从结果中也可以看到出现了段错误(segmentation fault)

再来对信号捕捉一下,看看这个错误是哪个信号?

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handler(int signo)
{
    printf("get a signal:signal no:%d, pid: %d\n", signo, getpid());
    sleep(1);
}

int main()
{
    int i = 1;
    for(i = 1; i <= 31; ++i)
    {
        signal(i, handler);
    }
    while(1)
    {
        int *p = NULL;
        *p = 10;
        sleep(1);
    }
    return 0;
}

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

软件上面的错误,通常会体现在硬件或者其他软件上!

总结:

信号产生的方式虽然不同,但是最终一定都是通过OS向目标进程发送的信号!

由于收到信号后可能不会立即执行对应操作,在Linux内核中使用变量会保存信号。

进程中,采用位图来标识进程是否收到信号。

所以OS发送信号的本质是向指定进程的task_struct中的信号位图写入比特为1,所以信号的发送也可称为信号的写入。

3. waitpid接收信号

在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置。

当一个进程异常的时候,进程的退出信号会被设置,表明当前进程的退出原因。

3.1. core dump

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

在云服务器上,将数据转储到磁盘上的功能默认是被关掉的。

查看:ulimit -a

打开该功能:ulimit -c 空间大小

如果现在运行,上面会导致程序崩溃的代码,core dump 就会起作用了,并在当前目录下形成core文件:

而在gdb调试时,通过使用dump保存的文件,就会给出崩溃原因和在哪里崩溃:这种调试方式称为事后调试。

不一定所有的退出信号都会被core dump 例如:9号信号。  

3.2. status获取信号

在进程控制章节,我们讲过waipid系统调用的参数status的低8位保存的是进程退出时的信号。

来验证一下:

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

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

4.信号产生

4.1. 信号产生

信号在产生时可能存在一下几种情况:

  1. 实际执行信号的处理动作称为信号递达(即包括上面提到的三种处理方式:自定义捕捉、默认、忽略)。

  2. 信号从产生到递达之间的状态,称为信号未决(即信号被暂存在task_struct信号位图中)。

  3. 进程可以选择阻塞某个信号(本质进程暂时屏蔽指定信号)。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

信号在内核中的表示:

pending:保存的时已经收到,但是还没有被递达的信号。

OS发送信号的本质:修改目标进程的pending位图。

block:状态位图,表示哪些信号不应该被递达,直到解除阻塞。

handler:函数指针数组,每个信号的编号就是该数组下标,里面放的是默认、忽略、自定义处理放式的函数指针

阻塞信号集也叫做当前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略 。

4.2. sigset_t

这是操作系统设置的类型,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态, 。

sigset_t类型的变量不能单独使用,必须要配合特定的系统调用接口使用。

信号集操作函数

#include <signal.h>
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.3. sigprocmask

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

#include <signal.h>
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

演示代码:

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

int main()
{
    sigset_t set, oset;
    sigemptyset(&set); // 初始化
    sigemptyset(&oset);
    
    sigaddset(&set, 2); // 向set中添加2号信号

    sigprocmask(SIG_BLOCK, &set, &oset); // 将set中的信号阻塞
    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

代码中将2号信号阻塞了,那么运行代码后,发送2号信号会被阻塞。

4.4. sigpending

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

#include <signal.h>
int sigpending(sigset_t *set); // 参数为输出型参数

演示代码:

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

void show_pending(sigset_t *set)
{
    int i = 1;
    for(i = 1; i <= 31; ++i)
    {
      if(sigismember(set, i))
      {
          printf("1");
      }
      else
      {
          printf("0");
      }
    }
    printf("\n");
}

void handler(int signo)
{
    printf("2号信号已被递达,已经处理完成!\n");
}

int main()
{

    signal(2, handler); // 捕捉2号信号
    sigset_t set, oset;
    sigemptyset(&set);
    sigemptyset(&oset);
    
    sigaddset(&set, 2);

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

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

5. 信号发送后

信号什么时候被处理?

当进程从内核态返回到用户态的时候,进行信号检测并处理信号。

5.1. 用户态与内核态

用户态:用户代码和数据被访问或者执行的时候,所处的状态。自己写的代码全部都是在用户态执行。

内核态:执行OS的代码和数据时,进程所处的状态。OS的代码的执行全部都是在内核态执行(例如系统调用)。

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

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

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

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

5.2. 信号捕捉

信号捕捉本质是修改handler表中的内容。

内核实现信号捕捉的过程大致是下图这样:

上图可简化抽象为:

5.2.1 signal、sigaction

signal方法文章开始已经讲解,这里不再演示。

#include <signal.h>

typedef void (*sighandler_t)(int); // 函数指针

sighandler_t signal(int signum, sighandler_t handler);

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

#include <signal.h>
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是实时信号的处理函数, 本章不详细解释这两个字段

演示代码:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<string.h>


void handler(int signo)
{
    printf("get a signo: %d\n", signo);
}

int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = handler;
    
	// 本质是修改当前进程的handler函数指针数组的特定内容
    sigaction(2, &act, NULL); // 捕捉2号信号
    while(1)
    {
        printf("hello world!\n");
        sleep(1);
    }
    return 0;
}

5.3. volatile

volatile 作用:

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

这句话是什么意思呢?我们用下面这段代码来解释:

#include<stdio.h>
#include<signal.h>

int flag = 0;
//volatile int flag = 0;

void handle(int signo)
{
    flag = 1;  // 将flag改为1
    printf("change flag 0 to 1!\n");
}


int main()
{
    signal(2, handle);
    while(!flag); // flag为0时死循环,flag为1时退出循环
    printf("该进程正常退出!\n");
    return 0;
}

正常编译情况下,该程序是死循环的,如果向进程发送2号信号,循环退出,进程结束。

但是如果,在编译时加上优化,例如gcc -O3 选项,会对该程序进行优化

优化过程:

首先,flag是个全局变量,会为他在内存上开辟空间,并且while循环条件的判断室友CPU完成的。

在main函数中的while循环中,编译认为没有地方会对flag变量进行修改,那么它就将flag变量直接放入CPU的寄存器中,下一次循环时将不会再从内存中寻找flag变量加载到CPU的寄存器上进行判断,而是直接在CPU的寄存器中判断flag。导致内存上的flag发生改变时,寄存器中的flag不会改变,所以程序的运行结果可能就会出现问题。

所以就算向进程发送了2号信号,进程也不会终止:

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

5.4. SIGCHLD信号

子进程在退出时其实会向e'eee父进程发送SGCHLD信号,表示自己退出了。

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>

void handle(int signo)
{
    printf("get a signo: %d\n", signo);
}

int main()
{
    signal(SIGCHLD, handle);
    pid_t pid = fork();
    if(pid==0)
    {//child
        int count = 5;
        while(count--)
        {
            printf("I am child, running!\n");
            sleep(1);
        }
        exit(0);
    }
    
  while(1);
    return 0;
}

并且,现在在子进程退出时,我们可以不用使用父进程等待子进程;

而可以直接将SIGCHLD信号捕获,将它的处理方式改为忽略:

signal(SIGCHLD, SIG_IGN); // 显式设置忽略17号信号,当子进程退出后,自动释放僵尸进程
  • 52
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 44
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 44
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

风继续吹TT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值