一、信号的基本概念
- 中断:中止当前正在执行的任务,转而执行其他任务(返回或不返回)。
1.1 硬件中断:硬件设备产生的中断(键盘按键产生的中断)。
1.2 软件中断:其他程序产生的中断。 - 信号:是一种软件中断,提供了一种异步执行任务的中断。
2.1 常见的信号:
SIGINT(2):终端中断符信号,用户按中断键(Ctrl+C),产生此信号,并送至前台进程组的所有进程
SIGQUIT(3):终端退出符信号,用户按退出键(Ctrl+‘\’),产生此信号,并送至前台进程组的所有进程
SIGABRT(6):异常终止信号,调用abort函数,产生此信号
SIGFPE(8):算术异常信号,表示一个算术运算异常,例如除以0、浮点溢出等
SIGKILL(9):终止信号,不能被捕获或忽略。常用于杀死进程
SIGSEGV(11):段错误信号,试图访问未分配的内存,或向没有写权限的内存写入数据
SIGCHLD(17):子进程状态改变信号,在一个进程终止或停止时,将此信号发送给其父进程
SIGTSTP(20):终端停止符信号,用户按停止键(Ctrl+Z),产生此信号,并送至前台进程组的所有进程
注意:在终端执行 kill -l 可以显示出所有的信号 - 不可靠信号:建立在早期机制上的信号,SIGHUP(1)~(SIGSYS(31),不支持排队可能会丢失,同一个信号产生多次进程可能只接收到一次。
- 可靠信号:采用新的机制产生的信号,SIGRTMIN(34)~SIGRTMAX(64),支持排队,不会丢失。
- 信号的来源:
5.1 硬件产生的信号:除0,非法内存访问。这些异常是硬件(驱动)检查到,并通知内核,然后内核再向引发这些异常的进程发送相应的信号。
5.2 软件产生的信号:通过kill / raise / alarm / setitmer / sigqueue 函数产生。 - 信号的处理:
忽略
终止进程
终止进程并产生core文件
捕获信号并出路
二、信号的捕获
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
- 功能:
- 信号处理注册函数
- signum:
- 信号的编号,1~31,也可以是宏
- handler:
- SIG_IGN:忽略该信号
- SIG_DEL:默认处理
- 函数指针
- 返回值:
- 成功返回原来信号函数处理指针,或者返回SIG_IGN / SIG_DEL;失败返回SIG_ERR
- 注意:
- 在某些UNIX系统上,signal注册的函数只执行一次,执行完后就恢复成默认处理方式。如果想 长期使用该函数处理信号,可以在函数结束前再注册一次。
- SIGKILL / SIGSTOP:不能被捕获和处理,SIGCONT可以使暂停的信号继续执行
- 普通用户只能给自己的进程发送信号号,而root可以给任何进程发送信号。
练习1:实现一个“死不掉”的进程,当收到信号后,给出信号产生的原因。
三、发送信号
#include <signal.h>
- 键盘:
Ctrl+cSIGINT
Ctrl+’ \ ’SIGQUIT
Ctrl+zSIGTSTP
- 错误:
除零SIGFPE
非法访问内存SIGSEGV
- 命令:
kill -signum(信号) pid(进程号)
ps-aux
查看所有进程 - kill:
int kill(pid_t pid, int sig);
功能:向指定的进程发送信号
pid:进程id
– pid>0向进程号为pid的进程发送信号
– pid=0向同组的进程组发送信号
– pid=-1向所有进程发送信号
– pid<-1向进程组为==abs(pid)==进程发送信号
sig:信号的编号
– sig=0:不会发送信号,但是会检查进程号或进程组id号是否存在
– 1<sig<=64:正常发送信号
– sig<64:不会发送信号,检查是否是非法信号 - raise
int raise(int sig);
功能:发送当前进程指定的信号(给自己发送信号)
四、休眠
#include <signal.h>
int pause(void);
功能:一旦执行进程就会进行无限的休眠(暂停)
注意:
– 直到遇到信号。先执行信号处理函数才会从休眠中醒来。
– 正常情况下不会返回,被信号中断,返回-1。unsigned int sleep(unsigned int seconds);
功能:休眠指定的秒数,当有信号来临时,会提前醒来,提前醒来会返回剩余的秒数,或者睡够了,返回0.
五、闹钟
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:告诉内核再seconds秒后,向当前进程发送SIGALRM信号。
返回值:如果之前设定的时间还没有到,会重新覆盖,并返回之前设置的剩余秒数。
六、信号集与信号屏蔽
- 信号集:信号的集合,由128位二进制组成,每一位代表一个信号。
- 头文件:
#include <signal.h>
int sigemptyset(sigset_t *set);
功能:清空信号集,把所有位设置为0int sigfillset(sigset_t *set);
功能:填满信号集,把所有位设置为1int sigaddset(sigset_t *set, int signum);
功能:向信号集添加一个信号,置1int sigdelset(sigset_t *set, int signum);
功能:在信号集中删除一个信号,置0int sigismember(const sigset_t *set, int signum);
功能:判断该信号是否属于该信号集,存在返回1,不存在返回0- 信号屏蔽:当做一些特殊操作时会希望有些信号来或者不来,与信号忽略相比,信号屏蔽可以查看获取已屏蔽信号。每个进程都有一个信号掩码(信号集),其中包括了需要屏蔽的信号。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:检查修改进程掩码。
how:
– SIG_BLOCK:设置当前信号集与set的并集为新的信号掩码(添加)
– SIG_UNBLOCK:新的信号掩码是当前掩码与set的补集的交集(删除)
– SIG_SETMASK:把set当做新的信号掩码(重新设置)
set:可以为空,则获取信号掩码
oldset:旧的信号屏蔽掩码int sigpending(sigset_t *set);
功能:获取信号屏蔽期间发生的信号,当信号屏蔽解除后就没有了
注意:在信号屏蔽期间发生的信号,无论多少次,只能捕获一次
七、带附加信息的信号捕获
#include <signal.h>
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_RESETHAND//表示信号只处理一次,然后就恢复默认处理方式
SA_RESTART//系统调用如果被signum信号中断,自行重启
SA_NOCLDSTOP//当子进程暂停时,不用通知父进程。
SA_NODEFER//当执行信号处理函数时,不屏蔽正常的处理的信号
SA_SIGINFO//使用第二个函数指针处理信号
void (*sa_restorer)(void);//保留,暂不使用
};
int sigqueue(pid_t pid, int sig, const union sigval value);
- 功能:信号发送函数,与kill不同的是可以附加一些额外的数据
- pid:目标进程号
- sig:要发送信号
- value:联合。成员可以是整数或指针
union sigval{
int sival_int;
void *sival_ptr;
};
八、计时器
#include <sys/time.h>
-
系统为每个进程维护三个计时器
- ITIMER_REAL:真实计时器,程序运行的实际所用时间
- ITIMER_VIRTUAL:虚拟计时器,程序运行在用户态所消耗的时间
- ITIMER_PROF:实用计时器,程序在用户态和内核态所消耗的时间
-
实际时间(真实计时器)=用户时间(虚拟)+内核时间+睡眠时间
-
int getitimer(int which, struct itimerval *curr_value);
- 功能:获取当前进程的计时器
- which:选择使用哪些计时器
-
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
- 功能:给当前进程设置计时器,与alarm的区别更精确
- which:选择使用哪些计时器
struct itimerval{
struct timeval it_interval; 每一次触发时钟信号所需要的时间
struct timeval it_value; 第一次触发时钟信号所需要的时间
};
struct timeval{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
练习:使用setitimer实现秒表的功能
由于文件读写时为了提高效率,增加了缓冲区,所以当进行写操作时,数据并没有立即写入文件,而是暂时存储到缓冲区中,只有达到某些条件时才写入文件
- 由写入状态切换到读取状态
- 遇到\n符号
- 缓冲区满4k
- 手动刷新fflush(FILE*)
- 文件关闭