[单刷APUE系列]第十章——信号[2]

浏览: 79 发布日期: 2016-08-17 分类: unix

目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]
[单刷APUE系列]第十章——信号[2]

SIGCLD信号

SIGCLD和SIGCHLD是两个很相似的信号,SIGCLD是SystemV的一个信号名字,而SIGCHLD是BSD信号,但是POSIX.1标准使用了BSD的SIGCHLD信号名称。
BSD的SIGCHLD信号是很普通的意思,就是子进程状态改变就会产生这个信号,父进程则是调用wait函数查看子进程的状态,而SystemV的SIGCLD信号则不同,基于SVR4的系统都会继承这个情况。

  1. 如果进程明确配置SIGCLD信号为SIG_IGN,则调用进程的子进程不产生僵尸进程,在前面说过,如果子进程退出,但是没有父进程进行清理,则会产生僵尸进程。而如果配置成SIG_IGN,则子进程在终止的时候,就会丢弃其退出状态,那么父进程使用wait函数就不会接收到任何的信息,直到所有的子进程终止。

  2. 如果SIGCLD被设置为捕捉,则内核就会立刻检查是否有子进程准备好了等待,

基本就是这些,实际上这个信号可看可不看。因为在实际开发中是不可能使用这个信号的,不少平台都不支持此信号。

可靠信号术语和语义

信号是事件发生时,为进程产生一个信号或者发送一个信号,当信号产生时,内核会在进程表中设置一个标志。在信号产生和传递中间,信号是阻塞的(pending)。
进程可以设置阻塞一个信号传送,如果对这个进程发送了一个已经设置为阻塞的信号,并且该信号的动作是系统默认动作或者捕捉该信号,换言之,就是不忽略该信号的处理,则为该进程将此信号设置为pending状态,直到该进程对此信号接触阻塞,或者设置该信号的动作为忽略。

int sigpending(sigset_t *set);

The sigpending function returns a mask of the signals pending for delivery to the calling process in the location indicated by set.  Signals may be pending because they are currently masked, or transiently before delivery (although the latter case is not normally detectable).

实际上,每个进程都有一个信号屏蔽字(signal mask),就和权限屏蔽字一样,这是用于记录当前要阻塞传递的信号集。

发送信号

发送信号有两种函数

int kill(pid_t pid, int sig);
int raise(int sig);

两个函数的区别就是一个是系统函数库,一个是ISO C函数库,并且raise函数允许进程向自身发送信号。
kill函数很常用,所以在这里讲一下kill函数的参数

If pid is greater than zero:
        Sig is sent to the process whose ID is equal to pid.

If pid is zero:
        Sig is sent to all processes whose group ID is equal to the process group ID of the sender, and for which the process has permission; this is a variant of killpg(2).

If pid is -1:
        If the user has super-user privileges, the signal is sent to all processes excluding system processes and the process sending the signal.  If the user is not the super user, the signal is sent to all processes with the same uid as the user, excluding the process sending the signal.  No error is returned if any process could be signaled.
For compatibility with System V, if the process number is negative but not -1, the signal is sent to all processes whose process group ID is equal to the absolute value of the process number.  This is a variant of killpg(2).
  1. pid大于0,信号发送给当前为pid的进程

  2. pid等于0,信号发送给当前进程同一进程组的所有进程

  3. pid等于-1,信号发送给发送进程有权限发送信号的所有进程

为了保持SystemV的兼容,当pid小于0且不等于-1的时候,信号发送给所有进程组ID等于当前进程绝对值的进程。

alarm和pause函数

unsigned alarm(unsigned seconds);

alarm函数就是设置一个定时器,当超时后会产生一个SIGALRM信号,如果忽略或者系统默认动作,就是进程终止,当然,一般情况下,进程都捕捉该信号。

int pause(void);

pause函数会强制进程暂停直到从kill函数或者setitimer函数收到一个信号。所以当看到这两个函数,基本想法就是这两个函数能让进程休眠

#include <signal.h>
#include <unistd.h>

static void sig_alrm(int signo)
{
}

unsigned int sleep1(unsigned int seconds)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        return seconds;
    alarm(seconds);
    pause();
    return(alarm(0));
}

当然,这样子写肯定是有问题的,比如

  1. 在sleep1之前,已经存在定时器了,那么sleep函数中第一次alarm调用将会擦除之前的闹钟,

  2. 程序修改了SIGALRM信号的配置

  3. 在第一课次调用alarm和apuse之间有一个竞争条件。

信号集

为了能够表示多个信号,系统提供了信号集的数据类型。在通常的开发中,经常会使用到二进制位来表示状态,二进制的每一位代表一种信号,但是实际上,由于信号的编号肯定会超过一个整形量的位数,所以一般都不是用一个整形量表示信号集。POSIX.1定义了数据类型sigset_t用以表示一个信号集,苹果系统下实际上是这么表示的

typedef __uint32_t    __darwin_sigset_t;
typedef __darwin_sigset_t sigset_t;

除此以外,系统还定一款了下列5个处理信号集的函数

int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigismember(const sigset_t *set, int signo);

sigemptyset函数初始化set指向的信号集,清除所有信号,sigfillset初始化set指向的信号集,被设置为包含所有信号,在使用之前,sigemptyset或者sigfillset必须被调用。sigaddset和sigdelset则是添加删除一个信号,sigismember函数则返回是否一个指定的signo信号被包含在这个信号集中。

sigprocmask函数

在前文中提及了信号屏蔽字指定了当前进程阻塞不能传递的信号集。而sigprocmask函数就是用来检测修改信号屏蔽字的函数

int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

如果set不是null,sigprocmask函数的行为依赖how参数。

  1. SIG_BLOCK,进程新的信号屏蔽字是当前信号屏蔽字和set指向信号集的并集运算

  2. SIG_UNBLOCK,信号新的屏蔽字是当前信号屏蔽字和set指向信号集的补集的交集

  3. SIG_SETMASK,新的信号屏蔽字是set指向信号集

如果oset不是null,那么当前的信号屏蔽字将会被设置给oset,如果set参数为null,则不改变信号屏蔽字,how参数也没有意义。

sigpending和sigaction函数

int sigpending(sigset_t *set);

The sigpending function returns a mask of the signals pending for delivery to the calling process in the location indicated by set.  Signals may be pending because they are currently masked, or transiently before delivery (although the latter case is not normally detectable).

sigpending函数返回当前进程阻塞不能传递信号的信号集。

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

sigaction函数用于检查或修改与制定信号相关联的处理动作,如果act指针非空,则修改其动作,如果oact非空,则通过oact指针返回该信号的上一个动作,是不是觉得很熟悉,这个函数很像sigprocmask函数,实际上这个函数就是替代了signal函数,在上面的参数中也可以看出实际上多了新结构体,手册上也写了,所以这里也列了出来。

struct  sigaction {
        union __sigaction_u __sigaction_u;  /* signal handler */
        sigset_t sa_mask;               /* signal mask to apply */
        int     sa_flags;               /* see signal options below */
};

union __sigaction_u {
        void    (*__sa_handler)(int);
        void    (*__sa_sigaction)(int, siginfo_t *,
                      void *);
};

#define sa_handler      __sigaction_u.__sa_handler
#define sa_sigaction    __sigaction_u.__sa_sigaction

看起来和原著上面不一样啊,其实就是一个道理,原著中有两种handler,所以用了两个成员存储,而这里实际上使用一个union来存储,因为handler实际上只有一个的。当更改信号的时候,如果sa_handler包含一个信号捕捉函数地址,则sa_mask字段说明了一个信号集,在调用信号捕捉函数之前,信号集要加入到进程信号屏蔽字中。
siginfo_t结构体包含了信号产生原因的有关信息,这里就不在继续列出。

sigsetjmp和siglongjmp函数

前面讲过setjmp和longjmp函数,主要用于非局部转移。信号处理程序经常会调用longjmp函数返回到main函数,但是,当使用longjmp函数的时候,信号会自动的加到进程屏蔽字中,如果使用longjmp跳出,则会在一些平台上导致信号屏蔽字无法恢复,所以Unix系统提供了两个新函数用于信号处理函数的非局部转移。这两个函数只在此介绍一下,不详细讲述了。

sigsuspend函数

从这个函数的名字中基本也能猜出是做什么的了,先来看看Unix系统手册是怎么讲的

int sigsuspend(const sigset_t *sigmask);

Sigsuspend() temporarily changes the blocked signal mask to the set to which sigmask points, and then waits for a signal to arrive; on return the previous set of masked signals is restored.  The signal mask set is usually empty to indicate that all signals are to be unblocked for the duration of the call.

In normal usage, a signal is blocked using sigprocmask(2) to begin a critical section, variables modified on the occurrence of the signal are examined to determine that there is no work to be done, and the process pauses awaiting work by using sigsuspend() with the previous mask returned by sigprocmask.

sigsuspend函数临时改变当前的阻塞信号屏蔽字为sigmask参数指定的信号集,然后等待一个信号到来,返回的时候,先前的信号屏蔽字将被恢复。可能有朋友想问,这个函数到底是干嘛的,如果按照我们之前的知识,完全可以使用sig信号集函数去除被阻塞信号,然后使用pause等待信号发生。实际上,我们需要考虑到这是两步操作,很有可能在pause之前,就已经有信号传递了,所以这个函数只是执行了原子操作的封装。

abort函数

abort函数也没有什么好说的,这个函数从名字就知道是程序终止,并且是异常终止。

void abort(void);

这是一个ISO C库函数,函数就是发送了SIGABRT信号给调用进程。abort函数会导致不正常的程序终止,除非信号SIGABRT被捕捉并且信号处理函数没有返回。实际上,信号处理函数不能返回的唯一方法是它调用exit、_exit、_Exit、longjmp或者siglongjmp。当执行此函数的时候,所有的stream都会被冲洗并且被关闭。其实结合上面的信息,很容易就知道了,abort函数肯定是会终止程序的,但是捕捉SIGABRT的唯一意图就是——在程序终止前由其执行所需的清理操作。

System函数

int system(const char *command);

system函数把command参数提交给命令解释器sh,调用的进程等待shell执行命令结束,忽略SIGINT、SIGQUIT,并且阻塞SIGCHLD信号,如果command参数是null指针,system函数将会返回非0当sh解释器可用,如果不可用,则返回0。
为什么system函数需要考虑信号处理,实际上原著讲述足够详细了,一句话,system函数创建的子进程不应当使用wait函数获得退出状态而导致system函数阻塞。

sleep、nanosleep和clock_nanosleep函数

unsigned int sleep(unsigned int seconds);

sleep函数会导致调用进程挂起,当进程超过了seconds指定的时间或者收到一个信号并且从信号处理程序返回,那么进程将会恢复。实际上,很容易就把sleep函数和alarm函数对比,而且alarm函数的信号是否会导致sleep函数的失败,所以sleep函数并不是那么好用。

int nanosleep(const struct timespec *rqtp, struct timespec *rmtp);

nanosleep和sleep函数差不多,但是提供了纳秒级精度,而且非常尴尬的是,系统很有可能不支持纳秒级精度,这就会导致时间取整。
由于现代化Unix操作系统有多个系统时钟,sleep函数也衍生出了clock_nanosleep函数用于相对特定时钟挂起,但是在苹果系统中,好像并没有存在这个函数,手册和头文件都没有找到,所以这里就不在提及。

sigqueue函数

int sigqueue(pid_t pid,int sig,const union sigval value);

sigqueue在队列中向指定进程发送一个信号和数据。但是好像苹果系统也没有提供这个函数,或者说是提供了自有函数,所以很遗憾,无法讲解,只能请各位朋友自行阅读原著。

小结

其实本章最后应该还有两节,但是这两节只是将作业控制信号和信号名编号转换稍稍讲解一下,实际上没有什么价值,信号在大多数的应用程序中都是非常重要的手段,所以应当掌握。

返回顶部