当前位置:主页 > 查看内容

进程间通信(管道与共享内存)

发布时间:2021-06-22 00:00| 位朋友查看

简介:文章目录 1. 进程间通信目的 2. 进程通信的发展 3. 管道 3.1 匿名管道 3.2 管道读写规则 3.2.1 写端不关闭文件描述符不写入管道为空读取条件不就绪读端就可能就会长时间阻塞 3.2.2 读端不关闭文件描述符不读管道满了写条件不就绪写端就会被阻塞 3.2.3 写端关……

1. 进程间通信目的

  • 数据传输:一个进程需要将他的数据发送给另一个进程
  • 资源共享:多个进程需要共享同样的资源
  • 通知事件:一个进程需要向一个或一组进程发送消息,通知他们发生
  • 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道他的状态改变。

2. 进程通信的发展

  • 管道
  • System V进程间通信
  • POSIX进程通信
    其中管道是用文件实现的,而System V与POSIX是两套标准,绕过文件来通信。

3. 管道

当进程在内存中执行这一步步代码时,并不是立即把文件内容写到硬盘上的文件,而是先写到文件缓存区,进程结束由操作系统刷新至硬盘上对应的文件。

在这里插入图片描述
进程间通信的本质就是让两个进程看到同一份资源,即这个文件内存缓冲区。
当fork创建子进程,子进程会继承父进程的文件描述符等。通过struct file就能找到文件缓冲区
在这里插入图片描述

3.1 匿名管道

先写一个makefile文件
在这里插入图片描述
现在我们对于这个命令就要有这深入的理解,bash创建子进程,子进程execl(ls)进程替换,使用dup2,将显示的文件重定向到makfile里。
创建。

在这里插入图片描述
int pipefd[2],是一个大小为2的形参数组,他是一个输出型参数,也就是说在这个函数内会改变这个数组,我们在外面会拿到他。

pipe这个系统调用创建匿名管道,但为什么要打开两次文件,分配两个文件描述符。而且一个以读方式打开文件,一个以写方式打开文件。

  • 不能open一次,给他只读或只写,因为子进程会拷贝那么子进程对那个文件也就只读或只写了)
  • 不能open一次,同时给他读和写,因为只能父进程写,子进程读,或者子进程写,父进程读。而这样一个进程(无论父子),一个文件描述符,同时拥有两种权限,是无法控制的)
  • open两次,拿两个文件描述符,一个描述符有读权限,一个描述符有写权限,虽然这个进程(无论父子)对文件也可以进行读和写,但是我们可以手动控制,例如:父进程读,子进程写,就把父进程写,子进程读的文件描述符关了。

上面讲到父进程必须以读写方式打开文件,pipe这个函数调用封装了这种操作方式,而返回的数组里,pipefd[0]表示读端,pipefd[1]表示写端。

下面代码表示,子进程写,父进程读,而且子进程关闭了读,父进程关闭了写,保证管道的单向数据传输。

   1 #include<stdio.h>                                                                                                                                                   
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include<unistd.h>
  6 #include<string.h>
  7 int main()
  8 {
  9    int pipefd[2]={0};
 10    pipe(pipefd);
 11    pid_t id= fork();
 12    if(id==0)
 13    {
 14      const char* str="i am child ,child writing";
 15      close(pipefd[0]);
 16      while(1)
 17      {
 18        //子进程写
 19        write(pipefd[1],str,strlen(str));
 20        sleep(1);
 21      }
 22    }
 23    else if(id>0){
 24        close(pipefd[1]);
 25        char buf[64];
 26        while(1)
 27        {
 28          //第三个参数为你期望读多少,返回值为实际读了多少字节
 29          ssize_t s=read(pipefd[0],buf,sizeof(buf)-1);
 30          if(s>0)
 31          {
 32            buf[s]=0;
 33          }
 34          printf("father get message:%s\n",buf);
 35  
 36        }
 37    }
 38   return 0;
 39 }                                            

在这里插入图片描述

这里有四个特征,需要进一步深入挖掘。

3.2 管道读写规则

3.2.1 写端不关闭文件描述符,不写入,(管道为空)读取条件不就绪,读端就可能就会长时间阻塞

让子进程(写端)sleep5s,父进程(读端)不变。那么就会发现子进程写一次,停顿的时候,父进程也在停,也就是说父进程以子进程节奏为主,子进程不动,父进程也就在阻塞式等待,就是将自己pcb由R状态设置为S状态,从运行队列挪到等待队列,那么假如写段不关闭文件描述符,而且一直不往里面写数据,那么读端就可能会一直堵塞(等待)。

3.2.2 读端不关闭文件描述符,不读,(管道满了)写条件不就绪,写端就会被阻塞

让父进程(读端)sleep5s,子进程(写端),就会发现写端一次性写满了管道,读端读一次,可能过一会写端才会往管道里在写数据。

3.2.3 写端关闭文件描述符,读端读取完之后,读到文件结尾

假如当子进程(写端)写了5次,关闭掉写段文件描述符,最后read会返回0,即读到文件结尾

3.2.4 读端关闭文件描述符,写端可能会被杀掉

假如当子进程(写端)一次写满,父进程(读端)读3次关掉文件描述符。写端进程(子进程)会被操作系统通过信号直接杀掉,又由于我们代码中父进程死循环,没有回收,所以子进程僵尸了。

3.3 站在内核理解管道

管道也是一种文件,他也有自己的inode,在struct file中存在path,path中有一个dentry,里面存着目录的inode,inode里面有和block的映射,就找到了文件名和inodeid,在找到文件的inode。

3.4 管道特点

  • 匿名管道只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

当我们敲下这个命令的时候就可以理解他的原理

who | wc - l

首先bash命令行,调用pipe()创建匿名管道,在创建两个子进程,execl进程替换为who和wc-l。两个进程都拷贝了bash的文件描述符,bash关闭两个文件描述符,who为写端,wc-l为读端,关掉who的读端文件描述符,关掉wc-l的写端文件描述符。who本来要往显示器上打印,结果要写到管道文件里。dup2(pipefd[1],1。严谨一点关掉pipefd[1]。wc-l原本读的是键盘,结果要读管道文件,所以dup2(pipefd[0],0)。然后关掉pipefd[0]。

3.5 命名管道

在这里插入图片描述
mkfifo为库函数,创建一个命名管道

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 int main()
  5 {                                                                                                                                                                   
  6   mkfifo("./fifo",0666);
  7   return 0;
  8 }

在这里插入图片描述
p为管道文件
在这里插入图片描述

匿名管道由pipe系统调用创建,我们并不关心他叫什么名字,只用他的文件内存缓冲区。常用于有亲缘关系的进程,子进程继承了文件信息。但两个没有关系的进程怎么通信呢?

mkfifo是一个库函数,作用为创建一个命名管道文件。
在这里插入图片描述
下面来写一个客户端与服务端,客户端发送,服务端接收并打印出来。

首先服务端创建管道,只读的方式open,返回fd,然后调用read方法读取到一个数组buf,最后打印出来

  1 #include<stdio.h>
  2 #include<sys/types.h>
  3 #include<sys/stat.h>
  4 #include<fcntl.h>
  5 #include<unistd.h>
  6 int main()
  7 {
  8   if(-1==mkfifo("./fifo",0644))
  9   {
 10    perror("err");
 11   }
 12   int fd=open("./fifo",O_RDONLY);
 13   if(fd>=0)
 14   {
 15      char buf[64];
 16      while(1)
 17      {
 18        ssize_t s=read(fd,buf,sizeof(buf)-1);
 19       if(s>0)
 20       {
 21         buf[s]=0;
 22         printf("client#:  %s",buf);                                                                                                                                 
 23       }
 24       else if(s==0)
 25       {
 26         //当实际读到字节为0时
            printf("client quit \n");
            break;
 27       }
 28       else{
 29         perror("read err");
 30         break;
 31       }
 32        return 0;
 33        }
 34   }
 35 

客户端负责写,读端已经创建管道,所以写端不必在创建。把什么写进管道呢,我们重定向,从键盘输入进管道。这样服务端就可以接受到了。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/types.h>
  4 #include<sys/stat.h>
  5 #include<fcntl.h>
  6 int main()
  7 {
  8   int fd=open("./fifo",O_WRONLY);
  9   if(fd>=0)
 10   {
 11    char buf[64];
 12    while(1)
 13    {
 14    //从键盘输入的字符到buf里
 15 
 16    printf("please write memsage# \n");
 17     
 18    ssize_t s=read(0,buf,sizeof(buf)-1);
 19    if(s>0)
 20    {
 21      buf[s]=0;
 22      //往fd里写buf,想发s个字节
 23      write(fd,buf,s);                                                                                                                                               
 24    }
 25   }
 26   }
 27   return 0;
 28 }

客户端写
在这里插入图片描述
服务端读,当客户端终止,服务端读到文件尾,结束
在这里插入图片描述

命名管道通过文件名+文件描述符来实现通信。

在这里插入图片描述
管道文件始终大小为0,这就是最开始提到的,操作系统直接创建文件,加载到内存中,但是只需要用到它的文件内存缓冲区,所以不需要把信息写入到硬盘中的fifo中。
而对于管道的4条规则,命名管道也同样适用。

4. 管道总结

  • 匿名管道,pipe(int pipefd[2])系统调用,在内存中创建了一个文件,形参输出型参数,会返回两个文件描述符,用于操作,fork之后,子进程会继承这两个文件描述符,一个进程有两个文件描述符,不要那个就关闭那个,它在内存中创建了一个匿名管道文件,亲缘进程继承了文件信息可以看见他,从而使用它的文件内存缓冲区。

  • 命名管道通过mkfifo库函数在硬盘上创建文件,加载进内存,任意进程通过文件名和文件描述符来看见他,也是只使用了它的文件内存缓冲区,实际上信息并未刷新至硬盘。

  • 管道的4个规则。

5. 共享内存

system V标准共享内存

进程间通信本质是让两个进程看到同一块资源,对于管道我们借助了pipe文件的文件内存缓冲区,那么共享内存就是绕过文件,直接在物理内存开辟一段空间,通过某种映射,让两个进程与这段空间关联起来,这样一个进程修改,另一个进程就能直接看见他,因为这段空间是两者共享的。
在这里插入图片描述
写端写到管道的文件内存缓冲区,读端从管道的文件内存缓冲区读。用户到内核,内核到用户。共享内存只需要一次写入。所以它几乎是最快的IPC方式。

系统中存在这大量进程无时无刻不在通信,所以我们对进程间通信也要先描述在组织,通信的本质在于看到同一块资源,而管道,共享内存都是资源,只是取决于操作系统通过文件系统还是内存管理来分配资源。
所以我们需要创建共享内存,关联共享内存,取消关联,删除共享内存。

共享内存的数据结构
在这里插入图片描述

  • ipc_perm:用户标识信息
  • shm_segsz:内存大小
  • shm_atime:最后一次挂接时间
  • shm_dtime:最后一次取消挂接时间
  • shm_ctime:最后一次改变时间

用户标识信息的结构体

struct kern_ipc_perm {
	spinlock_t	lock;
	bool		deleted;
	int		id;
	key_t		key;
	kuid_t		uid;
	kgid_t		gid;
	kuid_t		cuid;
	kgid_t		cgid;
	umode_t		mode;
	unsigned long	seq;
	void		*security;

	struct rhash_head khtnode;

	struct rcu_head rcu;
	refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;

其中key值代表这块共享内存的唯一值,操作系统把它分配给新创建的共享内存。

5.1 共享内存的函数,系统调用,bash命令

5.2 库函数ftok

先认识一个库函数
在这里插入图片描述
他用来创建一个唯一的key值,之后我们就可以把它分配给创建共享内存的函数,填到共享内存的ipc_perm里,来让共享内存有个唯一的标识。

5.3 系统调用shmget

在这里插入图片描述

在这里插入图片描述
key值是给操作系统看的,而在这个返回值是给用户看的。

     key_t key = ftok(PATHNAME,PROJ_ID);
    int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);  

形参IPC_CREAT|IPC_EXECL,代表共享内存不存在则创建,存在则出错返回。
在这里插入图片描述
第一次创建成功,第二次失败,说明共享内存的生命周期和管道不一样,是随内核的。

5.4 系统调用shmctl

在这里插入图片描述
这段代码是创建了共享内存,5s之后使用系统调用删掉。而实际上我们要知道操作系统分配资源,使用一个struct shmid_ds的结构体来描述他。

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/ipc.h>
  4 #include<sys/types.h>
  5 #include<sys/shm.h>
  6 #include"commnd.h"
  7 int main()
  8 {
  9   key_t key = ftok(PATHNAME,PROJ_ID);
 10   int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);                                                                                                                
 11   sleep(5);
 12   if(shmid<0)
 13   {
 14     perror("err");
 15     return 1;
 16   }
 17   sleep(5);
 18    shmctl(shmid,IPC_RMID,NULL);
 19   return 0;
 20 }

while :; do ipcs -m; sleep 1;echo "####";done

使用脚本监控一下
在这里插入图片描述

5.4 系统调用shmat与shmdt

在这里插入图片描述
返回值是虚拟地址空间的虚拟地址。
在这里插入图片描述
参数是shmat的返回值

所以可以写一个完整的共享内存的生命周期,先创建(同时操作系统分配资源,struct shmid_ds结构体描述),5s后此进程与内存相关联,nattch变成1,5s后又变成0,5s后被删除

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/ipc.h>
  4 #include<sys/types.h>
  5 #include<sys/shm.h>
  6 #include"commnd.h"
  7 int main()
  8 {
  9   key_t key = ftok(PATHNAME,PROJ_ID);
 10   int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
 11   sleep(5);
 12   if(shmid<0)
 13   {
 14     perror("err");
 15     return 1;
 16   }
 17   char* str= (char*)shmat(shmid,NULL,0);                                                                                                                             
 18   sleep(5);
 19   shmdt(str);
 20   sleep(5);
 21    shmctl(shmid,IPC_RMID,NULL);
 22   return 0;
 23 }

开始
在这里插入图片描述
过5s
在这里插入图片描述
在过5s
在这里插入图片描述

5.5 bash命令

  1. 查看共享内存
    ipcs-m
    在这里插入图片描述
    perms:权限,由于刚才代码没有 |权限这里显示0
    shmid:创建共享内存函数的返回值给用户用的。
    bytes:分配内存的大小,页的整数倍,假如大小设置为4097,分配的其实是两页,但是你只能用4097。
    nattch:挂接个数
  2. 删除共享内存
    ipcrm -m shmid
    在这里插入图片描述

6.共享内存的通信

和管道一样写一个服务器端,客户端是最好的验证。

服务器端,创建共享内存,直接可以看到数据

srever.c

 1 #include<stdio.h>                                                                                                                                                    
  2 #include<unistd.h>
  3 #include<sys/ipc.h>
  4 #include<sys/types.h>
  5 #include<sys/shm.h>
  6 #include"commnd.h"
  7 int main()
  8 {
  9   key_t key = ftok(PATHNAME,PROJ_ID);
 10   int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
 11   sleep(5);
 12   if(shmid<0)
 13   {
 14     perror("err");
 15     return 1;
 16   }
 17   char* str= (char*)shmat(shmid,NULL,0);
 18   while(1)
 19   {
 20 
 21     //str为首元素地址,直接可以用它打印字符串,就是打印共享区里的内容
 22     printf("%s\n",str);
 23     sleep(1);
 24   }
 25   shmdt(str);
 26   shmctl(shmid,IPC_RMID,NULL);
 27   return 0;
 28 }

client.c
客户端,ftok函数的路径与服务器端保持一致,这样才能拿到key,key是给操作系统用的。shmget不需要任何权限了,因为服务器端已经创建了共享内存,通过key值他们看到了同一块资源。返回值shmid供用户进行操作(挂接,删除等),注意!!客户端不能在删除共享内存因为在服务器端已经删除过了

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<sys/ipc.h>
  4 #include<sys/types.h>
  5 #include<sys/shm.h>
  6 #include"commnd.h"
  7 int main()
  8 {
  9   //形参一样得到的key值一样
 10   key_t key = ftok(PATHNAME,PROJ_ID);
 11   //去掉权限,因为在server端已经创建共享内存。
 12   int shmid=shmget(key,SIZE,0);
 13   sleep(5);
 14   if(shmid<0)
 15   {
 16     perror("err");
 17     return 1;
 18   }
 19   //挂接,拿到地址
 20   char* str= (char*)shmat(shmid,NULL,0);
 21   char s='a';                                                                                                                                                        
 22   for(;s<='z';s++)
 23   {
 24     str[s-'a']=s;
 25     //5s写一次
 26     sleep(5);
 27   }
 28   shmdt(str);
       
 29   //shmctl(shmid,IPC_RMID,NULL);
 30   return 0;
 31 }

在这里插入图片描述
在客户端,5s往共享内存写一次,但是服务器端在客户端不写的那4s,还是把东西重复打印出来了,而在管道中假如写端不写,读端是会阻塞等待,直到你在写。得出结论,共享内存不提供任何同步与互斥机制。

;原文链接:https://blog.csdn.net/qq_45928272/article/details/115570572
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐