一文搞懂Linux内核编程进程通信信号原理

一,Linux信号的概念

信号是 Linux 进程间通信的最古老的方式。信号是软件中断,它是在软件层次上对中断机制的一种模拟。

二,Linux信号的特点

1.信号是异步的,进程不需要等待信号的到来,也不需要有获得信号的操作,而是在进程内部设置与信号对应的处理函数,有信号到达的时候,系统异步触发对应的处理函数。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。

在Linux终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这个“中断任务”(默认的处理方式为结束掉当前进程)

2.信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。

嵌入式进阶教程分门别类整理好了,看的时候十分方便,由于内容较多,这里就截取一部分图吧。

需要的朋友私信【内核】即可领取

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂

三,信号的来源

程序错误:比如进行“除以0”运算这样的非法操作

外部信号:在Linux终端输入Ctrl+C,会产生SIGINT信号,定时器到期会产生SIGALRM信号

显式请求:比如kill -9 pid, kill函数允许进程发送信号给其他进程或进程组

常见信号:

特殊说明:

SIGPIPE, socket网络程序必须处理的信号,否则当客户端退出后,服务器端仍向客户端的socket发送数据,引起系统Crash。

SIGCHLD, Linux中当子进程结束时,子进程并未被完全销毁,因为父进程还要用它的信息。如果父进程没有处理SIGCHLD信号或者调用wait/waitpid()等待子进程结束,就会产生僵尸进程。。

四,信号的5种默认处理动作

TERM 终止进程

IGN 当前进程忽略掉这个信号

CORE 终止进程,并生成一个Core文件

STOP 暂停当前进程

CONT 继续执行当前被暂停的进程

五,信号的几种状态:产生、未决、递达

1) 产生

a) 当用户按某些终端键时,将产生信号。

终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT

终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT

终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。

b) 硬件异常将产生信号。

除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。

c) 软件异常将产生信号。

当检测到某种软件条件已发生(如:定时器alarm),并将其通知有关进程时,产生信号。

d) 调用系统函数(如:kill、raise、abort)将发送信号。

注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。

e) 运行 kill /killall命令将发送信号。

此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。

2) 未决状态:在信号产生和递送之间的时间间隔内,称信号是未决的(pending)

如果该进程产生了一个被该进程设置为阻塞的信号,而且对该信号的动作是默认或者捕捉该信号,则内核为该进程将此信号保持为未决状态,直到该进程对此信号解除阻塞,或者将对此信号的动作改为忽略。

内核在递送一个原来阻塞的信号给进程时(而不是在产生信号时),才决定对他的处理方式。所以,进程在信号递送给他之前仍可以改变该信号的处理动作。

“未决”和“阻塞”的区别:

信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。

信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。

每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)

3) 递送状态:产生的信号被通知给进程,信号被处理

六,信号的种类

类型

信号值范围

说明

不可靠信号

信号值< SIGRTMIN, Unix早期信号

注册函数为signal, .每次处理完信号后,要重置信号的值

可靠信号

信号值 在[SIGRTMIN, SIGRTMAX]之间

发送函数为sigqueue, 注册函数为sigaction

实时信号


实时信号都支持排队,都是可靠的信号

非实时信号


非实时信号都不支持排队,都不是可靠信号

七,进程对信号的处理

进程在执行信号相应处理函数之前,首先要把信号在进程中注销。

进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的生命终止。

当进程接收到一个信号时,就需要把接收到的信号添加 pending 这个队列中。

八,信号的处理流程

信号捕捉样例:

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

void sig_process(int signo){
    switch(signo){
        case SIGHUP:
            printf("Get a signal -- SIGHUP\n");
            break;
        case SIGINT:
            printf("Get a signal -- SIGINT\n");
            break;
        case SIGQUIT:
            printf("Get a signal -- SIGQUIT\n");
            break;
    }
    return;
}

int main(){
    signal(SIGHUP,  sig_process);
    signal(SIGINT,  sig_process);
    signal(SIGQUIT, sig_process);
    for (;;) {
        sleep(1);
    }
    return 0;
}

在终端输入Ctr+C,运行结果:

进程管理结构中,与信号有关的字段:

struct task_struct {
    ...
    int sigpending;
    ...
    struct signal_struct *sig;
    sigset_t blocked;
    struct sigpending pending;
    ...
}

sigpending 表示进程是否有信号需要处理(1表示有,0表示没有)

成员 blocked 表示被屏蔽的信息,每个位代表一个被屏蔽的信号

成员 sig 表示信号相应的处理方法,其类型是 struct signal_struct

#define  _NSIG  64
struct signal_struct {
  atomic_t  count;
  struct k_sigaction action[_NSIG];
  spinlock_t  siglock;
};
typedef void (*__sighandler_t)(int);
struct sigaction {
  __sighandler_t sa_handler;
  unsigned long sa_flags;
  void (*sa_restorer)(void);
  sigset_t sa_mask;
};
struct k_sigaction {
  struct sigaction sa;
};

九,常用的信号发送函数

函数

备注

函数名:int kill(pid_t pid, int sig)功能:给任意进程发送信号

1.pid =0时,表示信号将送往所有与调用 kill的那个进程属同一个使用组的进程。2.pid >0时,pid 是信号要送往的进程ID。3.pid = -1时,信号将送往调用进程有权给其发送信号的所有进程,除了进程1(init)。4.pid <-1时,信号将送往以-pid为组标识的进程。若sig=0,则不发送任何信号,但是参数检测仍然进行,这可以用来检查pid参数是否正确kill() 系统调用最终会进入内核态,并且调用内核函数 sys_kill()

函数名:int sigqueue(pid_t pid, int sig, const union sigval value)功能:给任意进程发送信号,并且可以传递数据value为随信号一起传递的数据

1.新的发送信号函数,主要用于实时信号,也支持前32种信号,常配合sigaction一起使用2.发送的信号只能发给一个进程,不能发送给进程组3.sig=0时的用法等同于kill函数

函数名:int raise(int sig)功能:给本进程或者线程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)

1.在单线程程序中等价于 kill(getpid(), sig)2.在多线程程序中等价于 pthread_kill(pthread_self(), sig)3.该函数会在信号处理函数执行完成后返回

函数名:unsigned int alarm(unsigned int seconds)功能:在seconds秒后给本进程发送SIGALRM信号

1.发送信号后的默认处理函数是终止进程2.若seconds=0, 则任何未决状态的SIGALRM都会被取消3.alarm函数,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸……无论进程处于何种状态,alarm都计时

函数名:void abort(void)功能:给自己发送异常终止信号SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);

1.该函数先解除对SIGABRT信号的屏蔽2.该函数最终的结果是终止进程3. 如果SIGABRT被注册了一个捕获函数,那么执行abort()还会导致进程终止吗?由于SIGABRT被执行完捕获函数后会恢复为默认,然后abort再次发送SIGABRT,进程依然被终止

十,常用的信号处理函数

signal该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它

关于sigaction函数中:

struct sigaction结构体:

struct sigaction {
    void(*sa_handler)(int); //旧的信号处理函数指针
    void(*sa_sigaction)(int, siginfo_t *, void *); //新的信号处理函数指针
    //不要同时设置sa_handle和sa_sigaction, 给其中之一赋值就行
    sigset_t   sa_mask;      //信号阻塞集
    int        sa_flags;     //信号处理的方式
    void(*sa_restorer)(void); //已弃用
};

sa_flags:通常设置为0,表示使用默认属性。

sa_handler:指定信号捕捉后的处理函数,即注册回调函数。该成员也可以赋值为SIG_IGN,表示忽略该信号,也可注册为SIG_DFL,表示执行信号的默认动作。

sa_mask:临时阻塞信号集(或信号屏蔽字)先来看这样一个情景:

某个信号已经注册了回调函数,当内核传递这个信号过来时,会先经过一个阻塞信号集,先阻塞掉部分信号。再去执行对应的回调函数。如下图示:

十一,信号集

多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t

信号集的常用函数:

#include <signal.h>  

//sigset_t set
int sigemptyset(sigset_t *set);       //将set集合置空
int sigfillset(sigset_t *set);          //将所有信号加入set集合
int sigaddset(sigset_t *set, int signo);  //将signo信号加入到set集合
int sigdelset(sigset_t *set, int signo);   //从set集合中移除signo信号
int sigismember(const sigset_t *set, int signo); //判断信号是否存在

sigset_t类型变量必须使用sigemptyset或sigfillset初始化,以防止该变量所在内存位置的原有数据对sigset_t的影响

进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集

阻塞信号集: 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。

未决信号集: 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态

阻塞信号集的处理:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:
    检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞集合进行修改,新的信号阻塞集由 set 指定,而原先的信号阻塞集合由 oldset 保存。
参数:
    how : 信号阻塞集合的修改方法,有 3 种情况:
        SIG_BLOCK:向信号阻塞集合中添加 set 信号集,新的信号掩码是set和旧信号掩码的并集。?相当于 mask = mask|set。
        SIG_UNBLOCK:从信号阻塞集合中删除 set 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。
        SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。
    set : 要操作的信号集地址。
        若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。
    oldset : 保存原先信号阻塞集地址
返回值:
    成功:0,
    失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。

未决信号集的处理:

#include <signal.h>
int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:
    set:未决信号集
返回值:
    成功:0
    失败:-1

十二,代码实例

Demo1:

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

void handler(int sig)
{
    printf("获取到SIGINT信号!\n");
    exit(0);
}

int main(void)
{
    int pid;
    pid = fork();

    if(pid < 0)
    {
        printf("进程创建出错:%d.\n",errno);
        exit(0);
    }
    else if(pid == 0)
    {
        printf("子进程创建成功,进程ID为:%d.\n",getpid());
        signal(SIGINT,handler);
        while(1)
        {
            sleep(1);
            printf("通过!\n");
        }
    }
    else
    {
        printf("这是父进程,进程ID为:%d.",getpid());
        sleep(6);
        kill(pid,SIGINT);
        wait(NULL);
        printf("父进程退出!\n");
    }
    return 0;
}

运行结果:

Demo2:

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

int main()
{
    pid_t pid = fork();
    if (pid == 0)
    {//子进程
        int i = 0;
        for (i = 0; i<5; i++)
        {
            printf("in son process\n");
            sleep(1);
        }
    }
    else
    {//父进程
        printf("in father process\n");
        sleep(2);
        printf("kill sub process now \n");
        kill(pid, SIGINT);
    }
    return 0;
}

运行结果:

Demo3:

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

int main()
{
    sigset_t set;   // 定义一个信号集变量
    int ret = 0;
    sigemptyset(&set); // 清空信号集的内容

    // 判断 SIGINT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 0)
    {
        printf("SIGINT is not a member of set \nret = %d\n", ret);
    }
    sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 set
    sigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set
    // 判断 SIGINT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGINT);
    if (ret == 1)
    {
        printf("SIGINT is a member of set \nret = %d\n", ret);
    }
    sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除
    // 判断 SIGQUIT 是否在信号集 set 里
    // 在返回 1, 不在返回 0
    ret = sigismember(&set, SIGQUIT);
    if (ret == 0)
    {
        printf("SIGQUIT is not a member of set \nret = %d\n", ret);
    }
    return 0;
}

运行结果:

原文地址:https://cloud.tencent.com/developer/article/1998049(版权归原作者所有,侵删)
举报
评论 0