信号概念:信号就是一个软件中断-----通知进程发生了某件事情(打断当前操作选择,合适的时机去处理信号)
信号种类:
kill -l 命令查看 62种(里面有缺省)
1~31:非可靠信号(信号不会丢失),非实时信号
34~62:可靠信号(信号不会丢失),实时信号
信号有生命周期:信号的产生-》信号在进程当中注册-》信号在进程中注销-》信号的处理
信号的产生:
硬件:ctrl+c (中断信号) ctrl+z(停止信号)ctrl+| (退出信号)
软件中断:kill命令 kill(),raise(),abort(),alarm();
core dump:
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁 盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许 产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: $ ulimit -c 1024
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
#include<errno.h>
int main(){
//int kill(pid_t pid, int sig);
//给指定的进程发送指定的信号
//kill(getpid(),SIGQUIT);
//int raise(int sig);
//给调用它的进程发送指定信号
//raise(SIGQUIT);
//void abort(void)
//给调用它的信号发送SIGABRT
//abort();
//unsigned int alarm(unsigned int seconds);
//seconds秒之后给调用进程发送一个SIGALRM信号
//如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置
即之前设置的秒数被新的闹钟时间取代
//seconds==0表示取消上一个定时器
alarm(3);
alarm(1);
while(1){}
return 0;
}
进程在信号中注册
- sigset_t结构体的认识:unsigned long int_val[SIGSET_NWORDS];--------是一个128位的位图,用于对信号到来做标记
- sigqueue是一个链表------------信号到来后会将一个对应的信号的节点添加到链表中
- 非可靠信号:首先判断位图已经做了标记,将什么都不做(每种信号只添加一个节点,信号同时到来时,有可能会丢事件), 否则添加节点标记位图
- 可靠信号:不管位图是否被标记都会为每一个到来的信号添加一个节点到链表当中;否则标记位图
- 未决信号:注册了但没有被处理的信号
- 未决 :是一个信号从产生到信号处理之间所处的状态
在进程中注销
- 可靠信号的注销:删除节点,判断链表当中是否还有相同的节点,如果没有在位图位置置0,否则位图不变
- 非可靠信号的注销:删除节点。修改位图为0;
信号的处理
- 默认处理方式:SIG_DEL
- 忽略处理方式:SIG_IGN
- 自定义处理方式:typedef void(*sighandler_t)(int signo)
signal(int signum,sighandler_t handle) ----------回调函数
sigaction()
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
printf("recv signo:%d\n", signo);
//在信号处理函数中,恢复信号的原来处理动作
sigaction(signo, &oldact, NULL);
}
//signal(SIGINT, SIG_DFL);
//signal(SIGINT, SIG_IGN);
//signal(SIGINT, sigcb);
//signal(SIGQUIT, sigcb);
//int sigaction(int signum,struct sigaction *act,struct sigaction *oldact);
// 使用act动作替换signum信号的处理方式,将原来的处理方式放到oldact中
// signum: 信号编号
// act: 信号新的处理动作
// oldact:用于保存信号原来的处理动作
act.sa_flags = 0;
//sa_flags:中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志
位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得>毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号
传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation
act.sa_handler = sigcb;
//int sigemptyset(sigset_t *set);\
// 清空信号集合
sigemptyset(&act.sa_mask);
//sa_mask:sa_mask指定在信号处理程序执行过程中,哪些信号应当被阻塞。缺省情况下当前信号本身被阻>塞,防止信号的嵌套发送,除非指定SA_NODEFER或者SA_NOMASK标志位
sigaction(SIGINT, &act, &oldact);
while(1) {
printf("hello bit!!\n");
sleep(1);
}
return 0;
}
自定义信号捕捉流程
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码 是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函 数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返 回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了
信号的阻塞:组织信号被递达(暂时你处理信号)
递达:动作----信号处理
在进程中标记信号,这些被标记的信号到来时,将不被处理
有两个信号无法被自定义处理,无法阻塞,无法忽略: SIGKILL-9 SIGTOP-19
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<errno.h>
void sigcb(int signo){
printf("signo is %d\n",signo);
}
int main(){
signal(SIGINT,sigcb);
signal(SIGKILL,sigcb);
signal(SIGRTMIN+5,sigcb);
//阻塞所有信号,按下回车后解除所有阻塞信号
sigset_t set,old;
sigemptyset(&set);
//将所有信号置0;
//int sigfillset(sigset_t *set);
//将所有信号添加到信号集当中
//int sigaddset(sigset_t *set,int signum);
//将一个指定信号添加到信号集合当中
sigfillset(&set);
//int sigprocmask(int how,const sigset_t *set,sigset_t *old);
// how:
// SIG_BLOCK: 阻塞set中的信号
// SIG_UNBLOCK:解除set中所阻塞的信号
// SIG_SETMASK:设置set中的信号阻塞
// set:阻塞/解除的信号集合
// old:被用于保存修改前,block中原有的数据
sigprocmask(SIG_BLOCK,&set,&old);
getchar();
//int sigpending(sigset_t *set);
//获取未决信号集合
sigset_t pending;
sigpending(&pending);
//int sigismember(sigset_t *set,int signum);
//用于查看某一个信号是否在集合当中
int i=0;
for(i=0;i<64;++i){
if(sigismember(&pending,i)){
printf("1");
}else{
printf("0");
}
}
printf("\n");
sigprocmask(SIG_UNBLOCK,&old,NULL);
return 0;
}
竞态条件:程序竞争执行
函数的可重入与不可重入:一个函数在不同的执行流下是否可以被重复调用,并且不会出现问题;有可能出现问题,这个函数就是不可重入函数;否则是可重入函数
函数可重入与不可重入关键点:函数中是否对全局数据进行了非原子性操作
原子操作:操作不可打断
用户设计接口,如果有可能在对各执行流中被调用,那么就要考虑函数中对全局数据的保护
用户在多个执行流中调用相同的函数的时候,也要考虑函数是否可重入
例如malloc/free
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int a = 0, b = 0;
int test()
{
a++;
sleep(3);
b++;
return a+b;
}
void sigcb(int signo)
{
printf("signal-------%d\n", test());
}
int main(int argc, char *argv[])
{
signal(SIGINT, sigcb);
printf("main--------%d\n", test());
return 0;
}
关键字:volatile
修饰变量---保持内存可见性---防止编译器过度优化代码---每次都从内存中重新获取数据变量数据
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
volatile int a = 1;
void sigcb(int signo)
{
a = 0;
printf("a=%d\n", a);
}
int main(int argc, char *argv[])
{
signal(SIGINT, sigcb);
while(a) {
}
printf("process normal exit\n");
return 0;
}
SIGCHLD:
- 在子进程退出后,操作系统通过SIGCHLD信号通知父进程,由于SIGCHLD是处理方式默认是忽略,因此以前父进程不知道子进程何时退出,因此调用wait死等,浪费了父进程资源。
- 现在学了信号我们可以用自定义SIGCHLD处理方式,在信号回调函数中调用wait子进程退出,向父进程发送SIGCHLD信号,出发回调函数,在执行wait
- 因为SIGCHLD是非可靠信号,如果同时又多个进程退出,有可能信号信号丢失,导致sigcb只会回调一次,只处理了一个子进程退出,其他退出的子进程成为了僵尸进程;因此要在一次回调中要将所有的僵尸进程全部处理
- waitpid(-1,NULL,WNOHANG) >0
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
void sigcb(int signo)
{
//有子进程退出,返回值大于0--返回退出子进程的pid
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
signal(SIGCHLD, sigcb);
int pid = fork();
if (pid == 0) {
sleep(3);
exit(0);
}
while(1) {
printf("打麻将~~~\n");
sleep(1);
}
return 0;
}