进程间通信(IPC)

进程间通信(Inter-Process Communication)简称IPC,下面介绍两种IPC方式:

  • 管道
  • system V

管道

什么是管道

以前我们学过管道操作,具体可以参考:shell
管道通过一系列操作使一个命令的输出作为另一个命令的输入数据,由于命令在系统看来都是一个个的进程,所以管道在系统层面是进程间的通信(就是两个进程间相互交换数据)。那么如何实现 两个进程之间实现通信?
管道的本质 让管道的读写段分别看到同一块资源

  • 如何描述这一块资源?
    用文件来表示这块资源,但是这个文件属性与普通文件不同,是管道文件文件属性显示的是p(也就是pipe的简写)
  • 如何让多个文件共享这一个文件
    不同的文件用不同的方式打开这个文件,对这个文件进行读或写就实现了“共享”
    在这里插入图片描述
    注意
  • 一个文件可以被多个进程打开,也可以被一个进程打开多次(这样一个文件就可能对应多个文件描述符!)
  • 管道只支持单向数据传输,如果想要实现双向数据传输可以建立两个管道

管道的分类:

  • 匿名管道:不可见
  • 命名管道:可见,文件属性是p管道类型文件

匿名管道

匿名管道的概念

匿名管道也是进程间通信的一种,但是通信的进程之间存在父子关系,通俗的解释是有亲缘关系进程之间的通信
匿名管道运用的原理的是 :子进程在被创建的时候会默认打开父进程中所有的文件(文件描述符的继承)。这也是为什么所有进程都默认打开了三个标准输入、输出、错误,只要最原始的进程打开了这三个,他的所有子进程都会打开这三个文件。

匿名管道的原理

  • 首先父进程用读和写的方式打开管道文件,这时候会产生两个文件描述符,我们用一个数组fd[2]存储,fd[0]代表读,fd[1]代表写
  • 接下来创建子进程由于子进程是按照父进程继承下来,所以fd[0]fd[1]也会在子进程中默认被打开
  • 最后根据自己数据传输方向关闭父进程中不用的读端或写段(也可以不关闭,但是管道文件只能单向传输)
    在这里插入图片描述

pipe(int fd[2])

#include <unistd.h>
int pipe(int pipefd[2]);

参数含义
这里pipefd[2]是一个输出型参数,存储的是文件描述符,输出的pipefd[2]pipefd[0]代表的是读端,pipefd[1]代表的是写端

这个函数执行完成之后会创建一个管道文件(不可见),下面就可以创建子进程了,我们让子进程不断写入管道,父进程从管道文件中读取:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();
  if(ret==0)  //子进程
  {
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    {
      sleep(1);
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\n",++count);

    }
    return 0;
  }
  else // 父进程
  {
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    {
      sleep(1);
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      buffer[sz]='\0';
      printf("father read message: %s\n",buffer);
    }
    return 0;
  }

在这里插入图片描述

读慢写快

如果我们让子进程写入的速度远大于父进程从管道读取的速度,会发生什么结果?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();

  if(ret==0)  //子进程
  {
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    {
      sleep(1);  //每隔1s向管道写入一次
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\n",++count);
    }
    return 0;
  }
  else // 父进程
  {
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    {
      sleep(5);  //没隔5s从管道读取一次
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      buffer[sz]='\0';
      printf("father read message: %s\n",buffer);
    }
    return 0;
  }
}

在这里插入图片描述
结果是
管道直接被写满之后,才开始读取。但是注意在读取的时候写入端停止写入被阻塞,这就是写快读慢的后果写入端阻塞等待读取管道内容

读快写慢

如果我们让子进程写入的速度小于父进程读取管道的速度结果会怎么样?

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();

  if(ret==0)  //子进程
  {
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    {
      sleep(5);    //每隔5s向管道写入一次
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\n",++count);
    }
    return 0;
  }
  else // 父进程
  {
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    {
      sleep(1);    //没隔1s从管道读取一次
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      buffer[sz]='\0';
      printf("father read message: %s\n",buffer);
    }
    return 0;
  }
}

在这里插入图片描述
结果是
虽然读取管道的速度远大于写入的速度(读端每隔1s读取一次),但是读端最终还是每隔5s读取一次,这中间的四秒被阻塞等待管道另一段写入,所以如果写端不关闭文件描述符且不写入,读端可能要长时间阻塞

写段关闭,读段不关闭

#include<stdio.h>
#include<unistd.h>
#include<string.h>
int main()
{
  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();

  if(ret==0)  //子进程
  {
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    {
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\n",++count);
      if(count==5)  //写端写入5次后就退出
      {
        close(fd[1]);
        _exit(0);
      }

    }
    return 0;
  }
  else // 父进程
  {
    close(fd[1]);
    char buffer[64];
    int sz=1;
    while(sz)
    {
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      if(sz==0)
      {
        printf("father read nothing!  \n");
        continue;
      }
      buffer[sz]='\0';
      printf("father read message: %s\n",buffer);
    }
    return 0;
  }

}

在这里插入图片描述
结果是
写端关闭文件描述符,读端会读取到文件结尾(也就是read的返回值为0的时候就是读到管道结尾)

读端关闭,写段不关闭

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>

int main()
{
  int fd[2];
  pipe(fd); // fd[0]->read  fd[1]->write
  int ret=fork();
  if(ret==0)  //子进程
  {
    close(fd[0]);
    const char buffer[20]="i am a idiot";
    int count=0;
    while(1)
    {
      sleep(1);
      write(fd[1],buffer,strlen(buffer));
      printf("child write %d\n",++count);
    }
    return 0;
  }
  else // 父进程
  {
    close(fd[1]);
    char buffer[64];
    int sz=1;
    int count=0;
    while(sz)
    {
      sleep(1);
      sz=read(fd[0],buffer,sizeof(buffer)-1);
      if(sz==0)
      {
        printf("father read nothing!  \n");
        continue;
      }
      buffer[sz]='\0';
      printf("father read message: %s\n",buffer);
      if(count++==5)
      {
        close(fd[0]);
        break;
      }
    }
    int status=0;
    waitpid(ret,&status,0); //回收子进程的终止信号
    printf("子进程的终止信号是:%d\n",status & 0x7f);
    return 0;
  }

}

在这里插入图片描述

结果是
如果读端关闭,写端可能直接被操作系统终止掉(如果你用ps命令一直监视a.out的两个进程你会发现一个进程会直接终止),由于这是异常终止所以一定是操作系统发送了信号将进程杀死,所以我们用waitpid来拿到子进程的返回值并打印出来(如上图)

对照命令列表:发现SIGPIPE信号将进程杀死
在这里插入图片描述

命名管道

命名管道的

server.c

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
  int fd=open("./FIFO",O_RDONLY);
  int sz=1;
  char buffer[64];
  while(sz)
  {
    sz=read(fd,buffer,sizeof(buffer)-1);
    if(sz>0)
    {
      buffer[sz]='\0';
      printf(">: %s",buffer);
    }
    else if(sz==0)
    {
      printf("connect done!\n");
    }
    else 
    {
      printf("connect error!\n");
    }
  }

  return 0;
}

client.c

#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
  mkfifo("./FIFO",0777);
  int fd=open("./FIFO",O_WRONLY);
  char buffer[64];
  int sz=0;
  while(1)
  {
    printf(">: ");
    fflush(stdout);
    sz=read(1,buffer,64);
    write(fd,buffer,sz);
  }
  return 0;

}

makefile

.PHONY:all  #由于makefile不能同时编译两个文件,这里用一个伪对象
all:server client

server:server.c
	gcc -o $@ $^
client:client.c
	gcc -o $@ $^

.PHONY:clean
clean:
	rm -rf server client FIFO

实现

你在左边输入右边的进程就会把你输入的打出来:

在这里插入图片描述

system V 共享内存

什么是共享内存?

共享内存也是一种进程间通信的方式,但是共享内存和文件系统没有半毛钱关系,而是直接在内存上进行数据通信不依赖文件系统

os如何实现共享内存?

在这里插入图片描述
这是我们虚拟地址空间的示意图,图中在堆栈相向之间存在一片共享内存区域,这块区域可以加载共享库,同时还可以加载共享内存,所以我们就可以在内存上开辟一块公共区域,让需要通信的进程的虚拟进程地址的共享内存区域通过页表挂载到这片公共区域上,从而实现进程间通信

在这里插入图片描述

为什么要用共享内存?

如果你自己去写一遍上面命名管道的代码你就会发现命名管道需要一个缓冲区buffer[]来将数据从管道文件拷贝出来/进去,而共享内存因为不涉及文件系统直接在内存上进行通信,所以访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。

如何使用命令查看共享内存

ipcs——查看

  • -m:查看共享内存
  • -q:查看消息队列

我们这里输入ipcs -m就可以查看共享内存的情况:
在这里插入图片描述

ipcrm——删除

  • -M:加上想要删除的共享内存的key值
  • -m :加上想要删除的共享内存的shmid值

如何使用共享内存?

上面的图中我们会有以下几个问题没有解决:

  • 内存中 有多个共享内存,操作系统如何描述和管理这些共享内存?
  • 如何标识共享内存让两个不同的进程都挂接到同一个共享内存?
    我们从下面四个方面来学习

创建共享内存

查看共享内存的内核代码,操作系统是用shmid_ds结构体来描述一个共享内存的,同时这个数据结构被宏定义作为一个接口暴露给用户,来查看、直接修改共享进程的信息,这个结构体被包含在头文件include/linux/shm.h中,所以你的代码只要出现#include<shm.h>就可以定义出这么一个结构体!

	struct shmid_ds{
		struct ipc_perm shm_perm;				/* 操作权限*/
		int shm_segsz;             				/*段的大小(以字节为单位)*/
		time_t shm_atime;          				/*最后一个进程附加到该段的时间*/
		time_t shm_dtime;          				/*最后一个进程离开该段的时间*/
		time_t shm_ctime;          				/*最后一个进程修改该段的时间*/
		unsigned short shm_cpid;   				/*创建该段进程的pid*/
		unsigned short shm_lpid;  				/*在该段上操作的最后1个进程的pid*/
		short shm_nattch;          				/*当前附加到该段的进程的个数*/
		unsigned short shm_npages;  			/*段的大小(以页为单位)*/
		unsigned long *shm_pages;   			/*指向frames->SHMMAX的指针数组*/
		struct vm_area_struct *attaches;	 	/*对共享段的描述*/
	};

其中struct ipc_perm shm_perm; /* 操作权限*/这个尤为重要,这个记录的是共享内存的属性(里面的key值是os用来唯一标识一个共享内存的标识,具有唯一性):

	struct ipc_perm {
	   key_t          __key;    /* 操作系统用来标识共享内存的标识*/
	   uid_t          uid;      /* Effective UID of owner */
	   gid_t          gid;      /* Effective GID of owner */
	   uid_t          cuid;     /* Effective UID of creator */
	   gid_t          cgid;     /* Effective GID of creator */
	   unsigned short mode;     /* Permissions + SHM_DEST and SHM_LOCKED flags */
	   unsigned short __seq;    /* Sequence number */
	};

所以你想要建立一个共享内存就需要一个key值来让操作系统标记这个共享内存,这里就需要介绍一个函数用来生成具有唯一性的key值:

#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

函数的参数要求是一个是字符串,一个是一个整型(非零),返回值就是key值。注意这两个参数如果相同的话产生的key值是相同的,所以不同进程里面传入相同的pathname和proj_id产生的key值就是相同的(这里pathname在函数说明中要求传入一个路径但是实际上只要是一个字符串就行,这个函数的功能就是传入相同的字符串和整形就产出一个唯一的key值,类似随机值模拟器)。

产出了一个具有唯一性的key值,接下就是生成共享内存的函数shmget

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数含义

  • key:就是上面ftok生成的具有唯一性供操作系统标识共享内存的key值
  • size:你想要开辟的共享内存的大小,这个大小是按页(4k)进行分配的,所以开辟空间时尽量按照4k的整数倍开辟
  • shmflg:一些选项:
    1. IPC_CREAT:创建一个新的共享内存,如果存在就打开返回
    2. IPC_EXCL:和上面的选项一起使用是:创建一个新的共享内存,如果存在就报错返回(这样就保证创建出来的共享内存都是新的!)

返回值
这里的返回值是用户层用来标识共享内存的标识符,类似于文件系统中的文件操作符fd,下面对共享内存所有操作的函数都要用到这个标识符!

#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
#define pathname "i love you"
#define proj_id 10

int main()
{
  key_t key;
  key=ftok(pathname,proj_id);
  
  int shm;
  shm=shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);
  if(shm<=0)
  {
    perror("shmget");
    return 0;
  }
  sleep(20);
  return 0;
}

上面的代码执行起来会创建一个共享内存:
在这里插入图片描述
如果你多次执行该可执行程序你就会发现(选项IPC_ECL的原因)
在这里插入图片描述

但是你等进程结束之后,你会发现还是两个共享内存,这说明:
ipc的生命周期随内核不随进程,所以进程结束共享内存依然存在!(除非用系统调用或者指令将其摧毁)

删除共享内存(查看、修改共享内存)

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数

  • shmid:共享内存的用户标识符
  • cmd:删除选项IPC_RMID,表示要删除该共享内存(但是这块内存必须没有进程关联,否则无法删除)
  • shmid_ds:对上文的shmid_ds结构体中的属性进行修改,如果没有修改的需要可以设置为NULL

其实cmd还有两个选项:

  • IPC_STAT:将共享内存中的shmid_id类型的结构体赋值给buf结构体,这里buf就是一个输出型参数,用来得到共享内存的信息
  • IPC_SET:将buf中的uid、gid、mode赋值到共享内存的shmid_ds结构内,这里buf是作为模板修改共享内存的属性
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
#define pathname "i love you"
#define proj_id 10
int main()
{
  key_t key;
  key=ftok(pathname,proj_id);
  int shm;
  shm=shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);

  if(shm<=0)
  {
    perror("shmget");
    return 0;
  }
  sleep(5);
  shmctl(shm,IPC_RMID,NULL);
  return 0;
}

执行过程中:
在这里插入图片描述
执行结束后:(共享内存被关闭)
在这里插入图片描述

关联共享内存 && 取消关联共享内存

上面讲了如何创建、销毁一个共享内存,但是创建出来的这块区域还没有和进程关联起来所以不能进行进程间通信,下面介绍一下关联共享内存的函数

#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);

shmat——挂接函数
参数

  • shmid:标识符
  • shmaddr:传入NULL指针
  • shmflg:如果想要只读的话可以设置成SHM_RDONLY,读写设置为0

返回值
返回的是共享内存在虚拟进程地址空间中的首地址,类似于void *malloc()的返回值,使用时要对特定的类型强转
shmdt——取消挂接函数
参数
-shnaddr:挂接的共享内存的首地址(虚拟地址空间),也就是shmat的返回值

实现一个进程通信(共享内存)

client.c

该进程完成的任务有:

  • 创建一个共享内存
  • 关联共享内存
  • 写入数据
  • 取消关联共享内存
  • 销毁共享内存
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
#include<sys/types.h>
#define pathname "i love you"
#define proj_id 10

int main()
{
  key_t key;
  key=ftok(pathname,proj_id);
  
  int shm;
  shm=shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);  

  if(shm<=0)
  {
    perror("shmget");
    return 0;
  }
  
  char *p=(char*)shmat(shm,NULL,0);
 
  for(int i=0;i<5;i++)
  {
    p[i]='a'+i;
    sleep(3);
  }  

  struct shmid_ds buf;
  while((shmctl(shm,IPC_STAT,&buf),buf.shm_nattch)!=1)  //不断获取共享内存的挂接数,等待server.c取消关联后在执行循环下面的代码,防止销毁共享内存时还有进程与该共享内存挂接
  {
  }

  shmdt(p);    //取消关联
  
  printf("client destroy the shm!\n");
  
  shmctl(shm,IPC_RMID,NULL);    //销毁共享内存
  return 0;
}

server.c

该进程完成的任务有:

  • 创建一个共享内存
  • 关联共享内存
  • 读取数据
  • 取消关联共享内存
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
#include<sys/types.h>
#define pathname "i love you"
#define proj_id 10
#include<string.h>

int main()
{
  key_t key;
  key=ftok(pathname,proj_id);
  
  int shm;
  shm=shmget(key,4096,0);

  if(shm<=0)
  {
    perror("shmget");
    return 0;
  }
  
  char *p=(char *)shmat(shm,NULL,SHM_RDONLY);  //关联

  while(strlen(p)<5)
  {
    printf("%s\n",p);
    sleep(1);
  }
  
  shmdt(p);      //取消关联
  
  printf("server dettach and exit\n");
  return 0;
}

注意一定要在所有进程都与共享内存取消关联之后再销毁共享内存,否则无法真正意义上的消除,未取消挂接的进程还是能访问共享内存

运行
首先编译一下这两个源文件,这里贴一下我写的makefile:

.PHONY:all

all:server client

server:server.c
	gcc -o $@ $^ -std=c99

client:client.c
	gcc -o $@ $^ -std=c99

.PHONY:clean

clean:
	rm -rf server client 

在两个终端分别运行./client./server
在这里插入图片描述

总结

  • 进程内存的进程通信时不提供任何同步与互斥机制,所以读共享资源的读写是无序的
  • 临界资源:系统中某些资源一次只允许一个进程使用,称这种资源为临界资源
  • 临界区:进程中涉及到互斥资源的代码叫做临界区

总结

进程间通信(IPC)不管通过管道还是共享内存都是让两个进程看到同一块资源,但是看到同一块资源之后又会出现另一个问题:如何控制进程有序的访问这片临界资源?无序的访问只会造成数据丢失和不同步,所以这里就需要加锁(同步与互斥)。

  • 34
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 31
    评论
评论 31
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值