linux——信号

目录

一.信号的保存

二.信号集操作

1.信号集

2.信号集操作函数

3.sigprocmask

4.sigpending

[三. 信号的捕捉](#三. 信号的捕捉)

1.内核态和用户态

[2. sigaction](#2. sigaction)

四.可重入函数

五.SIGCHLD信号


一.信号的保存

  1. 实际执行信号的处理动作称为信号递达(Delivery)。
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)。
  3. 进程可以选择阻塞 (Block )某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  5. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
  1. 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  2. SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  3. SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
  4. 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

二.信号集操作

1.信号集

  1. 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
  2. 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态。
  3. 在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。
  4. 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应该理解为阻塞而不是忽略。

2.信号集操作函数

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。

cpp 复制代码
#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在该信号集中添加或删除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。

3.sigprocmask

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

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

测试代码:

cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

int main()
{
    // 1.创建信号集
    sigset_t set, old_set;
    // 2.清空信号集
    sigemptyset(&set);
    sigemptyset(&old_set);
    // 3.添加2号信号到信号集中
    sigaddset(&set, SIGINT);
    // 4.将信号集添加进当前进程信号屏蔽字
    sigprocmask(SIG_BLOCK, &set, &old_set);
    int count = 0;
    while (1)
    {
        if (count == 7)
        {
            // 解除对2信号的屏蔽
            cout << "解除对2信号的屏蔽,直接递达" << endl;
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
        cout << "I am running" << endl;
        count++;
        sleep(1);
    }

    return 0;
}

测试结果:

首先屏蔽2号信号,7秒之后在解除对2号信号的屏蔽。如果再前7秒之间收到了2号信号,在解除屏蔽的一瞬间2号信号被递达。

观察BLOCK信号集的变化:

测试代码:

cpp 复制代码
//打印信号集
void Print_Set_Block()
{
    sigset_t set;
    sigemptyset(&set);

    sigprocmask(SIG_BLOCK, NULL, &set);
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&set, i))
            cout << '1';
        else
            cout << '0';
    }
    cout << "\n";
}
int main()
{
    // 1.创建信号集
    sigset_t set, old_set;
    // 2.清空信号集
    sigemptyset(&set);
    sigemptyset(&old_set);
    // 3.添加2号信号到信号集中
    sigaddset(&set, SIGINT);
    // 4.将信号集添加进当前进程信号屏蔽字
    cout << "对2信号的屏蔽" << endl;

    sigprocmask(SIG_BLOCK, &set, &old_set);
    int count = 0;
    while (1)
    {
        if (count == 7)
        {
            // 解除对2信号的屏蔽
            cout << "解除对2信号的屏蔽,直接递达" << endl;
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
        cout << "I am running" << endl;
        Print_Set_Block();
        count++;
        sleep(1);
    }

    return 0;
}

测试结果:

4.sigpending

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。

测试代码:

我们希望看到在2号信号被阻塞的情况下,看到他的pending表的情况,和解除阻塞后的pending表的情况。

cpp 复制代码
// 打印信号集
void Print_Set_Pend()
{
    sigset_t set;
    sigemptyset(&set);

    sigpending(&set);
    cout << "pending表:";
    for (int i = 1; i <= 31; i++)
    {
        if (sigismember(&set, i))
            cout << '1';
        else
            cout << '0';
    }
    cout << "\n";
}
void handler(int signo)
{
    cout << "收到信号:" << signo << endl;
}
int main()
{

    // 0.自定义捕捉2号信号
    signal(2, handler);

    // 1.创建信号集
    sigset_t set, old_set;
    // 2.清空信号集
    sigemptyset(&set);
    sigemptyset(&old_set);
    // 3.添加2号信号到信号集中
    sigaddset(&set, SIGINT);
    // 4.将信号集添加进当前进程信号屏蔽字
    cout << "对2信号屏蔽10秒" << endl;

    sigprocmask(SIG_BLOCK, &set, &old_set);
    int count = 0;
    while (1)
    {
        if (count == 10)
        {
            // 解除对2信号的屏蔽
            cout << "解除对2信号的屏蔽,直接递达" << endl;
            sigprocmask(SIG_UNBLOCK, &set, NULL);
        }
        // cout << "I am running" << endl;
        Print_Set_Pend();
        count++;
        sleep(1);
    }

    return 0;
}

测试结果:

三. 信号的捕捉

内核中什么时候对信号进行处理:

1.内核态和用户态

用户态和内核态是操作系统中的两种运行状态。用户态是指进程运行用户代码的状态,而内核态是指进程运行内核代码的状态 。在Linux中,进程从创建到退出,都会经历三种状态:用户态、内核态和系统调用态。当一个进程执行系统调用时,它会从用户态切换到内核态,然后等待内核处理完请求后,再从内核态切换回用户态。

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

  1. 用户程序注册了SIGQUIT信号的处理函数sighandler。
  2. 当前正在执行main函数,这时发生中断或异常切换到内核态。
  3. 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
  4. 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler。
  5. 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。
  6. sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,sys_sigreturn这次再返回用户态就是恢复main函数的上下文继续执行了。

2. sigaction

cpp 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
//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);
};
  1. sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回 - 1,signo是指定信号的编号。
  2. 若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体。
  3. 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
  4. 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段,有兴趣的同学可以在了解一下。

测试代码:

测试进程在收到二号信号以后,进入handler函数,屏蔽mask中的所有信号和2号信号。

cpp 复制代码
void handler(int signo)
{
    while (1)
    {
        cout << "收到信号:" << signo << endl;
        sleep(1);
    }
}

int main()
{

    // 1.创建sigaction结构体
    struct sigaction sigc, oldsigc;
    memset(&sigc, 0, sizeof(sigc));
    memset(&sigc, 0, sizeof(oldsigc));

    // 2.填写成员
    // 2.1自定义捕捉
    sigc.sa_handler = handler;
    // 2.2在执行自定义捕捉函数时,想要屏蔽的信号,屏蔽2.3.4.5.6信号
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, 3);
    sigaddset(&set, 4);
    sigaddset(&set, 5);
    sigaddset(&set, 6);
    sigc.sa_mask = set;
    // 3.写入进程
    sigaction(2, &sigc, &oldsigc);

    while (1)
    {
        cout << "I am running,pid:" << getpid() << endl;
        sleep(1);
    }

    return 0;
}

测试结果:

四.可重入函数

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
  • 因为两个执行流访问的是函数内部的局部变量,会分别开辟函数栈帧,相当于尽管是同一个函数,但是对于不同的执行流来说,函数都是各自私有的,不会互相干扰,且局部变量执行流出了作用域就会销毁。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

五.SIGCHLD信号

进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

测试代码:

cpp 复制代码
void handler(int signo)
{
    sleep(3);
    int status = 0;
    waitpid(-1, &status, WNOHANG);
}

int main()
{

    signal(SIGCHLD, handler);

    pid_t pid = fork();
    if (pid == 0)
    {
        int count = 3;
        while (count)
        {
            cout << "我是子进程,pid:" << getpid() << ":" << count << endl;
            sleep(1);
            count--;
        }
        exit(2);
    }
    while (1)
    {
        cout << "父进程" << endl;
        sleep(5);
    }

    return 0;
}

测试结果:

事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。 系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可 用。

测试代码:

cpp 复制代码
int main()
{
    // 1.创建sigaction结构体
    struct sigaction sigc;
    memset(&sigc, 0, sizeof(sigc));
    // 2.设置sa_handler为SIG_IGN;
    sigc.sa_handler = SIG_IGN;
    // 写入当前进程
    sigaction(SIGCHLD, &sigc, NULL);
    int tmp = 10;
    while (tmp--)
    {
        //  创建子进程
        pid_t pid = fork();
        if (pid == 0)
        {
            int count = 3;
            while (count)
            {
                cout << "我是子进程,pid:" << getpid() << ":" << count << endl;
                sleep(1);
                count--;
            }
            exit(2);
        }
    }

    while (1)
    {
        cout << "父进程" << endl;
        sleep(5);
    }

    return 0;
}

测试结果:

相关推荐
2301_1472583691 小时前
7月2日作业
java·linux·服务器
格调UI成品2 小时前
预警系统安全体系构建:数据加密、权限分级与误报过滤方案
大数据·运维·网络·数据库·安全·预警
盘古开天16663 小时前
如何用废弃电脑变成服务器搭建web网站(公网访问零成本)
服务器·电脑·免费公网ip
xuanzdhc5 小时前
Linux 基础IO
linux·运维·服务器
愚润求学5 小时前
【Linux】网络基础
linux·运维·网络
bantinghy6 小时前
Linux进程单例模式运行
linux·服务器·单例模式
小和尚同志7 小时前
29.4k!使用 1Panel 来管理你的服务器吧
linux·运维
帽儿山的枪手7 小时前
为什么Linux需要3种NAT地址转换?一探究竟
linux·网络协议·安全
shadon1789 天前
回答 如何通过inode client的SSLVPN登录之后,访问需要通过域名才能打开的服务
linux
AWS官方合作商9 天前
AWS ACM 重磅上线:公有 SSL/TLS 证书现可导出,突破 AWS 边界! (突出新功能的重要性和突破性)
服务器·https·ssl·aws