Day6
信号的基本概念
-
中断,中止(非终)当前正在运行的任务,转而执行其他任务(可能返回也可能不返回),中断分为硬件中断(硬件设备产生的中断)和软件中断(其他程序产生的中断)
-
信号:是一种软件中断,提供了一种异步执行任务的机制
-
常见的信号
SIGINT(2)
Ctrl+CSIGQUIT(3)
Ctrl+\SIGABRT(6)
调用abort函数,产生此信号SIGFPE(8)
表示一个算术运算异常,例如除以0、浮点溢出等SIGKILL(9)
不能被捕获或忽略。常用于杀死进程SIGSEGV(11)
试图访问未分配的内存,或向没有写权限的内存写入数据SIGCHLD(17)
在一个进程终止或停止时,将此信号发送给其父进程SIGTSTP(20)
Ctrl+Z
-
不可靠信号
kill -l
可以显示所有信号建立在早期机制上的信号被称为不可靠信号,1~31
不支持排队可能丢失,同一个信号产生多次,系统可能只接收到一次
-
可靠信号
采用新的机制产生的信号,34~64支持排队,不会丢失
-
信号的来源
-
硬件产生:除0,非法内存访问
这些异常是硬件(驱动)检测到,并通知内核,内核再向引发这些异常的进程发送相应信号
-
软件产生:通过
kill/raise/alarm/setitmer/sigqueue
函数
-
-
信号的处理
- 忽略
- 终止
- 终止进程并产生core文件
- 捕获信号并处理
信号 | 解释 | 产生条件 | 默认动作 |
---|---|---|---|
SIGHUP(1) | 连接断开信号 | 如果终端接口检测一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程) | 终止 |
SIGINT(2) | 终端中断符信号 | 用户按中断键(Ctrl+C),产生此信号,并送至前台进程组的所有进程 | 终止 |
SIGQUIT(3) | 终端退出符信号 | 用户按退出键(Ctrl+\),产生此信号,并送至前台进程组的所有进程 | 终止+core |
SIGILL(4) | 非法硬件指令信号 | 进程执行了一条非法硬件指令 | 终止+core |
SIGTRAP(5) | 硬件故障信号 | 指示一个实现定义的硬件故障。常用于调试 | 终止+core |
SIGABRT(6) | 异常终止信号 | 调用abort函数,产生此信号 | 终止+core |
SIGBUS(7) | 总线错误信号 | 指示一个实现定义的硬件故障,常用于内存故障 | 终止+core |
SIGFPE(8) | 算术异常信号 | 表示一个算术运算异常,例如除以0、浮点溢出等 | 终止+core |
SIGKILL(9) | 终止信号 | 不能被捕获或忽略。常用于杀死进程 | 终止 |
SIGUSR1(10) | 用户定义信号 | 用户定义信号,用于应用程序 | 终止 |
SIGSEGV(11) | 段错误信号 | 试图访问未分配的内存,或向没有写权限的内存写入数据 | 终止+core |
SIGUSR2(12) | 用户定义信号 | 用户定义信号,用于应用程序 | 终止 |
SIGPIPE(13) | 管道异常信号 | 写管道时读进程已终止,或写SOCK_STREAM类型套接字时连接已断开,均产生此信号 | 终止 |
SIGALRM(14) | 闹钟信号 | 以alarm函数设置的计时器到期,或以setitimer函数设置的间隔时间到期,均产生此信号 | 终止 |
SIGTERM(15) | 终止信号 | 由kill命令发送的系统默认终止信号 | 终止 |
SIGSTKFLT(16) | 数协器栈故障信号 | 表示数学协处理器发生栈故障 | 终止 |
SIGCHLD(17) | 子进程状态改变信号 | 在一个进程终止或停止时,将此信号发送给其父进程 | 忽略 |
SIGCONT(18) | 使停止的进程继续 | 向处于停止状态的进程发送此信号,令其继续运行 | 继续/忽略 |
SIGSTOP(19) | 停止信号 | 不能被捕获或忽略。停止一个进程 | 停止进程 |
SIGTSTP(20) | 终端停止符信号 | 用户按停止键(Ctrl+Z),产生此信号,并送至前台进程组的所有进程 | 停止进程 |
SIGTTIN(21) | 后台读控制终端信号 | 后台进程组中的进程试图读其控制终端,产生此信号 | 停止 |
SIGTTOU(22) | 后台写控制终端信号 | 后台进程组中的进程试图写其控制终端,产生此信号 | 停止 |
SIGURG(23) | 紧急情况信号 | 有紧急情况发生,或从网络上接收到带外数据,产生此信号 | 忽略 |
SIGXCPU(24) | 超过CPU限制信号 | 进程超过了其软CPU时间限制,产生此信号 | 终止+core |
SIGXFSZ(25) | 超过文件长度限制信号 | 进程超过了其软文件长度限制,产生此信号 | 终止+core |
SIGVTALRM(26) | 虚拟闹钟信号 | 以setitimer函数设置的虚拟间隔时间到期,产生此信号 | 终止 |
SIGPROF(27) | 虚拟梗概闹钟信号 | 以setitimer函数设置的虚拟梗概统计间隔时间到期,产生此信号 | 终止 |
SIGWINCH(28) | 终端窗口大小改变信号 | 以ioctl函数更改窗口大小,产生此信号 | 忽略 |
SIGIO(29) | 异步I/O信号 | 指示一个异步I/O事件 | 终止 |
SIGPWR(30) | 电源失效信号 | 电源失效,产生此信号 | 终止 |
SIGSYS(31) | 非法系统调用异常 | 指示一个无效的系统调用 | 终止+core |
信号的捕获
sighandler_t signal(int signum, sighandler_t handler);
- 信号处理注册函数
- signum 信号的编号,1~31 也可以是宏
- handler
- 函数指针
typedef void (*sighandler_t)(int);
- 宏
SIG_IGN
忽略该信号SIG_DEL
默认处理
- 函数指针
注意 在某些UNIX系统上,signal注册的函数只执行一次,执行完后恢复默认处理方式,如果想长期使用处理函数处理,在处理函数结尾再注册一遍
SIGTSTP(20)
可以被捕获,但不能被处理SIGKILL(9)/SIGSTOP(19)
既不能被捕获,也不能被处理SIGSTOP
信号可以让进程暂停,当再次收到SIGGCONT
信号时会继续执行- 普通用户只能给自己的进程发送信号,root可以给任何用户
- 在中断给进程传送信号
kill -[信号] [PID]
练习1 实现一个"死不掉的进程",当收到信号后给出信号产生的原因
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void sigfunc(int signum){
printf("我是进程%u,我收到了内核发送的%d\n",getpid(),signum);
exit(0);
}
int main(){
signal(SIGSEGV,sigfunc);
int* p = NULL;
*p = 20; //信号处理完后返回到收到信号前的那行
for(;;);
}
发送信号
- 键盘
- Ctrl+c
SIGINT(2)
- Ctrl+\
SIGQUUIT(3)
- Ctrl+z
SIGTSTP(20)
- Ctrl+c
- 错误
- 除0
SIGFPG(8)
- 非法访问内存
SIGSEGV(11)
- 除0
- 命令
kill -signum pid
ps -aux
查看所有进程
- 函数
int kill(pid_t pid, int sig);
- 向指定的进程发送信号
- pid 进程id
- pid > 0 向进程号为pid的进程发送信号
- pid = 0 向同一进程组的进程发送信号
- pid = -1 向所有(有权)进程发送信号
- pid < -1 向进程号为abs(pid)的进程组发送信号
- sig 信号的编号
- 值为0时,kill不会发送信号,但号进行错误检查(检查进程号或进程组id号是否操作)
int raise(int sig);
- 向当前进程(自己)发送信号
暂停与休眠
int pause(void);
- 一旦执行就会进入无限的休眠(暂停),直到遇到信号
- 先执行信号处理函数才会从休眠中醒来
unsigned int sleep(unsigned int seconds);
- 休眠指定的秒数,当有信号来临时会提前醒来,提前醒来会返回剩余的秒数,或者睡够了,返回0
闹钟
unsigned int alarm(unsigned int seconds);
- 告诉内核在seconds后向进程发送SIGALRM信号
- 如果之前设定的时间还没有到,则会覆盖,并返回之前设定的剩余秒数
信号集与信号屏蔽
-
信号集 信号的集合,由128位二进制组成,每一位代表一个信号
-
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);
- 从信号集中删除一个信号
-
int sigismember(const sigset_t *set, int signum);
- 判断信号集中是否有signum信号
-
-
信号屏蔽 当做一些特殊操作时,会希望有些信号来,而有些信号不要来,而与设置信号忽略 不同 的是:信号屏蔽只是暂时不来,而可以获取到着一段时间发生了哪些信号
- 每一个信号都有一个信号掩码(信号集),其中包括了需要屏蔽的信号,可以通过sigprocmask函数检查,修改进程的信号掩码
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- 检查修改进程的信号掩码
- how 可以为空(获取当前的信号掩码)
- SIG_BLOCK 设置当前信号掩码与set的并集为新的信号掩码,添加
- SIG_UNBLOCK 新的信号掩码是当前掩码与set补集的交集,删除
- SIG_SETMASK 把set当作新的信号掩码,更新
- old 旧的信号屏蔽掩码
int sigpending(sigset_t *set);
- 获取信号屏蔽期间发生的未处理的信号,当信号屏蔽解除后就没有了
注意 在信号屏蔽期间发生的不可靠信号只捕获一次
练习2 学生信息管理系统,在保存数据和加载数据时屏蔽Ctrl+c Ctrl+,等数据加载,保存完后再处理该信号
带附加信息的信号捕获
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 向内核注册信号处理函数
- signum 信号编码
- act 信号处理函数
- oldact 获取到次函数信号旧的处理方式,可以为NULL
struct sigaction {
void (*sa_handler)(int); //简单的信号处理函数指针
void (*sa_sigaction)(int, siginfo_t *, void *); //可以带附加信息的信号处理函数指针
sigset_t sa_mask; //当执行信号处理函数时需要屏蔽的信号
int sa_flags;
/*
SA_NOCLDSTOP //当子进程暂停时,不通知父进程
SA_NOCLDWAIT //
SA_NODEFER //当执行信号处理函数时,不屏蔽正常处理的信号
SA_ONSTACK //
SA_RESETHAND //信号只处理一次就恢复默认处理方式
SA_RESTART //系统调用如果被signum信号中断,自行重启
SA_RESTORER //
SA_SIGINFO //使用第二个函数指针处理信号
*/
void (*sa_restorer)(void); //保留
};
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sa_sigint(int sig, siginfo_t * info, void *ptr){
printf("%d,%s,%p",sig,info->si_ptr,ptr);
}
int main(){
struct sigaction act = {};
act.sa_signation = siginit;
act.sa_flags = SA_SIGINFO;
sigaction(SIGINT,&act,NULL);
union sigval value;
value.sival_ptr = "hehe";
sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
}
int sigqueue(pid_t pid, int sig, const union sigval value);
- 信号发送函数,与kill不同的是,可以复加一些额外数据
- pid 目标进程号
- sig 要发送信号
- value 联合,成员可以是整数或指针
计时器
- 系统为每个进程维护3个计时器
- ITIMER_REAL 真实计时器,程序运行实际所用时间
- ITIMER_VIRTUAL 虚拟计时器,程序运行在用户态所消耗时间
- ITIMER_PROF 实用计时器,程序在用户态和内核态所消耗的时间
- 实际时间(真实计时器) = 用户时间(虚拟) + 内核 + 睡眠时间
int getitimer(int which, struct itimerval *curr_value);
- 获取当前进程的定时器
- which 选择使用哪一种定时器
-
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 0~999999 */ };
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
- 给当前进程设置定时器,
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
void sigalrm(int sig){
printf("its time\n");
}
int main(){
signal(SIGALRM,sigalrm);
//struct timeval it_value = {0,999999};
//struct timeval it_interval = {0,999999};
struct itimerval curr_value = {{0,999999},{0,999999}};
setitimer(ITIMER_REAL,&curr_value,NULL);
for(;;);
}
练习 使用setitimer实现秒表
由于文件读写时为了提高效率,增加了缓冲区,所以当进行写操作时,数据并没有立即写入文件,而是暂时存储缓冲区中,只有达到某些条件时才写入文件。
- 由写入状态切换到读取状态
- 遇到\n符
- 缓冲区满4k
- 手动刷新fflush(FILE*)
- 文件关闭