• 一文搞定Linux信号


    Linux信号概述

    信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。

    Linux信号可由如下条件产生:

    对于前台进程,用户可以通过输人特殊的终端字符来给它发送信号。比如输入Ctrl+C 通常会给进系统异常。

    比如浮点异常和非法内存段访问。

    系统状态变化。比如 alarm定时器到期将引起SIGALRM信号。

    运行kill命令或调用kill函数。

    发送信号

    1. #include
    2. #include
    3. int kill(pid_t pid,int sig);

    kill函数的pid参数以及意义

    pid参数含义
    pid>0信号发送给PID为pid的进程
    pid=0信号发送给本进程组内的所有进程
    pid=-1信号发送给楚init进程外所有进程,但发送者需要拥有对目标进程发送信号的权限
    pid<-1信号发送给组ID为-pid的进程组中所有成员

    Linux定义的信号值都大于0,如果sig取值为0,则kill函数不发送任何信号。但将sig设置为О可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。不过这种检测方式是不可靠的。一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID:

    另一方面,这种检测方法不是原子操作

    函数成功返回0,失败则返回errno,几种可能如下表格

    kill出错情况
    error含义
    EINVAL无效的信号
    EPERM该进程没有权限发送信号给任何一个目标进程

    ESRCH

    目标进程或进程组不存在

    信号处理方式

    接受信号就要处理信号

    1. #include
    2. typedef void (*_sighandler_t)(int);
    3. #include
    4. #define SIG_DFL ((_sighandler_t) 0);
    5. #define SIG_ING ((_sighandler_t) 1);

    信号处理函数只带有一个整型参数,该参数用来指示信号类型。信号处理函数应该是可重入的,否则很容易引发一些竞态条件。所以在信号处理函数中严禁调用一些不安全的函数。

    SIG_IGN表示忽略目标信号,SIG_DFL表示使用信号的默认处理方式。信号的默认处理方式有如下几种:结束进程(Term)、忽略信号(Ign)、结束进程并生成核心转储文件( Core)、暂停进程( Stop),以及继续进程(Cont)。

    中断系统的调用

    如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。我们可以使用sigaction函数(见后文)为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
    对于默认行为是暂停进程的信号(比如 SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如 connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。

    信号函数

    signal的系统调用

    #include

    _sighandler_t signal(int sig,_sighandler_t  _handler);

    sig 参数指出要捕获的信号类型。_handler参数是_sighandler_t类型的函数指针,用于指定信号sig 的处理函数。

    signal函数成功时返回一个函数指针,该函数指针的类型也是_sighandler_t。这个返回值是前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用signal 的话)。

    sigaction

    1. #include
    2. int sigaction(int sig,const struct sigaction* act,struct sigaction* oact);
    3. struct sigaction {
    4. void (*sa_handler)(int);
    5. void (*sa_sigaction)(int, siginfo_t *, void *);
    6. sigset_t sa_mask;
    7. int sa_flags;
    8. void (*sa_restorer)(void);
    9. };

    该结构体中的sa_hander成员指定信号处理函数。sa_mask成员设置进程的信号掩码(确切地说是在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程。sa_mask 是信号集sigset_t (_sigset_t的同义词)类型,该类型指定一组信号。关于信号集,我们将在后面介绍。sa_flags 成员用于设置程序收到信号时的行为,其可选值如表下图所示。

    sa_flags
    选项含义
    SA_NOCLDSTOP设置成表示该子进程暂停时不生成SIGCHLD
    SA_NOCLDWAIT子进程结束时不产生僵尸进程
    SA_SIGINFO使用sa_sigaction作为信号处理函数(而不是默认sa_hanler),提供更多的信息
    SA_ONSTACK调用由sigaltstack函数设置的可选信号栈上的信号函数
    SA_RESTART重新调用被信号终止的系统调用
    SA_NODEFER当接收到信号并进入其信号处理函数,不屏蔽信号,默认情况下,我们期望在进程处理一个信号时不在接受到同种信号
    SA_RESETHAND信号处理函数执行完,恢复信号的默认处理方式
    SA_INTERRUPT中断系统调用
    SA_NOMSAK同SA_NODEFER
    SA_ONESHOT同SA_RESETHAND
    SA_STACK同SA_ONSTACK

     

    sa_restorer成员已经过时啦

    信号集

    信号集函数

    信号集原型:

    1. #include
    2. int sigemptyset(sigset_t *set);
    3. int sigfillset(sigset_t *set);
    4. int sigaddset(sigset_t *set, int signum);
    5. int sigdelset(sigset_t *set, int signum);
    6. int sigismember(const sigset_t *set, int signum);

    进程信号掩码

    1. #include
    2. int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

    _set参数指定新的信号掩码,_oset参数则输出原来的信号掩码(如果不为NULL的话)。如果_set参数不为NULL,则_how参数指定设置进程信号掩码的方式,

    hou参数
    _how参数含义
    SIG_BLOCK新的进程信号就是当前值和_set指定信号的并集
    SIG_UNBLOCK新的进程信号就是当前值和_set指定信号的交集
    SIG_SETMAsk直接将进程信号掩码设置成_set

     

    注意:如果_set为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获得进程当前的信号集

    被挂起的信号

    设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对被挂起信号的屏蔽,则它能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集:

    1. #include
    2. int sigpending(sigset_t* set);

    set参数用于保存被挂起的信号集。显然,进程即使多次接收到同一个被挂起的信号,sigpending 函数也只能反映一次。并且,当我们再次使用sigprocmask使能该挂起的信号时,该信号的处理函数也只被触发一次。

    关于信号和信号集,Linux还提供了很多有用的API,这里就不一一介绍了。需要提醒读者的是,要始终清楚地知道进程在每个运行时刻的信号掩码,以及如何适当地处理捕获到的信号。在多进程、多线程环境中,我们要以进程、线程为单位来处理信号和信号掩码。我们不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征。比如,fork调用产生的子进程将继承父进程的信号掩码,但具有一个空的挂起信号集

    同一事件源

    信号是一种异步事件:信号处理函数和程序的主循环是两条不同的执行路线。很显然,信号处理函数需要尽可能快地执行完毕,以确保该信号不被屏蔽(前面提到过,为了避免一些竞态条件,信号在处理期间,系统不会再次触发它〉太久。

    一种典型的解决方案是;把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到的信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道来将信号“传递”给主循环﹔信号处理函数往管道的写端写入信号值,主循环则从管道的读端读出该信号值。那么主循环怎么知道管道上何时有数据可读呢﹖这很简单,我们只需要使用IO复用系统调用来监听管道的读端文件描述符上的可读事件。如此一来,信号事件就能和其他I/O事件一样被处理,即统一事件源

    网络编程相关的信号

    SIGHUP

    控制终端被挂起

    当挂起进程的控制终端时,SIGHUP信号将被触发。对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。一个典型的例子是xinetd超级服务程序。

    SIGPIPE

    往读端被关闭的管道写入数据或socket连接写数据

    默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。引起SIGPIPE信号的写操作将设置errno为EPIPE。

    这个博客,我们可以使用send函数的MSG_NOSIGNAL标志来禁止写操作触发

    SIGURG

    检测带外数据到达

    附录:LINUX信号大全

  • 相关阅读:
    linux驱动-CCF-2 of_clk_provider
    开发一个自己的前端脚手架
    SQL教程之递归 CTE Common Table Expression
    Android自定义控件
    特斯拉“断网”,数百车主“被锁车外”,马斯克道歉
    【全开源】JAVA打车小程序APP打车顺风车滴滴车跑腿源码微信小程序打车源码
    一个敏感词检查功能是怎么来的
    HDFS系统操作命令大全
    vue学习(基础1)
    target is not existed: .page-component__scroll .el-scrollbar__wrap
  • 原文地址:https://blog.csdn.net/qq_62309585/article/details/126632819