linux系统编程-信号
文章目录
0x00 . 基本概念
信号4要素:
- 编号
- 名称
- 事件
- 默认处理动作
信号特征:
- 简单,开销小;
- 不能携带大量信息;
- 满足某个特设条件才发送。
信号是软件层面上实现的中断,早期常被称为“软中断”。
时钟中断基于硬件
由于信号是通过软件方法实现,所以信号有延时性,cpu可以察觉。
每个进程收到的所有信号,都是由内核负责发送的,内核处理。
kill -l
显示信号列表。
1-31为普通信号
34-64为实时信号,用于嵌入式开发。
常用按键信号:
Ctrl + c
,2-SIGINT(终止/中断)
Ctrl + z
,20-SIGTSTP(暂停/停止),和19-SIGSTP有区别。
Ctrl + \
,3-SIGQUIT(退出)
man 7 signal
查看信号帮助文档。
有些信号有3个值,对应不同平台,linux对应中间的值。
文档中有一句要注意:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. 只能执行默认动作。甚至不能将其设置为阻塞。
产生信号的方式
- 按键产生,如:Ctrl+c、Ctrl+z、Ctrl+\
- 系统调用产生,如:kill、raise、abort
- 软件条件产生,如:定时器alarm
- 硬件异常产生,如:非法访问内存(段错误)、除0(浮点数例外)、内存对齐出错(总线错误)
- 命令产生,如:kill命令
信号处理
递达:递送并且到达进程。
未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。
信号的处理方式::
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用其它处理函数)
默认动作:
- Term:终止进程;
- Ign: 忽略信号 (默认即时对该种信号忽略操作),比如内核通知父进程回收子进程;
- Core:终止进程,生成Core文件。(查验进程死亡原因, 用于gdb调试,和
-g
有关); - Stop:停止(暂停)进程;
- Cont:继续运行进程。
0x01. 未决信号集和阻塞信号集
PCB(task_struct
)还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
它们都是set
。
阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽某信号后,再收到该信号时,对该信号的处理将推迟到解除屏蔽。
未决信号集
信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合叫做未决信号集。在屏蔽解除前,信号一直处于未决状态。
0x02. kill()
int kill(pid_t pid, int sig);
成功返回0;失败返回-1。
第2个参数建议使用宏,因为不同系统编号可能不同。
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程。
pid < 0: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
super用户(root)可以发送信号给任意用户,普通用户只能向自己创建的进程发送信号。
//杀死第3个子进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#define N 5
int main(void)
{
int i;
pid_t pid, q;
raise(SIGSEGV);
abort();
for (i = 0; i < N; i++) {
pid = fork();
if (pid == 0)
break;
if (i == 2)
q = pid;
}
if (i < 5) {
while(1) {
printf("I'm child %d, getpid = %u\n", i, getpid());
sleep(1);
}
} else {
sleep(1);
kill(q, SIGKILL);
while (1);
}
return 0;
}
raise() & abort()
int raise(int sig);
成功返回0,失败返回非0值。给自己发送信号,相当于kill(getpid(), signo);
void abort(void);
给自己发送异常终止信号 6) SIGABRT
信号,终止并产生core文件。
0x03. alarm()
每个进程都有且只有唯一个定时器。
unsigned int alarm(unsigned int seconds);
返回0或剩余的秒数,无失败。
到达时间后,内核会给当前进程发送14)SIGALRM
信号。
alarm(5);
sleep(3);
alarm(0); //return 2
alarm(0)
相当于终止闹钟。
//测试1秒内运算次数
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int i;
alarm(1);
for(i = 0; ; i++)
printf("%d\n", i);
return 0;
}
time ./a.out
可以显示执行时间,real = user + sys + wait
,会发现io等待时间很多。
time ./a.out > out
,则实际时间约等于用户运行时间+内核运行时间
setitimer()
可代替alarm函数。精度微秒us,可以实现周期定时。
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
成功返回0;失败返回-1,设置errno。
参数which
指定定时方式:
ITIMER_REAL
,自然定时 ,14)SIGLARM
ITIMER_VIRTUAL
,虚拟空间计时(用户空间),26)SIGVTALRM
,只计算进程占用cpu的时间ITIMER_PROF
,运行时计时(用户+内核),27)SIGPROF
,计算占用cpu及执行系统调用的时间
注意两个结构体。
struct itimerval {
struct timeval it_interval; /* Interval for periodic timer ,两次定时任务之间间隔的时间*/
struct timeval it_value; /* Time until next expiration */
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
//测试1秒内运算次数
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
//手动实现alarm()
unsigned int my_alarm(unsigned int sec)
{
struct itimerval it, oldit;
int ret;
it.it_value.tv_sec = sec;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &it, &oldit);
if (ret == -1) {
perror("setitimer");
exit(1);
}
return oldit.it_value.tv_sec;
}
int main(void)
{
int i;
my_alarm(1); //alarm(sec);
for(i = 0; ; i++)
printf("%d\n", i);
return 0;
}
0x04. 信号集操作函数
内核通过读取未决信号集来判断信号是否应被处理。
信号屏蔽字mask可以影响未决信号集,所以可以在应用程序中自定义set来改变mask,达到屏蔽指定信号的目的。
sigset_t set; // typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set);
将某个信号集清0
int sigfillset(sigset_t *set);
将某个信号集置1
int sigaddset(sigset_t *set, int signum);
将某个信号加入信号集
int sigdelset(sigset_t *set, int signum);
将某个信号清出信号集
以上返回值,成功0,失败-1
int sigismember(const sigset_t *set, int signum);
判断某个信号是否在信号集中 返回值:在集合1,不在0,出错-1
sigset_t
类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
下面这个函数用了屏蔽信号和接触屏蔽,本质是读取或修改进程PCB中的信号屏蔽字。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
set
:传入参数,是一个位图,set中哪位置1,就表示当前进程屏蔽哪个信号。
oldset
:传出参数,保存旧的信号屏蔽集。
how
,假设当前的信号屏蔽字为mask:
SIG_BLOCK
:set
表示需要屏蔽的信号,mask= mask|set
SIG_UNBLOCK
:set
表示需要解除屏蔽的信号,mask= mask& ~set
SIG_SETMASK
:set
表示用于替代原始屏蔽集的新屏蔽集,mask= set
//将3号信号置为1:
sigemptyset(&set);
sigaddset(&set, 3);
sigprocmask(SIG_BLOCK,&set,&oldset);
int sigpending(sigset_t *set);
读取当前进程的未决信号集并传给传出参数。返回值:成功0,失败-1并设置errno。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void printped(sigset_t *ped)
{
int i;
for (i = 1; i < 32; i++) {
if (sigismember(ped, i)) {
putchar('1');
} else {
putchar('0');
}
}
printf("\n");
}
int main(void)
{
sigset_t myset, oldset, ped;
sigemptyset(&myset);
sigaddset(&myset, SIGQUIT);
sigaddset(&myset, SIGINT); // not working when kill -9
sigprocmask(SIG_BLOCK, &myset, &oldset);
while (1) {
sigpending(&ped);
printped(&ped);
sleep(1);
}
return 0;
}
/*
0000000000000000000000000000000
^\0010000000000000000000000000000
0010000000000000000000000000000
*/
0x05. 信号捕捉
signal()
该函数注册一个信号捕捉函数.
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
void myfunc(int signo)
{
printf("hello world\n");
raise(SIGALRM);
}
int main(void)
{
struct itimerval it, oldit;
signal(SIGALRM, myfunc);
it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 3;
it.it_interval.tv_usec = 0;
if(setitimer(ITIMER_REAL, &it, &oldit) == -1){
perror("setitimer error");
return -1;
}
while(1);
return 0;
}
//
#include <signal.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
typedef void (*sighandler_t) (int);
void catchsigint(int signo)
{
printf("-----------------catch\n");
}
int main(void)
{
sighandler_t handler;
handler = signal(SIGINT, catchsigint);
if (handler == SIG_ERR) {
perror("signal error");
exit(1);
}
while (1);
return 0;
}
sigaction()
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_restorer
:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction
:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
sa_flags
:通常设置为0,表使用默认属性。
sa_handler
注册函数也可赋值为SIG_IGN
忽略 或 SIG_DFL
执行默认动作
sa_mask
:调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意,仅在处理函数被调用期间屏蔽生效,是临时性设置。
关于信号捕捉特性
进程正常运行时,默认PCB中有一个信号屏蔽字。
捕捉到信号执行注册函数时,有可能执行很长时间,在这期间所屏蔽的信号不由PCB信号屏蔽字来指定。而是由
sigaction.sa_mask
来指定。执行完后再恢复。
阻塞的常规信号不支持排队,产生多次只记录一次。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void docatch(int signo)
{
printf("%d signal is catched\n", signo);
sleep(10); //此时ctrl+c没有反应。
printf("-------finish------\n");
}
int main(void)
{
int ret;
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0; //默认属性 信号捕捉函数执行期间,自动屏蔽本信号
ret = sigaction(SIGINT, &act, NULL);
if (ret < 0) {
perror("sigaction error");
exit(1);
}
while (1);
return 0;
}