(1)概述
1.信号是一种软件中断,用来处理异步事件
2.信号的本质是一种进程间的通信,一个进程向另一个进程发送信号
3.执行kill -l可查看系统所有的信号
4.作用:ctl+c时用来做一些收尾工作:
1.删除管道.删除共享内存.删除信号量.删除消息队列..
2.进程间通信
(2)信号的生命周期
进程之间约定好:如果发生了某件事情T ( trigger ),就向目标进程( destination process )
发送某特定信号 X ,而目标进程看到 X ,就意识到 T 事件发生了,目标进程就会执行相应的动作 A ( action )
1.Linux 内核收到了产生的信号,然后就在目标进程的进程描述符里记录了一笔:收到信号一枚。
2.Linux 内核会在适当的时机,将信号递送( deliver )给进程。
3.在内核收到信号,但是还没有递送给目标进程的这一段时间里,信号处于挂起状态,也称为未决信号。
4.内核将信号递送给进程,进程就会暂停当前的控制流,转而去执行信号处理函数。
5.实际情况还应该考虑的问题
1.目标进程正在执行关键代码,不能被信号中断,需要阻塞某些信号
2.如何处理重复的信号,排队还是丢弃?
3.已有多个不同的信号被挂起,应该优先递送哪个信号?
4.对于多线程的进程,如果向该进程发送信号,应该由哪个线程来负责响应?
(3)信号的产生
[1] 硬件异常
1.硬件检测到了错误并通知内核,由内核发送相应的信号给相关进程
2.常见硬件异常的信号
SIGBUS: 总线异常
SIGFPE: 算数错误
SIGILL: 非法及其指令
SIGSEGV: 段错误
进程访问未初始化的指针或 NULL 指针指向的地址
进程在用户态访问内核部分的地址
进程修改只读的内存地址
[2] 终端相关的信号
·Ctrl+C :产生 SIGINT 信号。
·Ctrl+\ :产生 SIGQUIT 信号。
·Ctrl+Z :产生 SIGTSTP 信号。
键入这些信号生成字符,相当于向前台进程组发送了对应的信号。
[3] 软件事件相关的信号
· 子进程退出,内核可能会向父进程发送 SIGCHLD 信号。
· 父进程退出,内核可能会给子进程发送信号。
· 定时器到期,给进程发送信号。
(4)信号默认处理函数
[1]信号的默认操作
· 显式地忽略信号:ignore
· 终止进程:terminate
· 生成核心转储文件并终止进程(用于调试):core
· 停止进程(暂停进程):stop
· 恢复进程的执行: continue
(5)信号的分类
[1]不可靠信号
1.信号值在 [1,31] 之间的所有信号,都被称为不可靠信号
2.不可靠信号是从传统的 Unix 继承而来的
3.不可靠信号如果收到某不可靠信号,内核发现已经存在该信号处于未决状态,
就会简单地丢弃该信号。因此发送不可靠信号,信号可能会丢失,
[2]可靠信号
1.在 [SIGRTMIN,SIGRTMAX] 之间的信号,被称为可靠信号
2.内核内部有队列来维护,如果多次收到可靠信号,内核会将信号挂到相应的队列中,因此不会丢失。
(6)传统信号(System V风格)
[1]在相同的 Linux 平台上,由于 glibc 版本的差异,提供的 signal 函数的语义也有差异。
在早期的 libc4 和 libc5 中, signal 函数的语义是 Syetem V 风格的。因此,从可移植的角度来看,不应该使用 signal 函数。
[2]信号执行时屏蔽自身的特性
1.对于传统的 System V 信号机制,在信号处理期间,不会屏蔽对应的信号,而这就会引起信号处理函
数的重入。
2.System V 风格的信号,在其信号处理期间没有屏蔽任何信号,换句话说,执行信号处理函数期间,
处理流程可以被任意信号中断,包括正在处理的信号。
[3]信号中断系统调用的重启特性
1.系统调用在执行期间,很可能会收到信号,此时进程可能不得不从系统调用中返回,去执行信号处理函数
2.对于执行时间比较久的系统调用(如 wait 、 read 等)被信号中断的可能性会大大增加。
3.系统调用被中断后,一般会返回失败,并置错误码为 EINTR
4.如果程序员希望处理完信号之后,被中断的系统调用能够重启,则需要通过判断 errno 的值来解决,
即如果发现错误码是 EINTR ,就重新调用系统调用。
(7)signal---kill.raise.alarm(信号安装和发送)
[1]signal函数
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回值: sighandler_t类型的函数指针
形参: int, sighandler_t类型函数指针
void (*signal(int signum, sighandler_t handler))(int);
使用例子1:
int sig_int(int sig) {printf("sig_int");}
signal(SIGINT, sig_int);
使用例子2:
signal(SIGINT, SIG_IGN); // 忽略SIGINT信号
signal(SIGINT, SIG_DFL); // 对于SIGINT信号,使用默认处理函数
==>写法二: void (*signal(int signum, void (*handler)(int)))(int);
[2]kill函数(和signal搭配)
int kill(pid_t pid, int sig);
·pid > 0 :发送信号给进程 ID 等于 pid 的进程。
·pid = 0 :发送信号给调用进程所在的同一个进程组的每一个进程。
·pid = -1 :有权限向调用进程发送信号的所有进程发出信号, init 进程和进程自身除外。
·pid < -1 :向进程组 -pid 发送信号。
当函数成功时,返回 0 ,失败时,返回 -1 ,并置 errno
1. kill 函数不仅可以向特定进程发送信号,也可以向特进程组发送信号
2. 所有信号值都是>0的。 若第二个参数 signo的值为0,这种情况下,来检测目标进程或进程组是否存在,
如果 kill 函数返回 -1 且 errno 为 ESRCH ,则可以断定我们关注的进程或进程组并不存在
[3]raise函数
int raise(int sig);
向进程自身发送信号的接口
int raise(int sig);
1.单线程的程序而言
相当于:kill(getpid(),sig)
2.对于多线程的程序而言
相当于:pthread_kill(pthread_self(),sig) // 给当前线程发信号
[4]alarm函数(闹钟)
unsigned int alarm(unsigned int seconds);
描述: 每隔5s给当前进程发送一个SIGALRM信号。
alarm也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它向进程发送SIGALRM信号。
可以设置忽略或者不捕获此信号,默认动作是终止调用该alarm函数的进程。
成功:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0。
出错:-1
(8)sigaction---sigqueue(信号的发送和安装)
[1]sigqueue(发送信号)
int sigqueue(pid_t pid, int sig, const union sigval value);
参数: pid: 要发送信号的进程ID
sig: 要发送的信号
value: 发送的伴随数据,该参数的数据类型是联合体
union sigval {
int sival_int;
void *sival_ptr; // 几乎不用(每个进程都有独立的地址空间)
};
//考虑到不同的进程有各自独立的地址空间,传递指针到另一个进程几乎没有任何意义。因此 sigqueue 函数很少传递指针( sival_ptr ),大多是传递整型( sival_int )。
1.传统的信号多用 signal/kill 这两个函数搭配
2.signal函数的表达力有限,控制不够精准;所以引入了sigqueue函数来完成实时信号的发送
3.sigqueue函数也可以发送空信号(信号0)来检查进程是否存在。
4.和 kill 函数不同的地方在于,它不能通过将pid指定为负值而向整个进程组发送信号。
[2] sigaction(安装信号)
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
1.参数: signum: 信号编号
1.struct sigaction { // 第二个参数
union {
void (*sa_handler)(int);//sa_flags里没有设置SA_SIGINFO标记的信号处理函数
//sa_flags里设置了SA_SIGINFO标志, 的信号处理函数
void handle(int signum, siginfo_t *info, void *ucontext);
// 收到的额外数据保存在info->si_value中的si_int和si_ptr中
}
sigset_t sa_mask; // 阻塞信号集
int sa_flags; // 标志
void (*sa_restorer)(void); // 恢复处理程序
};
2.siginfo_t { // handle信号处理函数的第二个参数
int si_signo; // 信号的值
int si_code; // 信号来源:SI_USER.SI_TKILL.SI_QUEUE..
pid_t si_pid; // 信号发送进程的进程 ID 。
uid_t si_uid; //信号发送进程的真实用户 ID 。
union sigval si_value; //sigqueue 函数发送信号时所带的伴随数据。
...
}
3.ucontext是 void* 类型的,其实它是一个 ucontext_t 类型的变量。
这个结构体提供了进程上下文的信息,用于描述进程执行信号处理函数之前进程所处的状态。
通常情况下信号处理函数很少会用到这个变量
4. sa_flags的含义
1.SA_NOCLDSTOP
一旦父进程为SIGCHLD信号设置了这个标志位,那么子进程停止和子进程恢复这两件事情,就不会向父进程发送SIGCHLD信号了
但是子进程切换为SIGCONT时还是会给父进程发送SIGCHLD信号。
2.SA_NOCLDWAIT
如果父进程为SIGCHLD设置了SA_NOCLDWAIT 标志位,那么子进程退出时,就不会进入僵尸状态,而是直接自行了断。
对于Linux而言,子进程转换切换为SIGSTOP.SIGCONT.SIGKILL时都会给父进程发送SIGCHLD信号。这点和上面的 SA_NOCLDSTOP 略有不同。
3.SA_ONESHOT 和 SA_RESETHAND
这两个标志位的本质是一样的,表示信号处理函数是一次性的,信号递送出去之后,信号处理函数便恢复成默认值 SIG_DFL 。
4.SA_NODEFER 和 SA_NOMASK
这两个标志位的作用是一样的,在信号处理函数执行期间,不阻塞当前信号。
5.SA_RESTART
这个标志位表示,如果系统调用被信号中断,则不返回错误,而是自动重启系统调用
6.SA_SIGINFO
没有设置SA_SIGINFO:
跟signal使用方法相同, 使用一个参数的信号处理函数
void (*sa_handler)(int);
设置了SA_SIGINFO:
1.这个标志位表示信号发送者会提供伴随数据。这时使用带3个参数的信号处理函数
void handle(int, siginfo_t *info, void *ucontext);
2.能获取到发送进程的PID、UID.信号来源.及发送的额外信息...
2.注意
1.对SIGKILL 和 SIGSTOP,不可以为它们安装信号处理函数,也不能屏蔽掉这些信号。
若通过 sigaction 强行给 SIGKILL 或 SIGSTOP 注册信号处理函数,则会返回-1,并置errno为EINVAL。
(9)等待信号
[1]pause函数
int pause(void);
作用:使调用进程(线程)进入休眠状态(就是挂起);直到接收到信号且信号函数成功返回
pause函数才会返回
返回值:始终返回-1
(10)线程的阻塞信号集
[1]概述
1.每个线程都拥有独立的阻塞信号掩码。
2.开会时关闭手机是一种比较极端的例子。更合理的做法是暂时屏蔽部分人的电话。对于某些重要的电话,比如儿子老师的电话、父母的电
话或老板的电话,是不希望被屏蔽的。信号也是如此。进程在执行某些操作的时候,可能只需要屏蔽一部分信号,而不是所有信号。
3.信号集: 数据类型为 sigset_t,sigset_t 的类型是位掩码,每一个比特代表一个信号。
4.SIGKILL 信号和 SIGSTOP 信号不能被阻塞。(设置信号集时会被内核剔除)(避免出现神仙进程)
5.对于多线程的进程而言,每一个线程都有自己的阻塞信号集。
[2]常用API
int sigemptyset(sigset_t *set); // 初始化信号集set中的信号为空
int sigfillset(sigset_t *set); // 将所有信号添加进信号集set
int sigaddset(sigset_t *set, int signum); // 在信号集中添加signum信号
int sigdelset(sigset_t *set, int signum); // 在信号集中删除signum信号
int sigismember(const sigset_t *set, int signum); // 判断signum是否存在于set信号集中
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); // 设置信号集
oldset: 如果oldset为非NULL,则信号掩码的先前值存储在oldset中,故一般设置为NULL。
how的选项:
SIG_BLOCK: 在当前阻塞信号集中增加set信号集中的信号
SIG_UNBLOCK:在当前阻塞信号集中删除set信号集中的信号
SIG_SETMASK:阻塞信号集被设置为set信号集。
[3]pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
1.为了更显式地设置线程的阻塞信号掩码,线程库提供了 pthread_sigmask 函数来设置线程的阻塞信号掩码。
2.事实上 pthread_sigmask 函数和 sigprocmask 函数的行为是一样的。
pthread_sigmask函数将调用sigprocmask函数(内核源码)
[4]注意
1.如果阻塞了某个信号A, 然后调用pause。在程序的执行过程中如果一直给进程发送
信号A,pause函数将不会返回, 因为发送的信号都被阻塞。
2.对于信号集中阻塞的不可靠信号a, 在阻塞过程中, 发送多个信号a时,之前挂起的信号a会被抛弃;
解除阻塞后, 最终发送到目标进程的信号a只有一个。
3.对于信号集中阻塞的可靠信号b,在阻塞过程中, 发送多个信号a时,会创建一个队列来管理阻塞的信号;
解除阻塞后, 最终发送到目标进程的信号b = 信号b的发送次数。
4.SIGKILL 信号和 SIGSTOP 信号不能被阻塞。(设置信号集时会被内核剔除)
(11)SIGCHID信号
1.父进程可以监测子进程的以下三种事件; 每次状态改变,子进程会发SIGCHID给父进程
· 子进程终止(即子进程死亡)
· 子进程停止(即子进程暂停)
· 子进程恢复(即子进程从暂停中恢复执行)
2.若使用sigaction---sigqueue(信号的发送和安装)
1.sigaction使用了宏SA_NOCLDSTOP:
一旦父进程为SIGCHLD信号设置了这个标志位,那么子进程停止和子进程恢复这两件事情,就不会向父进程发送SIGCHLD信号了。
但是子进程切换为SIGCONT时还是会给父进程发送SIGCHLD信号。
2.sigaction使用了宏SA_NOCLDWAIT:
如果父进程为SIGCHLD设置了SA_NOCLDWAIT 标志位,那么子进程退出时,就不会进入僵尸状态,而是直接自行了断。对于Linux而言,子进程转换切换为SIGSTOP.SIGCONT.SIGKILL时都会给父进程发送SIGCHLD信号。这点和上面的 SA_NOCLDSTOP 略有不同。
(12)信号的练习
信号示例: (通过信号模拟 <司机--售票员>)
1.平常司机在车上休息,售票员在观察上车人数
2.售票员发现车上人满了就提醒司机发车
3.中途停两个站: 9km. 15km处, 这时售票员要提醒司机
6.总里程20公里
7.到终点站后司机提醒售票员让所有乘客下车
8.售票员退出后, 司机才能退出
(13)信号与线程的关系
POSIX 标准:
0.给线程发送信号用pthread_kill()
1.信号处理函数必须在多线程进程的所有线程之间共享,但是每个线程要有自己的挂起信号集合和阻塞信号掩码。
2.POSIX 函数 kill/sigqueue 必须面向进程,而不是进程下的某个特定的线程。
3.每个发给多线程应用的信号仅递送给一个线程,这个线程是由内核从不会阻塞该信号的线程中随意选出来的。
4. 如果发送一个致命信号到多线程,那么内核将杀死该应用的所有线程,而不仅仅是接收信号的那个线程。