Linux信号的系统调用与示例

 
Category: Linux-Shell

写在前面

总结一下信号处理部分的系统调用与示例

信号基础

简介

信号(signal), 又称软件中断, 是系统中事件发生时对进程的通知机制. 也正是这种通知机制, 打断了程序执行的正常流程, 所以其与硬件中断很相似.

一个具有合适权限的进程可以向另一进程发送信号, 所以信号可以作为一种进程间通信方式(IPC). 同时, 进程也可以向自身发送信号, 一些常见的引发内核为进程产生信号的各类事件如下:

  • 硬件异常: 硬件错误, 内存访问错误(段错误), 除零异常等
  • 特殊字符: 用户输入了Ctrl+C或者Ctrl+Z或者Ctrl+\.
  • 软件事件: 调整程序窗口大小, 进程定时器到期, 进程运行时间超限.

signal.h头文件中详细定义了上述提到的各种信号以及符号对应的常量值. 主要分为两大类

  1. 内核向进程通知事件: 构成传统(标准)信号, 标准信号的编号范围在1~31.
  2. 实时信号:

信号因某些事件而产生, 信号产生后, 会于稍后传递给某一进程, 而进程也会采取某些措施来响应信号.

在产生和到达期间, 信号处于等待状态(pending, 挂起).

信号到达后的操作

默认操作

  1. 忽略信号
  2. 终止(杀死, kill)进程
  3. 产生核心转储文件
  4. 停止进程(暂停)
  5. 恢复被暂停的进程

自定义行为

  • 默认行为: 相当于撤销之前对信号处置的更改, 恢复默认设置
  • 忽略信号
  • $\bigstar$自定义信号处理器程序

信号掩码(mask)

通常, 一旦内核接下来要调度某进程运行, 那么等待信号就会立刻送达, 或者如果进程正在运行, 则会立即传递信号. 但是有时候需要确保一段代码不为传递来的信号所中断, 为了这样做成为可能, 就可将信号添加到对应进程的信号掩码中, 以此阻塞信号的到达.

如果所产生的信号在信号的阻塞范围内, 那么信号将保持等待状态, 直至稍后对其解除阻塞(移除信号掩码).

信号处理器: 改变信号处置

signal

#include <signal.h>

void ( *signal(int sig, void (*handler)(int)) ) (int);

这个函数签名看起来比较复杂, 之前我介绍过这块内容, 就是关于C语言函数指针的声明方法的, 可以参考一下:

C语言函数指针在形参列表和返回值中的函数声明写法 - Zorch’s Blog (apocaly-pse.github.io);

通过typedef重写函数声明如下:

typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);

一个简单的信号处理器函数

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

// 自定义的信号处理器
void sigHandler(int sig) { printf("Ouch!\n"); }

int main(int argc, char *argv[]) {
    int j;
    if (signal(SIGINT, sigHandler) == SIG_ERR) fprintf(stderr, "signal\n");
    for (j = 0;; ++j) {
        printf("%d\n", j);
        sleep(3);
    }
    return 0;
}

执行:

$ ./a.out
0
^COuch!
1
^COuch!
2
^COuch!
3
^\Quit (core dumped)

捕获不同信号的处理函数

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
static void sigHandler(int sig) {
    static int count = 0;

    if (sig == SIGINT) {
        count++;
        printf("Caught SIGINT (%d)\n", count);
        return; /* Resume execution at point of interruption */
    }

    /* Must be SIGQUIT - print a message and terminate the process */

    printf("Caught SIGQUIT - that's all folks!\n");
    exit(EXIT_SUCCESS);
}

int main(int argc, char *argv[]) {
    if (signal(SIGINT, sigHandler) == SIG_ERR) fprintf(stderr, "signal");
    if (signal(SIGQUIT, sigHandler) == SIG_ERR) fprintf(stderr, "signal");

    for (;;)     /* Loop forever, waiting for signals */
        pause(); /* Block until a signal is caught */
}

执行:

$ ./a.out
^CCaught SIGINT (1)
^CCaught SIGINT (2)
^CCaught SIGINT (3)
^CCaught SIGINT (4)
^CCaught SIGINT (5)
^\Caught SIGQUIT - that's all folks!

sigaction

指定进程发送信号: kill

#include <signal.h>

int kill(pid_t pid, int sig); 

pid参数标识一个或多个目标进程, 而sig指定了要发送的信号, pid有以下几种情况:

  • $pid>0$: 发送信号给指定进程
  • $pid=0$: 发送信号给与调用进程同组的每一个进程, 也包括调用者(进程)自身.
  • $pid<-1$: 向组ID等于该pid绝对值的进程组内所有下属进程发送信号.
  • $pid=-1$: 调用进程有权将信号发往每一个目标进程, 除去init(pid=1)进程和调用进程自身. 如果特权级进程发起这一调用, 那么会发送信号给系统中的所有进程, 上述两个进程除外. (广播信号)
  • 无匹配pid的进程, 则kill调用失败, 返回-1, 并置errno为ESRCH(无此进程)

权限规则

  1. 特权级进程可以向任何进程发送信号
  2. 以root用户和组运行的init进程(pid=1)是特例, 仅能接收已安装了处理器函数的信号.
  3. 特殊处理SIGCONT信号.
  4. 若无权限, 则调用失败, errno置为EPERM.

检查进程是否存在

kill(pid, 0);

表示无信号发送, 此时kill仅会执行错误检查, 查看是否可以向目标进程发送信号.

可以使用空信号检测具有特定进程ID的进程是否存在.

例子

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char *argv[]) {
    int s, sig;
    pid_t pid = 702158;
    sig = 13;
    s = kill(pid, sig);

    if (sig) {
        if (s == -1) fprintf(stderr, "kill error\n");
    } else {
        if (s == 0)
            printf("process exists and we can send it a signal\n");
        else {
            switch (errno) {
                case EPERM:
                    printf(
                        "process exists, but we don't have permission to send "
                        "it a signal\n");
                case ESRCH:
                    printf("process does not exist\n");
                default:
                    fprintf(stderr, "kill\n");
            }
        }
    }
    return 0;
}

实验:

$ sleep 30 &
[1] 702158
$ cc kill-1.c && ./a.out
[1]+  Broken pipe             sleep 30

显示信号描述信息

用两种方法来读取, 以13号信号管道破裂(broken pipe)信号为例:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#define getsig(x) (#x)

extern const char* const sys_siglist[];
void t1() {
    int sig = SIGPIPE;
    printf("sig:%d, %s, info:%s\n", sig, getsig(SIGPIPE), sys_siglist[sig]);
    /* sig:13, SIGPIPE, info:Broken pipe */
}

extern char* strsignal(int);
void t2() {
    printf("sig:%d, %s, info:%s\n", SIGPIPE, getsig(SIGPIPE),
           strsignal(SIGPIPE));
    /* sig:13, SIGPIPE, info:Broken pipe */
}

int main(int argc, char* argv[]) {
    /* t1(); */
    t2();
    return 0;
}

信号集: sigset

多个信号可以用一个信号集来表示, 系统数据类型为sigset_t.

#include <signal.h>.

基本系统调用

  • int sigemptyset(sigset_t *set); 初始化一个未包含任何成员的信号集
  • int sigfillset(sigset_t *set*); 初始化一个信号集, 使其包含所有信号(所有实时信号)
  • int sigaddset(sigset_t *set, int sig); 向信号集中添加一个信号
  • int sigdelset(sigset_t *set, int sig); 向信号集中删除一个信号
  • int sigismember(const sigset_t *set, int sig); 判断信号sig是否为信号集中成员

  • int sigandset(sigset_t *dest, sigset_t *left, sigset_t *right); 交集置于dest
  • int sigorset(sigset_t *dest, sigset_t *left, sigset_t *right); 并集置于dest
  • int sigisemptyset(const sigset_t *set); 若信号集未包含任何信号, 返回true(1)

信号掩码部分

  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 显式向信号掩码添加或移除信号, 修改或获取信号掩码
  • int sigpending(sigset_t *set); 确定进程中处于等待状态的信号

实例: 信号集上的操作

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>


void printSigset(FILE *of, const char *prefix, const sigset_t *sigset) {
    int sig, cnt;
    cnt = 0;
    for (sig = 1; sig < NSIG; ++sig) {
        if (sigismember(sigset, sig)) {
            ++cnt;
            fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
        }
    }

    if (cnt == 0) fprintf(of, "%s<empty signal set>\n", prefix);
}

// 显示进程的信号掩码
int printSigMask(FILE *of, const char *msg) {
    sigset_t currMask;
    if (msg != NULL) fprintf(of, "%s\n", msg);

    // 显式向信号掩码添加或移除信号, 修改或获取信号掩码
    if (sigprocmask(SIG_BLOCK, NULL, &currMask) == -1) return -1;
    printSigset(of, "\t\t", &currMask);
    return 0;
}

// 显示当前处于等待状态的信号集
int printPendingSigs(FILE *of, const char *msg) {
    sigset_t pendingSigs;
    if (msg != NULL) fprintf(of, "%s\n", msg);

    if (sigpending(&pendingSigs) == -1) return -1;
    printSigset(of, "\t\t", &pendingSigs);
    return 0;
}

void t1() {
    sigset_t set;
    printSigset(stdout, "prefix ", &set); // prefix <empty signal set>
    sigaddset(&set, SIGKILL);
    printSigset(stdout, "prefix ", &set); // prefix 9 (Killed)
    sigaddset(&set, SIGPIPE);
    printSigset(stdout, "prefix ", &set);
    /* prefix 9 (Killed) */
    /* prefix 13 (Broken pipe) */
}

void t2() {
    sigset_t set;
    sigaddset(&set, SIGKILL);
    printSigMask(stdout, NULL);
}

int main(int argc, char *argv[]) {
    // test
    /* t1(); */
    t2();
    return 0;
}

实例: 发送多个信号

等待信号集只是一个掩码, 仅表明一个信号是否发生, 而不显示其发生的次数, 所以, 如果一个信号在阻塞状态下产生多次, 则会将其记录在等待信号集中, 并在稍后只传递一次(从上面的遍历方式也可看出, 信号集的存储只有一次)

void t1() {
    sigset_t set;
    printSigset(stdout, "prefix ", &set); // prefix <empty signal set>
    sigaddset(&set, SIGKILL);
    printSigset(stdout, "prefix ", &set); // prefix 9 (Killed)
    printf("-------------\n");
    sigaddset(&set, SIGKILL);
    printSigset(stdout, "prefix ", &set);
}