暗无天日

=============>DarkSun的个人博客

信号

1 编号为0的信号

不存在编号为0的信号,kill函数对编号为0的信号有特殊处理. POSIX.1将这种信号称为空信号

2 信号的处理

有两种信号是不能忽略的,就是SIGKILL和SIGSTOP,因为进程收到这两个信号后必须终止或停止0运行.

3 不产生core文件的条件

  1. 进度是设置用户ID的,但当前用户并非程序文件的所有者
  2. 进程是设置组ID的,但当前用户组并非程序文件的组所有者
  3. 用户没有写当前工作目录的权限,也就无法产生新的core文件
  4. core文件已存在,且用户对该文件没有写权限,也就无法覆盖原core文件
  5. core文件太大,超过了RLIMIT_CORE的限制

4 常用信号说明

信号 说明
SIGABRT 调用abort函数产生的信号. 进程异常终止
SIGALRM 函数alarm设置的计时器超时,或setitimer函数设置的间隔时间超时产生该信号
SIGCHLD 子进程终止或停止时,发送该信号給父进程
SIGCONT 该作业控制信号发送给处于停止状态的进程,让其继续运行
SIGFPE 算术运算异常
SIGHUP 终端接口检测到连接断开,会将该信号发送给该终端相关的会话首进程. 会话首进程关闭时也会发送该信号到该会话中的所有进程. 常用于使用该信号通知守护进程,以便重新读取它们的配置文件.
SIGINT 用户按下中断键(一般为C-c或DEL键)
SIGIO 该信号指示一个异步IO事件
SIGKILL 该信号无法被捕捉,进程接收后必定终止
SIGPIPE 写到管道时读进程已终止,则产生该信号. SOCK_STREAM的套接字不再连接时,进程写到该套接字也产生此信号
SIGPOLL 在一个可轮询设备上发生特定事件时产生此信号
SIGQUIT 用户在终端上按退出键(一般为C-\)
SIGSEGV 进程进行了一次无效的内存引用
SIGSTOP 这是一个作业控制进程,用于暂停一个进程. 类似于SIGTSTP信号,但不能被捕获或忽略
SIGSYS 产生一个无效的系统调用
SIGTERM kill命令发送的系统默认终止信号
SIGTSTP 交互式停止信号,当用户在终端上按挂起键(C-z)时产生,并发送至前台进程组中的 所有进程
SIGTTIN 当后台进程组中的进程试图读取控制终端时产生. 但若 读取进程所属的进程组为孤儿进程组,此时操作返回出错,且errno为EIO
SIGTTOU 当后台进程组中的进程试图写到其控制终端时产生. 若 终端设置为不允许写且写进程所属进程组为孤儿进程组,则写操作返回出错,errnro为EIO
SIGURG 通知进程已发生一个紧急情况
SIGUSR1 用户自定义进程
SIGUSR2 用户自定义进程
SIGVTALRM setitimer函数设置的虚拟间隔时间到期时产生此信号
SIGXCPU 进程超过其软CPU时间限制
SIGXFSZ 进程超过了其软文件长度限制
   

5 注册信号处理函数

5.1 signal

#include <signal.h>

typedef void (*SIGN_HANDER_P)(int);

/* signal函数的返回值是之前的信号处理函数指针,若失败,返回SIG_ERR */
SIGN_HANDER_P signal(int signo,SIGN_HANDER_P func);
  • signal函数由ISO C定义. 因为ISO C不涉及多进程,进程组以及终端IO等,所以它对信号的定义非常模糊,以至于 对UNIX系统而言几乎毫无用处
  • signal的语义与实现有关,所以最好使用sigaction函数代替signal函数
  • 系统预先定义了SIG_IGN和SIG_DEF两个信号处理函数.
  • signal函数的一个缺陷在于,不改变信号的处理方式,就无法确定信号的当前处理方式.

6 不可靠的信号

早期UNIX中的信号是不可靠的,即信号可能会丢失

  • 当发生信号到调用信号处理函数之间又发生另一次中断信号,这时程序会转去执行第二个中断点的处理函数
  • 当信号发生时,进程只能选择立即处理该信号或忽略该信号, 而无法让内核暂时记录下该信号

7 可重入函数与不可重入函数

进程在捕获到信号后,会中断正在执行的指令序列,转而调用该信号的处理程序,然后再接着执行在捕捉到信号时进程正在执行的正常命令序列.

若函数被中断后接着执行指令序列会照成函数处理失败,则该函数为不可重入函数,否则叫做可重入函数.

同样的,由于多线程也会中断一个线程的执行,转而执行另一个线程的内容,因此也跟信号中断类似.

不可重入函数的特点有:

  1. 使用了静态数据结构
  2. 使用了全局数据结构 标准IO库的很多实现都以不可重入方式使用了全局数据结构
  3. 调用了malloc或free

此外,为了防止中断处理函数改变errno变量,因此作为一个通用规则,当在信号函数中调用可重入函数时,应当将errno的值先保存,然后再恢复.

8 抛出信号

kill函数将信号发送給进程或进程组. raise函数允许进程向 自身 发送信号

#include <signal.h>

/* 成功返回0,出错返回-1 */
int kill(pid_t pid,init signo);
/* 成功返回0,出错返回-1 */
int raise(int signo);

kill的pid参数有4种不同的情况:

pid > 0
将信号发送給进程号为pid的进程
pid == 0
将信号发送給与该进程 同一个进程组的 的所有进程.(不包括系统定义的系统进程集)
pid < 0
发送信号給 进程组ID为pid绝对值 的所有进程
pid == -1
将信号发送给 系统中有权限向它们发送信号的所有进程. 同样不包括系统进程

进程不能随意对其他进程发起信号,发起信号的规则是:

  • 超级用户权限的进程可以对任一进程发起信号
  • 发送者的实际或有效用户ID必须等于接受者的实际或有效用户ID
  • 若系统支持_POSIX_SAVED_IDS,则系统检查接受者的实际用户ID和保存设置用户ID
  • 若发送的信号是 SIGCONT 则进程可以将它发送給属于 同一会话 的任何其他进程

若signo参数为0,则kill仍检查是否能发送信号給指定进程,但并不实际发送信号. 这通常用来确定一个特定进程是否仍然存在. 若不存在则kill返回-1,并将errno设置为ESRCH 但,由于这种测试不具有原子性,在kill向调用者返回结果的过程中,被测试进程可能已经终止,因此这种测试意义不大.

9 alarm设置计时器

alarm函数设置一个定时器,当定时器超时后会产生SIGALRM信号. 系统的默认动作是终止调用该alarm函数的进程

#include <unistd.h>

/* 返回0,或以前设置的闹铃时间的剩余秒数 */
unsigned int alarm(unsigned int seconds);
  • 需要注意的是,由于信号由内核产生,再加上进程调度的延迟,所以进程真正收到信号还需要一些时间.
  • 每个进程只能有一个闹铃
  • 若参数seconds值为0,则表示取消以前的闹铃,并返回剩余秒数

10 pause函数挂起进程

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

#include <unistd.h>

/* 返回-1,且errno为EINTR */
int pause();

只有执行了一个信号处理函数并从其返回时,pause函数返回. 并且返回值为-1,errno为EINTR

使用alarm和pause,进程可以使自己休眠一段时间,但是要注意调用alarm和pause之间有一个竞争条件. 即有可能alarm在调用pause之前就超时了.

除了用来实现sleep函数外,alarm还常用于对可能阻塞的操作设置时间上限值.

11 信号集

当我们在调用sigprocmask来告诉内核暂时阻塞哪些信号传递給进程时,需要一种方式来一次传递多个信号的集合.

由于信号种类的数量可能超过一个整型量所包含的位数,所以不能用整型量中的一位代表一种信号,也就不能用一个整型量表示信号集.

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);

/* signo是否包含在set信号集中 */
int sigismember(const sigset_t* set,int signo);

12 sigprocmask函数

sigprocmask函数可以检测或更改进程的信号屏蔽字,该进程屏蔽字告诉内核阻塞哪些信号传递給该进程.

#include <signal.h>

int sigprocmask(int how, const sigset_t* set,sigset_t* old_set);
  • 若old_set不为NULL,则通过old_set返回进程当前的信号屏蔽字
  • 若set不为NULL,则参数how指明了如何修改当前信号屏蔽字.

    how 说明
    SIG_BLOCK 新阻塞set表示的信号集
    SIG_UNBLOCK 不再阻塞set表示的信号集
    SIG_SETMASK 设置进程的新信号屏蔽字为set所表示的信号集
  • 若参数set为NULL,则并不改变该进程的信号屏蔽字,参数how也无意义.
  • 在调用sigprocmask后,如果有任何未决的,不再阻塞的信号,则在sigprocmask返回前,至少将其中一个信号递送給该进程(不明白什么意思…)

    #include <signal.h>
    #include <unistd.h>
    #include <stdio.h>
    
    static void sig_quit(int);
    
    int main()
    {
      sigset_t newmask,oldmask,pendmask;
    
      signal(SIGQUIT,sig_quit);
      sigemptyset(&newmask);
      sigaddset(&newmask,SIGQUIT);
    
      sigprocmask(SIG_BLOCK,&newmask,&oldmask); /* 阻塞SIGQUIT */
    
      raise(SIGQUIT);               /* 产生SIGQUIT信号 */
    
      sigpending(&pendmask);        /* 查看哪些信号被阻塞 */
    
      if(sigismember(&pendmask,SIGQUIT)){
        printf("SIGQUIT pending\n");
      }
    
      sigprocmask(SIG_SETMASK,&oldmask,NULL); /* 在该函数返回前,raise产生的SIGQUIT会发送到该进程,进而触发函数sig_quit */
      printf("SIGQUIT unblocked\n");
      return 0;
    }
    
    static void sig_quit(int signo)
    {
      printf("caught SIGOUT\n");
    }
    
  • sigprocmask是线程不安全的!

13 sigpendng函数

sigpending函数返回当前应该传递給进程但被阻塞的信号集合

#include <signal.h>

int sigpending(sigset_t* set);

注意区分与sigprocmask函数的区别

  • sigprocmask返回的是哪些信号将会被阻塞
  • ssigpending函数返回的是哪些函数已经被阻塞了.

14 sigaction函数

sigaction函数检查或修改与指定信号相关联的处理动作, 其被取代UNIX早期版本中的signal函数

#include <signal.h>

int sigaction(int signo,const struct sigaction* act,struct sigaction* old_act);
struct sigaction{
  void (*sa_handler)(int signo);      /* signal处理函数的地址,或SIG_IGN,SIG_DFL */
  sigset_t sa_mask;             /* 调用sa_handler时要新阻塞的信号集 */
  int sa_flags;                 /* 其他操作标志 */
  void (*sa_sigaction)(int signo,siginfo_t* iinfo,void* context);
};
  • 参数signo是要检测或修改其具体动作的信号编号
  • 若参数act不为NULL,则根据act修改信号signo的处理函数.
  • 若参数old_act不为NULL,则保存该信号的原处理函数.

14.1 参数act的说明

  • sa_handler为信号处理函数的地址,当收到信号signo时,转到该函数来处理
  • 参数sa_mask为一个信号集,该信号集会在调用sa_handler之前被 临时加入到进程的信号屏蔽字中,并在从sa_handler返回时再将进程的信号屏蔽字还原. 这样在调用信号处理函数时就能阻塞某些信号.
  • 为了防止sa_handler在处理信号signo时,该信号再次发生造成信号丢失,sa_handler被调用时,操作系统建立的新信号屏蔽字会包含正在传递的信号signo
  • sa_flags指定了对信号进行处理的各个选项,各选项可以使用`|'组合

    sa_flag 说明
    SA_INTERRUPT 被信号中断的系统调用不会自动重启动
    SA_NOCLDSTOP 当子进程停止时不产生SIGCHLD信号,同时若停止的进程继续运行时,也不发送SIGCHLD信号
    SA_NOCLDWAIT 子进程终止时,并不创建僵死进程,并不发送SIGCHLD信号
    SA_NODEFER 在执行sa_handler时,并不自动阻塞当前signo,此时的操作对应于早期的不可靠信号
    SA_ONSTACK 若sigaltstack函数声明了替换栈,则将该信号发送到替换栈上的进程
    SA_RESETHAND 在执行sa_handler前,将信号处理方式复位为SIG_DFL,并清除SA_SIGINFO标志
    SA_RESTART 被信号中断的系统调用会自动重启动, 默认不重启!
    SA_SIGINFO 该选项使用sa_sigaction代替sa_handler作为信号处理函数.

14.2 函数sa_sigaction说明

当在sigaction结构中使用了SA_SIGINFO标志时,使用sa_sigaction信号处理程序. 该函数接收两个附加参数:

  • siginfo结构的指针包含了信号产生的原因相关信息

    struct siginfo{
      int si_signo;                 /* signal编号 */
      int si_errno;
      int si_code;                  /* signal的进一步说明 */
      pid_t si_pid;                 /* 发送信号的进程号 */
      uid_t si_uid;                 /* 发送信号进程的实际用户id */
      void* si_addr;                /* 引起fault的内存地址 */
      int si_status;                /* signal编号或退出值 */
      long si_band;                 /* SIGPOLL的band number,即STREAMS消息的优先级段 */
    };
    
  • 一个指向进程上下文标识符的指针,可强制转换为ucntext_t结构类型

15 sigsetjmp和siglongjmp函数

在可靠的信号机制中,当进程捕捉到信号,进入信号处理函数时,当前信号会被自动地加到进程的信号屏蔽字中,此时,若信号处理函数中用longjmp跳出信号处理函数,那么 对此进程的信号屏蔽字是否回退是未定义的.

在信号处理函数中进行非局部跳转时 应该使用sigsetjmp和siglongjmp函数代替

#include <setjmp.h>

/* 直接调用返回0,从siglongjmp调用返回返回非0 */
ing sigsetjmp(sigjmp_buf env, int savemask);

void siglongjmp(sigjmp_buf env,int val);

这两个函数与setjmp和longjmp之间的唯一区别是sigsetjmp增加了一个参数savemask用于标识是否在env中保存进程的当前信号屏蔽字.

若使用非0的savemask参数调用sigsetjmp,则siglongjmp跳转后,会恢复保存的信号屏蔽字

由于信号在任何时候都可能发生,因此在信号处理函数中需要用到siglongjmp的话,都需要提供一种保护措施,只有在设置了sigsetjmp后才能用siglongjmp来跳转. 这种保护措施一般通过定义一个 全局的voliatile sig_atomic_t类型的变量 来实现. 仅在调用sigsetjmp后才将该变量设置为非0,在信号处理函数中则检查该值,只有当它非0时才调用siglongjmp

数据类型sig_atomic_t是ISO C标准定义的变量类型,这种类型的变量不会被中断. 这种类型的变量总是包括ISO类型修饰符volatile,原因是该变量将由main函数和信号处理函数两个不同的控制线程访问.

16 sigsuspend

若希望对一个信号接触阻塞,然后通过pause函数休眠以等待被阻塞的信号发生,该如何实现呢?

若通过先修改进程信号屏蔽字,再调用pause函数的方法来进行,则由于可能在调用pause函数之前就已经收到信号,从而造成该信号丢失,使得进程永远阻塞下去.

解决的方法是提供一个 原子操作,先修改信号屏蔽字,并信号被捕获之前就挂起该进程

#include <signal.h>

/* 返回-1,并将errno设为EINTR */
int sigsuspend(const sigset_t *sigmask);

sigsuspend函数临时将进程的信号屏蔽字设置为由sigmask指向的值. 并在捕捉到一个信号或发生了一个会终止该进程的信号之前挂起该进程.

该函数仅在进程捕获到信号后返回,并会 恢复原信号屏蔽字

但该函数仅当进程等待信号区间休眠时才有用,若想在等待信号期间还能调用其他系统函数,则无法实现完美的解决方案

17 abort函数

abort函数发送SIGABRT信号到调用进程,并且 不管信号处理函数如何处理,abort都使进程异常终止.

#include <stdlib.h>

void abort();

一般捕获SIGABRT的目的是执行进程终止之前的清理操作.

18 其他特性

这些特性依赖于具体的实现

  • sys_siglist数组变量

    extern char* sys_siglist[];
    

    数组下表是信号编号,指向一个信号字符串名称

  • psignal函数

    #include <signal.h>
    
    void psignal(int signo,const char* msg);
    

    类似perror,将字符串msg输出到stderr,后接`: ',再接对信号的说明,最后一个回车

  • strsignal函数

    #include <string.h>
    
    char* strsignal(int signo);
    

    类似strerrno,根据信号编号返回说明信号的字符串

18.1