信号——读书笔记

信号

信号是软件中断。信号提供了一种处理异步事件的方法。

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单得测试一个变量来判断是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列动作”

在某个信号出现时,可以告诉内核按下列3中方法之一进行处理,我们称之为信号的处理或与信号相关的动作。

  1. 忽略此信号。多数信号都可使用这种方式进行处理,但有两种信号却绝不能被忽略,它们是SIGKILLSIGSTOP。这两个信号不能忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(如非法内存引用或除以0),则进程的运行行为是未定义的。
  2. 捕捉信号。为了做到这一点,要通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行处理。如果捕捉到SIGCHLD信号,则表示一个子进程已经终止,所以此信号的捕捉函数可以调用waitpid以取得该子进程的进程ID以及它的终止状态。例如,如果进程创建了临时文件,那么可能要为SIGTERM信号编写一个信号捕捉函数以清除临时文件。注意,不能捕捉SIGKILL和SIGSTOP信号
  3. 执行系统默认动作注意,大多数信号的系统默认动作是终止该进程

函数signal

#include <signal.h>
void (*signal(int signo, void(*func)(int)))(int);
//返回值,若成功,返回以前的信号处理程序;若出错,返回SIG_ERR

signo参数是信号名,func的值是常量SIG_IGN,常量SIG_DFL或当接到此信号后要调用的函数地址。如果指定SIG_IGN,则向内核表示忽略此信号。如果指定SIG_DEL,则表示接到此信号的动作是系统默认动作。当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为捕捉该信号,称此函数为信号处理程序或信号捕捉函数。

注意,在UNIX系统中,杀死kill这个术语是不恰当的。kill(1)命令和kill(2)函数只是将一个信号发送给一个进程或进程组。该信号是否终止进程则取决于该信号的类型,以及进程是否安排了捕捉该信号

程序启动

当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为它们的默认动作,除非调用exec的进程忽略该信号。确切的说,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他的信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。

进程创建

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的

中断的系统调用

早期UNIX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其errno设置为EINTR。这样处理是因为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。

为了支持这种特性,将系统调用分成两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

  • 如果某些类型文件(如读管道,终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞。
  • 如果这些数据不能被相同的类型文件立即接受,则写操作可能会使调用者永远阻塞。
  • 在某种条件发生之前打开某些类型文件,可能会发生阻塞。
  • pause函数和wait函数。
  • 某些ioctl操作。
  • 某些进程间通信函数。

在这些低速系统调用中,一个值得注意的例外是与磁盘I/O有关的系统调用。虽然读,写一个磁盘文件可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,I/O操作总会很快返回,并使调用者不再处于阻塞状态

与被中断的系统调用相关的问题是必须显式地处理出错返回。

典型的代码序列(假设进行一个读操作,它被中断,我们希望重新启动它)如下:

again:
	if((n = read(fd, bufm BUFFSIZE)) < 0)
    {
        if(errno == EINTR)
            goto again;
    }

为了帮助应用程序使其不必处理被中断的系统调用,BSD引进了某些被中断系统调用的自动重启动

自动重启动的系统调用包括ioctl,read,readv,write,writev,waitwaitpid。其中前5个函数只有对低速设备进行操作时才会被信号中断。而wait,waitpid在捕捉信号时总是被中断。

BSD引入自动重启动功能的一个理由是:有时用户并不知道所使用的输入,输出设备是否是低速设备。如果我们编写的程序可以用交互方式运行,则它可能读,写终端低速设备。如果在程序中捕捉信号,而且系统并不提供重启动功能,则它每次读,写系统调用就要进行是否出错返回的测试,如果是被中断的,则再调用读,写系统调用。

可重入函数

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回,则继续执行再捕捉到信号时进程正在执行的正常指令序列。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。如果进程正在执行malloc,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,这是会发生什么?这样可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时,进程可能正在更改此链表。

在信号处理程序中保证调用安全的函数。这些函数是可重入的并被称为异步信号安全的。除了可重入以外,在信号处理操作期间,它会阻塞任何会引起不一致的信号发送

大多数函数是不可重入的,因为(a)已知它们使用静态数据结构(b)它们调用mallocfree(c)它们是标准I/O函数

SIGCLD语义

对于SIGCLD的早期处理方式是

  1. 如果进程明确地将该信号的配置设置为SIG_IGN,则调用进程的子进程将不产生僵尸进程。注意,这与默认动作(SIG_DEL)“忽略”不同。子进程在终止时,将其状态丢弃。如果调用进程随后调用一个wait函数,那么它将阻塞直到所有子进程都终止,然后该wait会返回-1,并将其errno设置为ECHILD。(此信号的默认配置是忽略,但这不会使上述语义起作用,必须将其配置明确指定为SIG_IGN才可以)。
  2. 如果将SIGCLD的配置设置为捕捉,则内核立即检查是否有子进程准备好被等待,如果是这样,则调用SIGCLD处理程序。

可靠信号术语和语义

当一个信号产生时,内核通常在进程表中以某种形式设置一个标志。

当对信号采取了这种动作时,我们说向进程递送了一个信号。在信号产生和递送之间的时间间隔内,称信号是未决的

进程可以选用“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时),才决定对它的处理方式。于是进程在信号递送给它之前仍可改变对该信号的动作。

进程调用sigpending函数来判断哪些信号是设置为阻塞并处于未决状态的。

如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么将如何呢?POSIX.1允许系统递送该信号一次或多次。如果递送该信号多次,则称这些信号进行了排。但是除非支持POSIX.1实时扩展,否则大多数UNIX并不对信号排队,而是只递送这种信号一次。

每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已经设置,则它当前是被阻塞的。

进程可以调用sigprocmask来检测和更改当前信号屏蔽字。

函数kill和raise

kill函数将信号发送给进程或进程组。raise函数则允许进程向自身发送信号。

#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
//返回值,若成功返回0,若出错,返回-1

调用

raise(signo);

等价于调用

kill(getpid(), signo);

killpid参数有以下4中不同的情况

  • pid>0:将该信号发送给进程ID为pid的进程。
  • pid==0:将该信号发送给与发送进程属于同一进程组的所有进程(这些进程的进程组ID等于发送进程的进程组ID),而且发送进程具有权限向这些进程发送信号。这里的术语“所有进程”不包括实现定义的系统进程集。系统进程集包括内核进程和init
  • pid<0:将该信号发送给其进程组ID等于pid绝对值,而且发送进程具有权限向其发送信号的所有进程。
  • pid==-1:将该信号发送给发送进程有权限向它们发送信号的所有进程。

进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。

POSIX.1将信号编号0定义为空信号。如果signo参数是0,则kill仍执行正常的错误检查,但是不发送信号。这通常被用来确定一个特定进程是否存在。如果向一个并不存在的进程发送空信号,则kill返回-1,errno被设置为ESRCH。但是,应当注意,UNIX系统在经过一定时间后会重新使用进程ID,所以一个现有的具有所给定进程ID的进程并不一定就是你想要的进程。

还应理解的是,测试进程是否存在的操作不是原子操作。在kill向调用者返回测试结果时,原来已存在的被测试进程此时可能已经终止,所以这种测试并无多大价值。

如果调用kill为调用进程产生信号,而且此信号是不被阻塞的,那么在kill返回之前,signo或者某个其他未决的,非阻塞信号被传送至该进程

函数alarm和pause

使用alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0或以前设置的闹钟时间的余留秒数

参数seconds的值是产生信号SIGALRM需要经过的时钟秒数。当这一时刻到来时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一个时间间隔。

每个进程只能有一个闹钟时间。如果在调用alarm时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前注册的闹钟时间则被新值代替。

如果有以前注册的尚未超过的闹钟时间,而且本次调用的seconds值是0,则取消以前的闹钟时间,其余留值仍作为alarm函数的返回值。

SIGALRM的默认动作是终止进程,但是大多数使用闹钟的进程捕捉此信号。如果此时进程要终止,则在终止之前它可以执行所需的清理操作。如果我们想捕捉SIGALRM信号,则必须在调用alarm之前安装该信号的处理程序。如果我们先调用alarm,然后在我们能够安装SIGALRM处理程序之前已接到该信号,那么进程将终止。

pause函数使调用进程挂起直至捕捉到一个信号。

#include <unistd.h>
int pause(void);
//返回值:-1,errno设置为EINTR

只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回-1,errno设置为EINTR

信号集

POSIX.1定义数据类型sigset_t以包含一个信号集,并且定义了下列5个处理信号集的函数。

#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);
//4个函数返回值:若成功,返回0;若出错,返回-1

int sigismember(const sigset_t* set, int signo);
//返回值:若真,返回1;若假,返回0

函数sigemptyset初始化由set指向的信号集,清除其中所有信号。函数sigfillset初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用sigemptysetsigfillset一次。

一旦已经初始化了一个信号集,以后就可在该信号集中增,删特定的信号。函数sigaddset将一个信号添加到已有的信号集中,sigdelset则从信号集中删除一个信号。

函数sigprocmask

调用函数sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。

#include <signal.h>
int sigprocmask(int how, const sigset_t* restrict set, sigset_t* restrict oset);
//返回值,若成功返回0,若出错,返回-1

oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。

其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。

SIG_BLOCK是或操作,SIG_SETMASK是赋值操作。

注意,不能阻塞SIGKILL和SIGSTOP信号

how说明
SIG_BLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了希望阻塞的附加信号。
SIG_UNBLOCK该进程新的信号屏蔽字是其当前信号屏蔽字和set所指向信号集补集的交集。set包含了希望解除阻塞的信号。
SIG_SETMASK该进程新的信号屏蔽是set指向的值。

如果set是个空指针,则不改变该进程的信号屏蔽字,how的值也无意义。

在调用sigprocmask后如果有任何未决的,不再阻塞的信号,则在sigprocmask返回前,至少将其中之一递送给该进程

函数sigpending

sigpending函数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返回。

#include <signal.h>
int sigpending(sigset_t* set);
//返回值:若成功,返回0,若出错,返回-1

函数sigaction

sigaction函数的功能是检查或修改与指定信号相关联的处理动作。

#include <signal.h>
int sigaction(int signo, const struct sigaction* restrict act,
             struct sigaction* restrict oact);
//返回值:若成功,返回0,若出错,返回-1

其中,参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。如果oact指针非空,则系统经由oact指针返回该信号的上一个动作。

struct sigaction{
	void (*sa_handler)(int);	//addr of signal handler
    							//or SIG_IGN, or SIG_DFL
    sigset_t sa_mask;			//additional signals to block
    int 	 sa_flags;			//siganl options
    //alternate handler
    void (*sa_sigaction)(int, siginfo_t*, void *);
};

当更改动作时,如果sa_handler字段包含一个信号捕捉函数的地址(不是常量SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加到进程的信号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字恢复为原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。

act结构的sa_flags字段指定对信号进行处理的各个选项。

选项说明
SA_NOCLDSTOPsigno是SIGCHLD,当子进程停止,不产生此信号。当子进程终止时,产生此信号。
SA_NOCLDWAITsigno是SIGCHLD,则当调用进程的子进程终止时,不创建僵死进程。若调用进程随后调用wait,则阻塞到它所有子进程都终止,此时返回-1,errno设置为ECHILD。
SA_NODEFER当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号。
SA_RESETHAND在此信号捕捉函数的入口处,将此信号的处理方式重置为SIG_DFL,并清除SA_SIGINFO标志。不能自动重置SIGILL和SIGTRAP这两个信号的配置。
SA_RESTART由此信号中断的系统调用自动重启动
SA_SIGINFO此选项对信号处理程序提供了附加信息:一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针。

sa_sigaction字段是一个替代的信号处理程序,在sigaction结构中使用了SA_SIGINFO标志时,使用该信号处理程序。对于sa_sigaction字段和sa_handler字段两者,实现可能使用同一存储区,所以应用只能一次使用这两个字段中的一个。

通常,按下列方式调用信号处理程序:

void handler(int signo);

但是,如果设置了SA_SIGINFO标志,那么按下列方式调用信号处理程序;

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

siginfo结构包含了信号产生原因的有关信息。

struct siginfo{
	int 	si_signo;		//signal number
    int 	si_errno;		//if nonzero,errno value from <errno.h>
    int		si_code;		//additional info
    pid_t	si_pid;		//sending process ID
	uid_t   si_uid;		//sending process real user ID
    void	*si_addr;	//address that caused the fault
    int 	si_status;	//exit value or signal number
   	union sigval si_value;	//application-specific value
    //possibly other fields also
};

sigval联合包含下列字段

int 	sival_int;
void* 	sival_ptr;

应用程序在递送信号时,在si_value.sival_int中传递一个整型数或者在si_value.sival_ptr中传递一个指针值。

函数sigsetjmp和siglongjmp

在信号处理程序中进行非局部转移时应当使用以下两个函数。

#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savemask);
//返回值,若直接调用,返回0;若从siglongjmp调用返回,则返回非0
void siglongjmp(sigjmp_buf env, int val);

如果savemask非0,则sigsetjmpenv中保存进程的当前信号屏蔽字。调用siglongjmp时,如果带非0savemasksigsetjmp调用已经保存了env,则siglongjmp从其中恢复保存的信号屏蔽字。

函数sigsuspend

#include <signal.h>
int sigsuspend(const sigset_t* sigmask);
//返回值:-1,并将errno设置为EINTR

进程的信号屏蔽字设置为由sigmask指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend返回,并且该进程的信号屏蔽字设置为调用sigsuspend之前的值。

注意,此函数没有成功返回值。如果它返回到调用者,则总是返回-1,并将errno设置为EINTR(表示一个被中断的系统调用)。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值