信号的基础知识

这篇博客详细介绍了Linux系统中的信号机制,包括raise()、alarm()函数的使用,信号处理方式,如signal和sigaction系统调用,以及与网络编程相关的信号如SIGHUP、SIGPIPE和SIGURG。讨论了如何中断系统调用,设置进程信号掩码,并分析了信号函数的处理函数和信号集函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

发送信号

信号处理方式 

Linux信号

中断系统调用

信号函数 

signal系统调用 

sigaction系统调用 

信号集 

信号集函数 

 进程信号掩码

被挂起的信号 

网络编程相关信号 

SIGHUP 

SIGPIPE 

SIGURG 


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

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入Ctrl+C通常会给进程发送一个中断信号。
  • 系统异常。比如浮点异常和非法内存段访问。
  • 系统状态变化。比如alarm定时器到期将引起SIGALRM信号。
  • 运行kill命令或调用kill函数,“kill -l”可以列出系统中的所有信号。“kill -SIGTERM 进程号”来发送信号给特定的进程

服务器程序必须处理(或至少忽略)。一些常见的信号,以免异常终止。

发送信号

#include<sys/types.h>

#include<signal.h>

int kill(pid_t pid , int sig);

该函数把信号sig发送给目标进程;目标进程由pid参数指定,其可能的取值及含义如表所示。

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

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

该函数成功时返回0,失败则返回-1并设置errno。几种可能的errno如表所示:

errno含义
EINVAL无效的信号
EPERM该进程没有权限发送信号给任何一个目标进程
ESRCH目标进程或进程组不存在

raise()函数

raise()函数主要用于将信号发送给当前进程:

#include<signal.h>

int raise(int sig);

sig为发送信号类型的编号

alarm()函数

alarm()函数主要用于为发送的信号设定一个时间警告,使系统在设定的时间之后发送信号:

#include<unistd.h>

unsigned int alarm(unsigned int seconds); 

参数seconds为设定的时间值。如果seconds设置为0值,那么alarm()函数设置的警告时钟将无效。alarm()函数安排在second

时间之后,发送一个信号SIGALRM给进程。默认情况下,进程收到SIGALRM信号会终止运行。

信号处理方式 

目标进程在收到信号时,需要定义一个接收函数来处理之。信号处理函数的原型如下:

#include<signal.h>

typedef void (*__sighandler_t) (int);

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

除了用户自定义信号处理函数外,bits/signum.h头文件中还定义了信号的两种其他处理方式——SIG_IGN和SIG_DEL:

#include<bits/signum.h>

#define SIG_DFL((__sighandler_t) 0)

#define SIG_IGN((__sighandler_t) 1)

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

Linux信号

信号起源默认行为含义
SIGHUPPOSIXTerm控制终端挂起
SIGINTANSITerm

键盘输入以中断进程(Ctrl+C)

SIGQUITPOSIXCore键盘输入使进程退出(Ctrl+\)
SIGILLANSICore非法指令
SIGTRAPPOSIXCore断点陷阱,用于调试
SIGABRTANSICore进程调用abort函数时生成该信号
SIGIOT4.2BSDCore和SIGABRT相同
SIGBUS4.2BSDCore总线错误,错误内存访问
SIGFPEANSICore浮点异常
SIGKILLPOSIXTerm终止一个进程。该信号不可被捕获或者忽略
SIGUSR1POSIXTerm用户自定义信号之一
SIGSEGVANSICore非法内存段引用
SIGUSR2POSIXTerm

用户自定义信号之二

SIGPIPEPOSIXTerm往读端被关闭的管道或者socket连接中写数据
SIGALRMPOSIXTerm由alarm或setitimer设置的实时闹钟超时引起
SIGTERMPOSIXTerm终止进程。kill命令默认发送的信号就是SIGTERM
SIGSTKFLTLinuxTerm早期的Linux使用该信号来报告数学协处理器栈错误
SIGCLDSystem VIgn和SIGCHLD相同
SIGCHLDPOSIXIgn子进程状态发生变化(退出或者暂停)
SIGCONTPOSIXCont启动被暂停的进程(Ctrl+Q)。如果目标进程未处于暂停状态,则信号被忽略
SIGSTOPPOSIXStop暂停进程(Ctrl+S)。该信号不可被捕获或者忽略。
SIGSTPPOSICStop挂起进程(Ctrl+Z)
SIGTTINPOSIXStop后台进程试图从终端读取输入
SIGTTOUPOSIXStop后台进程试图往终端输出内容
SIGURG4.2BSDIgnsocket连接上接收到紧急数据
SIGXCPU4.2BSDCore进程CPU使用时间超过其软限制
SIGXFSZ4.2BSDCore文件尺寸超过其软限制
SIGVTALRM4.2BSDTerm

与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间

SIGPROF4.2BSDTerm与SIGALRM类似,它同时统计用户代码和内核的运行时间
SIGWINCH4.3BSDIgn终端窗口大小发生变化
SIGPOLLSystem VTerm与SIGIO类似
SIGIO4.2BSDTermIO就绪,比如socket上发生可读、可写事件。因为TCP服务器可触发SIGIO的条件很多,故而SIGIO无法在TCP服务器中使用。SIGIO信号可用在UDP服务器中,不过也非常少见
SIGPWRSystem VTerm对于使用UPS(Uninterruptable Power Supply)的系统,当电池电量过低时,SIGPWR信号将被触发
SIGSYSPOSIXCore非法系统调用
SIGUNUSEDCore保留,通常和SIGSYS效果相同

我们并不需要在代码中处理所有这些信号。本章后面将重点介绍与网络编程关系紧密的几个信号:SIGHUP、SIGPIPE和SIGURG。

中断系统调用

如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。我们可以使用sigaction函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。

对于默认行为是暂停进程的信号(比如SIGSTOP、SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如connect、epoll_wait)。POSIX没有规定这种行为,这是Linux独有的。

信号函数 

signal系统调用 

要为一个信号设置处理函数,可以使用下面的signal系统调用:

#include<signal.h>

_sighandler_t   signal(int sig , _sighandler_t _handler)

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

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

signal系统调用出错时返回SIG_ERR,并设置errno。 

sigaction系统调用 

设置信号处理函数的更健壮的接口是如下的系统调用:

#include<signal.h>

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

sig参数指出要捕获的信号类型,act参数指定新的信号处理方式,oact参数则输出信号先前的处理方式(如果不为NULL的话)。act和oact都是sigaction结构体类型的指针,sigaction结构体描述了信号处理的细节,其定义如下:

struct sigaction
{
#ifdef __USE_POSIX199309
    union
{
    _sighandler_t sa_handler;
    void (*sa_sigaction)(int,siginfo_t*,void*);
}
    _sigaction_handler;
#define sa_handler __sigaction_handler.sa_handler
#define sa_sigaction __sigaction_handler.sa_sigaction
#else
    _sighandler_t sa_handler;
#endif
    _sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

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

选项含义
SA_NOCLDSTOP

如果sigaction的sig参数是SIGCHLD,则设置该标志标志表示子进程暂停时不生成SIGCHLD信号。

SA_NOCLDWAIT如果sigaction的sig参数是SIGCHLD,则设置该标志表示子进程结束时不产生僵尸进程。
SA_SIGINFO使用sa_sigaction作为信号处理函数(而不是默认的sa_handler)它给进程提供更多相关的信息
SA_ONSTACK调用sigaltstack函数设置的可选信号栈上的信号处理函数
SA_RESTART重新调用被该信号终止的系统调用
SA_NODEFER当接收到信号并进入其信号处理函数时,不屏蔽该信号。默认情况下,我们期望进程在处理一个信号时不再接收到同种吸纳后,否则将引起一些竞态条件
SA_RESETHAND信号处理函数执行完以后,恢复信号的默认处理方式
SA_INTERRUPT中断系统调用
SA_NOMASK同SA_NODEFER
SA_ONESHOT同SA_RESETHAND
SA_STACK同SA_ONSTACK

sa_restore成员已经过时,最好不要使用。sigaction成功时返回0,失败则返回-1并设置errno。

信号集 

信号集函数 

Linux使用数据结构sigset_t来表示一组信号,其定义如下: 

#include<bits/signal.h>
#define _SIGSET_NWORDS (1024/(8*sizeof(unsigned long int)))
typedef struct
{
    unsigned long int __val[_SIGSET_NWORDS];
}__sigset_t;

 由该定义可见,sigset_t实际上是一个长整型数组,每个数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符集fd_set类似。Linux提供了如下一组函数来设置、修改、删除和查询信号集。

#include<signal.h>

int sigemptyset(sigset_t* _set)                                /*清空信号集*/

int sigfillset(sigset_t* _set)                                       /*在信号集中设置所有信号*/

int sigaddset(sigset_t* _set , int _signo)                  /*将信号_signo添加至信号集中*/

int sigdelset(sigset_t* _set , int _signo)                   /*将信号_signo从信号集中删除*/

int sigismember(_const sigset_t* _set , int _signo) /*测试_signo是否在信号集中*/

 进程信号掩码

前文提到,我们可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码。此外,如下函数也可以用于设置或查看进程的信号掩码:

#include<signal.h>

int sigprocmask(int how , _const sigset_t* _set , sigset_t* _oset);

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

_how参数含义
SIG_BLOCK新的进程信号掩码是当前值和_set指定信号集的并集
SIG_UNBLOCK新的进程信号掩码是其当前值和~_set信号集的交集,因此_set指定的信号集将不被屏蔽
SIG_SETMASK直接将进程信号掩码设置为_set

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

sigprocmask成功时返回0,失败则返回-1并设置errno 

被挂起的信号 

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

#include<signal.h>

int sigpending(sigset_t* set);

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

sigpending成功时返回0,失败时返回-1,并设置errno。 

sigsuspend()函数

sigsuspend()函数主要是等待一个信号的到来,即将当前进程挂起:

#include<signal.h>

int sigsuspend(const sigset_t* mask);

参数mask是一个sigset_t结构体类型的指针,指向一个信号集。当函数sigsuspend()被调用时,参数mask所指向的信号集中的信号被复制给信号掩码。随后,进程会被挂起,,直到信号被捕捉到,执行信号相应的处理方法返回时,该函数才会返回。此时,信号掩码恢复为函数调用前的值 

网络编程相关信号 

SIGHUP 

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

SIGPIPE 

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

SIGURG 

在Linux环境下,内核通知应用程序带外数据到达主要有两种方法,一种是I/O复用技术,select等系统调用在接收到带外数据时将返回,并向应用程序报告socket上的异常事件。另一种方法就是使用SIGURG信号。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值