Linux 信号 signal

信号注册

入门版函数 signal

使用需要包含 <signal.h> 这个头文件。

signal(参数1,参数2);
参数1:我们要进行处理的信号。系统的信号我们可以再终端键入 kill -l 查看(64)。
参数2:我们处理的方式(系统默认 / 忽略 / 捕获)。
signal(SIGINT, SIG_ING ); // ignore the signal
signal(SIGINT, SIG_DFL);  // use the default handler
signal(SIGINT, userfunc); // user defined handler 

signal函数的原型为
void ( *signal( int sig, void (* handler)( int )))( int );
这个比较有意思,容易把人看懵。

void (*p)(int); // p 是一个函数指针,参数是 int 类型,无返回值
void (*p())(int); // p 是一个函数,p的返回值是一个函数指针,参数是 int 类型,无返回值
// 然后再来看signal的定义,有没有清楚一些...
signal 函数的返回值是一个函数指针,该指针指向一个参数是 int 类型,无返回值的函数。
signal 函数本身的参数是 int 类型以及一个函数指针,void (* handler)(int)// 简化的写法如下
typedef void (*handler)(int);
handler signal(int, handler);
// signal 函数返回的是上一次处理该信号的函数指针,如果没有就返回 NULL

signal 函数返回值1

signal 函数返回的是上一次处理该信号的函数指针,如果没有就返回 NULL。
如果不关心返回值也可以不写,好的编程习惯是加个判断, if (signal(SIGINT, sigint_handler)) == SIG_ERR)

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

void catch1(int signo) {
  printf("catch1 received signal %d\n", signo);
}

void catch2(int signo) {
  printf("catch2 received signal %d\n", signo);
}

int main(int argc, char *argv[]) {
  sig_t prev_sigint_handler1 = signal(SIGINT, catch1);
  assert(prev_sigint_handler1 == NULL);
  
  sig_t prev_sighup_handler1 = signal(SIGHUP, catch2);
  assert(prev_sighup_handler1 == NULL);

  raise(SIGINT);  // calls catch1
  raise(SIGHUP);  // calls catch2

  // Now let's swap the handlers

  sig_t prev_sigint_handler2 = signal(SIGINT, catch2);
  assert(prev_sigint_handler2 == catch1);

  sig_t prev_sighup_handler2 = signal(SIGHUP, catch1);
  assert(prev_sighup_handler2 == catch2);

  raise(SIGINT);  // calls catch2
  raise(SIGHUP);  // calls catch1

  return 0;
}
% ./a.out
catch1 received signal 2
catch2 received signal 1
catch2 received signal 2
catch1 received signal 1

高级版函数 sigaction

// man sigaction
#include <signal.h>
/**
 *  注册信号处理函数,成功返回0,失败返回-1并置 errno
 *  参数 act 存储待注册的信号处理函数结构体
 *  oldact 非空的话,旧的信号处理函数会存储到该结构体中
 */
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
 
struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
// 回调函数句柄 sa_handler、sa_sigaction 只能任选其一
该结构在注册信号处理函数sigaction中使用
1. sa_handler 是一个参数为信号值的处理函数
2. sa_sigaction 也是一个信号处理函数,不过它有三个参数,能够获取到处信号值以外更多
   信息,当 sa_flags 中包含 SA_SIGINFO 标志位的时候需要用到该函数。
3. sa_mask 是信号处理函数执行期间的屏蔽信号集。就是说在信号处理函数执行期间,屏蔽某
   些信号。但是不是所有信号都能够被屏蔽,SIGKILL 和 SIGSTOP 这两个信号就无法屏
   蔽,因为 OS 自身要能够控制住进程。
4. sa_flags可以是下面这些值的集合:
   1. SA_NOCLDSTOP,
      这个标志位只用于SIGCHLD信号。父进程可以检测子进程三个事件,子进程终止、
      子进程停止、子进程恢复。SA_NOCLDSTOP标志位用于控制后两个事件。即一旦父进程
      为SIGCHLD信号设置了这个标志位,那么子进程停止和子进程恢复这两件事情,就无需
      向父进程发送 SIGCHLD 信号。
   2. SA_NOCLDWAIT
      这个标志只用于 SIGCHLD 信号,它可控制子进程终止时候的行为,如果父进程
      为 SIGCHLD 设置了 SA_NOCLDWAIT 标志位,那么子进程终止退出时,就不会进入僵尸
      状态,而是直接自行了断。但是对 Linux 而言,子进程仍然会发送 SIGCHLD 信号,这
      点和上面的 SA_NOCLDSTOP 略有不同。
   3. SA_ONESHOT 和 SA_RESETHAND
      这两个标志位本质是一样的,表示信号处理函数是一次性的,信号递送出去以后,信号
      处理函数便恢复成默认值 SIG_DFL。
   4. SA_NODEFER 和 SA_NOMASK
      这两个标志位的作用是一样的,信号处理函数执行期间,不阻塞当前信号。
   5. SA_RESTART
      这个标志位表示,如果系统调用被信号中断,则不返回错误,而是自动重启系统调用。
   6. SA_SIGINFO
      这个标志位表示信号发送者会提供额外的信息。这种情况下,信号处理函数应该为
      三参数的函数。

siginfo_t 的内容如下

siginfo_t {
	int      si_signo;     /* Signal number */
	int      si_errno;     /* An errno value */
	int      si_code;      /* Signal code */
	int      si_trapno;    /* Trap number that caused
							 hardware-generated signal
							 (unused on most architectures) */
	pid_t    si_pid;       /* Sending process ID */
	uid_t    si_uid;       /* Real user ID of sending process */
	int      si_status;    /* Exit value or signal */
	clock_t  si_utime;     /* User time consumed */
	clock_t  si_stime;     /* System time consumed */
	sigval_t si_value;     /* Signal value */
	int      si_int;       /* POSIX.1b signal */
	void    *si_ptr;       /* POSIX.1b signal */
	int      si_overrun;   /* Timer overrun count;
							 POSIX.1b timers */
	int      si_timerid;   /* Timer ID; POSIX.1b timers */
	void    *si_addr;      /* Memory location which caused fault */
	long     si_band;      /* Band event (was int in
							 glibc 2.3.2 and earlier) */
	int      si_fd;        /* File descriptor */
	short    si_addr_lsb;  /* Least significant bit of address
							 (since Linux 2.6.32) */
	void    *si_lower;     /* Lower bound when address violation
							 occurred (since Linux 3.19) */
	void    *si_upper;     /* Upper bound when address violation
							 occurred (since Linux 3.19) */
	int      si_pkey;      /* Protection key on PTE that caused
							 fault (since Linux 4.6) */
	void    *si_call_addr; /* Address of system call instruction
							 (since Linux 3.5) */
	int      si_syscall;   /* Number of attempted system call
	unsigned int si_arch;  /* Architecture of attempted system call
							 (since Linux 3.5) */
}

常用信号

其中SIGSTOP以及SIGKILL 无法被捕获和忽略。无法被捕获指不能指定用户的处理函数。

Signal Description
SIGABRT 由调用abort函数产生,进程非正常退出
SIGALRM 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时
SIGBUS 某种特定的硬件异常,通常由内存访问引起
SIGCANCEL 由Solaris Thread Library内部使用,通常不会使用
SIGCHLD 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略
SIGCONT 当被stop的进程恢复运行的时候,自动发送
SIGEMT 和实现相关的硬件异常
SIGFPE 数学相关的异常,如被0除,浮点溢出,等等
SIGFREEZE Solaris专用,Hiberate或者Suspended时候发送
SIGHUP 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送
SIGILL 非法指令异常
SIGINFO BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程
SIGINT 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程
SIGIO 异步IO事件
SIGIOT 实现相关的硬件异常,一般对应SIGABRT
SIGKILL 无法处理和忽略。中止某个进程
SIGLWP 由Solaris Thread Libray内部使用
SIGPIPE 在reader中止之后写Pipe的时候发送
SIGPOLL 当某个事件发送给Pollable Device的时候发送
SIGPROF Setitimer指定的Profiling Interval Timer所产生
SIGPWR 和系统相关。和UPS相关。
SIGQUIT 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程
SIGSEGV 非法内存访问
SIGSTKFLT Linux专用,数学协处理器的栈异常
SIGSTOP 中止进程。无法处理和忽略。
SIGSYS 非法系统调用
SIGTERM 请求中止进程,kill命令缺省发送
SIGTHAW Solaris专用,从Suspend恢复时候发送
SIGTRAP 实现相关的硬件异常。一般是调试异常
SIGTSTP Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程
SIGTTIN 当Background Group的进程尝试读取Terminal的时候发送
SIGTTOU 当Background Group的进程尝试写Terminal的时候发送
SIGURG 当out-of-band data接收的时候可能发送
SIGUSR1 用户自定义signal 1
SIGUSR2 用户自定义signal 2
SIGVTALRM setitimer函数设置的Virtual Interval Timer超时的时候
SIGWAITING Solaris Thread Library内部实现专用
SIGWINCH 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程
SIGXCPU 当CPU时间限制超时的时候
SIGXFSZ 进程超过文件大小限制
SIGXRES Solaris专用,进程超过资源限制的时候发送

信号的处理

接收到信号后的处理动作有以下三种:

  • 忽略此信号
  • 执行信号的默认动作
  • 提供一个信号处理函数,要求内核处理该信号时切换到用户执行这个处理函数,这种方式称为捕捉一个信号。

实际上我们还可以在接收信号前屏蔽它,叫做信号阻塞,这里不详述,感兴趣的可以再查阅资料。

注意:忽略此信号是指接收到了信号后不做处理,阻塞的意思是信号不送达,但是还是有可能在我们更改信号的处理方式后被处理的,比如将忽略信号调整为执行默认的动作后不再阻塞该信号。

信号 SIGCHLD 的产生条件

  • 子进程终止时
  • 子进程接收到 SIGSTOP 信号停止时
  • 子进程处在停止态,接受到 SIGCONT 后唤醒时

wait & waitpid

自看宝典

pid_t wait(int *status);
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
pid_t waitpid(pid_t pid, int *status, int option);
wait(&status) == waitpid(-1, &status, 0);

These system calls are used to wait for state changes in a child of the calling process, and obtain information about the child whose state has changed. A state change is considered to be:

  • the child terminated;
  • the child was stopped by a signal;
  • or the child was resumed by a signal.

In the case of a terminated child, performing a wait allows the system to release the resources associated with the child; if a wait is not performed, then the terminated child remains in a “zombie” state.

pid 参数

  • pid == -1 等待任一子进程。于是在这一功能方面 waitpid 与 wait 等效。
  • pid > 0 等待其进程 ID 与 pid 相等的子进程。
  • pid == 0 等待其组 ID 等于调用进程的组 ID 的任一子进程。换句话说是与调用者进程同在一个组的进程。
  • pid < -1 等待其组 ID 等于 pid 的绝对值的任一子进程。

option 参数

By default, waitpid() waits only for terminated children, but this behavior is modifiable via the options argument, as described below. The value of options is an OR of zero or more of the following constants:

option description
WNOHANG 没有子进程结束,立即返回
WUNTRACED 如果子进程由于被停止产生的SIGCHLD,waitpid则立即返回
WCONTINUED 如果子进程由于被SIGCONT唤醒而产生的SIGCHLD,waitpid则立即返回

返回值

如果成功返回等待子进程的 ID,失败返回-1。

获取 status

Marcro description
WIFEXITED(status) 子进程正常exit终止,返回真
WEXITSTATUS(status) 返回子进程正常退出值
WIFSIGNALED(status) 子进程被信号终止,返回真
WTERMSIG(status) 返回终止子进程的信号值
WIFSTOPPED(status) 子进程被信号停止,返回真
WSTOPSIG(status) 返回停止子进程的信号值
WIFCONTINUED(status) 子进程被信号SIGCONT恢复运行,返回真
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>

void _exit1(void) {
    printf("parent _exit1\n");
}

void _exit2(void) {
    printf("child _exit2\n");
}

void signalhandler(int signum) {
    if (signum == SIGIO) {
       printf("pid: %d catch SIGIO\n", getpid());
    } else if (signum == SIGUSR2) {
       printf("pid: %d catch SIGUSR2\n", getpid());
    } else {
       printf("pid: %d catch error\n", getpid());
    }
}

int main(int argc, char *argv[]) {
    pid_t pid, ret;
    int status;
    atexit(_exit1); // child inherit the funtion

    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(EXIT_FAILURE);
    }

    if( pid == 0) {
        //signal(SIGUSR2, SIG_DFL); // exit abnomal with SIGUSR2
        signal(SIGUSR2, signalhandler); // child handle this signal and exit normally
        atexit(_exit2); // called first
        printf("This is the child process\n");
        //exit(100);  // atexit functions called
        //_exit(200); // no atexit functions called
        //abort();    // exit abnormal with SIGABORT
        //raise(SIGSTOP); // raise SIGSTOP to itself
        raise(SIGUSR2); // raise SIGUSER2 to itself
        return 0;
    }
    
    ret = waitpid(pid, &status, 0 | WUNTRACED); // wait for child termial or stopped
    if (ret < 0) {
        perror("wait error");
        exit(EXIT_FAILURE);
    }

    printf("parent waitpid: ret = %d pid = %d\n", ret, pid);

    if (WIFEXITED(status)) {
        printf("child exited normal exit status = %d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("child exited abnormal signal number = %d\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
        printf("child stoped signal number = %d\n", WSTOPSIG(status));
    }

    return 0;
}

在这里插入图片描述

信号的发送

sigqueue

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {
   int   sival_int;
   void *sival_ptr;
 };

raise

raise(signum)

kill

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

日常我们想结束一个进程会使用这样的命令,kill -9 pid,实际上就是在向进程号为 pid 的进程发送 SIGKILL

关于 kill 函数,还有一点需要额外说明, kill 函数传入的 pid 可以是小于等于0的整数。
pid > 0:将发送给该 pid 的进程
pid == 0: 将会把信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限向这些进程发送信号。
pid < 0:将信号发送给进程组ID 为 pid 的绝对值的,并且发送进程具有权限向其发送信号的所有进程。
pid == -1:将该信号发送给发送进程的有权限向他发送信号的所有进程。

关于信号,还有更多的话题,比如,信号是否都能够准确的送达到目标进程呢?
答案其实是不一定,因为有可靠信号和不可靠信号的区分。

可靠信号与不可靠信号

对于信号来说,信号编号<=31的信号都是不可靠信号,之后的信号为可靠信号,系统会根据有信号队列,将信号在递达之前进行阻塞。信号的阻塞和未决是通过信号的状态字来管理的,状态字按位管理。每个信号都有独立的阻塞字,规定了当前要阻塞到达该进程的信号集。

  • 信号阻塞状态字(block),1代表阻塞、0代表不阻塞;
  • 信号未决状态字(pending),1代表未决,0代表信号可以抵达了。

阻塞状态字用户可读写,未决状态字用户只读,它由内核来设置,表示信号递达状态。

block 设置

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how 变量决定了是如何操作该状态字。

  • SIG_BLOCK:set 包含了我们希望添加到当前信号阻塞字的信号,相当于mask=mask|set
  • SIG_UNBLOCK:set 包含了我们希望从当前信号阻塞字中解除阻塞的信号,相当于mask=mask&~set
  • SIG_SETMASK:设置当前信号阻塞字为set所指的值,相当于mask=set

上面的函数通过信号集的数据结构来进行信号阻塞的管理。
而信号集本身可以通过以下的函数进行配置。

#include <signal.h>
int sigemptyset(sigset_t *set);           // 初始化 set 中传入的信号集,清空其中所有信号
int sigfillset(sigset_t *set);            // 把信号集填1,让 set 包含所有的信号
int sigaddset(sigset_t *set, int signum); // 信号集对应位置 1
int sigdelset(sigset_t *set, int signum); // 信号集对应位清 0
int sigismember(const sigset_t *set, int signum); // 判断 signal 是否在信号集

常用的步骤如下:

  • 分配内存空间 sigset bset;
  • 清空信号集,sigempty(&bset);
  • 添加要处理的信号 sigaddset(&bset, SIGINT);
  • 继续添加其他的信号…
  • 设置相关的处理方案,阻塞与否。sigprocmask(SIG_UNBLOCK, &bset, NULL);

  1. https://jameshfisher.com/2017/01/10/c-signal-return-value/ ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章