6. 信号

目录

6.1 信号概述

6.2 信号的处理

6.2.1 signal函数

6.2.2 信号的发生

6.2.3 Alarm函数和pause函数

6.3 信号集

6.3.1 信号集设置

6.3.2 函数sigprocmask

6.3.3 函数sigpending

6.3.4 函数sigaction

6.3.5 sigsetjump & siglongjump

6.3.6 sigsuspend

6.4 其它信号函数

6.4.1 abort函数

6.4.2 system函数

6.4.3 sigqueue函数

6.5 信号名和编号


信号是软件中断,它提供了一种处理异步时间的典型方法。本章将简单介绍下信号的概念,使用的方法,以及信号集的相关概率和使用方法,并穿插着介绍了一些常用的信号。

6.1 信号概述

在POSIX.1标准中,信号被定义成以SIG开头的字符串,同时在signal.h中都用正整数常量来表示。编号为0的信号被称为空信号,kill函数可以使用空信号来判断当前信号是否存在。

通常意义上,以下的条件可以产生一个信号:

  • 用户按某些终端按键时,终端会发出信号,比如对应“Ctrl+C”的通常是中断信号(SIGINT);
  • 硬件异常产生信号,例如当引用无效的内存时,硬件检测到异常并通知内核,内核会发出对应的SIGSEGV信号;
  • 进程调用kill函数发送信号,但发送信号的进程所有者必须是超级用户或者与接收信号的所有者相同;
  • 用户通过kill命令发送信号,实际上kill命令会调用kill函数;
  • 检测到软件条件发生也会发送信号,列如SIGUSR,SIGPIPE,SIGALARM等等。

当进程接收到信号,可以告诉内核以下面三种方式处理:

  • 执行默认操作,对大部分的信号内核默认动作是终止该信号;
  • 忽略该信号,大部分的信号都可以用这种方式处理,但是SIGKILL和SIGSTOP除外,它们向内核提供了使进程终止或停止的方法;
  • 捕捉信号,用户需要编写用户函数,并通知内存在某种信号发生时调用,SIGKILL和SIGSTOP这两种信号也不能被捕捉。

6.2 信号的处理

6.2.1 signal函数

Signal函数用来指定对信号的处理方式:

#inlcude<signal.h>
void (*signal(nt signo,  void (*func)(int)) ) (int);

看起来这个定义很复杂,我们来简化下:

typedef  void  Sigfunc(int);
Sigfunc *signal(int , Sigfunc *);

参数: signo就是信号名,func 指定对该信号的处理方式,可以由以下三种:

  • SIG_IGN: 忽略该信号,定义为:#define SIG_IGN  (void(*)()) 1
  • SIG_DFL: 忽略该信号,定义为:#define SIG_IGN  (void(*)()) 0
  • 信号捕捉函数:用户自定义函数

返回值:出错返回SIG_ERR,定义为:#define SIG_ERR  (void(*)()) -1;成功则返回没有调用signal之前该信号的处理行为。

关于信号的处理方式还有两点需要说明:

  • 对所有信号没有设置前,缺省的处理方式通常是执行系统默认动作;
  • 在进程fork之后,子进程会继承父进程的信号处理方式,因为子进程创建后复制了父进程的内存映像;
  • 在调用exec之后,会将原先设置为要捕捉的信号更改为默认动作,而其他信号的状态则不变。这也很好理解,执行exec之后信号捕捉函数的地址很可能已经都发生了变化。

6.2.2 信号的发生

POSIX.1标准中定义了两种简单的发生信号的接口:kill和raise。其中raise试想进程自身发送信号,而kill可以将信号发送给进程或这进程组。

#inlcude<signal.h>
int kill(pid_t pid, int signo);
Int raise(int signo);

Kill函数的 pid有以下四种情况:

  • pid > 0:  将信号发送给进程pid;
  • pid == 0:将进程发送给调用进程同一进程组的所有进程;
  • pid < -1:  将进程发送给进程组等于-pid的所有进程;
  • pid == -1:将进程发送给所有进程。

关于kill函数也有两点需要说明:

  • 进程将信号发送者其它进程,接收进程一定要有权限接收;要求发送进程拥有超级用户权限或者收发进程的实际用户ID/有效用户ID一致;
  • Kill的 signo参数如果是空信号,kill仍会执行正常的错误检查,但是并不会发送信号,这场被用户确认特定的进程是否仍然存在。如果向一个不存在的进程发送空信号,kill返回-1,errno被设置为ESRCH。

6.2.3 Alarm函数和pause函数

Pause函数使调用进程阻塞,直到捕捉到一个信号,只有执行了一个信号处理程序并返回,pause才返回-1,并将errno设置为EINTR。注意:如果接收到忽略的信号是无法让pause返回的。

#inlcude<signal.h>
int pause(void);
int alarm(unsigned int seconds);

Alarm函数设置一个定时器,定时器到时候会向当前进程发送一个SIGALARM信号,对于该信号默认动作是终止该进程,但是我们也可以自己设置用户调用函数。

Alarm函数正常返回0,如果在调用时有之前已经注册过但尚未到时的alarm,则alarm函数返回上次alarm 的残余值;如果调用alarm时参数为0,则取消之前的alarm函数,并且返回上次alarm的残余值。

6.3 信号集

信号在linux内核中有两种状态:

  • Delivery:执行默认动作,信号捕捉函数或者忽略时;
  • Pending:信号在发生到 执行默认动作之前。

同时linux 还定义了一种对信号的操作,阻塞信号。信号的pending状态和阻塞操作在PCB 都有一个信号集来表示。信号集的定义为sigset_t,其中的每个bit表示每个信号的状态,1表示pending 或者block,0 表示没有pending或者不会block,这样每个PCB中都会有这样的两个信号集:pending信号集和block信号集。

一旦信号集中信号的block状态被解除,其对应pending信号集中的pending 状态也会被清掉,其状态会变为delivery,在pending期间我们仍然可以更改对此信号的处理行为。

我们再来考虑以下的情况:同一个信号在pending状态被递送多次,会怎么样? Linux中处理方式通常就是只传送这种信号一次。

如果有多个信号要传送给同一个进程会怎么样?POSIX.1中并没有规定传送顺序,但是通常与进程状态有关的信号会优先被传送。后面我们还会详细的介绍对block,pending 的处理。我们还是先来看下信号集的一些处理函数。

6.3.1 信号集设置

#inlcude<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);
//执行成功返回0,失败返回-1

int sigismember(const sigset_t *set, int signo);
//返回对应信号量的状态,0/1

在使用一个信号集前需要对其初始化,上面的sigemptyset和sigfillset都可以完成初始化。Sigfillset会将set中所有bit置1,而sigempty会将set中所有bit置0。

Sigaddset会将signo对应bit置1,sigdelset会将signo对应bit置0。

6.3.2 函数sigprocmask

前面介绍过我们可以阻塞一个信号,我们可以sigprocmask函数实现这个功能。sigprocmask也可以检测和更改进程的信号屏蔽字。

#inlcude<signal.h>
int sigprocmask(int how,  const sigset_t *set,  const sigset_t *oset);
//返回对应信号量的状态,0/1,失败时返回-1,并设置errno
  • 如果oset非空,那么进程的当前信号屏蔽字通过oset返回;
  • 如果set非空,那么可以根据how参数去修改当前信号屏蔽字,说明如下表:

How

Detail

SIG_BLOCK

该进程新的信号屏蔽字是当前信号屏蔽字与set信号集的并集

SIG_UNBLOCK

该进程新的信号屏蔽字是当前信号屏蔽字与set信号集补集的交集

SIG_SETMASK

该进程新的信号屏蔽字设置成set信号集

需要注意的是sigprocmask针对的是单线程编程,每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这样,如果有线程更改了对某个信号的处理方式,其它线程都需要去共享这种新的行为。

对于硬件异常相关的信号,该信号一般会被发送到引起该事件的线程,但是其它的信号可能会被送到任一线程。

鉴于sigprocmask在多线程进程中行为不定,pthread库定义了线程中使用的同类函数pthread_sigmask。此函数参数同sigprocmask,但返回值在执行失败时直接返回信号编号。

#inlcude<pthread.h>
int pthread_sigmask (int how,  const sigset_t *set,  const sigset_t *oset);
//成功返回0,失败返回错误编号

6.3.3 函数sigpending

Sigpending函数用来获取被设置为SIG_BLOCK的信号集。

#inlcude<signal.h>
int sigpending (sigset_t *set);
//成功返回0,失败返回错误编号

来看下 sigprocmask和sigpending 的用法:

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

static void sig_quit2(int signo)
{
	printf("caught SIGOUT2\n");
	if(signal(SIGQUIT,SIG_DFL) == SIG_ERR)
	{
		printf("can't reset SIGQUIT\n");
	}
}

static void sig_quit(int signo)
{
	printf("caught SIGOUT\n");
	if(signal(SIGQUIT,sig_quit2) == SIG_ERR)
	{
		printf("can't reset SIGQUIT\n");
	}
}

int main()
{
	sigset_t newmask, oldmask, pendingmask;

	if(signal(SIGQUIT,sig_quit) == SIG_ERR)
	{
		printf("can't signal SIGQUIT\n");
	}

	sigemptyset(&newmask);
	sigaddset(&newmask,SIGQUIT);

	if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) == -1)
	{
		printf("sigproc new mask error\n");
	}

	sleep(5);

	if(sigpending(&pendingmask) == -1)
	{
		printf("sigpending error\n");
	}
	
	if(sigismember(&pendingmask,SIGQUIT))
	{
		printf("SIG_QUIT block\n");
	}

	if(sigprocmask(SIG_SETMASK,&oldmask,NULL) == -1)
	{
		printf("sigproc old mask error\n");
	}
	printf("sigquit unblocked\n");
	sleep(5);

	getchar();
}

示例中我们将SIG_QUIT的动作设为执行sig_quit, 并设置SIG_QUIT信号阻塞,所以在第一个sleep 函数期间,一直没有对quit信号处理,等unblock这个信号后,立即执行第一个quit信号的信号处理程序;在第一个信号处理程序中将SIG_QUIT的 动作改成执行sig_quit2,此时没有对SIG_QUIT设置阻塞,一旦收到这个信号,立即执行sig_quit2;在sig_quit2 中将SIG_QUIT信号改为默认操作,再次收到这个信号后立即退出进程。

在shell命令行中,我们可以通过“ctrl + \”产生SIG_QUIT信号。

6.3.4 函数sigaction

Sigaction函数是signal函数的升级版,可以用来检测或者修改制定信号的处理动作。先来看下其定义:

#inlcude<signal.h>
int sigaction(int signo,  const struct sigaction *act,  const struct sigaction *act);
//返回对应信号量的状态,0/1,失败时返回-1,并设置errno

参数signo就是执行的信号,act如果非空,则要修改信号的处理动作,oact如果非空,则用来保存该信号的上一个动作。我们再来看下struct sigaction结构 以及如何更改信号的处理动作。

struct sigaction{
         void (*sa_handler)(int);
         sigset_t sa_mask;
         int sa_flags;
         void  (*sa_sigaction)(int ,siginfo_t *, void);
};
  • sa_handler: 信号的处理动作,可以是SIG_DFL,SIG_IGN,用户定义个信号处理程序;
  • sa_mask 阻塞信号集,执行该信号时,系统会阻塞sa_mask中指定阻塞的其它信号;
  • sa_flags: 信号处理的选项,可以是下面表格中的值:

选项

说明

SA_INTERRUPT

由此信号中断的系统调用在中断信号处理完后并不会恢复执行,注意只是系统调用,而不是说程序不会恢复执行。以read函数为例:read 函数在被阻塞期间收到信号,等信号处理程序执行完后,会直接跳出read函数 。

SA_RESTART

与SA_INTERRUPT相反,会恢复系统调用。

SA_ NOCLDSTOP

只针对SIGCHLD信号。通常子进程终止或停止时都会产生SIGCHLD信号,但若调用进程设置了该标志,其子进程停止时不产生此信号,终止时才产生此信号。

SA_ NOCLDWAIT

若调用进程指定该标志,则其子进程终止后不产生僵尸进程;如果调用进程后面调用wait,则会一直阻塞到所有子进程终止。

SA_ONSTACK

如果之前有用sigtstack声明一个替换栈,则此信号传送给替换栈上的进程,很复杂,后面有机会再讲

SA_SIGINFO

指定了该信号,信号的捕捉函数就被下面介绍的sa_sgiaction函数锁取代

此外,还有SA_NODEFER,SA_RESETHADN用于早起的不可靠信号,基本已不使用,不再介绍。

sa_sgiaction: 可替代的信号处理程序,如果sa_flags 中指定了SA_SIGINFO标志,则用此替代函数作为信号处理程序。需要注意的是这两个信号处理程序通常使用同一个存储区,所以应用只能一次使用这两个字段中的一个。该信号处理程序可以表示成如下形式:

void handler(int signo, siginfo_t *info, void *context);

6.3.5 sigsetjump & siglongjump

前面有介绍实现函数局部内跳转的setjump和setlongjump函数,我们来看看这两个函数在信号处理程序中使用的情况。

设想下,进程捕捉到信号并进入信号处理程序,此时当前信号会被加入进程的信号屏蔽字,这阻止了后面产生的这种信号再次进入信号处理程序,此时如果调用setlongjump返回后,该信号还在进程的信号屏蔽字中吗? Linux系统中,答案是yes。这种情况就会造成该进程再也接收不到这个信号。别急,sigsetjump和siglongjump有办法。

#inlcude<signal.h>

int sigsetjmp(sigjmp_buf env, int savemask);
//直接调用返回0,从siglongjump返回,返回val

int siglongjump(sigjmp_buf env, int val);

Sigjmp_buf中增加了信号集的结构,当sigsetjmp的 参数savemask非0时,则调用sigsetjmp时会在env中保存当前进程信号屏蔽字,从siglongjump返回时,会将当前进程信号屏蔽字恢复成env中保存的信号集。

     我们可改装下APUE中的示例10-20:

 

#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include <setjmp.h>
#include <time.h>
#include<unistd.h>
#include<fcntl.h>

static void						sig_usr1(int);
static void						sig_usr2(int);

static void						sig_alrm(int);
static sigjmp_buf				jmpbuf;
static volatile sig_atomic_t	canjump;

int main(void)
{
	sigset_t mask;
	char buf[100];
	int fd[2];
	struct sigaction siga;
	pipe(fd);

	siga.sa_handler = sig_usr1;
	siga.sa_flags = SA_INTERRUPT;

	if(sigaction(SIGUSR1,&siga,NULL) == -1)
	//if(signal(SIGUSR1,sig_usr1) == SIG_ERR)
	{
		printf("signal(SIGUSR1) error\n");
	}

	if (signal(SIGALRM, sig_alrm) == SIG_ERR)
	{
		printf("signal(SIGALRM) error\n");
	}

	if (sigsetjmp(jmpbuf,1)) {
		sigemptyset(&mask);
		if(sigprocmask(SIG_BLOCK,NULL,&mask) < 0)
		{
			printf("sig pending fail\n");
		}
		if(sigismember(&mask,SIGUSR1))
		{
			printf("sig usr1 blcok\n");
		}

		printf("Return main\n");
	}

	canjump = 1;
	printf("process to here\n");

	read(fd[0],buf,100);
	printf("after pause\n");

	return;
}

static void sig_usr1(int signo)
{
	time_t	starttime;
	struct sigaction siga;

	if (canjump == 0)
		return;

	printf("Starting USR1:\n");

	alarm(2);
	starttime = time(NULL);
	while(1)
	{
		if (time(NULL) > starttime + 3)
			break;
	}

	siga.sa_handler = sig_usr2;
	siga.sa_flags = SA_INTERRUPT;

	if(sigaction(SIGUSR1,&siga,NULL) == -1)
	{
		printf("signal(SIGUSR2) error\n");
	}
	canjump = 0;

	printf("Finishing USR1 \n");
	siglongjmp(jmpbuf, 1);
}

static void sig_usr2(int signo)
{
	printf("Finishing USR2 \n");
}

static void sig_alrm(int signo)
{
	printf("Alarm timeout\n");
}

 程序执行的流程图如下:

我们来分步解析下程序的执行步骤:

  1. Main函数中通过SA_INTERRUPT标志使用sigaction函数设置了SIG_USR1的信号处理程序,设置SIG_ALARM的信号处理程序,设置了setjmp的点,随后执行到read pipe,因为pipe没有建立起来,所以block在此处;
  2. 进程收到SIG_USR1信号,随后收到SIG_ALARM信号从其信号处理程序返回,将SIG_USR1信号信号捕捉函数重设为sig_usr2,最后执行setlongjmp回到main函数中setjmp点,再次执行到read pipe阻塞;
  3. 收到第二次SIG_USR1信号,执行sig_usr2,此时 因为设置sig_usr2时标志为SA_INTERUPT, 所有会从read 函数跳出,随后退出进程。

执行结果为:

我们如果将程序中改用setjmp和 setlongjmp,会发现回来的时候 判断到此时SIG_USR1信号被block,这也符合我们之前的分析。

6.3.6 sigsuspend

我们先来看下下面这段代码:

sigset_t newmask,oldmask;
sigempty(&newmask);
sigaddset(&newmask,SIGINT);
sigprocmask(SIG_BLOCK,&newmask,&oldmask);

sigprocmask(SIG_SETMASK,&oldmask,NULL);

pause();

如果在上面的unblock 和 pause之间 我们收到了SIG_INT信号会怎么样?那样我们在执行pause后就再也没有办法收到信号,进程彻底被阻塞起来。

是不是很头疼?没关系,本节介绍的sig_suspend函数就提供一种将unblock & pause 作为原子操作的方法。

#inlcude<signal.h>
int sigsuspend(const sigset_t  *sigset);
//返回值-1.并设置errno

在执行sigsuspend之后,我们会用指定的信号集sigset去替换当前进程的信号屏蔽字,并阻塞起来,等收到信号后,恢复之前的进程信号屏蔽字,并返回-1。

Sigsuspend通常用于等待信号时希望进程去休眠,例如常用的在等待的信号其信号处理程序里对全局变量置1。例如我们可以用下面的实现的这组函数去实现两个进程间的同步:

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

static voliatile sig_atomic_t sig_falg;
static sigset_t newmask, oldmask, zeromask;

static void sig_usr(int signo)
{
	sigflag = 1;
}

void TELL_WAIT(void)
{
	if(signal(SIGUSR1,sig_usr) == SIG_ERR)
	{
		printf("signal SIGUSR1 error\n");
		exit(-1);
	}
	if(signal(SIGUSR2,sig_usr) == SIG_ERR)
	{
		printf("signal SIGUSR2 error\n");
		exit(-1);
	}

	sigemptyset(&zeromask);
	sigemptyset(&newmask);
	sigaddset(&newmask,SIG_USR1);
	sigaddset(&newmask,SIG_USR2);

	if(sigprocmask(SIG_BLOCK,&newmask,&oldmask) == -1)
	{
		printf("SIGUSR1 BLOCK error\n");
		exit(-1);
	}
}

void TELL_PARENT(void)
{
	kill(pid,SIGUSR2);
}

void WAIT_PARENT(VOID)
{
	while(sigflag == 0)
	{
		sigsuspend(&zeromask);
	}
	sigflag = 0;

	if(sigprocmask(SIG_SETMASK,&oldmask,NULL) == -1)
	{
		printf("SIGUSR1 BLOCK error\n");
		exit(-1);
	}
}

void TELL_CHILD(void)
{
	kill(pid,SIGUSR1);
}

void WAIT_CHILD(VOID)
{
	while(sigflag == 0)
	{
		sigsuspend(&zeromask);
	}
	sigflag = 0;

	if(sigprocmask(SIG_SETMASK,&oldmask,NULL) == -1)
	{
		printf("SIGUSR1 BLOCK error\n");
		exit(-1);
	}
}

父进程向子进程发送SIG_USR1信号,子进程向父进程发送SIG_USR2信号,实现父子进程同步。

6.4 其它信号函数

6.4.1 abort函数

Abort函数向调用的进程发送SIGABRT信号。进程捕捉到该信号后通常会执行一些清理操作,然后终止该进程。

#inlcude<stdlib.h>
void abort(void);

6.4.2 system函数

#inlcude<stdlib.h>
int system(const char *cmdstring);

system函数的执行分为以下三个步骤:

  • fork一个子进程;
  • 子进程中调用exec函数去执行”/bin/sh -c " cmdstring” ;
  • 父进程中调用wait去等待子进程结束。

针对以上三步,system函数由三种返回值:

  • fork失败或者waitpid返回除EINTR以外的错误,system返回-1,并设置错误编号;
  • exec失败,则返回值如果shell执行exit;
  • 如果都执行成功,system返回值是shell 的终止状态。

POSIX.1要求system函数忽略SIGINT和SIGQUIT信号,阻塞SIGCHLD信号。如果不阻塞SIGCHLD信号,若调用进程中有wait函数,则此wait函数可能就回收了对应的子进程,system函数中调用的wait函数无法再等到子进程。

6.4.3 sigqueue函数

POSIX.1中还定义了一种发送信号的函数sigqueue以支持信号的排队。

#inlcude<stdlib.h>
int sigqueue(pid_t pid, int signo, const union sigval value);
//执行成功返回0,失败返回-1

使用sigqueue函数有以下注意事项:

  • 使用Sa_sigaction替代sa_handler来作为信号处理程序;
  • 使用sigaction函数安装信号处理程序,且需要制定SA_SIGINFO标志其实不制定该SA_SIGINFO标志也行,但是就不能获取到sigqueue函数发送来的额外信息;
  • 只能把信号发送给单个进程。

6.5 信号名和编号

Linux系统中信号编号和信号名用一个数组指针来定义,同时linux中也定义了一组函数用来转换信号编号到信号名。

extern char *sys_siglist[];  //数组下表是编号,对应的字符串就是信号名

1. psignal函数

#inlcude<signal.h>
int psignal(int signo, const char *msg);
//执行成功返回0,失败返回-1

将signo对应的信号名输出到标准错误文件,且将msg输出到标准错误文件。文件格式如下:msg: “该信号的说明”\n如果msgNULL,则只输出信号的说明部分到文件中。

2. psiginfo函数

#inlcude<signal.h>
int psiginfo(const siginfo_t *info, const char *msg);
//执行成功返回0,失败返回-1

psiginfo用于打印与siginfo结构体相关的信息。

3. strsignal函数

#inlcude<string.h>
int strsignal(int signo;
//执行成功返回0,失败返回-1

Strsignal会将信号对应的描述写到文件,可以不用是标准错误文件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值