写在前面
总结一下信号处理部分的系统调用与示例
信号基础
简介
信号(signal), 又称软件中断, 是系统中事件发生时对进程的通知机制. 也正是这种通知机制, 打断了程序执行的正常流程, 所以其与硬件中断很相似.
一个具有合适权限的进程可以向另一进程发送信号, 所以信号可以作为一种进程间通信方式(IPC). 同时, 进程也可以向自身发送信号, 一些常见的引发内核为进程产生信号的各类事件如下:
- 硬件异常: 硬件错误, 内存访问错误(段错误), 除零异常等
- 特殊字符: 用户输入了
Ctrl+C
或者Ctrl+Z
或者Ctrl+\
. - 软件事件: 调整程序窗口大小, 进程定时器到期, 进程运行时间超限.
在signal.h
头文件中详细定义了上述提到的各种信号以及符号对应的常量值. 主要分为两大类
- 内核向进程通知事件: 构成传统(标准)信号, 标准信号的编号范围在
1~31
. - 实时信号:
信号因某些事件而产生, 信号产生后, 会于稍后传递给某一进程, 而进程也会采取某些措施来响应信号.
在产生和到达期间, 信号处于等待状态(pending, 挂起).
信号到达后的操作
默认操作
- 忽略信号
- 终止(杀死, kill)进程
- 产生核心转储文件
- 停止进程(暂停)
- 恢复被暂停的进程
自定义行为
- 默认行为: 相当于撤销之前对信号处置的更改, 恢复默认设置
- 忽略信号
- $\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
(无此进程)
权限规则
- 特权级进程可以向任何进程发送信号
- 以root用户和组运行的init进程(pid=1)是特例, 仅能接收已安装了处理器函数的信号.
- 特殊处理
SIGCONT
信号. - 若无权限, 则调用失败, 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);
交集置于destint sigorset(sigset_t *dest, sigset_t *left, sigset_t *right);
并集置于destint 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);
}