信号
中断键(interrupt key, Delete 或 Ctrl+C) 退出键(quit key, Ctrl+)
# 信号相关概念
#查看信号1~31为非实时信号,34~64为实时信号
kill -l
#查看信号说明
man 7 signal
# 信号的状态
- 产生 按键、系统调用、命令、软件条件、硬件条件
- 未决状态(没有被处理的状态,等待被进程处理)
- 递达(递送并且到达进程)
# 信号的处理方式:
- 执行默认动作
- 忽略(丢弃)
- 捕捉(调用户处理函数)
# 信号的特质
信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常短,不易察觉。 Linux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。
- 阻塞信号集(信号屏蔽字): 将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)
- 未决信号集:
- 信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
- 信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
警告
- SIGKILL 和19) SIGSTOP信号,不允许屏蔽、忽略和捕捉,只能执行默认动作。
信号是由内核产生发送的,信号的优先级比较高,进程收到信号之后,暂停正在处理的工作,优先处理信号,处理完成之后再继续暂停的工作。
# 产生信号函数
# kill函数/命令产生信号
kill命令产生信号:kill -SIGKILL 进程ID kill函数:给指定进程发送指定信号(不一定杀死)
//成功:0;失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno
int kill(pid_t pid, int sig);
//sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
//pid > 0: 发送信号给指定的进程。
//pid = 0: 发送信号给与调用kill函数进程属于同一进程组的所有进程。
//pid < -1: 取 |pid| 发给对应进程组。
//pid = -1:发送给进程有权限发送的系统中所有进程。
//进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。
//权限保护:super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。 kill -9 (root用户的pid) 是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 只能向自己创建的进程发送信号。普通用户基本规则是:发送者实际或有效用户ID == 接收者实际或有效用户ID
//kill(getppid(), SIGKILL);
# raise和abort系统函数
- raise 函数:给当前进程发送指定信号(自己给自己发) raise(signo) == kill(getpid(), signo); int raise(int sig); 成功:0,失败非0值
- abort 函数:给自己发送异常终止信号 6) SIGABRT 信号,终止并产生core文件 void abort(void); 该函数无返回
# 软件条件产生信号
# alarm函数
设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALARM信号。进程收到该信号,默认动作终止。 每个进程都有且只有唯一的一个定时器。 unsigned int alarm(unsigned int seconds); 返回0或剩余的秒数,无失败。 常用:取消定时器alarm(0),返回旧闹钟余下秒数。 例:alarm(5) → 3sec → alarm(4) → 5sec → alarm(5) → alarm(0) 定时,与进程状态无关(自然定时法)!就绪、运行、挂起(阻塞、暂停)、终止、僵尸...无论进程处于何种状态,alarm都计时。
//练习:编写程序,测试你使用的计算机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命令查看程序执行的时间。程序运行的瓶颈在于IO,优化程序,首选优化IO。 time ./a.out //实际执行时间 = 系统时间 + 用户时间 + 等待时间
# setitimer函数
设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
//成功:0;失败:-1,设置errno
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
//参数:which:指定定时方式
// ① 自然定时:ITIMER_REAL → 14)SIGLARM 计算自然时间
// ② 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间
// ③ 运行时计时(用户+内核):ITIMER_PROF → 27)SIGPROF 计算占用cpu及执行系统调用的时间
struct itimerval{
struct timeval it_interval; //定时器循环周期
struct timeval it_value; //第一次触发定时器的时间
};
struct timeval{
time_t tv_sec; //seconds
suseconds_t tv_usec; //microseconds
};
# 终端按键产生信号
Ctrl + c → 2) SIGINT(终止/中断) "INT" ----Interrupt Ctrl + z → 20) SIGTSTP(暂停/停止) "T" ----Terminal 终端。 Ctrl + \ → 3) SIGQUIT(退出)
# 硬件异常产生信号
除0操作 → 8) SIGFPE (浮点数例外) "F" -----float 浮点数。 非法访问内存 → 11) SIGSEGV (段错误) 总线错误 → 7) SIGBUS
# 信号集操作函数
信号产生,信号处于未决状态,进程收到信号之后,信号被放入未决信号集;放入未决信号集中信号等待处理,再处理之前需要做意见事情,判断阻塞信号集中该信号对应的标志位是否为1,如果为1,不处理,如果为0,则处理该信号;当阻塞信号集中该信号对应的标志位为0时,该信号被处理。
# 自定义信号集设定
//将某个信号集清0 成功:0;失败:-1,设置errno
int sigemptyset(sigset_t *set);
//将某个信号集置1 成功:0;失败:-1,设置errno
int sigfillset(sigset_t *set);
//将某个信号加入信号集合中 成功:0;失败:-1,设置errno
int sigaddset(sigset_t *set, int signum);
//将某信号从信号清出信号集 成功:0;失败:-1,设置errno
int sigdelset(sigset_t *set, int signum);
//判断某个信号是否在信号集中:在:1;不在:0;出错:-1,设置errno
int sigismember(const sigset_t *set, int signum);
//除sigismember外,其余操作函数中的set均为传出参数。sigset_t类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
//对比认知select 函数。
# sigprocmask函数
用来屏蔽信号、解除屏蔽也使用该函数。其本质,读取或修改进程控制块中的信号屏蔽字。 严格注意,屏蔽信号:只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。 将自定义信号集设置给内核阻塞信号集
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 成功:0;失败:-1,设置errno
// 参数:
// set:传入参数,是一个自定义信号集合。由参数how来指示如何修改当前信号屏蔽字。
// oldset:传出参数,保存旧的信号屏蔽字。
// how参数取值:假设当前的信号屏蔽字为mask
// 1. SIG_BLOCK: 当how设置为此值,set表示需要屏蔽的信号。相当于 mask = mask|set
// 2. SIG_UNBLOCK: 当how设置为此,set表示需要解除屏蔽的信号。相当于 mask = mask & ~set
// 3. SIG_SETMASK: 当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask = set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
# sigpending函数
读取当前进程的未决信号集 int sigpending(sigset_t *set); set传出参数。 返回值:成功:0;失败:-1,设置errno 练习:编写程序。把所有常规信号的未决状态打印至屏幕。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void printset(sigset_t *ped)
{
int i;
for(i = 1; i < 32; i++){
if((sigismember(ped, i) == 1)){
putchar('1');
} else {
putchar('0');
}
}
printf("\n");
}
int main(void)
{
sigset_t set, ped;
#if 1
sigemptyset(&set);
sigaddset(&set, SIGINT);
#else
sigaddset(&set, SIGSEGV);
sigaddset(&set, SIGKILL);
sigaddset(&set, SIGQUIT);
sigfillset(&set);
#endif
sigprocmask(SIG_BLOCK, &set, NULL); //不获取原屏蔽字
while (1) {
sigpending(&ped); //获取未决信号集
printset(&ped);
sleep(1);
}
return 0;
}
# 信号捕捉
# signal函数
//注册一个信号捕捉函数:
typedef void (*sighandler_t)(int);
//成功:返回函数指针;失败:返回SIG_ERR,设置errno
sighandler_t signal(int signum, sighandler_t handler);
//该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用
sigaction函数。
void (*signal(int signum, void (*sighandler_t)(int))) (int);
能看出这个函数代表什么意思吗? 注意多在复杂结构中使用typedef。
# sigaction函数
//修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)
//成功:0;失败:-1,设置errno
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
// 参数:
// act:传入参数,新的处理方式。
// 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_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作
// ② sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
// ③ sa_flags:通常设置为0,表使用默认属性。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
/*自定义的信号捕捉函数*/
void sig_int(int signo)
{
printf("catch signal SIGINT\n");//单次打印
sleep(10);
printf("----slept 10 s\n");
}
int main(void)
{
struct sigaction act;
act.sa_handler = sig_int;
act.sa_flags = 0;
sigemptyset(&act.sa_mask); //不屏蔽任何信号
sigaddset(&act.sa_mask, SIGQUIT);
sigaction(SIGINT, &act, NULL);
printf("------------main slept 10\n");
sleep(10);
while(1);//该循环只是为了保证有足够的时间来测试函数特性
return 0;
}