信号基础函数

    signal 函数可为特定的信号指定信号处理函数,可以是常量 SIG_IGN(表示忽略,但 SIGKILL 和 SIGSTOP 信号不能忽略)、SIG_DFL(表示使用默认处理动作,多数为终止)或自定义的信号处理函数地址。
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
          /* 返回值:若成功,返回该信号以前的信号处理函数地址;否则,返回 SIG_ERR */

#define SIG_ERR    (void (*)())-1
#define SIG_DFL    (void (*)())0
#define SIG_IGN    (void (*)())1

    其中,signo 参数是信号名,func 是信号处理函数的地址。由于该函数原型过于复杂,可使用 typedef 来使其简单一些:
            typedef void Sigfunc(int);
            Sigfunc *signal(int, Sigfunc *);
    常量 SIG_ERR、SIG_DFL 和 SIG_IGN 在 <signal.h> 在的定义表示“指向函数的指针,该函数要求一个整型参数,而且无返回值”。
    当执行一个程序时,通常所有信号都被设置为它们的默认动作,除非调用 exec 的进程忽略该信号。确切地讲,exec 函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变,因为原先的信号捕捉函数的地址很可能在所执行的新程序文件中已无意义。
    一个具体例子是一个交互 shell 如何处理针对后台进程的中断和退出信号。通常 shell 会自动将后台进程对中断和退出信号的处理方式设置为忽略,否则当按下中断字符时,它不但会终止前台进程,也终止所有后台进程。
    很多捕捉这两个信号的交互程序都具有下列形式的代码:
...
void sig_int(int), sig_quit(int);
if(signal(SIGINT, SIG_IGN) != SIG_IGN)
    signal(SIGINT, sig_int);
if(signal(SIGQUIT, SIG_IGN) != SIG_IGN)
    signal(SIGQUIT, sig_quit);
...

    这样处理后,仅当 SIGINT 和 SIGQUIT 当前未被忽略时,进程才会捕捉它们。
    而当一个进程调用 fork 时,其子进程一般会继承父进程的信号处理方式。因为子进程是复制的父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的。

    kill 函数将信号发送给进程或进程组,raise 函数则允许进程向自身发送信号。
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
                  /* 返回值:若成功,都返回 0;否则,都返回 -1 */

    kill 的 pid 参数有以下 4 种不同的情况。
    (1)pid > 0:将该信号发送给进程 ID 为 pid 的进程。
    (2)pid == 0:将该信号发送给与发送进程属于同一进程组的所有进程,要求发送进程具有向这些进程发送信号的权限。
    (3)pid < 0:将该信号发送给其进程组 ID 等于 pid 绝对值,而且发送进程具有发送信号权限的所有进程。
    (4)pid == -1:将该信号发送给发送进程具有发送信号权限的所有进程。
    注意,这里的“所有进程”不包括实现定义的系统进程集(如内核进程和 init)。至于发送信号的权限,超级用户可将信号发送给任一进程,而对于非超级用户,其基本规则是发送者的实际用户 ID 或有效用户 ID 必须同接收者的一样。如果实现支持 _POSIX_SAVED_IDS,则检查接收者的保存设置用户 ID 而不是有效用户 ID。不过在对权限进行测试时也有一个特例:如果被发送的信号是 SIGCONT,则进程可将它发送给同一会话的任一其他进程。
    POSIX.1 将信号编号 0 定义为空信号。如果 signo 参数是 0,则 kill 仍执行正常的错误检查,但不发送信号。如果向一个并不存在的进程发送空信号,则 kill 返回 -1,并把errno 置为 ESRCH,这常被用来测试一个特定进程是否仍然存在(不过由于进程 ID 具有可复用性,以及该测试操作并非原子操作,所以这种测试并无多大价值)。
    如果调用 kill 为调用进程产生信号,而且此信号是不被阻塞的,那么在 kill 返回之前,signo 或者某个其他未决的、非阻塞信号就被传送至该进程。

    函数 alarm 可以设置一个定时器,当超时时会产生 SIGALRM 信号,其默认动作是终止调用进程。而函数 pause 可以使调用进程挂起直到捕捉到一个信号,只有执行了一个信号处理程序并从其返回时,pause 函数才返回。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
                       /* 返回值:0 或以前设置的闹钟时间的余留秒数 */
int pause(void);       /* 返回值:-1,并把 errno 设置为 EINTR */

    每个进程只能有一个闹钟时间。如果在调用 alarm 时,之前已注册的闹钟时间还没有超时,则该闹钟剩余的时间将作为本次 alarm 函数的值返回,而以前注册的闹钟时间则被新值替代。当参数 seconds 的值是 0 时,则会取消以前的闹钟时间,但剩余的时间仍作为 alarm 的值返回。另外,如果想捕捉 SIGALRM 信号,则应在调用 alarm 之前安装该信号的处理程序,以免在安装之前就已接到该信号使进程终止。

    信号编号可能会超过一个整型的位数,因此 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 */

    其中,函数 segemptyset/sigfillset 初始化 set 指向的信号集,使其清除/包括所有信号,函数 sigaddset/sigdelset 则用于向其中添加/删除一个信号。
    每个进程都有一个信号屏蔽字,它规定了当前要阻塞的信号集,可以调用 sigprocmask 函数来检测和更改当前的信号屏蔽字。
#include <signal.h>
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
                                     /* 返回值:若成功,返回 0;否则,返回 -1 */

    其中,若参数 oset 是非空指针,则进程的当前信号屏蔽字就通过它返回;若 set 是个空指针,则不改变该进程的信号屏蔽字,此时 how 的值也无意义;若 set 是非空指针,则 how 指示如何修改当前信号屏蔽字,其可选值如下表。

    当产生一个信号时,内核通常会在进程表中设置一个标志。在信号产生和递送之间的时间间隔内,称信号是未决的(pending)。进程可以选用“阻塞信号递送”。如果为一个进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉,则将此信号保持为未决状态,直到对此信号解除了阻塞,或者将动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时,才决定对它的处理方式,所以进程在信号递送给它之前仍可改变对该信号的动作。
    进程可以调用 sigpending 函数来判定哪些信号是设置为阻塞并处于未决状态的。该函数通过 set 参数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。
#include <signal.h>
int sigpending(sigset_t *set);    /* 返回值:若成功,返回 0;否则,返回 -1 */

    如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,则 POSIX.1 允许系统递送该信号一次或多次。如果递送了多次,则称这些信号进行了排队。不过除非支持 POSIX.1 实时扩展,否则大多数 UNIX 并不对信号排队,而是只递送这种信号一次。当有多个信号要递送给一个进程时,POSIX.1 并没规定它们的递送顺序,只是建议在其他信号之前递送与进程当前状态有关的信号,如 SIGSEGV。
    下面这个程序展示了上面提到的很多信号功能(忽略了很多函数调用检查):进程阻塞 SIGQUIT 信号,保存了当前信号屏蔽字以便后面恢复,然后休眠 5 秒,在此期间所产生的退出信号 SIGQUIT 都被阻塞,不递送至该进程,直到该信号不再被阻塞。在 5 秒休眠结束后,检查该信号是否是未决的,然后将 SIGQUIT 设置为不再阻塞。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

static void sig_quit(int signo){
	printf("caught SIGQUIT\n");
	signal(SIGQUIT, SIG_DFL);
}

int main(void){
	sigset_t	newmask, oldmask, pendmask;
	if(signal(SIGQUIT, sig_quit) == SIG_ERR){
		printf("can't catch SIGQUIT\n");
		exit(1);
	}
	/* Block SIGQUIT and save current signal mask */
	sigemptyset(&newmask);
	sigaddset(&newmask, SIGQUIT);
	sigprocmask(SIG_BLOCK, &newmask, &oldmask);
	sleep(5);		// SIGQUIT here will remain pending

	sigpending(&pendmask);
	if(sigismember(&pendmask, SIGQUIT))
		printf("\nSIGQUIT pending\n");
	
	/* Restore signal mask which unblock SIGQUIT */
	sigprocmask(SIG_SETMASK, &oldmask, NULL);
	printf("SIGQUIT unblocked\n");
	sleep(5);		// SIGQUIT here will terminate with core file
	exit(0);
}

    运行结果如下:
$ ./sigmask.out 
^\                           # 5 秒内产生 SIGQUIT 信号一次
SIGQUIT pending              # 从 sleep 返回后
caught SIGQUIT               # 在信号处理程序中
SIGQUIT unblocked            # 从 sigprocmask 返回后
^\退出 (core dumped)         # 再次产生信号
$ 
$ ./sigmask.out 
^\^\^\^\^\^\^\^\^\           # 5 秒内多次产生 SIGQUIT 信号
SIGQUIT pending
caught SIGQUIT               # 只捕获一次
SIGQUIT unblocked
^\退出 (core dumped)         # 再次产生信号

    这里,在休眠期间如果产生了退出信号,那么此时该信号是未决的,但不再受阻塞,所以在 sigprocmask 返回前,它被递送到调用进程,因此 SIGQUIT 处理程序中的 printf 语句先于 sigprocmask 之后的 printf 语句前执行。然后该进程再休眠 5 秒。如果在此期间再产生退出信号,那么因为在上次捕捉到该信号时,已将其处理方式设置为默认动作,所以这一次它就会使该进程终止。注意,第二次运行该程序时,在进程休眠期间产生了多次 SIGQUIT 信号,但解除了对该信号的阻塞后,只向进程传送一次 SIGQUIT,从中可以看出此系统上没有将信号排队。

猜你喜欢

转载自aisxyz.iteye.com/blog/2394924
今日推荐