操作系统 —— 进程间的通信

文章目录

      • 1. 进程通信的本质以及目的
      • 2. 利用管道进行进程通信
        • 2.1 管道的概念
        • 2.2 匿名管道
          • 2.2.1 匿名管道的原理
          • 2.2.2 创建匿名管道
          • 2.2.3 匿名管道实现父子进程通信
          • 2.2.4 匿名管道的读写规则以及特点
        • 2.3 命名管道
          • 2.3.1 命名管道的原理
          • 2.3.2 创建命名管道
          • 2.3.3 利用命名管道实现进程间的通信
      • 3. system V方案
        • 3.1 共享内存
          • 3.1.1 共享内存的管理
          • 3.1.2 共享内存的接口函数
          • 3.1.3 利用共享内存完成进程间通信
        • 3.2 消息队列
          • 3.2.1 消息队列的原理
          • 3.2.2 消息队列的接口函数
        • 3.3 信号量
          • 3.3.1 认识信号量的基础
          • 3.3.2 信号量的本质
      • 4.对system V方案的总结

前言: 进程是有独立性的,所以要进行进程之间的通信,必须要有操作系统的管理。有几种通信方式呢?总的来说有两类:管道,system V方案。我会一 一 介绍,并且本章还会引入信号量这个概念,为后续的章节打个基础。


1. 进程通信的本质以及目的

进程是如何完成通信的呢?上面说了一共有俩类,但是都用的是一个思想:进程通信,必须使得进程间可以读写同一块内存。这块内存用于进程间通信,这是好理解的。

目的:

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

2. 利用管道进行进程通信

2.1 管道的概念

什么是管道?简单说,就是一个数据流,它用于两进程之间的数据交互,称做管道。

管道的本质:是内核中的缓冲区,用于完成进程间的通信,它的生命周期随进程。

举个例子:我有一个文件test.txt,内部有1~20个数字,有序的排列。现在我想要查看中间的5个数据,该怎么办?

(1) 先来看一下这个文件:

操作系统 —— 进程间的通信_第1张图片
(2) 利用管道来拿出中间的数据

cat test.txt | head -12 | tail -5

操作系统 —— 进程间的通信_第2张图片
(3) 解释原理

首先 cat 命令会将test.txt的内容输出到屏幕上,但是有了这个 | ,是将test.txt的内容输入到管道中,就类似这样:

操作系统 —— 进程间的通信_第3张图片

那么head将从管道中读取数据,加上选项 -12 就是看前12行,本来是要输出到屏幕上的,但是又加了一个管道,使得发生类似上述情况,tail又会读取此管道的内容,取出后5行。也就是中间的5个数据。

操作系统 —— 进程间的通信_第4张图片

(4) 总结

上述的过程就是就是利用管道进行通信,cat,head,tail都是不同的进程,但是它们都能够读写同一区域(管道)。

但是一个进程进行写,另一个进程进行读这种方式。下面我们来具体的讲讲,我们创建的进程,该如何利用进程进行通信。


2.2 匿名管道

什么是匿名管道呢?它只用于父子进程中,所以不需要给它命名,只用于父子进程进行通信。

2.2.1 匿名管道的原理

子进程会继承父进程的代码和数据,但是进程控制块是独立,这保证了进程间的独立性;进程控制块中有一个*file ,它指向了管理文件的结构体,这个结构体中有文件指针数组。进程默认打开了三个文件:标准输入,标准输出,标准错误输出,它们的文件描述符分别是0,1,2。文件描述符就是文件指针数组的下标。那么匿名管道它也是个文件,创建匿名管道后自然会有文件指针指向它,但是需要注意指向它的文件指针有俩个,一个是进行读操作的标识,一个是进行写操作的标识。

图解:

(1)父子进程通信的图解:
操作系统 —— 进程间的通信_第5张图片

那么父进程创建好管道,子进程继承后,也会向父进程那样看待管道:

操作系统 —— 进程间的通信_第6张图片

如何实现:父子进程进行通信?假如实现子进程读,父进程写,那么就关闭子进程的写,关闭父进程的读。

操作系统 —— 进程间的通信_第7张图片

(2) 创建匿名管道的原理
操作系统 —— 进程间的通信_第8张图片

如果创建了一个子进程,那么情况会变成这样:

操作系统 —— 进程间的通信_第9张图片

2.2.2 创建匿名管道

操作系统 —— 进程间的通信_第10张图片

用的函数是int pipe(int fd[2])以及·int pipe2(int pipefd[2], int flags).

这两个函数的差别在于:参数不同,pipe2的第二个参数,如果pipe2的第二个参数设了0,那么和pipe毫无差别。关键加这么一个参数是为了,对创建出的管理进一步限制(管理)。

  • 函数的返回值:int整型,返回为0 表示管道创建成功;返回为 -1 表示管道设置失败,并设置errno。

  • 函数的参数: 函数的参数是一个数组,输出型参数,创建管道成功后,pipefd[0] ,pipefd[1] 存的都是文件描述符,pipefd[0] 是读管道的描述符,pipefd[1]是向管道写的描述符。

  • 单独讲讲pipe2()的第二个参数:

  1. O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  2. O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  3. O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
  4. O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  5. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  6. 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

先简单的实现匿名管道,顺便验证一下匿名管道的文件描述符:

操作系统 —— 进程间的通信_第11张图片

这就创建好了匿名管道,运行看看情况:

在这里插入图片描述
没问题,匿名管道的文件描述符是3 ,4 。0,1,2的话已经被占用。


2.2.3 匿名管道实现父子进程通信

图解很清楚:

两种情况:

  1. 父进程读子进程写:那么需要关闭父进程的写,子进程的读
  2. 父进程写子进程读:那么需要关闭父进程的读,子进程的写

为什么要这样做?其实也是为了保护原子性,防止进程又写又读导致非常混乱的情况。

假如我要实现:子进程读,父进程写

#include
#include
#include

int main()
{
  int ret[2]={0};
  pipe(ret);
  if(fork() == 0)
  {
     close(ret[1]);
     
     // 子进程读
     while(1)
     { 
        //更新一下
        char buffer[64] = {0};
        // //zero indicates end of file,如果read的返回值是0,意味子进程关闭文件描述符了
        // //只要有数据,就可以一直读取
         ssize_t s = read(ret[0], buffer, sizeof(buffer)-1);
         if(s == 0){
             printf("father quit...\n");
             break;
         }
         else if(s > 0){
             buffer[s] = 0;
             printf("father say to child# %s\n", buffer);
         }
         else{
           printf("read error...\n");
             break;
         }    
       } 
  }

  close(ret[0]);
  // 父进程写
  while(1) 
  {
   const char* str = "hollow everyone";
   write(ret[1],str,strlen(str));
  }

  return 0;
  }

这就完成了父子进程间的通信,运行的时候,不是很人性,因为写和读都很快,导致满屏飞。不过没关系,我们接下来会详细的讲解匿名管道的读写规则以及特点。


2.2.4 匿名管道的读写规则以及特点

(1) 读写规则

  1. 读端不读或者读的慢,写端等待读端

验证一下,就上面的程序,因为可以让子进程(读端),sleep(2),看看情况。

操作系统 —— 进程间的通信_第12张图片
因为写端快于读端,所以看到写段向管道写入了不少内容,所以读端每次都读的满满的。

  1. 读端关闭,写端直接终止

这是好理解的,都没人去管道读数据了,何必还要往里面写呢?用什么终止,->信号

验证一下,我让子进程读一次,立马就退出,看看父进程还会写吗?父进程如果不写了,那么我们获取一下它的退出信息:

在这里插入图片描述
可以看到,运行直接终止,退出码是141,哎呀,退出码总共就130多个,他直接退出码141,说明父进程是被信号干掉得,这个信号就是13。

就是这个货:SIGPIPE
操作系统 —— 进程间的通信_第13张图片

  1. 写端不写或者写的慢,读端会等待写端

这次我们让写端父进程,等待2s,看看具体情况:

操作系统 —— 进程间的通信_第14张图片
就是这样的,因为写的慢,读的快,所以每次只读出写入的一行。

  1. 写端关闭,读端会读完管道的内容,后在退出

这个就不验证了,留个大家。


(2) 匿名管道的特点

  1. 一个只能单向通信的通信管道
  2. 面向字节流
  3. 仅限于父子通信
  4. 管道自带同步机制,原子性写入
  5. 管道的生命周期随进程
  6. 如果想要实现双方都能读写管道,只能是再另加一个管道

2.3 命名管道

命名管道是有名字的管道,它可以使得不同的进程,使用此管道。创建命名管道可以在命令行上创建,也就是创建一个文件,当然也能在程序中创建。命名管道也是内核的缓冲区,不过它是有一个命名管道文件的,进程通过命名管道文件,来找到内核中的缓冲区,所以命名管道文件是命名管道缓冲区的一个标识,那么如果删除命名管道文件,会不会导致进程间无法通信?不会,之前已经在通信的进程,是不需要命名管道文件的,它们依旧可以通信。

2.3.1 命名管道的原理

实现进程间通信,必须使得进程看到同一块内存,并进行读写操作。但是该如何找到这块内存呢?那就给管道命名,这是具有唯一性的,利用算法生成的管道名,这后面会讲到的。

操作系统 —— 进程间的通信_第15张图片

命名管道文件的内容,一般不会刷到磁盘中,这是为了程序的效率:

操作系统 —— 进程间的通信_第16张图片

注意:

建好命名管道后,程序在进行通信时,其实用的是文件操作,就是向文件写,向文件读,这种基操。

2.3.2 创建命名管道

命令行创建一个命名管道:

用的命令:mkfifo filename

操作一下吧,方便理解:

操作系统 —— 进程间的通信_第17张图片
可以看到我创建的新文件,fifo的开头是 p 说明是管道文件。

这是两个不同的进程:
操作系统 —— 进程间的通信_第18张图片
现在我利用fifo(命名管道),进行通信:

操作系统 —— 进程间的通信_第19张图片
操作系统 —— 进程间的通信_第20张图片


2.3.3 利用命名管道实现进程间的通信

首先认识一个函数:

int mkfifo(const char *pathname,mode_t mode);

操作系统 —— 进程间的通信_第21张图片

  • 函数的参数:pathname 是个路径+fifo名(自定义),在哪个路径下创建一个命名管道;mode是创建的命名管道的权限,注意这个权限收到umask的影响
  • 函数的返回值:创建成功,返回0;创建失败,返回-1。

然后,我们创建俩个程序,ly.c,ybw.c。要实现它俩之间的通信,为了让它们看到同一份资源,那么我必须要创建一个命名管道,但是俩程序如何看到这个命名管道呢?可以包一个头文件,有点巧妙,看完就懂了。

ybw.c 发出信息,ly.c 接受消息:

  • 首先我创建一个头文件:
    这两个进程包同一个头文件,目的就是使得它俩能操作同一个管道
#pragma once

#include 
#include 
#include 
#include 
#include 
#include

#define MY_FIFO "./fifo"

  • 编写ly.c
#include"communi.h"

int main()
{
  umask(0);
if(mkfifo(MY_FIFO,0666)<0)
{
  perror("mkfifo");
  return 1;
}
 
 int fd= open(MY_FIFO,O_RDONLY);

 if(fd<0)
{
  perror("open");
  return 2;
}
 while(1) 
{
 char str[64]={0};

  read(fd,str,sizeof(str)-1);
 
  printf("%s\n",str);
 }

close(fd);
}

umask(0),表示将权限掩码设置为0;mkfino()创建管道;open()以只读的方式打开管道(操作文件);read()将管道的内容输入到str数组中;printf打印str内容;每次循环开始,都要将str置空。

  • 编写ybw.c
#include"communi.h"

int main()
{
  
  int fd= open(MY_FIFO,O_WRONLY);
 while(1) 
 { 
  char str[64]={0};

  printf("请输入: ");

  fflush(stdout);
 
  
  int s=  read(0,str,sizeof(str)-1);
  
  str[s-1]=0;
  printf("%s\n",str);


  write(fd,str,strlen(str));
 }
 close(fd);
  return 0;
}

这里就不需要创建管道了,直接open()以只写的方式打开管道;然后用read()从键盘(文件描述符0)读取数据到str,注意不能读到‘\n’;将末尾设置为‘\0’;printf()打印一下str;最后用write()向管道写入str。


我们来看看现在的效果:

操作系统 —— 进程间的通信_第22张图片
这就是简单的,一个进程传过去消息,然后另一个进程读消息;可不可以扩展一下,感觉有点low;我们可以畅想一下:如果再创建一个命名管道,是不是就可以完成聊天了?这个大家感兴趣可以去做着玩。但是如果是基础功能我到是还可以添加进去的,这涉及到了进程替换,比如ybw输出一个look,ly接受到后,比较一下,发现传过来的消息是look,那么就进程替换成 打印当下目录:ls -a -l,就是这个。

我们来试一下:

#include"communi.h"

int main()
{
  umask(0);
if(mkfifo(MY_FIFO,0666)<0)
{
  perror("mkfifo");
  return 1;
}
 
 int fd= open(MY_FIFO,O_RDONLY);

 if(fd<0)
{
  perror("open");
  return 2;
}
 while(1) 
{
 char str[64]={0};

  read(fd,str,sizeof(str)-1);
   
  if(strcmp(str,"look")==0)
  {
     if(fork() == 0)
     {
        execl("/usr/bin/ls", "ls", "-l", NULL);
        _exit(1);

     }
  }

  else 
  {  
     printf("ybw say:%s\n",str); 
  }
 }
close(fd);
}

运行一下:

操作系统 —— 进程间的通信_第23张图片
确实,还行。


3. system V方案

上面我们是通过管道进行的进程通信,难道就没有更简单的方式,操作系统不能给个接口使唤一下吗?答案是有的。操作系统提供了system V标准的进程通信,当然这是科学家兼程序员设计的;进程通信的本质是让不同的进程使用到同一块内存,这是我多次强调的。

  • system V也是如此,不过有三种组织方式:
  1. 共享内存
  2. 消息队列(有点落后)
  3. 信号量(讲一点点)

3.1 共享内存

共享内存真的是相当的快捷。共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据。

简单说就是,搞一块内存,映射到进程的页表中,这就表明进程和此内存挂接上来;如果要完成进程间通信,只需要使另一个进程也挂接上这个内存;通过这个内存就完成了进程间通信。这需要理解虚拟地址空间,不懂的童鞋需要补一下这块知识。

共享内存的删除的本质:是一种引用计数删除,也就是说只要有进程链接着共享内存,共享内存就不会被删除,进程要删除共享内存,只不过是共享内存中的链接计数减一,直到共享内存中的链接数为0,这时候进程对共享内存进行删除操作,才是真正的释放共享内存所占空间。

图解:

这是进程A和进程B,通过各自页表,在内存的映射关系:
操作系统 —— 进程间的通信_第24张图片

现在,我开辟一个共享内存,并使得A,B进程都挂接上此内存:

操作系统 —— 进程间的通信_第25张图片
就是这么个意思。


3.1.1 共享内存的管理

我们是需要管理共享内存的,应该都知道,进程管理。进程加载到内存需要管理,同样搞一个共享内存也是需要进行管理的。而且是有多个共享内存块的,如何区分?必然有个标号来标识共享内存,就是shmid。

使用指令 ipcs -m 来查看共享内存段的使用情况:

操作系统 —— 进程间的通信_第26张图片

是由一个结构体来管理的:

struct shmid_ds 
{
 struct ipc_perm shm_perm; /* operation perms */
 int shm_segsz; /* size of segment (bytes) */
 __kernel_time_t shm_atime; /* last attach time */
 __kernel_time_t shm_dtime; /* last detach time */
 __kernel_time_t shm_ctime; /* last change time */
 __kernel_ipc_pid_t shm_cpid; /* pid of creator */
 __kernel_ipc_pid_t shm_lpid; /* pid of last operator */
 unsigned short shm_nattch; /* no. of current attaches */
 unsigned short shm_unused; /* compatibility */
 void *shm_unused2; /* ditto - used by DIPC */
 void *shm_unused3; /* unused */
 }

3.1.2 共享内存的接口函数

操作系统 —— 进程间的通信_第27张图片

  • shmget函数:

功能:用来创建共享内存
原型: int shmget(key_t key, size_t size, int shmflg);
参数:

  • key: 这个共享内存段名字
  • size: 共享内存大小
  • shmflg: 由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的

返回值:成功返回一个非负整数,即该共享内存段的标识码(shmid) ; 失败返回 -1

第一个参数key,是用一个函数ftok()来设置的,这个共享内存的名字也是有说法的,用算法来形成的,防止共享内存段名字重复:

操作系统 —— 进程间的通信_第28张图片

  • ftok()的参数:第一个是自定义路径名,第二个是自定义项目id。
  • 返回值是形成的内存段名字key。如果失败,返回 -1。

第二参数size:表示创建的共享内存段的大小,这里我建议创建的4KB的整数倍

第三个参数shmflg:有九个权限,也可以用 |的方式,组合使用。

在这里插入图片描述
先学习这两个权限,感兴趣可以在自己去man手册中查阅:

IPC_CREAT : 如果共享内存不存在,则创建一个新的共享内存;如果共享内存已经存在,则返回当前已经存在共享内存的shmid。

IPC_EXCL:这个必须和IPC_CREAT 配合使用,如果共享内存不存在,则创建新的共享内存;如果共享内存已经存在,则报错。这就保证拿到的共享内存必须是全新的。


操作系统 —— 进程间的通信_第29张图片

  • shmat函数:

功能:将共享内存段连接到进程地址空间
原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:

  • shmid: 共享内存标识
  • shmaddr:指定连接的地址
  • shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY

返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1

第一个参数shmid是由shmget获取的;第二个参数shmaddr是指定连接的地址,也就是指定共享内存链接到进程内存地址的具体位置,如果设置为null,那么内核会自己决定在进程地址空间中找一个合适的位置;第三个参数是设置权限,SHM_RDONLY是只附加读权限,如果设置为0,那么就是可读可写;至于设置SHM_RND权限,要配合着第二个参数shmaddr使用,使用规则如下:

  • shmaddr为NULL,核心自动选择一个地址
  • shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
  • shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
  • shmflg=SHM_RDONLY,表示连接操作用来只读共享内存

返回值是共享内存段的地址,挂接失败的话返回 -1。


操作系统 —— 进程间的通信_第30张图片

  • shmdt函数:

功能:将共享内存段与当前进程脱离
原型:
int shmdt(const void *shmaddr);
参数:

  • shmaddr: 由shmat所返回的指针

返回值:成功返回0;失败返回-1

注意:将共享内存段与当前进程脱离不等于删除共享内存段

函数的参数是shmat()的返回值,就是解除进程与共享内存段的挂接。


操作系统 —— 进程间的通信_第31张图片

  • shmctl函数:

功能:用于控制共享内存

原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

  • shmid:由shmget返回的共享内存标识码
  • cmd:将要采取的动作(有三个可取值)
  • buf:指向一个保存着共享内存的模式状态和访问权限的数据结构

返回值:成功返回0;失败返回-1

第二个参数:cmd有三个可取值:

操作系统 —— 进程间的通信_第32张图片

  • IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
  • IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
  • IPC_RMID:删除这片共享内存

总结一下:shmget用来创建共享内存,返回值是shmid(标识符),这有点像open()打开文件,返回文件的描述符fd;shmat是用于程序和共享内存的挂接;shmdt是用于解除程序和共享内存的挂接;shmctl是用于控制创建的共享内存,可以删除等操作。

有么有人,感觉有点疑问: key和shmid有必要同时存在吗?

答案是有必要:

  • key是用于操作系统识别的,更具这个key,去创建共享内存段
  • shmid是给用户层的,让用户层通过这个shmid,去调用相关接口,从而管理共享内存段

3.1.3 利用共享内存完成进程间通信

首先还是得有共同的头文件,这个头文件中我定义几个宏,为的是两个程序,拿到同一个key。

头文件名为communite.h:

#pragma once

#include
#include
#include
#include
#include

#define PATH_name "./"
#define my_ID 0x6666
#define size 4097 

我定义了三个宏:PATH_name,my_ID为的是得到的key相同。

我只要给ftok()传参,都传这两个宏,那么两个程序得到的key就是相同,key是ftok()的返回值。

size是我规定的,创建共享内存的大小。


然后我开始写 读取数据的程序,我要求读取数据的程序先开辟共享内存,然后读取数据完毕后,删除共享内存。
此程序我命名为ybw.c。

#include"communite.h"

int main()
{
  key_t key = ftok(PATH_name,my_ID);
 //可以看到shmid的第二个参数我设置中加上了IPC_EXCL,保证了共享内存是新开辟的,后面的0666是共享内存设置的权限
  int shmid = shmget(key,size,IPC_CREAT|IPC_EXCL|0666);
  
  if(shmid<0)
  {
    perror("no shmid\n");
  }
 // 用mem接收shmat返回的共享内存地址,第二参数设置为null,要求操作系统自己找合适的地址空间进行挂接,最后一个参数是可读可写,不设限
  char *mem =(char*)shmat(shmid,NULL,0);

  printf("shmat success\n");

  //开始通信
  //读出共享内存中的数据
  while(1)
  {
    sleep(1);
    printf("%s\n",mem);
    //写不到24个字母就结束
    if(strlen(mem)>24)
      break;
  }
  //解除挂机
  shmdt(mem);

  printf("shmde success\n");
  // 删除共享内存,必须手动释放,否则就是内存泄漏
  shmctl(shmid, IPC_RMID, NULL);
  return 0;
}  


最后我需要完成向共享内存中写入数据,但是需要注意,还需要开辟共享内存吗?不需要,但是还得用shmget()函数,拿到共享内存的shmid,因为有了shmid才能完成挂接。挂接成功后,我们就能够向共享内存写入数据。

我将此程序命名为ly.c:

#include"communite.h"

int main()
{
 //拿到同一个key
  key_t key = ftok(PATH_name,my_ID); 
  
  if(key<0)
  {
    perror("no key");
    return 1;
  }
  //注意shmget的第二个参数是IPC_CREAT,说明共享内存创建好了的话,会直接返回
  int shimid = shmget(key,size,IPC_CREAT);

  if(shimid<0)
  {
    perror("no shmget");
    return 1;
  }
  // 用mem指针接收shmat返回的共享内存指针
  char *mem = (char *)shmat(shimid,NULL,0);
  
  printf("shmat success\n");
  
   
  //开始通信
  char c = 'A';
  while(c<'Z')
  {
    mem[c-'A']=c;
    c++;
    //字符串末尾加 \0 没毛病
    mem[c-'A']=0;

    sleep(2);
  }
  //解除挂机
  shmdt(mem);
  
  printf("shmdt success\n");
  //不需要删除共享内存了,让ybw去删吧
  return 0;
}

以上其实就完成了,利用共享内存完成进程通信:

看看效果:

操作系统 —— 进程间的通信_第33张图片

注意:程序应该要手动的删除共享内存,否则会造成内存泄漏,假如上面的进程,我用信号 ctrl+c 干掉它,那么它的共享内存就没有释放,该怎么办呢?可以通过命令行进行删除:ipcrm -m +shmid ,不知道shimid可以用指令
ipcs -m 查看一下。

比如:

我用信号干掉了进程ybw,来看看共享内存的情况:
操作系统 —— 进程间的通信_第34张图片

所以只能用命令行,来释放共享内存了:

操作系统 —— 进程间的通信_第35张图片

大家可能觉得很神奇:向共享内存中写,读;都没提供接口函数。是直接往里写,从里面读。就类似于malloc开辟的空间一样。所以共享内存,没有什么互斥,同步,全靠程序员自己去控制。但是共享内存是真的快。


3.2 消息队列

这个选学内容,我们不要求用它来实现进程通信,有点落伍。但是我们还是简单的介绍一下它的原理和函数接口。

3.2.1 消息队列的原理
  • 消息队列遵守队列先进先出的规则,这就遵守了时序性。
  • 消息队列:用一个一个的节点保存信息,然后用队列的方式组织。
  • 任何进程都可以用消息队列通信
  • 不足之处:消息队列的读写涉及数据拷贝比较耗时
3.2.2 消息队列的接口函数

(1) 创建或打开消息队列

操作系统 —— 进程间的通信_第36张图片

  • msgget()的参数:key就是利用ftok()函数生成的,这和上面的共享内存一样;msgflg是对创建的消息队列的管理
  • msgget()的返回值:创建成功返回消息队列的标识符(maqid),失败返回 -1

关于第二个参数 msgflg:

  • IPC_CREAT:如果消息队列对象不存在,则创建之,否则则进行打开操作
  • IPC_EXCL: 和IPC_CREAT 一起使用(用”|”连接),如果消息对象不存在则创建之,
    否则产生一个错误并返回。
    这和共享内存一样。

(2) 控制消息队列
操作系统 —— 进程间的通信_第37张图片

  • msgctl()的参数:第一个参数 msqid是msgget()的返回值,也就是消息对立的标识符;第二个参数 cmd是对消息队列要进行的控制操作;最后一个参数 buf是指向msgid_ds结构的指针,删除时设为NULL。

  • msgctl()的返回值:至于返回值,是根据cmd来变化的。但是控制失败返回的是 -1。

  • 关于第二个参数cmd:
    IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。

    IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值

    IPC_RMID:删除消息队列

(3) 向消息队列中写入
操作系统 —— 进程间的通信_第38张图片
这两个接口,都可以向消息队列中写入。

不过向消息队列写入,必须要定义一个结构体,结构体中必须定义long 类型整型(消息类型)。

操作系统 —— 进程间的通信_第39张图片

然后介绍一下msgsnd()函数:

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)

  • 函数参数:
  1. msqid:消息队列返回的标志符
  2. msgp:待发送消息的结构体指针
  3. msgsz:除去消息类型(mtype)的最大消息大小
  4. msgflg:向消息队列中写入的限制
  • 函数返回值: 写入成功返回 0,写入失败返回 -1。

(4) 读取消息队列的内容

操作系统 —— 进程间的通信_第40张图片
依旧提供了两个接口函数,和上面的写入是对应的。

函数参数和返回值都一样,同样需要有一个结构体来接收消息。

操作系统 —— 进程间的通信_第41张图片


3.3 信号量

之前讲的进程间通信,通信的内容都是数据;但是信号量的通信目的不是为了通信数据,而是通过共享资源,使得进程间通信达到同步和互斥的目的。

3.3.1 认识信号量的基础

在认识信号量之前,我们先搞懂以下的几个概念:

  • 临界资源:凡是被多个执行流同时访问的资源就是临界资源,比如进程间的通信,都是临界资源
  • 临界区:进程的代码中,访问临界资源的代码段就是临界区
  • 原子性:一个事件从始到终,没有中间态。就好比一件事要做就做到底,不能中途停留
  • 互斥:在任意时刻,只允许一个执行流进入临界资源,执行临界区
  • 同步:是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。
3.3.2 信号量的本质

信号量的本质:是一个计数器 count,用于衡量临界资源的资源数目,这是一种保护临界资源的原子性的手段。

我举个例子:坐出租车。

出租车上的座位(临界资源),可以被不同的人坐上去(对应不同的进程查看临界资源)。总共还有多少座位,用一个标号来控制(信号量)。如果标号大于0,证明还有座位,可以拉人,拉上人后标识符减一;如果标识==0,那么表示没有座位,只能等待;如果有人要离开,那么标识符+1。

对此,信号量就对临界资源做到了管理,有个疑问,座位必须有人座,才算临界资源被占用吗?不是,也可以预约,只要有人预定座位,那么信号量也要减一。

信号量通过管理临界资源的数目,确实是做到了保护临界资源的原子性,如果有个进程想对某个临界资源横插一脚,是做不到的,这个进程只能去等待,或者去瞅瞅别的空余的临界资源。

但是还有一个问题:信号量本身也是临界资源,它的原子性怎么保护?那就是它的count+1和count-1都是原子性的,这就是传说中的v() -> 信号量+1,p() -> 信号量-1操作,所以就是保证v()和p()的原子性,就保证了信号量的原子性。


4.对system V方案的总结

其实管理共享内存,消息队列,信号量用的是一个数组。这不扯吗?管理它们的结构体都不一样,怎么能用一个数组统一管理呢?我们先来依次的看看管理它们的结构体。

共享内存
操作系统 —— 进程间的通信_第42张图片

消息队列

操作系统 —— 进程间的通信_第43张图片
信号量

操作系统 —— 进程间的通信_第44张图片

注意到了吗?我标红的地方,也就是结构体的一个内容,都是一样的,包的都是同一个结构体
struct ipc_perm 。
se
查看一下:
操作系统 —— 进程间的通信_第45张图片

通过这个东西,我就可以完成对上面三个结构体的统一管理,其实了解到这里,我不得感叹一下,设计出此内容的人,真是个小天才。

我可以搞一个结构体指针数组,每个数组元素都是ipc_perm的指针。那么我就可以通过强转的方式拿到共享内存,消息队列,信号量的结构体指针中的头部数据,这是一种切片技术。

如果想要通过这个结构体指针数组,看到具体的共享内存,消息队列,信号量的结构体,只需要再强转回去就可以了。这里有点难理解,我画个图。

操作系统 —— 进程间的通信_第46张图片

我通过强转的方式,确实是可以让结构体指针指向下面三个结构体:
比如 -> ipc_perm[0] =(ipc_perm *) semid_ds。

那么如果我想要看到semid_ds的具体内容,怎么办?

只需要 -> (semid_ds*) ipc_perm[0] ,秒呀,再强转回去,真是骚操作。

上面也就解释了我们创建时,那些标识符0,1,2,3……;其实都是 ipc_perm 指针数组的下标罢了。


结尾语: 以上就是本篇内容,带大家了解进程间的通信,有问题的朋友可以私信或评论。觉得有帮助的,可以点个赞支持一下哦 !!!

你可能感兴趣的:(操作系统,linux,开发语言,c++)