进程通信(2)---信号

一、消息队列

1、本质

内核中的一个队列,多个进程通过向同一个队列添加节点和获取节点,传输一个有类型(优先级)的数据块。

//用户自己定义这个结构体的内容
 struct msgbuf
   {
       long mtype;       /* message type, must be > 0 ,优先级*/
       char mtext[1];    /* message data ,数据块大小*/
   };

2、接口

//创建消息队列
msgget();
//添加节点
msgsnd();
//获取节点
msgrcv();
//操作-删除消息队列(IPC_RMID)
msgctl();

3、特性:

  • 自带同步与互斥
  • 生命周期随内核
  • 数据传输自带优先级

二、信号量

信号量用于实现进程间的互斥与同步,是对系统中资源及其组织情况的抽象,由一个记录型(或结构体类型)数据表示。它包含两个数据项:
第一个为value,表示可用资源数目:

  • S.value>0时,表示有value个可用资源;
  • S.value=0时,表示资源正好用完;
  • S.value<0时,表示有-value个进程正在等待此类资源。

第二个为L,为等待此类资源的进程PCB队列。

在访问临界资源之前先获取信号量,S.value - 1,减完之后判断:
(1)若S.value > 0则进程可以对临界资源进行访问,并且在访问期间,已经将临界资源的状态置为不可访问状态,这样可以保证其他进程此时不会访问这个临界资源,访问完毕之后,S.value+1,若S.value <= 0,则唤醒一个进程(将一个进程从L中出队,置为运行状态)。
(2)若S.value <= 0则使进程等待(将进程加入L队列中);

三、信号

1、信号产生的方式

进程信号就是一个软件中断,通知进程发生了某个事件,打断进程此刻的操作,去处理这个事件。在操作系统中,信号是多种多样的,一个信号对应一个事件,这样就保证了进程收到某个信号后就知道要去处理什么事件,但是要保证这个信号能被识别。

1、硬件: 
	ctrl +z(进程中断,后台挂起)  
  	ctrl + c(进程中断)  
  	ctrl + |(进程中断,产生coredumped文件) 
2、软件: 
    int kill(pid_t pid,int signum);  
    //(#include<signal.h>)给指定进程发送指定的信号,kill命令就是通过它实现的。  
    int raise(int signum); 
    //(#include<signal.h>)给当前进程发送指定信号。
    void abort(void);  
    //(#include<stdlib.h>)和exit函数一样,总是能成功。
    unsigned int alarm(unsigned int seconds);
    //(#include<unistd.h>)定一个闹钟,在seconds秒后给当前进程发送一个SIGALARM信号,该信号默认处理是终止当前进程。
    int sigqueue(pid_t pid,int sig,const union sigval value);
    //给任意进程(pid)发送任意信号(sig),额外带一个参数value。

2、信号的种类

通过kill -l命令查看信号的种类,共有62种信号,1-31种是非可靠信号,34-64是可靠信号。
信号表:
在这里插入图片描述

解释几个常见的信号:
(1)SIGHUP ---从终端上发出的结束信号
(2)SIGINT ---键盘中断信号//ctrl + c
(3)SIGQUIT ---键盘的退出信号 //ctrl + \
(8)SIGFPE ---浮点异常信号(例如浮点运算溢出)
(9)SIGKILL ---该信号结束接收信号的进程;
(14)SIGALRM ---进程的定时器到期时,发送该信号
(15)SIGTERM ---kill命令发出的信号
(17)SIGCHLD ---标识子进程停止或结束的信号
(19)SIGSTOP ---停止执行信号
(20)SIGTSTP ---来自键盘或调试程序的停止执行信号//ctrl + z

3、信号的注册
在pcb中有一个未决信号集合pending,信号的注册就是在这个64位数的每一位上置1即可。

1.非可靠信号(1-31)的注册: 若信号未注册(pending位图的那一位不为1)时,则注册,添加一个sigqueue节点 若信号已经注册,则什么都不做。
在这里插入图片描述
2.可靠信号(34-64)的注册:若信号未注册(pending位图的那一位不为1)时,则注册,添加一个sigqueue节点若信号已经注册,则再添加一个sigqueue节点。
在这里插入图片描述

4、信号的注销:

1.非可靠信号(1-31)的注销: 将dending位图的那一位置0,并删除要处理sigqueue节点
2.可靠信号(34-64)的注销: 将dending位图的那一位置0,并循环删除要处理的sigqueue节点,直至删完

由此可以看出,不可靠信号只接受一次信号,可能会丢失进程的事件。

4、信号的捕捉处理

分为三种处理方式:
1、默认处理:SIG_DEL
每一个信号都有一个缺省动作,它是当进程没有给这个信号指定处理程序时,内核对信号的处理。有5种缺省的动作:

  • 异常终止(abort):在进程的当前目录下,把进程的地址空间内容、寄存器内容保存到一个叫做core的文件中,而后终止进程。
  • 退出(exit):不产生core文件,直接终止进程。
  • 忽略(ignore):忽略该信号。
  • 停止(stop):挂起该进程。
  • 继续(continue):如果进程被挂起,则恢复进程的运行。否则,忽略信号。

需要指出的是,对信号的任何处理,包括终止进程,都必须由接收到信号的进程来执行。而进程要执行信号处理程序,就必须等到它真正运行时。因此,对信号的处理可能需要延迟一段时间。 信号没有固有的优先级。如果为一个进程同时产生了两个信号,这两个信号会以任意顺序出现在进程中并会按任意顺序被处理。另外,也没有机制用于区分同一种类的多个信号。如果进程在处理某个信号之前,又有相同的信号发出,则进程只能接收到一个信号。进程无法知道它接收了1个还是42个SIGCONT信号。

2、忽略处理:SIG_IGN
进程可忽略产生的信号,但 SIGKILL 和 SIGSTOP 信号不能被忽略,必须处理(由进程自己或由内核处理)。进程可以忽略掉系统产生的大多数信号。
3、自定义处理:void sigcb(int signum) //自定义处理函数
通过下面两种接口修改信号的处理方式:

1.修改信号的回调函数
sighandler_t signal (int signal,sighandler_t )handler);
//signal ---信号值
//handler---操作句柄(函数指针)
#include<stdio.h>    
#include<stdlib.h>    
#include<unistd.h>                                                      
#include<signal.h>     
void sigcb(int signum)    
{    
    printf("signum = %d\n",signum);    
}    
    
int main()    
{    
    //sighandle_t signal(int signum,sighandler_t handler);    
    signal(SIGINT,sigcb);    
    signal(SIGQUIT,SIG_IGN);    
    
    while(1)    
    {    
        printf("hello\n");    
        sleep(1);    
    }
    return 0;
}

在这里插入图片描述

程序运行起来,ctrl+c(SIGINT)中断信号被替换成sigcb函数 ctrl+(SIGQUIT)中断信号被忽略。

2.修改信号的整个处理动作
int sigaction(int signum,struct sigaction* new,struct sigaction* old);
//signum ---信号值
//new ---要修改后的信号动作
//old ---保存原来的信号动作
struct sigaction
{
	void (*sa_handler)(int);//回调函数1
    void (*sa_sigaction)(int, siginfo_t *, void *);//回调函数2
    sigset_t  sa_mask;//信号集合,在处理当前信号时,若到来其他信号,则阻塞这些信号
    int  sa_flags;
    //设置为0,调用sa_handler回调函数操作;设置为SA_SIGINFO则调用sa_sigaction回调函数(不常用)   
    void (*sa_restorer)(void);
};

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

struct sigaction oldact;//保存原来的信号的处理动作
void sigcb(int signum)//回调函数
{
    printf("recv signum:%d\n", signum);
    sigaction(signum, &oldact, NULL);//替换一次后,又换回默认的处理
}
int main (int argc, char *argv[])
{
    struct sigaction newact;//要处理的信号动作
    newact.sa_handler = sigcb;//调用回调函数sigcb
    newact.sa_flags = 0;//使用sa_handler回调函数就要置0
    sigemptyset(&newact.sa_mask);//先将信号集合清空,避免对当前信号处理产生影响

    //sigaction修改信号的处理动作为newact,原来的动作使用oldact保存
    sigaction(SIGINT, &newact, &oldact);

    while(1) 
    {
        printf("hello\n");
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

因为回调函数中又将oldact动作置为空,newact置为原来保存在oldact中的信号动作。所以,ctrl+c这个信号只会被修改一次,往后又恢复了原来的信号动作。可以看出,这个函数比signal使用起来更加灵活,单也更加复杂。下面sigaction函数里边调用的结构体

5、自定义信号的捕捉流程

在这里插入图片描述

6、信号的阻塞

阻止一个信号的递达,信号依然可以注册,只是暂时不作处理。在pcb中有一个block位图(阻塞信号集合),凡是添加到这个集合中的信号,都表示需要阻塞,暂时不处理。
在这里插入图片描述

接口:
1.int sigprocmask(int how, sigset_t *set, sigset_t* old);
//对信号集合set进行how操作
//how---当前要对block进行的操作
 	//1.SIG_BLOCK:将set集合中的信号添加到block进程阻塞的信号集合中,block |= set,
 	// 表示阻塞set集合中的信号及原有的阻塞信号,并将原有的阻塞信号保存在old集合中(便于还原)。
 	//2.SIG_UNBLOCK:将set集合中的信号从block中移除,将set集合中的信号解除阻塞,block &=(~set);
 	//3.SIG_SETMASK:直接将block集合中的信号修改为set集合中的信号,block = set;
//set---要处理信号的集合
//old---保存处理信号原有的状态(便于还原)
2.int sigemptyset(sigset_t* set);
//清空set集合里面所有的信号
//成功返回0,失败返回-1
3.int sigfillset(sigset_t* set);
//将所有信号填充到set集合中
//成功返回0,失败返回-1
4.int sigaddset(int signum,sigset_t* set);
//将指定信号signum加入到set集合中
//成功返回0,失败返回-1
5.int sigdelset(sigset_t* set,int signum);
//将指定信号signum从set集合中移除
//成功返回0,失败返回-1
6.int sigismember(const sigset_t* set,int signum);
//实现: (1 << signum) & set
//判断信号signum是否在set集合中
//在返回1,不在返回0,出现错误返回-1
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>                                                                                                 

void sigcb(int signum)
{
    printf("signum = %d\n",signum);
}

int main()
{
    signal(2,sigcb);
    signal(40,sigcb);

    sigset_t set,old;//信号集合
    sigemptyset(&set);//清空信号集合
    sigemptyset(&old);


    //sigaddset(int signum,sigset_t *set);将指定自信号添加到集合
    sigfillset(&set);//将所有的信号添加到set集合中来
    sigprocmask(SIG_BLOCK,&set,&old);//阻塞所有信号

    printf("press enter to continue\n");
    getchar();//按下回车之前,程序卡在这里

    sigprocmask(SIG_UNBLOCK,&set,NULL);//解除阻塞
    return 0;
}

在这里插入图片描述
在这里插入图片描述

不可靠信号只注册了一次,可靠信号就处理了6次。
在Linux系统下,有两信号不可被阻塞,不可自定义修改处理方式,也不可能被忽略处理。 SIGKILL-9 和SIGSTOP-19

利用上述信号接口实现一个sleep函数,牛刀小试,点这里

7、函数可重入和不可重入

1、函数的重入:在多个执行流当中,进入同一个函数运行处理。
2、函数的可重入:函数重入后,不会造成数据的二义性和程序逻辑混乱,则这个函数就是可重入函数。
3、函数的不可重入:函数重入后,可能会造成数据的二义性和程序逻辑混乱,则这个函数就是不可重入函数。
函数的可重入和不可冲入的判定点:一个函数中是否对全局数据进行非原子操作(操作不可被打断)。
malloc 和free都是不可重入函数,在多个执行流当中操作需要注意。
举个栗子:

//loop.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

int a = 0, b = 0;
void test() {
    a++;
    sleep(3);
    b++;
    printf("sum:%d\n", a + b);
}
void sigcb(int signum)
{
    test();
}
int main()
{
    signal(SIGINT, sigcb);
    test();
    return 0;
}

在这里插入图片描述

由此可以看出,在main函数执行过程中,如果没有收到信号SIGINT,只执行test函数,这时就是一个正常状态。但是如果在test函数执行过程中,收到SIGINT信号,将会导致cpu去处理SIGINT信号所对应的事件sigcb,sigcb再调用函数test,将全局变量a++,b++,这样就会使main中的test函数中的处理结果偏离预期,出现上面的第二种结果。这个test函数就是不可重入函数。

8、SIGCHLD信号

子进程退出后,操作系统发送SIGCHLD信号给父进程,但是SIGCHLD信号的默认处理方式是忽略处理,因此在之前的程序中并没有感受到操作系统的通知,因此只能固定的使用进程等待来避免僵尸进程,但是在这个进程中父进程是一直阻塞的,只能一直等待子进程退出。
若在程序初始换阶段,将SIGCHLD信号的处理方式自定义,并且在自定义函数冲调用 waitpid ,这样的话子进程退出的时候,则会自动回调处理了,父进程就不需要一直等待了。
多个子进程退出时,都会想父进程发送SIGCHLD信号,但是SIGCHLD信号是非可靠信号,有可能会丢事件。例如:三个子进程同时退出,但是信号只注册了一次,意味着只会执行一次回调函数,调用一次waitpid,只能处理一个僵尸进程。
非可靠信号的丢失是不可避免的,因此只能再一次信号毁掉中处理完所有的僵尸进程。

while(waitpid(-1,NULL,WNOHANG) > 0);
//waitpid 返回值 > 0---退出子进程的pid ; ==0 ---没有子进程退出 ; < 0---出错
//循环是为了若有子进程退出则一直处理,直到没有子进程退出,则退出循环信号调用完毕
//WNOHANG ---将 waitpid设置为非阻塞,没有子进程退出的时候返回 0,退出循环,不要导致程序流程一直卡在回调函数中。
  //zombie.c
  #include<stdio.h>    
  #include<stdlib.h>    
  #include<unistd.h>    
  #include<signal.h>    
  #include<sys/wait.h>    
      
 void sigcb(int signum)    
  {                                                                                                                                                                        
      while(waitpid(-1,NULL,WNOHANG)> 0)    
      {    
              printf("子程序退出\n");    
      }    
          //返回值为子进程的pid,没有子进程退出,返回0,出错返回-1    
  }   
   
  int main()    
  {    
      signal(SIGCHLD,sigcb);//将信号SIGCHLD自定义为sigcb    
      
      pid_t pid = fork();    
      if(pid == 0)    
      {    
          sleep(5);//子进程退出,产生僵尸进程    
          exit(0);    
      }    
      
      if(fork() == 0)//再创建一个子进程,不退出    
      {    
          while(1)    
          {    
              printf("this is child\n");    
              sleep(1);    
           }
       }
  
      while(1)
      {
          sleep(1);
          printf("this is parent\n");
      }
      return 0;
  }  

在这里插入图片描述
这样处理,就不会出现,子进程退出之前,父进程一直死等得现象。

发布了77 篇原创文章 · 获赞 16 · 访问量 6514

猜你喜欢

转载自blog.csdn.net/weixin_43886592/article/details/103399306
今日推荐