首页 > 系统相关 >linux信号集与信号掩码-保护信号处理程序,确保进程正确运行

linux信号集与信号掩码-保护信号处理程序,确保进程正确运行

时间:2024-06-05 21:32:41浏览次数:26  
标签:set 阻塞 进程 信号 linux 掩码 include


在Linux环境下,当进程收到信号时,如何优雅地处理并确保程序的正常运行?这就需要借助信号集和信号掩码的功能。本文将为你揭开信号集和信号掩码的神秘面纱,并通过生动的代码示例,让你彻底掌握在C++程序中使用它们的技巧。


一、信号集:表示信号的数据结构

信号集(signal set)是一种用于表示进程当前阻塞了哪些信号的数据结构,它本质上是一个数组 bitmap,使用sigset_t结构体来表示。每一种信号对应一个位,如果该位被置位(值为1),则表示该信号被阻塞,否则(值为0)表示未被阻塞。


在这里插入图片描述


我们可以使用以下函数来操作信号集:

  • sigemptyset(&set): 将set中所有位清零,即不阻塞任何信号
  • sigfillset(&set): 将set中所有位置1,即阻塞全部信号
  • sigaddset(&set, sig): 将信号sig对应的位置1,即阻塞该信号
  • sigdelset(&set, sig): 将信号sig对应的位清零,即解除对该信号的阻塞
  • sigismember(&set, sig): 检查信号sig是否被set阻塞

示例代码:

#include <signal.h>
#include <iostream>

int main() {
    sigset_t set;
    
    // 初始化为空集
    sigemptyset(&set); 
    
    // 阻塞所有信号的传递
    sigset_t newset;
	  sigfillset(&newset);
    
    // 添加SIGINT(Ctrl+C)信号
    sigaddset(&set, SIGINT);
    
    // 检查SIGINT是否在集合中
    if (sigismember(&set, SIGINT)) {
        std::cout << "SIGINT is blocked" << std::endl;
    }
    
    // 移除SIGINT
    sigdelset(&set, SIGINT);
    
    return 0;
}

二、信号掩码:阻塞信号的机制


1、信号掩码(signal mask)

信号掩码(signal mask)是Linux内核为每个进程维护的一个信号集,用于暂时阻塞该进程接收某些信号。除了SIGKILL和SIGSTOP这两个特殊信号外,其他信号都可以被阻塞。

内核会为每一个进程都维护一个信号掩码,也就是一组信号,阻塞其针对该进程的传递,直到进程从信号掩码中将该信号移除。

我们可以通过sigprocmask系统调用来修改进程的当前信号掩码:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how 参数指定如何修改掩码
    • SIG_BLOCK: 将set指向的信号集并入当前掩码
    • SIG_UNBLOCK: 将set指向的信号集从当前掩码中移除
    • SIG_SETMASK: 使用set指向的信号集作为新的信号掩码
  • oldset 用于保存修改前的信号掩码,如果不需要可设为NULL

例如,阻塞SIGINT(Ctrl+C)信号:

#include <signal.h>
#include <iostream>

int main() {
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    
    // 阻塞SIGINT信号
    sigprocmask(SIG_BLOCK, &set, NULL);
    
    // 此时按下Ctrl+C将不会终止程序
    while (true) {
        std::cout << "Program running..." << std::endl;
        sleep(1);
    }
    
    return 0;
}

2、系统的默认行为

前面我们提到过,当前进程正在调用
SIGX
的信号处理函数,那么紧接着而来的
SIGX
信号将会被阻塞,直到上一个信号处理函数结束,这一现象我们可以用一个简单的例子证明 :

void handler(int signum)
{ 
  printf("Got a SIGINT\n"); 
  sigset_t currentset; 
  sigprocmask(SIG_BLOCK,NULL,&currentset); 
  int res=sigismember(&currentset,SIGINT);
  printf("SIGINT is blocked ?:%d\n",res); 
}

我们用
SIGINT
作为捕获信号,当我们键入
Ctrl-C 时,将会发现 SIGINT
信号的确是在当前进程的信号掩码中。

引发对处理器程序调用的信号将自动添加到进程信号掩码中。这意味着,当正在执行处理器程序时,如果同一个信号 实例第二次抵达,信号处理器程序将不会递归中断自己。


3、sigpending 获取等待的信号集


有时候我们想要知道当前进程阻塞了哪些信号,此时可以使用
sigpending() 来获得处于等待状态的信号集。

sigpending 是一个用于检查哪些信号当前正被一个进程阻塞的系统调用。在 Unix 和类 Unix 系统中,进程可以选择性地阻塞某些信号,这意味着在它们被阻塞期间,这些信号不会被传递给进程。当进程准备好处理这些信号时,可以解除信号的阻塞。

sigpending 函数通常与一个信号集(sigset_t 类型)一起使用,这个信号集包含了所有当前被阻塞的信号。

以下是 sigpending 的典型用法:

#include <signal.h>
#include <iostream>

int main() {
    sigset_t pending_set;

    // 获取当前被阻塞的信号集
    if (sigpending(&pending_set) == -1) {
        perror("sigpending");
        return 1;
    }

    // 检查特定信号是否在被阻塞的信号集中
    if (sigismember(&pending_set, SIGINT)) {
        std::cout << "SIGINT is pending." << std::endl;
    } else {
        std::cout << "SIGINT is not pending." << std::endl;
    }

    // ... 其他操作 ...

    return 0;
}

在上面的示例中,我们首先声明了一个 sigset_t 类型的变量 pending_set,然后调用 sigpending 函数来填充这个变量,它包含了当前所有被阻塞的信号。接着,我们使用 sigismember 函数来检查 SIGINT 信号是否在被阻塞的信号集中。

sigpending 函数的原型如下:

int sigpending(sigset_t *set);
  • set 是一个指向 sigset_t 结构的指针,该结构将被 sigpending 填充为包含当前被阻塞信号的集合。

如果 sigpending 成功执行,它将返回 0,并将被阻塞的信号集存储在 set 指向的 sigset_t 结构中。如果发生错误,它将返回 -1 并设置 errno 以指示错误类型。

使用 sigpending 可以帮助进程了解哪些信号正在等待被处理,这在多线程环境中尤其有用,因为信号通常只能被传递给线程组中的一个线程。了解哪些信号被阻塞可以帮助进程做出适当的响应。


三、信号集和信号掩码的应用场景


通过合理使用信号集和信号掩码,我们可以更好地控制进程对信号的响应方式,确保关键代码路径不被意外打断。具体应用场景包括但不限于:


1、保护信号处理函数

在执行信号处理函数期间,自动将相同的信号添加到进程掩码中,以避免递归调用导致的问题。

保护信号处理函数通常意味着在信号处理函数执行期间,系统会自动将该信号添加到进程的信号掩码中,以防止递归调用。这是通过操作系统自动处理的,通常不需要程序员手动设置。

然而,如果你需要手动控制信号掩码,可以使用sigprocmask函数。以下是一个C++示例,演示如何在信号处理函数执行前后手动修改信号掩码,以确保信号处理函数不会被递归调用:

#include <iostream>
#include <csignal>
#include <setjmp.h>
#include <sys/types.h>
#include <unistd.h>
#include <cerrno>

void signalHandler(int signum) {
    static int inHandler = 0; // 用于检测递归调用

    if (inHandler) {
        std::cout << "SignalHandler: Already in handler, recursion detected." << std::endl;
        return;
    }

    inHandler = 1; // 标记信号处理函数正在执行

    // 执行信号处理逻辑
    std::cout << "SignalHandler: Handling signal " << signum << std::endl;

    // 重置标记并解除信号阻塞
    inHandler = 0;
}

int main() {
    // 设置信号处理函数
    signal(SIGINT, signalHandler);

    // 主循环
    while (true) {
        pause(); // 等待信号
    }

    return 0;
}

在这个示例中,我们定义了一个静态变量inHandler来检测递归调用。当信号处理函数被调用时,我们检查inHandler是否已经被设置,如果是,则表示我们已经在处理一个信号,并且现在又收到了相同的信号,这是递归调用。我们打印一条消息并返回,而不执行任何操作。

请注意,这个示例中的inHandler变量用于演示目的,实际上操作系统会自动处理信号的递归调用问题,通常不需要程序员手动设置。

signal函数在多线程环境中可能不够安全,可以考虑使用sigaction函数来设置信号处理行为,其中sigaction允许你更精确地控制信号处理的行为,包括信号掩码。


2、进程间通信

父进程可以通过发送SIGCHLD信号来等待子进程结束;而子进程也可以通过发送指定信号来通知父进程某些事件发生。


(1)、父进程等待子进程结束

父进程可以通过捕获SIGCHLD信号来等待子进程结束。通常,当子进程结束时,内核会向父进程发送SIGCHLD信号。

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>

void sigchld_handler(int signum) {
    // 等待所有已终止的子进程
    while (waitpid(-1, NULL, WNOHANG) > 0)
        ;
    std::cout << "SIGCHLD received, child process has terminated." << std::endl;
}

int main() {
    // 设置SIGCHLD信号的处理函数
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; // SA_NOCLDSTOP 阻止父进程接收子进程停止的信号
    sigaction(SIGCHLD, &sa, NULL);

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "Fork failed" << std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::cout << "Child process exiting." << std::endl;
        exit(0);
    } else {
        // 父进程
        std::cout << "Parent process waiting for child to exit." << std::endl;
        pause(); // 等待信号
    }

    return 0;
}

在这个示例中,父进程设置了SIGCHLD信号的处理函数sigchld_handler。当子进程结束时,内核会发送SIGCHLD信号给父进程,父进程接收到信号后会调用sigchld_handler函数,该函数使用waitpid系统调用来收集子进程的退出状态。

(2)、子进程通知父进程事件

子进程可以通过发送指定的信号来通知父进程某些事件的发生。例如,子进程可以通过发送SIGUSR1信号来通知父进程它已经完成了某个任务。

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

void sigusr1_handler(int signum) {
    std::cout << "Parent process received SIGUSR1." << std::endl;
    // 处理信号,例如更新状态或执行其他任务
}

int main() {
    // 设置SIGUSR1信号的处理函数
    struct sigaction sa;
    sa.sa_handler = sigusr1_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGUSR1, &sa, NULL);

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "Fork failed" << std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程
        std::cout << "Child process is about to notify parent." << std::endl;
        kill(getppid(), SIGUSR1); // 向父进程发送SIGUSR1信号
        exit(0);
    } else {
        // 父进程
        pause(); // 等待信号
    }

    return 0;
}

在这个示例中,父进程设置了SIGUSR1信号的处理函数sigusr1_handler。子进程使用kill函数发送SIGUSR1信号给父进程。父进程接收到信号后会调用sigusr1_handler函数进行处理。


3、同步与互斥

信号可以作为一种简单的通知机制,在某些情况下辅助实现同步和互斥。例如,一个进程可以发送信号给另一个进程以指示某个事件的发生,接收进程在其信号处理函数中执行同步或互斥操作。


以下是一个简化的示例,演示如何使用信号在两个进程之间进行基本的同步操作:

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <cstring>

// 定义一个全局变量作为锁
volatile bool lock = false;

// 信号处理函数,用于释放锁
void signal_handler(int signum) {
    if (signum == SIGUSR1) {
        std::cout << "Signal received, releasing lock." << std::endl;
        lock = false;
    }
}

int main() {
    // 设置信号处理函数
    signal(SIGUSR1, signal_handler);

    // 创建子进程
    pid_t pid = fork();
    if (pid == -1) {
        std::cerr << "Fork failed" << std::endl;
        return 1;
    } else if (pid == 0) {
        // 子进程
        while (lock) {
            // 等待锁被释放
            std::cout << "Child process waiting for lock to be released." << std::endl;
            sleep(1);
        }
        std::cout << "Child process continuing after lock is released." << std::endl;
        // 子进程的其余逻辑...
        exit(0);
    } else {
        // 父进程
        lock = true; // 设置锁
        std::cout << "Lock is set." << std::endl;

        // 执行一些操作...
        sleep(5); // 模拟长时间运行的任务

        // 释放锁,并通知子进程
        lock = false;
        kill(pid, SIGUSR1);

        // 等待子进程结束
        wait(NULL);
        std::cout << "Parent process finished." << std::endl;
    }

    return 0;
}

在这个示例中,我们使用一个全局变量`lock`作为锁。父进程在开始执行任务时设置锁,并在任务完成后释放锁,同时发送`SIGUSR1`信号给子进程。子进程在一个无限循环中检查锁的状态,如果锁被设置(`lock`为`true`),则等待。一旦收到信号,子进程知道锁已经被释放,可以继续执行。

请注意,这个示例仅用于演示目的,实际使用中应避免在信号处理函数中执行复杂的逻辑或访问非线程安全的全局变量。在多线程环境中,应使用线程安全的同步机制,如互斥锁或条件变量,来实现同步和互斥。此外,信号的发送和接收可能会有竞态条件,因此在实际应用中需要仔细设计以确保正确性和安全性。


4、非阻塞I/O

信号驱动I/O(也称为异步I/O)是一种高效的非阻塞I/O操作方式。它允许程序在等待I/O操作完成时继续执行,当I/O操作准备就绪时,操作系统会发送一个信号通知程序。


以下是一个使用信号驱动I/O的简单示例,演示如何在网络编程中实现非阻塞I/O操作:

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/ioctl.h>

volatile int socket_ready = 0;

void sigio_handler(int signum) {
    if (signum == SIGIO) {
        socket_ready = 1; // 标记socket操作准备就绪
    }
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket");
        return 1;
    }

    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);

    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        if (errno == EINPROGRESS) {
            std::cout << "Connection in progress, waiting for socket to become ready..." << std::endl;
        } else {
            perror("connect");
            close(sockfd);
            return 1;
        }
    }

    // 设置信号驱动I/O
    fcntl(sockfd, F_SETOWN, getpid());
    fcntl(sockfd, F_SETFL, O_ASYNC);

    // 设置SIGIO信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigio_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGIO, &sa, NULL);

    while (!socket_ready) {
        std::cout << "Waiting for socket to become ready..." << std::endl;
        sleep(1); // 在等待期间休眠,避免占用CPU资源
    }

    std::cout << "Socket is ready!" << std::endl;

    // 执行I/O操作...

    close(sockfd);
    return 0;
}

在这个示例中,我们首先创建了一个socket,并尝试连接到指定的服务器地址。如果连接操作立即完成,则表示连接成功;如果连接操作返回EINPROGRESS错误,则表示连接正在进行中,是非阻塞的。

接下来,我们使用fcntl函数设置socket以支持信号驱动I/O。我们通过F_SETOWN设置当前进程为接收SIGIO信号的所有者,并通过F_SETFL设置O_ASYNC标志,以使socket操作变为异步。

然后,我们设置SIGIO信号的处理函数sigio_handler。当socket操作准备就绪时(例如,连接建立),操作系统会发送SIGIO信号给进程,调用sigio_handler函数,并将socket_ready标志设置为1。

最后,我们在主循环中等待socket_ready标志变为1,表示socket操作已经准备就绪。一旦收到信号,程序就可以执行I/O操作。

请注意,信号驱动I/O是一种高级特性,需要对系统调用和信号处理有深入的理解。此外,不同的操作系统和编译器可能有不同的实现和限制。上述代码仅供学习和参考,实际应用时需要根据具体情况进行调整。


四、信号处理的高级API:sigaction


虽然signal函数也可以注册信号处理程序,但由于其移植性和功能局限性,在实际开发中通常使用sigaction函数:

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

sigaction结构体中除了包含信号处理函数指针外,还有sa_mask和sa_flags字段,用于设置更多选项:

  • sa_mask: 一个信号集,在执行该信号处理程序期间将自动阻塞该信号集中的信号
  • sa_flags: 控制信号处理过程的标志位,比如SA_RESTART可以自动重启被信号中断的系统调用

示例:

#include <signal.h>
#include <iostream>

void handler(int sig) {
    std::cout << "Caught signal " << sig << std::endl;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    
    // 在处理SIGINT时阻塞SIGQUIT信号
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT); 
    
    sa.sa_flags = SA_RESTART;
    
    sigaction(SIGINT, &sa, NULL);
    
    // 进程将一直运行直到收到SIGQUIT信号
    while (true) {
        std::cout << "Program running..." << std::endl;
        sleep(1);  
    }
    
    return 0;
}

通过掌握信号集和信号掩码的使用技巧,相信你已经对Linux信号处理机制有了更深入的理解。但这仅仅是信号强大功能的一个缩影,在网络编程、多线程同步等更高级的应用场景中,信号还有更多精彩的应用等待你去发掘!你有兴趣了解更多吗?欢迎在评论区留言交流探讨。


标签:set,阻塞,进程,信号,linux,掩码,include
From: https://blog.csdn.net/lizhong2008/article/details/139482613

相关文章

  • 【Linux】(六)—— vim编辑器
    vim文件编辑器Vim(ViImproved)是一个高度可配置的文本编辑器,最初基于UNIX下的Vi编辑器发展而来,广泛用于程序开发和系统管理中。vim编辑器可以只通过终端命令即可编写修改文件,不需要和gedit一样需要打开类似于记事本的窗口。Vim以其高效、灵活和强大著称,但对初学者来说可能有......
  • Linux基础 (十四):socket网络编程
         我们用户是处在应用层的,根据不同的场景和业务需求,传输层就要为我们应用层提供不同的传输协议,常见的就是TCP协议和UDP协议,二者各自有不同的特点,网络中的数据的传输其实就是两个进程间的通信,两个进程在通信时,传输层使用TCP协议将一方进程的应用层的数据传输给另一......
  • Linux基础 (十三):计算机网络基础概论
    一、网络基本概念1.1网络    把独立自主的计算机通过传输介质和网络设备链接起来,就构成一个网络,网络是由若干结点和连接这些结点的链路组成,网络中的结点可以是计算机,交换机、路由器等设备。网络设备有:交换机、路由器、集线器传输介质有:双绞线、同轴电缆、光纤......
  • Linux容器架构
    1.Iaas:基础设施即服务Infrastructure-as-a-ServicePaas:平台即服务Platform-as-a-ServiceSaas:软件即服务Software-as-a-ServiceCaas:容器即服务介于IAAS和PAASIAAS,PAAS,SAAS这些服务,用于帮助人们更快实现目标(搭建环境,使用产品)从左到右,人们需要管理与维护的地方......
  • 嵌入式 Linux LED 驱动开发实验学习
    I.MX6U-ALPHA开发板上的LED连接到I.MX6ULL的GPIO1_IO03这个引脚上,进行这个驱动开发实验之前,需要了解下地址映射。地址映射MMU全称叫做MemoryManageUnit,也就是内存管理单元。在老版本的Linux中要求处理器必须有MMU,但是现在Linux内核已经支持无MMU的处理器了。M......
  • 【linux-IMX6ULL-pinctrl和gpio子系统】
    目录1.pinctrl子系统1.1pinctrl子系统简介1.2pinctrl子系统使用1.2.1追加pin节点1.2.2配置引脚信息节点2.GPIO子系统2.1GPIO子系统简介2.1gpio子系统API函数3.检查PIN是否被其他外设使用1.pinctrl子系统1.1pinctrl子系统简介  pinctrl和gpio子系......
  • 【调试笔记-20240601-Linux-在 OpenWRT-23.05 上配置 frpc 实现内网穿透】
    调试笔记-系列文章目录调试笔记-20240601-Linux-在OpenWRT-23.05上配置frpc实现内网穿透文章目录调试笔记-系列文章目录调试笔记-20240601-Linux-在OpenWRT-23.05上配置frpc实现内网穿透前言一、调试环境操作系统:OpenWrt23.05.3调试环境调试目标二、调试步......
  • 进程间的通信(信号通信)
    进程间的通信(信号通信)进程的信号通信是操作系统中进程间通信(IPC)的一种方式,它允许一个进程向另一个进程发送一个信号,从而改变另一个进程的状态或执行某个操作。信号是异步的,意味着信号的发送和接收并不依赖于接收进程的执行状态。信号通信的基本概念信号类型:操作系统定义了一系......
  • linux 测试磁盘读写压力
    目录linux测试磁盘读写压力确定唯一性验证写入速度验证读取速度linux测试磁盘读写压力新到两台服务器,上面均没有运行服务,服务器品牌不一样,现在我想测试一下两台服务器的磁盘读写速度来比较哪个服务器好。确定唯一性比如我sdb是数据盘,已经格式化为sdb1,挂载到了/data目录下......
  • 在Linux中,如何进行网络性能的模拟测试?
    在Linux中进行网络性能的模拟测试,主要是为了评估网络子系统的效率、稳定性和容错能力。这包括测试网络带宽、延迟、抖动、丢包率等关键指标。以下是一些常用工具和方法来实现网络性能的模拟测试:1.使用iperf3测量带宽和延迟安装:首先,确保iperf3已安装。如果未安装,可以通过......