Linux征途——进程间通信

进程之间是独立的,但是进程之间还是要相互协作的,这种协作叫通信。

本片博文非常的长~~~请按需求阅读

目录

1》》目的

2》》通信方式 

3》》管道

4》》共享内存

5》》进程信号

1、Linux信号的种类:

2、信号的产生:

3、信号的注册:

4、信号的注销:

5、信号的处理:

6、信号的阻塞:

6》》可重入函数和不可重入函数

7》》volatile关键字



  • 1》》目的

    数据传输:一个进程的数据发送给另一个进程
    数据共享:多个进程共享同样的进程
    通知事件:一个进程需要通知另一个进程发生了某种事件
    进程控制:一个进程完全控制    另一个进程的执行

  • 2》》通信方式 

    由于进程的独立性,通信需要双方拥有公共的媒介才能通信,而媒介由操作系统提供;由于通信的场景不同或者目的不同,操作系统提供了不同的进程间通信方式。
    a、管道
    b、共享内存
    c、消息队列
    d、信号量

     ipcs    查看进程间通信方式(-m共享内存        -s信号量        -q消息队列)
     ipcrm 删除通信方式

  • 3》》管道

  •     概念:
    一个进程连接到另一个进程的数据流,称之为管道。这便可以达成一个目的——数据传输

  •     原理:
         
    操作系统在内核创建一块缓冲区,缓冲区便就是管道,并为用户提供管道的操作句柄----->传输的是字节流
           一共有两个句柄:fd[2]—— ( fd[0] 和 fd[1] ) fd[0]用于读取,fd[1]用于写入

  •     特性:

         1>若管道中没有数据,read读取数据,会发生阻塞,直到读到数据返回。
         2>若管道中数据满了,如果继续写入数据,则会发生阻塞,直到能够写入数据或者写入完成。
         3>若所有读端关闭,继续写入数据会触发异常。SIGPIPE
         4>若所有写端关闭,进程会将管道数据全部读取完后退出程序。
         5>声明周期随进程,自带同步和互斥
                    (同步:对临界资源访问的时序可控    互斥:对资源的访问唯一)

  •     命名管道/匿名管道    ——都是半双工通信
  •      匿名管道:
    没有名字的缓冲区,因此在内核中,如果是两个独立的进程,即使其中一个进程创建了管道,另一个进程是找不到它的句柄的,所以匿名管道只能是用于具有亲缘关系的进程进行通信(fork()创建子进程,父子进程同时指向匿名管道)
     int pipe(int fd[2])————成功返回0,失败返回错误代码

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<string.h>
  5
  6 int main()
  7 {
  8     int pipefd[2];
  9     int ret = pipe(pipefd);
 10
 11     if(ret < 0)
 12     {
 13         printf("PIPE ERROR\n");
 14         return -1;
 15     }
 16
 17     int pid = fork();
 18     if(pid < 0)
 19     {
 20         printf("FORK ERROR\n");
 21         exit(-1);
 22     }
 23     else if(pid == 0)//子进程
 24     {
 25         char buff[1024];
 26         read(pipefd[0],buff,1023);
 27         printf("child readed:%s\n",buff);
 28     }
 29     else
 30     {
 31         char buff[1024] = {0};
 32         scanf("%s",buff);
 33         printf("father write:%s\n",buff);
 34         write(pipefd[1],buff,strlen(buff));
 35     }
 36     wait(NULL);
 37     return 0;
 38 }

  • 命名管道:
    文件系统的可见性,有文件可见于文件系统之中。
    创建一个带名字的缓冲区,操作和匿名管道相同。
    open打开管道文件的特性:

                      若管道文件以只读方式打开,若没有被其他进程以写的方式打开,则被阻塞,直到被其他进程以写的方式打开这个管道文件

                     若管道文件以只写方式打开,若没有被其他进程以读的方式打开,则被阻塞,直到被其他进程以读的方式打开这个管道文件

        管道的实例:管道符的实现 |
            主进程创建两个子进程,在两个中分别进行程序替换让两个子进程分别运行ls程序和grep程序
                ls 将结果写入到标准输出
                grep从标准输入读取数据

  • 4》》共享内存

    进程间通信最快的一种

  •     概念:在物理内存中开辟一块空间,并将空间映射到各个进程的虚拟地址空间的共享区,其他进程可以通过虚拟地址直接对内存进行操作。

  •     共享内存的操作步骤:(shared memory------------>shm)

        a、创建共享内存——指定标识符,大小,权限,并返回一个句柄    ---->shmget
        b、将共享内存映射到虚拟地址空间——对句柄操作-------------------->shmat
        c、对共享内存进行任意操作
        d、解除映射关系----------------------------------------------------------->shmdt
        e、删除共享内存----------------------------------------------------------->shmctl    
    int shmget(size_t key, size_t size, int shmflg)
        key:共享内存的标识符/名称
        size:共享内存的大小
        shmflg:与mode_t权限类似
        成功返回shmid,失败返回-1
    void* shmat(int shmid, const void* shmaddr, int shmflg)
        shmid:共享内存标识符
        shmaddr:指定连接的地址
        shmflg:SHM_RND\SHM_RDONLY
        成功返回共享内存首地址    失败返回 -1
     int shmdt(const void *shmaddr);
         shmaddr: 由shmat所返回的指针
        返回值:成功返回0;失败返回-1
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        shmid:由shmget返回的共享内存标识码
        cmd:将要采取的动作(有三个可取值)
        buf:指向⼀个保存着共享内存的模式状态和访问权限的数据结构
        返回值:成功返回0;失败返回-1

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
#define SHM_SIZE 4096
  int main()
  {    //创建共享内存
       int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|0664);
        if(shmid < 0)
       {
             perror("CREATE ERROR\n");
              return -1;
        }
  //映射首地址
      char* address = (char*)shmat(shmid,NULL,0);
 //映射首地址
      char* address = (char*)shmat(shmid,NULL,0);
      if(address ==(void*)-1)
        {
             perror("CONECT ERROR\n");
              return -1;
        }
//进行内存操作
     int i = 0;
        while(1)
  {
        sprintf(address,"------%d",++i);

            sleep(1);
    }
 //接触映射关系
       shmdt(address);
       shmctl(shmid,IPC_RMID,NULL);
     return 0;
 }

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<unistd.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
#define SHM_SIZE 4096
  int main()
  {    //创建共享内存
       int shmid = shmget(IPC_KEY,SHM_SIZE,IPC_CREAT|0664);
        if(shmid < 0)
       {
             perror("CREATE ERROR\n");
              return -1;
        }
  //映射首地址
      char* address = (char*)shmat(shmid,NULL,0);
 //映射首地址
      char* address = (char*)shmat(shmid,NULL,0);
      if(address ==(void*)-1)
        {
             perror("CONECT ERROR\n");
              return -1;
        }
//进行内存操作
     int i = 0;
        while(1)
  {
        printf("收到:%s\n",address);
            sleep(1);
    }
 //接触映射关系
       shmdt(address);
       shmctl(shmid,IPC_RMID,NULL);
     return 0;
 }

第一个程序运行之后,运行第二个程序,便会得到下面的图1。如果第一个程序停止,第二个的数据不会变化,图2。第二个程序停止在运行,数据不会从停止的时候开始,因为共享区的数据一直在被改变图3。


ipcs -m

ipcrm:删除该id之后,我并没有结束我的进程,所以并不会真正的删除我们的共享内存,当我们后面的链接数nattch为0的时候,就真正的删除了

  • 5》》进程信号

信号:就像我们听到下课铃声,铃声响起(信号产生)--->我们听到,并且识别(信号注册)--->知道下课了,忘记这个提示(信号的注销)---->开始玩耍(信号的处理)/拖堂(信号的阻塞)

    作用:事件通知——实际是软中断(软件中断)

  • 1、Linux信号的种类

            查看:kill -l    (1~31 非可靠信号,继承Unix    34~64    可靠信号(实时信号))
            

  • 2、信号的产生:

      就像下课铃声打了~~~~。信号产生。

  •        软件方式:

          函数:

            int kill(pid_t pid, int sig):指定进程,指定信号

            int raise(int sig):给调用进程指定信号

            int abort(void):给调用进程发送SIGBART信号

        size_t alarm(size_t second):second秒后发送SIGALRM信号
   
     我需要每次睡眠一秒,不然不知道打印会很多很多~


总的代码。

  •     硬件方式:

             ctrl + c        ctrl + z(只是停止进程,并未退出)        ctrl + l
           (信号的到来会打断当前可中断睡眠状态)
   core dumped———核心转储:异常退出时保存程序运行信息--->默认关闭
             ulimit -a        查看转储文件大小
            
             ulimit -c        设置转储文件大小-----》只有设置了大小,才会生成core文件
             core文件命名方式:core.pid
            
             core文件的使用:
                gdb   可执行文件     --->        core-file    core.pid
            

  • 3、信号的注册:

       下课铃声进入我们和老师的大脑。
      pcb中有 struct sigpending,该结构体中有sigset_t(信号集合——位图0\1)
    操作系统给一个进程发送信号,实际就是向进程的信号pending集合中添加信号(修改位图)
    位图只能标记信号是否存在,不能标记到来的信号的个数,这就牵出可靠信号和不可靠信号。
    pcb中有sigqueue链表,信号到来会组织一个结点添加到链表中
    可靠信号到来:修改位图,每个信号都组织结点添加到链表中
    非可靠信号到来:修改位图,若位图已经为一,修改位则什么也不做,否则添加一个结点。
    

  • 4、信号的注销:

     铃声响完之后,铃声不会一直在脑海里
    可靠信号:可能结点有多个,所以删除结点后判断是否有多个相同结点,判断是否修改位图
    非可靠信号:结点只有一个,删除结点位图修改为0

  • 5、信号的处理:

     准备下课~~~~
     当我们收到接收到一个信号,就需要来处理这个信号,也就是处理每个信号对应的操作。每个信号都有其默认的操作,但是操作系统也提供了修改其默认操作的接口。

  •    1) signal(int signum, sighander_t handler)

    这是给信号修改一个处理函数,这个也可以当成是信号的注册,因为它就是给信号赋一个操作。当出现signum号信号,开始执行这个handler操作。
    signum:信号编号
    handler:操作,操作有一下三种:
             默认:每个结点对应了一个事件,每个事件在操作系统都有定义完毕的处理方式    SIG_DFL
             忽略:忽略信号    SIG_IGN
             自定义:用户自定义函数(回调函数),替换内核中的处理方式

我定义一个处理的函数,每当我发起SIGINT信号,就会去调我们的fun函数。

  •  int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact); 

  struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;//通常为 0,调用第一个函数sa_handler,如果是SA_SIGINFO就调用第二个函数
               void     (*sa_restorer)(void);
           };

 sigaction函数可以读取和修改与指定信号相关联的处理动作。signal(int signum, sighander_t handler)内部也是调用的这个函数。

         参数:igno是指定信号的编号。
                     若act指针⾮空,则根据act修改该信号的处理动作。
                    若oact指针⾮空,则通 过oact传出该信号原来的处理动作。

  void fun(int num)//自定义操作
  {
      printf("action !! %d\n",num);
  }
  int main()
  {
      struct sigaction act;
      struct sigaction old;
      sigemptyset(&act.sa_mask);
      act.sa_handler = fun;//自定义操作
      act.sa_flags = 0;
      sigaction(2,&act,&old);//不可靠信号
      sigaction(SIGRTMIN+5,&act,&old);//可靠信号
//下面是信号的阻塞,在下下一小节/
      sigset_t newset;
      sigset_t oldset;
      sigemptyset(&newset);
      sigfillset(&newset);
      sigprocmask(SIG_BLOCK,&newset,&oldset);//阻塞所有信号
      printf("bolck………………………………\n");
      getchar();
      sigprocmask(SIG_UNBLOCK,&newset,NULL);
  }
2号信号触发了4次,39号信号也调用了4次,但是不可靠信号之处理了一次。说明改信号只注册了一次,这就验证了前面两种信号的注册
 

  • 信号的捕捉

     处理操作是我们用户自定义操作的时候,进程会从用户态和内核态相互切换。
     如果信号的处理动作是⽤户⾃定义函数,在信号递达时就调这个函数,这称为捕捉信号

    进程从用户态到内核态三种形式:系统调用接口,程序异常,中断

  • 6、信号的阻塞:

    就是说信号来了,但是暂时不处理这个信号,阻止信号的递达。就像下课铃声响了,老师拖堂,阻塞下课。
       (注意: 9信号和19信号不能被阻塞和自定义)

  1.     int sigemptyset(sigset_t *set);                   清空信号集合
  2.     int sigfillset(sigset_t *set);                          将所有信号添加到集合中
  3.     int sigaddset (sigset_t *set, int signo);       将指定信号添加到集合中
  4.     int sigdelset(sigset_t *set, int signo);         删除指定信号
  5.     int sigismember(const sigset_t *set, in t signo);
  6.     int sigprocmask(int how, const sigset_t *set, sigset_t *oset);     返回值:若成功则为0,若出错则为-1

        SIG_BLOCK :    阻塞set集合中的信号,将原有阻塞放入oset
        SIG_UNBLOCK:    对set集合中的信号解除阻塞
        SIG_SETMASK:    将set集合中的信号添加到阻塞集合中

          如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。
          如果set是⾮空指针,则 更改进程的信号屏蔽字,参数how指⽰如何更改。
          如果oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到oset⾥,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

//如果oset和set都是⾮空指针,则先将原来的信号 屏蔽字备份到 oset⾥,然后根据set和how参数更改信号屏蔽字
  /*实现的过程:
   *1、定义一个集合
   *2、向集合哄添加要阻塞的信号
   *3、阻塞这个集合中的所有信号
   *4、获取换行getchar()
   *5、对集合中的信号进行解除阻塞
   */
   #include<stdio.h>
   #include<unistd.h>
   #include<signal.h>
  int main()
  {
      sigset_t newset;
      sigset_t oldset;
      sigemptyset(&newset);
      sigfillset(&newset);
      sigprocmask(SIG_BLOCK,&newset,&oldset);
      printf("bolck………………………………\n");
      getchar();
      sigprocmask(SIG_UNBLOCK,&newset,NULL);
      return 0;
 }

//如果oset是⾮空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是⾮空指针,则 更改进程 的信号屏蔽字,参数how指⽰如何更改。。
void print(sigset_t* p)
 {
      int i = 1;//信号的集合是从1开始的
      for(i; i < 32; ++i)
      {
          if(sigismember(p,i))//判断指定信号是否在目标信号集中
          {
              printf("1");
          }
          else
          {
              printf("0");
          }
      }
      printf("\n");
  }
  int main()
  {
     sigset_t newset,oldset,pending;
     sigemptyset(&newset);
      sigfillset(&newset);
      sigprocmask(SIG_BLOCK,&newset,NULL);
      raise(5);//给调用进程发送5号信号
      raise(6);//给调用进程发送6号信号
      raise(7);//给调用进程发送7号信号
      raise(8);//给调用进程发送8号信号
      getchar();
      sigpending(&pending);
      print(&pending);//打印我们的未决集合
      sigprocmask(SIG_UNBLOCK,&newset,&oldset);//解除阻塞
      return 0;
}
//由于阻塞了所有信号,所以2号和3号也阻塞着。未决集合,注册了,但是没有处理的信号。
   所以23 5678号信号都打印了

  • 6》》可重入函数和不可重入函数

        :重入,我们可以理解为多个执行流可以进入。函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数, 这称为重⼊。那么这样我们得考虑数据安全的问题。所以能否在多个时序操作中对全局数据能时序性的访问,也就是全局数据是正常的改变就是重入,反之就是不可重入。看下面的例子

//C语言
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
int a = 1;
int b = 1;
int test()
{
     ++a;
 
     sleep(4);
 
     ++b;
      return a+b;
 }
 
 void sigcb(int num)
 {
     printf("signal-----> %d\n",test());
 }
 int main()
 {
      signal(SIGINT,sigcb);//CTRL + C 触发
      printf("main-----> %d\n",test());
      return 0;
 }
//如果我们的主控流程先执行,即信号后执行,我们预期结果是不是应该是
signal----->6
main----->4

但是………………4秒类按CTRL + C,不然触发不了信号。

为什么和预期结果不一样呢?
这就是因为这个test是不可重入的函数,也就是说如果有其他执行流进入,会造成数据错误,造成数据不安全问题。在4秒类,a先++、a==2,等四秒,这时信号进来,a++、a==3、,b++,b==2,然后主控流程的printf最后执行的时候,b++、b==3,得到上面的结果。

  • 7》》volatile关键字

保持内存可见性,防止编译器对代码过度优化。看下面的例子

//C语言
  #include<stdio.h>
  #include<signal.h> 
  int a = 1;
  void sigcb(int num)
  {
      printf("signal-----》%d\n",num);//CTRL+C,触发这个操作,改变a的值
      a = 0;
  }
  int main()
  {
      signal(SIGINT,sigcb);
      while(a)//值改变之后,while退出循环
      { }
      return 0;
  }
//真的是这样的吗??

 有没有发现我编译的时候加了-O2,这就是让编译器对代码进行优化。对于上面的代码,如果a这个数据用的频率偏高,而且值一直没有变化,所以它就将a的值放入寄存器中,那么CPU在寄存器中拿数据就不去内存中了,但是我们改变的是内存中的a的值,所以a的值并没有改变。这时我们对 a 加个关键字volatile。

//C语言
//C语言
  #include<stdio.h>
  #include<signal.h> 
  volatile int a = 1;
  void sigcb(int num)
  {
      printf("signal-----》%d\n",num);//CTRL+C,触发这个操作,改变a的值
      a = 0;
  }
  int main()
  {
      signal(SIGINT,sigcb);
      while(a)//值改变之后,while退出循环
      { }
      return 0;
  }
//真的是这样的吗??

这时一下就退出了,加这个关键字的时候,编译器就不会把a加入到寄存器中。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值