进程间通信:信号

匿名 (未验证) 提交于 2019-12-03 00:22:01
一、信号产生、处理、分类
二、signal信号处理机制
五、信号相关函数
六、计时器


信号就像古代战场上打仗,摇什么旗子摆什么阵。双方已经约定好。能不使用信号就不要使用信号,因为是异步。

信号是硬件中断的软件模拟(软中断)。


需要掌握:1到31号信号。
-------------------------------------------------------------------------------------------------------------------------------------------------------------
信号的产生:
信号的生成来自内核,让内核生成信号的请求来自3个地方:
1.用户:用户能够通过输入CTRL+c、Ctrl+\,或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
3.进程:一个进程可以通过系统调用kill给另一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信。
信号的处理:
进程接收到这两个信号后,只能接受系统的默认处理,即终止进程.
钩子:回调函数
默认处理的话,一般会把程序搞崩溃。
进程事先注册信号处理函数。当信号来了的时候

信号的分类:
同步信号:由进程的某个操作产生的信号,例如除0、内存访问违规;
异步信号:由像用户击键这样的进程外部事件产生的信号,再如,ctrl+z.

可以使用函数signal()注册一个信号处理函数。原型为:

typedef void (*sighandler_t//函数指针。定义一种sighandler_t的类型。这个类型是指向某种函数的指针。这个函数以int型为参
signal(int signumhandler//信号处理函数。signum是要捕捉的信号。handler是函数指针,也就是函数的地址,而函数名就是该函数所占内存的首地址。所以可以直接传入函数名。该以是SIG_DFL或SIG_IGN。若为SIG_DFL:表示交由系统缺省处理,相当于白注册了。若为:SIG_IGN,表示忽略该信号。

例1:signal的使用,捕捉ctrl+c产生的信号
void sig_func(int sig)
调用的。方法:使用全局变量。
}

int main(){
signal(SIGINT,sig_func)
//这样就可以在函数的执行过程中给函数发信号了。函数执行到某一句时给他发信号,这是跳转执行sif_func,执行完之后会继续循环。
}

例2:忽略信号的例子
int main(){
SIGINT,SIG_IGN//当输入2号信号时,内核接受到以后不做任何处理。
SIGINT,SIG_DFL//到这个地方又不想忽略了。
}

2号信号正在执行。2号又来了,怎么办?不能重入。
2号信号正在执行,3号又来了,怎么办?可以重入。
2号在执行,3号来了打断,此时有来了个2号,则后面的2号不能重入。

内核存储:
按一次ctrl+c,然后在ctrl+c处理函数执行过程中连续多次按ctrl+c。则后面多次ctrl+c只执行一次处理函数,也就是总共执行两次处理函数。原理:内核针对信号,存储每个信号只有一个位置。存放在信号队列中。第一个2号信号来了之后,队列中对应位置立即从0变为1,然后内核看到有1,就拿走并执行。此时来了第二个2号信号,发现对应位置是0,于是填上1。之后又来了多个2号信号。因为内核还在执行第一个2号信号。所以第二个2号信号填上的1没有拿走,对应的位置还是1.这时后面的多个2号信号只能在对应位置置1(不变)。等第一个处理程序处理完之后,会把1拿走,去执行第二次。

void
}

int main(){
signal(SIGINT,sig_func)
signal(SIGQUIT,sig_func)
}

1.希望信号处理函数只执行一次,下次重新注册。而signal函数是永久注册,每次都有效,不满足需求。这时就需要signaction.
2.一个信号正在处理信号,又出现了同一类型的信号,而我想要递归去执行这个信号。因为signal函数是不能打断的,且最多执行2次,不满足需求。这时就需要signaction.
3.一个信号正在处理信号,又出现了不同类型的信号,但是我不希望被打断。在signal中会去执行不同类型的处理函数,无法满足要求。这时我就需要signaction.
4.如果程序阻塞在内核态,如read(stdin)中。这时我给了一个信号。这个时候有两种情况:第一种是,系统调用错误返回,去执行信号。第二种是不错误返回,执行信号,执行完之后回到read()继续执行。signal默认是第二种,不错误返回。这个用处不大。一般在信号处理函数中就退出了。

const struct sigaction *act为,往往不保存旧的行为,所以写NULL。第二个参数act是新的行为。
//新的行为act结构体
不需要用,用的话还不如用signal函数。
void (*sa_sigaction)(int, siginfo_t *, void *);我们用新的,高级版的。我们要实现这个函数。这是一
个指向一个函数的指针。该函数有三个参数。返回值为void。
sigset_t sa_mask;将要被阻塞的信号集合
SA_RESETHANDSA_NODEFERSA_RESTARTSA_SIGINFO
//保留,不使用。

字段sa_mask是一个包含信号集合的结构体该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。针对sigset_t结构体,有一组专门的函数对它进行处理

//信号处理函数。
}
int main(){
struct sigaction act
act.sa_sigaction=sig//sigaction为一个信号处理函数指针,指向信号处理函数sig()
act.sa_flags=SA_SIGINFO//设置掩码
sigaction(SIGINT,&act,NULL)//调用sigaction函数。注册的是2号信号。新的行为是act。不保存就的行为。
//这时候就可以发信号了
}

例2:参数sa_flags为SA_RESTART的使用:阻塞后继续执行。
void sig(int signum,siginfo_t* p,void* p1){
}

int main(){
SA_SIGINFO|SA_RESTART//如果不写这个参数,会默认返回失败。也就是:执行到read的时候,程序阻塞。这ʱ我给了2这个信号,会执行sig函数,sig函数执行完之后,程序结束。并不会打印出:
read(0,buf,sizeof(buf))//从标准输入读数据,如果没有数据会阻塞。
}

例3:参数sa_flags为SA_NODEFER

sleep原理:alarm信号就是把进城唤醒。如果有信号来,则被提前唤醒。
提前醒来。
void sig(int signum,siginfo_t* p,void* p1){
}

int main(){
SA_SIGINFO|SA_NODEFER
}

例4:参数sa_flags为:SA_RESETHAND
void sig(int signum,siginfo_t* p,void* p1){
}

int main(){
SA_SIGINFO|SA_RESETHAND;
//输入第一次ctrl +c时,可以执行信号处理函数。第二次ctrl +c 直接退出。
}

本来没有加屏蔽的时候,在执行2号的信号处理函数的时候,3号可以打断2号。但是在设置了屏蔽以后,在2号执行处理函数时,3号会被阻塞,不会打断2号。当2号执行完成后3号再执行。注意:一、是在2号处理函数执行的过程中,才阻塞。没有2号信号的时候,不影响3号处理函数的执行。二、阻塞不是忽略。在二号执行完以后会再执行3号的处理函数。

}

int main(){
sigaddset(&act.sa_mask,SIGQUIT)//act是新的行为。act.sa_mask是将要被阻塞的信号集合。这句话指向集合中添加3号信号。
//设置了2号的信号处理函数。
}

例6:查看
void sig(int signum,siginfo_t* p,void* p1)
{
sigpending(&set)
}

int main(){
}

通过man sigaction 查看siginfo_t结构体,如下:
siginfo_t{
}

void sig(int signum,siginfo_t* p,void* p1){
p->si_pid//查看给我发信号的进程的pid和uid
p->si_uid);
}

int main(){
}

sigaction只是在信号处理过程中对其他信号进行阻塞。


针对sigset_t结构体(与sigaction中的一样),有一组专门的函数对它进行处理

例1:sigprocmask的使用
int main(){
sigset_t set;
sigemptyset(&set)
sigaddset(&set,SIGINT)//在set中添加2号信号
sigprocmask(SIG_BLOCK,&set,NULL)//将set信号集合添加到进程原有的阻塞信号集合中
sigismember(&set,SIGINT)//判断2号进程是否在set中。
SIG_UNBLOCK
}

int main(){
sigpending(&set)//将被阻塞的信号集合由参数set指针返回
}
int kill(pid_t pid, int sig);
sig是要发的信号,如SIGINT等。pid是对方进程的pid,有以下几种情况:
同一个进程组的所有进程
进程组ID等于pid绝对值的所有进程

例1:
int main(int argc,char* argv[]){
pid_t pid;
atoi(argv[1])//将参数转化成数字。
kill(pid,SIGINT)//给pid发2号信号。
}

例2:
int main(int argc,char* argv[]){
0//子进程和父进程在同一个进程组,所以发信号后,子进程也可以收到。一个.c中有两个进程在跑,然后ctrl+c ,两个进程都关了,这就是因为我们给进程组发的信号。
}

2.sleep函数
sleep()函数设定多长时间以后,让系统给我当前进程发信号。
sleep相当于进行了两步操作:一、调用alarm函数,秒数作为alarm的参数。即,多少秒之后自己给自己发送SIGALRM信号。
所以如果提前收到SIGALRM信号,或其他把自己唤醒的信号。那么自己就不再睡眠了。

3.alarm函数
在多少秒之后,自己给自己发送一个SIGALRM信号。

例:
void sig_handle(int sig){
}
int main(){
//捕捉SIGALRM信号 。
//3秒以后自己给自己的进程发送SIGALRM信号。结果为,3秒之后程序执行sleep函数就是这个机制,首先调用alarm(t),t秒后给自己发送信号,然后主动放弃CPU进入到睡眠队列。
}

4.pause函数
linux下没有pause命令。所以在程序中也就不能使用system("pause");这条语句。因为这条语句就是在c语言中嵌套了脚本。写程序很忌讳嵌套别的语言,相当于重新起了一个进程,很影响效率。
int pause(void); //将自身进程挂起,直到有信号发生时才从pause返回。不管什么信号。

int main(){
//执行到这会暂停。
}

Linux为每个进程维护3个计时器,分别是真实计时器、虚拟计时器和实用计时器。 分三个计时器,是因为linux分为用户态和内核态。
自己的程序运行在用户态,当系统调用时会到内核态执行。服务器编程,没法控制内核态运行时间。

真实计时器:程序运行的实际时间。从程序开始执行到退出的时间。
虚拟计时器:程序运行在用户态时所消耗的时间。做嵌入式开发的时候会用。
实用计时器:程序在R状态的时间。相当于统计用了多少时间片。比较常用。

真实计时器时间=实用计时器时间+睡眠时间。
实用计时器时间=用户态运行时间+内核态运行时间。
虚拟计时器时间=用户态运行时间。


setitimerwhichconst struct itimerval *value
//value为出入参数,指定计时器的初始间隔时间和重复间隔时间。
ovalue为传出参数,用于传出旧的时间设置,一般不用,写NULL。
//用指定的初始间隔和重复间隔时间为进程设定好一个计时器后,该计时器就会定时地向进为sleep函数和alarm函数也都是发送的SIGALRM信号。所以不要跟真实计时器一起用。
it_interval
it_value

void sig_handle(int sig){
}

int main(){
//捕捉SIGALRM信号。
//通过kill函数给同一进程组的进程(这里也就是自己)发送信号。打印出时间。
struct itimerval rt;
memset(&rt,0,sizeof(rt))//清零,这样就不用设置初始间隔的微妙部分和重复间隔的微妙部分。
rt.it_value.tv_sec=5//初始间隔的秒数部分为5.
rt.it_interval.tv_sec=2//重复间隔的秒数部分为2.
setitimer(ITIMER_REAL,&rt,NULL//使用真实计时器。
//程序执行后,先打印时间。然后过5秒后打印时间。之后每隔2秒打印时间。即程
}

void sig_handle(int sig){
}

int main(){
//初始间隔的秒数部分为5.
//重复间隔的秒数部分为5.
ITIMER_PROF//实用计时器。
//先打印出时间,开始计时
//因为实用计时器不统计sleep的时间,所以要等到sleep完才开始计算初始间隔。所以程序的时间结果为:0 10 12 14 16...
}

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!