Linux--进程信号


本片博客会粘贴部分代码,想要了解更多代码信息,可访问 小编的GitHub关于本篇的代码

  • 信号概念

生活中的信号:红绿灯

  1. 信号量与信号的区别
    信号实际上是一个软中断,用于通知进程发生了某些事件实际上也可以算作进程间通讯方式之一。因为我们可以在一个进程中通过给另一个进程发送信号,另一个进程会对这个信号做出相对应的处理。
  2. 信号的功能
    实际上就是为了通知进程发生了哪些事件,应该怎么处理。信号也可以归类为一类进程间通信方式
  3. 信号生命周期
    信号的产生——>信号的注册——>(信号的阻塞/屏蔽)——>信号的注销——>信号的处理
  4. 信号的查看
    1、kill -l 查看所有信号
    2、cat /usr/include/bits/signum.h查看所有信号编号对应含义
    3、man 7 signal查看所有信号编号对应含义
    4、vim /usr/include/signal.h查看各个信号结构体的定义
  5. 信号种类
Linux下有62个信号类别特点
1-31非可靠信号(非实时信号)非可靠代表这个信号有可能会丢失,如果有相同的信号已经注册到这个进程(即在pending位图中该信号对应位置已经置1),那么接下来的相同信号就会被丢掉,不作处理
34-64可靠信号(实时信号)信号不会丢失,如果有相同的信号已经注册到这个进程(即在pending位图中该信号对应位置已经置1),依然会在sigqueue链表中添加该信号结点。

在这里插入图片描述

注册:就是修改pending位图中对应的信号位,并且添加sigqueue结构到链表
可靠信号注册、不可靠信号注册不同之处(是否会添加多个节点到链表)
注销:在处理信号之前删除链表中的节点,并且修改位图
阻塞:将信号添加到blocked位图,向进程备注说明这些信号暂时不处理

常见的Linux信号

	   SIGHUP        1       Term    Hangup detected on controlling terminal
                                     or death of controlling process
       SIGINT        2       Term    Interrupt from keyboard
       SIGQUIT       3       Core    Quit from keyboard
       SIGILL        4       Core    Illegal Instruction
       SIGABRT       6       Core    Abort signal from abort(3)
       SIGFPE        8       Core    Floating point exception
       SIGKILL       9       Term    Kill signal
       SIGSEGV      11       Core    Invalid memory reference
       SIGPIPE      13       Term    Broken pipe: write to pipe with no
                                     readers
       SIGALRM      14       Term    Timer signal from alarm(2)
       SIGTERM      15       Term    Termination signal
       SIGUSR1   30,10,16    Term    User-defined signal 1
       SIGUSR2   31,12,17    Term    User-defined signal 2
       SIGCHLD   20,17,18    Ign     Child stopped or terminated
       SIGCONT   19,18,25    Cont    Continue if stopped
       SIGSTOP   17,19,23    Stop    Stop process
       SIGTSTP   18,20,24    Stop    Stop typed at terminal
       SIGTTIN   21,21,26    Stop    Terminal input for background process
       SIGTTOU   22,22,27    Stop    Terminal output for background process

信号的产生方式

  1. 硬件中断:ctrl+c中断前台进程, ctrl+\退出前台进程
  2. 软件条件:
    第一个函数:
int kill(pid_t pid, int sig);		//给进程id为pid的进程发送信号sig

The kill() system call can be used to send any signal to any process group or process.

第二个函数:

int raise(int sig);				//给调用进程或线程发送信号sig

The raise() function sends a signal to the calling process or thread. In a single-threaded program it is equivalent to kill(getpid(), sig);

第三个函数:

int sigqueue(pid_t pid, int sig, const union sigval value);
//给指定进程发送指定信号,同时可以携带一个参数过去

第四个函数:
void abort ()函数使当前进程接收到信号⽽异常终⽌。就像exit函数⼀样,abort函数总是会成功的,所以没有返回值。
第五个函数:

unsigned int alarm(unsigned int seconds);

指定在seconds秒后发送一个SIGALRM信号到进程
定时器是会覆盖的,每次调用的时候都会覆盖上一个
seconds==0 取消以前的定时器
返回值:返回上一个定时器剩余的定时时间或0

  1. 程序异常core dump
    程序异常时候,会产生一个名为core的文件,存储了进程的用户的内存空间的数据全部拷贝,以备事后调试。
    gdb 运行可执行程序,然后在gdb中使用命令:core-file core.3996(转储文件名称)来加载程序的运行数据,然后就可定位错误了。
    但是程序的核心转出功能默认是关闭的,转储文件的默认大小是0,因为运行数据中可能会存在有安全性信息,以及文件曾多会占用资源。

    查看转储文件大小和设置
    ulimit -c
    ulimit -c size 通过设置转储文件大小来开启转储功能
    在这里插入图片描述

  2. 命令产生
    kill命令用来删除执行中的程序或工作。kill可将指定的信息送至程序。预设的信息为SIGTERM(15),可将指定程序终止。若仍无法终止该程序,可使用SIGKILL(9)信息尝试强制删除程序。程序或工作的编号可利用ps指令或job指令查看。

kill -9干不死的进程:僵尸进程、D(不可中断睡眠)状态的进程

信号的注册

信号的注册就是说将这个信号传递给这个进程
将这个进程记录到进程中,让进程知道用这么一个信号来了

信号是记录在进程PCB中
信号集合:sigset_t结构体
进程记录一个信号的时候,是通过这个结构体的位图来记录的,这个位图的位数+1代表的就是指定的信号的信号存储位置

信号的阻塞与屏蔽

在PCB中有一个pending结构中存储当前接收到的信号,还有一个结构blocked用于存储现在都有哪些信号需要被阻塞。
进程看到pending集合中都收到了哪些信号,然后就要开始处理这些信号,但是在处理这些信号之前,进程会先对比一下这些信号有没有存在于block集合中,如果存在,意味着这个信号将不会被处理,直到解除了阻塞。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how:对集合所做的操作
SIG_BLOCK对set集合中的信号进行阻塞,oldset保留原来blocked集合中的元素
SIG_UNBLOCK对set集合中的信号解除阻塞,oldset忽略
SIG_SETMASK

#include <signal.h>
int sigemptyset(sigset_t *set);				//清空信号集合set
int sigfillset(sigset_t *set);				//将所有信号都添加到集合set中
int sigaddset(sigset_t *set, int signum);	//向集合set中添加指定信号
int sigdelset(sigset_t *set, int signum);	//删除set集合中的signum信号
int sigismember(const sigset_t *set, int signum);//判断signum信号是否在set集合中

将当前pending集合(信号注册集合)中的信号取出来放到set中

int sigpending(sigset_t *set);

将2号信号SIGINT(Interrupt from keyboard)加入到block集合中,使ctrl+C失效

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
int main()
{
    sigset_t set;//定义了一个信号集合
    sigemptyset(&set);//将set清空
    sigaddset(&set,SIGINT);//将2号信号加入到信号集合
    sigset_t oldmask;
    sigprocmask(SIG_BLOCK,&set,&oldmask);//将2号信号放入block结构体
    //sigprocmask(SIG_UNBLOCK,&set,NULL);
    while(1){
        printf("nihao\n");
        sleep(1);

        //打印pending集合的信号
        sigset_t wait;
        sigemptyset(&wait);
        sigpending(&wait);//将在pending结构体中等待的信号取出放到wait集合中
        int i;
        for(i=1;i<64;i++)
        {   
            if(sigismember(&wait,i))
            printf("pending:%d",i);
        }   
    }   
    return 0;
}

在这里插入图片描述

信号的注销

就是从pending集合中将要处理的信号移除,但是这个移除分情况
非可靠信号:
非可靠信号注册的时候,是给sigqueue链表添加一个信号节点,并且将pending集合对应的位图置1,当这个信号注册的时候,如果位图已经置1,代表信号注册过了,因此不做任何操作,不会添加信号节点。后来的信号就会丢弃。
注销:删除链表中的节点,并将对应位图置0
可靠信号:
注册:可靠信号注册时候,是给sigqueue链表添加一个信号节点(不管这个可靠信号是否已经注册),如果没有注册则添加新节点的同时,更改对应位图置1,
注销:删除一个节点,然后查看链表中还有没有相同的节点,如果有那么,信号对应的位置依然置1,如果信号没有相同的节点,代表这个信号已经全部被处理了,因此将对应的位图置0。
可靠信号因为每次信号到来都会添加新节点,因此可靠信号不会丢失。

信号处理

每一个信号实际都对应了某个事件,当进程收到了某个信号,那么就意味着现在有一个重要的事件需要处理,因此会打断我们当前的操作,然后去处理这个事件。信号处理还有一个名字:信号的递达。
那么进程到底什么时候才回去检测pending集合,看有没有信号需要处理呢?什么时候处理信号?
进程是从内核态切换到用户态的时候会去检测一下是否有信号需要被处理,进程间切换也是在从内核态切换到用户态的时候实现
在这里插入图片描述
该过程总共有四次状态切换。

处理方式

复习函数指针的知识:

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

typedef void (*Func)(int a); //相当于一个函数类型

void func(int a)
{
    printf("%d\n",a);
}

void test(Func func,int a)
{
    func(a);
}
int main()
{
    test(func,20);
    return 0;
}

默认处理方式:操作系统以原有定义好的,对于一个信号所对应事件的处理。
忽略处理方式:忽略与阻塞完全不同,一个被忽略信号来了之后就直接丢弃了。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 用于修改一个信号的处理方式
    signum: 用于指定修改哪个的处理
    handler:用于指定处理方式(函数)
    SIG_IGN 忽略处理
    SIG_DFL 默认处理
    自己命名的函数:自定义处理
    在这里插入图片描述

体现忽略和阻塞的区别:
阻塞一个信号后,信号依然会注册在pending集合中
忽略一个信号后,信号来了就直接被丢弃,不会注册

第一种自定义处理方式:我们用户自己定义一个信号的处理方式,然后告诉操作系统,当这个信号来了之后,就按我定义的这个方法处理。

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

typedef void (*sighandler_t)(int signum);

void sigcb(int signum)
{
    printf("信号:%d\n",signum);
    sleep(10);
}

int main()
{
    //typedef void (*sighandler_t)(int);
    //sighandler_t signal(int signum, sighandler_t handler);
    signal(SIGINT,sigcb);
    while(1){
        printf("自定义处理方式\n");
        sleep(2);
    }   
    return 0;
}

在这里插入图片描述
第二种自定义处理方法:

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum: 用于指定修改哪个信号的处理动作
    act: 给指定信号要指定的处理动作
    oldact:用于保存这个信号原来的处理动作
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);//还可以接受信号携带的参数
               sigset_t   sa_mask;
               //sa_mask在处理信号期间,不希望收到SIGQUIT影响,因此将在处理信号期间将sa_mask中的信号全部阻塞
               int        sa_flags;//决定那个成员函数作处理接口,0:sa_handler	SA_SIGINFO:sa_sigaction
               void     (*sa_restorer)(void);
           };

//使用sigaction进行自定义处理
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
/*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 sigcb(int signum)
{
    printf("信号:%d\n",signum);
    sleep(10);
}

int main()
{
    //int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
    struct sigaction act;
    act.sa_handler = sigcb;
    act.sa_flags = 0;
    struct sigaction old;
    sigaction(SIGINT,&act,&old);
    while(1){
        printf("hhhhhhh\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

信号的捕捉流程

针对的是自定义处理方式
一个进程如何捕捉到一个信号然后处理的过程。

  • 进程如何实现用户态到内核态的转换:中断,异常,系统调用。
//用pause和alarm函数实现sleep功能
#include<unistd.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<signal.h>

void sigcb()
{
}
unsigned int mysleep(const unsigned int n)
{
    struct sigaction act,old;
    act.sa_handler = sigcb;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGALRM,&act,&old);
    alarm(n);
    pause();
    unsigned int slept=alarm(0);
    sigaction(SIGALRM,&old,NULL);
    return slept;
}
int main()
{
    while(1)
    {
        mysleep(1);
        printf("sleep\n");
    }
    return 0;
}

在这里插入图片描述

可重入函数与不可重入函数

这个函数在运行过程中,如果有地方重复调用这个函数,而这个函数调用不会对其他地方的调用产生影响,那么就称这种函数是可重入函数。

/*  这是一个体验不可重入函数的代码
 *
 */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

//可重入函数:
//  这个函数调用的时候如果中间操作被打断,在其他地方有多次调用,
//  但是并不会对运行结果造成影响
//  那么这种函数就叫可重入函数

//不可重入函数:一旦重入就会出问题
//  这个函数调用的时候如果中间操作被打断,在其他地方有多次调用,
//  这些多次调用会对运行结果造成影响
//  那么这类函数就叫不可重入函数
//
//  特点:操作了一些公共数据
int a = 10;
int b = 20;
int sum()
{
    printf("%d+%d\n", a, b);
    a++;
    ///----------
    sleep(10);
    b++;
    return (a+b);
}

void sigcb (int signo)
{
    printf("signal---sum=%d\n", sum());
    return;
}
int main()
{
    signal(SIGINT, sigcb);
    printf("%d\n",sum());
    while(1) {
        sleep(1);
    }
}

在这里插入图片描述

volatile关键字

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

int a=1;
void sigcb(int signu)
{
    printf("signu:%d\n",signu);
    a=0;
}
int main()
{
    signal(SIGINT,sigcb);
    while(a){

    }
    return 0;
}

在这里插入图片描述
volatile:保持内存可见性:每次处理这个被volatile修饰的变量的时候,都会从内存中重新加载变量的值到寄存器。
因为程序在优化的时候,如果一个变量使用频率非常高,那么这个变量有可能就会被优化为,只向寄存器家在一次,往后直接使用寄存器中保存的值,而不关心这个变量内存里边的值,因此就有可能造成程序的一些错误。

gcc进行编译链接过程中,如果未调用-O选项(代码优化),a变量来自内存,优化后a变量来自寄存器,为了保证内存可见性,加volatile 关键字,即使代码优化也能保证a的内存可见性,即每次处理volatile关键都会去内存加载变量a到寄存器。

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

volatile int a=1;
void sigcb(int signu)
{
    printf("signu:%d\n",signu);
    a=0;
}
int main()
{
    signal(SIGINT,sigcb);
    while(a){

    }   
    return 0;
}

在这里插入图片描述

//模拟实现SIGCHLD信号的处理:等待子进程
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>

void sigcb()
{
    printf("child exit\n");
    while(waitpid(-1,NULL,WNOHANG)>0);
}
int main()
{
    struct sigaction act,old;
    act.sa_handler=sigcb;
    act.sa_flags=0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD,&act,&old);

    if(fork()==0)
    {
        sleep(1);
        exit(1);
    }
    while(1){
        printf("sleeping!!!!\n");
        sleep(10);
    }
    return 0;
}

SIGCHLD 子进程退出时通过这个信号来通知父进程的,父进程也是通过收到这个信号,才知道子进程状态改变了(退出)
这时候就需要调用
wait()来为子进程收尸
while(waitpid(-1,NULL,WNOHANG)>0);
这样的话我们的子进程资源将随之被回收。并且不回影响我们父进程的大致程序逻辑。信号会打断进程的阻塞等待操作的,唤醒正在休眠的进程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值