信号是信息的载体,怎么理解呢?就类似于烽火戏诸侯的周幽王点燃了烽火台发出了这个信号,然后各路诸侯收到了这个信号就过来救驾勤王了。
进程也是可以收到来自四面八方的信号然后做收到信号之后要做的事情,要做的事也称处理信号。就好比我们正在学习,接到了一个电话然后去聊天了,电话结束后继续回来学习。进程也是一样,收到信号时无论执行到哪一步都要先处理信号
,然后继续做自己的事情。
信号分为三个状态,分别是产生,未决和递达。
产生:产生就是产生信号,比如我们用案件CTRL+C可以终止程序 还有使用kill命令 都是产生一个信号。
未决:未决是产生和递达中间的一个状态,主要是因为信号发送过去中间被阻塞了,被拦住了这种状态。
递达:递达就是收到这个信号了,然后处理完它了之后这个信号的状态。
信号有三种处理方式:
①执行默认动作:大部分信号,进程对于这种信号执行的默认处理方式就是终止进程。
②忽略信号:信号来了直接丢掉,不做任何处理,相当于信号从来没来过一样
③捕捉信号:用户自定义这种信号的处理方式,信号来了就按照用户自定义的来处理。(注意:SIGKILL和SIGSTOP这两个信号是不能忽略,捕获和阻塞的。这两个相当于进程用来结束的克星法宝,如果这两个也可以随便定义的话,进程就没法结束,进程就真的无法无天了。)
信号的阻塞这里要提及两个概念,分别是未决信号集和阻塞信号集。这两个信号集存放在内核的PCB中。信号集是一个能表示多种信号的数据类型,能表示1-64一共是64个信号。不用想的太复杂,就把它当作简单的位图。
当有信号产生的时候不会立刻处理这个信号,前面我们有提到未决状态其实就是这里了,信号产生之后内核区的这个未决信号集就要进行相应的改变了。每个信号都有其相应的编号,哪个信号来了就让位图相应的位置上的0变成1
。这里比如说收到了一个SIGINT信号(信号编号为2)
在未决信号集的时候,会判断阻塞信号集有没有阻塞它,如果阻塞了就继续先留在未决信号集中,如果没有阻塞它那么就进行下一步信号的处理了。处理完之后这个未决信号集上的1就变回原来的0了。
那么怎么看有没有阻塞,其实这两个集合是一 一对应的,对应位置上的值如果是1就代表着阻塞,是0就代表着不阻塞,就可以进行下一步的操作。这里显然是没有阻塞的,所以这个信号可以进行下一步的处理。
如果阻塞了,未决信号集上的值就不会变化,直到阻塞信号集解除阻塞,也就是将对应的1变成0之后,信号再进行下一步操作,处理完之后未决信号集相应的位置就从1变回0,此时信号也就处于上面说的递达状态了。
这里需要注意的一点是,如果在阻塞期间这个信号产生到了多次,最后解除阻塞之后也就执行一次信号处理操作。因为这个位图不会记录次数
只会记录有没有这个信号产生。
信号集是内核区的内容,我们没办法操作,但是系统为我们提供了api接口来间接的操作。
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); 作用:将某信号从信号清出信号集
以上函数的返回值都是成功返回0 失败返回-1并设置errno值
int sigismember(const sigset_t *set, int signum); 作用:判断某个信号是否在信号集中。 当信号信号集上返回1,不在返回0,出错了就会返回-1也会设置相应errno值
你可能会想,不是不能操作内核里的吗,连集合都没拿到手,怎么传参这个集合怎么做相应操作?
信号集本质上是一个sigset_t类型结构体,我们需要在栈上先创建好一个这样类型的结构体(创建之后记得要初始化,栈上的内容不初始化,默认值是随机的,不要以为默认都是0)做好相应的设置然后就是接下来看这个函数的操作了。这个函数可以修改内核区真正的阻塞信号集。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数成功返回0,失败返回-1并设置相应errno值。
第二个是传入参数,也就是你栈上创建的信号集,第三个是传出参数,如果不关心可以填NULL,如果想要没有改变前的内核区的阻塞信号集的内容可以填一个,函数运行结束之后,传入的这个oldset的内容就变成了没调用这个sigprocmask函数之前的阻塞信号集的内容。
这个how的传入有三个选项:
SIG_BLOCK:表示要增加的阻塞的信号都在传入的set里了,内核区的阻塞信号集会和这个set做一个或运算,也就是原来内核区阻塞信号集位置的1还是1,0的话看这个set上的设置 0|1=1 0|0=0.
SIG_UNBLOCK:表示要解除阻塞的信号都在传入的set里了,内核区的阻塞信号集会和这个set取反之后集合做一个与运算。0&0 0&1结果都是0,也就是说阻塞信号集0的位置不需要改变,1的话就是1 & (~1)或 1 &(~0)。简单来看就是说两个信号集相应位置都有1,那么就把阻塞信号集的1解除阻塞变成0;传入的信号集对应位置是0,阻塞信号集上的不需要做任何改变,原来是多少,现在还是多少。
SIG_SETMASK:这个杀伤力就比较大了,直接将内核区的阻塞信号集直接变成你传入的这个信号集。是一种赋值操作,阻塞信号集=set。
int sigpending(sigset_t *set); 作用:读取当前进程的未决信号集。
返回值为0表示成功,返回值为-1表示失败并设置相应errno值。
这个set是一个传出参数,把未决信号集的内容赋值给你传入的这个set。
信号的捕获就需要我们自己在内核中注册信号捕捉,注册了信号捕捉之后,有相对应的信号来的时候就对它进行处理,就可以按照我们用户自定义的来了。
系统为我们提供了相应的api接口让我们来操作
signal函数:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数为信号的编号(注意这个编号最好写宏值,不同系统中编号不一样,但是宏值是一样的),第二个参数为用户自定义的函数名。这里要严格按照函数的类型传参,比如说第一个必须传int型数据,第二个必须传sighandler
类型的函数指针,函数的名字本身就是一个函数指针,所以这里可以直接传函数的名字。
sigaction函数:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
第一个参数和上面的函数一样传的是信号的编号,第二个参数传一个结构体,第三个参数和上面说的sigprocmask用法一致,这里重点讲下第二个参数。
传入的结构体具体原型:
struct sigaction {
void (*sa_handler)(int); // 信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *); //信号处理函数
sigset_t sa_mask; //信号处理函数执行期间需要阻塞的信号
int sa_flags; //通常为0,表示使用默认标识
void (*sa_restorer)(void);
};
结构体的第一个元素和signal一样,设置自定义的函数名即可。也可以将sa_handler设置为SIG_IGN表示忽略,设置为SIG_DFL表示执行默认动作。
int main()
{
struct sigaction act;
act.sa_handler=SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGINT,&act,NULL);
while(1)
{
sleep(1);
}
return 0;
}
第二个元素和第一个元素一样,基本上不用。
第三个参数是设置信号处理函数期间需要阻塞的信号(注意,这个并没有涉及到内核的阻塞信号函数)默认情况下也就是不阻塞任何信号,一个信号先执行此时处于信号处理函数执行期间,之后收到了多种相同的信号,这个执行期间不会被后面来的信号所打断,而是等这个信号处理函数执行完之后再执行一次
这个信号处理函数。那如果来的不是同一个信号呢?这时候就会打断这个信号处理函数,处理这个不同的信号,这个不同的信号处理完了之后再回到被打断的地方继续执行。
void sighandler(int sig)
{
printf("signal is [%d]\n",sig);
sleep(10);
printf("hello word!!!\n");
}
int main()
{
struct sigaction act;
act.sa_handler=sighandler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGINT,&act,NULL);
sigaction(SIGQUIT,&act,NULL);
while(1)
{
sleep(1);
}
return 0;
}
设置阻塞之后,就不会在这个信号处理函数期间打断这个信号处理函数,而是先阻塞起来,等这个信号处理函数执行完毕后,再处理这个被阻塞的信号,同样,如果有多个同样的信号也只会执行一次。相应的设置信号集函数可以参考未决信号集。
第四个参数通常设置为0,表示使用默认表示。
第五个已经舍弃了,也就是不用了。
在不同的linux版本是signal这个函数表现出的行为是不一样的,要避免它的使用,应使用sigaction函数来代替它。
常用的信号发送函数是kill函数,它本身也是外部的一个命令,也是系统提供给我们的一个函数。它可以给指定进程发送信号。
signal函数:
int kill(pid_t pid, int sig);
函数的返回值 0代表着成功 -1代表着失败还会设置相应的errno
sig的传入就是传信号的编号。