进程间通信(管道,共享内存,消息队列,信号量)

文章目录

  • 1.管道
    • 1.1 匿名管道
    • 1.2 命名管道
    • 1.3 管道特性
  • 2.共享内存
  • 3.消息队列
  • 4.信号量

进程与进程之间是相互独立的,无法直接进行数据传输,所以操作系统给用户提供了用于进程间通讯的接口,根据使用场景不同,进程间通信可分为四类:管道,共享内存,消息队列,信号量。

1.管道

本质:是在内存中开辟的一块缓冲区。
实现原理:多个进程通过访问同一块内核中的缓冲区来实现通信。
管道可分为:匿名管道和命名管道。

1.1 匿名管道

匿名管道的缓冲区没有标识符,无法被其他进程找到,只能通过子进程复制父进程的PCB这种方式来获取到缓冲区的操作句柄来实现对这块缓冲区的操作,进而实现进程间的通信。故匿名管道只能用于具有亲缘关系的进程间通信,这里不光是父子进程,也可以是兄弟进程,毕竟兄弟进程的pcb都是复制其父进程的,都可通过PCB找到这块匿名管道对应的缓冲区进行操作。
我们常用到的管道符号 | 就是一种匿名管道 eg: ps -ef |grep ssh 就是将 | 前面命令输出的结果传输给后面的命令进行处理;
进程间通信(管道,共享内存,消息队列,信号量)_第1张图片

需要用到的函数:

#include 
int pipe(int pipefd[2]);

参数 pipefd[2]:为开辟的缓冲区的读写句柄,其中pipefd[0]用于读,pipefd[1]用于写。对于一个进程不能同时使用读和写,只能用一个。一般会将不用的那一个pipefd关闭掉。
返回值成功返回0 失败 -1;
进程间通信(管道,共享内存,消息队列,信号量)_第2张图片

管道的读写特性:

若管道中无数据,则read会阻塞,直到有数据被write;
若管道中数据已满,则write会阻塞,直到有数据被read;
所有的管道read端被关闭,继续write则会触发异常,程序崩溃;
所有的管道write端被关闭,继续read则会读完数据后返回0(表示不会再有write数据到管道了),不在阻塞;

练习:先创建管道,然后创建子进程,这样父子进程就能通过管道进行通信,然后父进程向管道中写入:“i am father”,子进程从管道中读取并打印出来。

#include     
#include     
#include    
int  main ()    
  {    
    int pipefd[2];    
    pipe(pipefd);          
    pid_t ret = fork();    
    if(ret<0){    
      perror("fork error");    
      return -1;    
    }    
    else if (ret ==0)//子进程    
    {    
      close(pipefd[1]);//关闭写    
      char buf[1024]={0};    
      if(read(pipefd[0],buf,1023)!= -1)    //读取成功则打印
        printf("%s\n",buf);    
    }    

    ///父进程  /  
    close(pipefd[0]);//关闭读    
         char *str= "i am father\n";    
      int retw=write(pipefd[1],str,strlen(str));                      
      if(retw==-1){    
        perror("write error");    
        return -1;                
      }               
    return 0;    
  } 

进程间通信(管道,共享内存,消息队列,信号量)_第3张图片

1.2 命名管道

匿名管道的缓冲区有标识符,可以被其他进程找到,进而实现同一主机上的任意进程间的通信。
命名管道的标识符就是一个可见于文件系统的管道类型文件。多个进程通过打开同一文件访问同一块内核中的缓冲区实现通信。

进程间通信(管道,共享内存,消息队列,信号量)_第4张图片

注意:匿名管道是通过文件描述符的形式使用。而命名管道,是通过该命令管道文件的“文件名”来使用。
需要用到的函数:

mkfifo [filename]
用于创建一个管道文件(有进程访问时才会真实开辟出这块缓冲区);
如果只写入管道,没有读,则会阻塞,知道管道被其他进程以读的方式打开;
如果只读入管道,没有写,则会阻塞,知道管道被其他进程以写的方式打开;
#include 
#include 
int mkfifo(const char *pathname, mode_t mode);

参数:

pathname:该命名管道的绝对路径。
mode:该管道的访问权限,(相当于创建文件时给的操作权限,eg:0664)
返回值:成功 0,失败 -1;

练习:创建命名管道实现进程mkfifo1和mkfifo2之间的通信,在进程mkfifo1中写入,然后在进程mkfifo2中读取并打印;

mkfifo1.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
  char* filename = "./mkfifo.txt";
  int ret = mkfifo(filename,0664);  //创建命名管道
  if(ret<0 &&  errno!=EEXIST){
    perror("mkkfifo error");
    return -1;
  }
  int fd = open(filename,O_WRONLY);//只写方式打开文件。
  if(fd<0){
    perror("open error");
    return -1;
  }
  while(1){          
    char buf[1024]={0};
    scanf("%s",buf);
    int retw = write(fd,buf ,strlen(buf));
    if(retw <0){
      perror("write error");
      return -1;
    }
  }
  close(fd);
  return  0;
}
mkfifo2.c
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(){
  char * filename = "./mkfifo.txt";
  int ret = mkfifo(filename,0664);
  if(ret<0 && errno!=EEXIST){
    perror("mkfifo error ");
    return -1;
  }
  int fd  = open(filename,O_RDONLY);
  if(fd<0){
    perror("open error");
    return -1;
  }
  while(1){
    char buf[1024]={0};
    int retr = read(fd,buf,1023);
    if(retr <0){
      perror("retr error");
      return -1;
    }
    else if(retr ==0){
      printf("\nall write is closed\n");
      return -1;
    }
    printf("%s",buf);
  }
  close(fd);
  return 0;
}

运行结果:进程间通信(管道,共享内存,消息队列,信号量)_第5张图片
mkfifo1输入结束后,mkfifo2中读取到数据并打印

1.3 管道特性

读写特性:

若管道中无数据,则read会阻塞,直到有数据被write;
若管道中数据已满,则write会阻塞,直到有数据被read;
所有的管道read端被关闭,继续write则会触发异常,程序崩溃;
所有的管道write端被关闭,继续read则会读完数据后返回0(表示不会再有write数据到管道了),不在阻塞;

管道特性:
1.管道提供字节流传输服务:一种有序的(先进先出),可靠的,基于连接的流式传输。
2.管道的生命周期随内核,不人为干预的情况下,所有打管道缓冲区的进程退出,则缓冲区被释放。
3.管道自带同步与互斥功能:
(1) 互斥:通过同一时间进程对临界资源的唯一访问实现访问操作安全。比如:不能对一串连续的数据还没有写入完成就开始写入下一串数据,这样数据就会乱的。(临界资源即为公共资源,多个进程都可以访问的资源。)体现在:对管道的写入操作大小不会超过PIPEBUF—> 4096个字节的大小,保证操作的原子性。
(2)同步:通过一些条件的判断,让进程对临界资源的访问更加合理有序,比如:管道中无数据则read会阻塞,管道中数据满了则write会阻塞。
4.半双工通信(同一时刻同一端只能读或些,不能同时度和写。)

2.共享内存

共享内存本质上是一块物理内存;
共享内存实现进程间通信原理:开辟一块物理内存空间,多个进程通过页表将同一块内存映射到自己的虚拟地址空间通过虚拟地址直接访问,进而实现数据共享。

进程间通信(管道,共享内存,消息队列,信号量)_第6张图片

操作流程:
1.创建或打开文件
2.将共享内存映射到进程的虚拟地址空间
3.通过映射的虚拟地址进行各种内存操作
4.解除映射关系
5.删除共享内存

需要用到的函数接口:
1.创建或打开文件

#include 
#include 
int shmget(key_t key, size_t size, int shmflg);
//创建一个共享内存对象返回这个共享内存的标识符

参数:
key:共享内存标识符
size:创建时所开辟的大小(以内存页为单位,一页4096个字节,即开辟的大小为4096字节的倍数)
shmflg:打开方式+创建权限

		IPC_CREAT  |    IPC_EXCL  |   0664
		无则创建    |    已存在则报错  |   创建权限

返回值:成功返回非负整数,即为文件描述符(操作句柄);失败返回-1.

2.将共享内存映射到进程的虚拟地址空间

#include 
#include 
void *shmat(int shmid, const void *shmaddr, int shmflg);
//把共享内存区对象映射到调用进程的地址空间

参数:
shmid: shaget()返回的操作句柄
shmaddr:映射地址,常设为NULL,让操作系统来分配映射。
shmflg:映射成功后的访问方式。 SHM_RDONLY:表示只读权限,0 :表示读写权限。

返回值:成功返回映射后的首地址,失败返回(void*)-1。

3.通过映射的虚拟地址进行各种内存操作
strcpy(),strcmp(),memset(),memcpy()…等

4.解除映射关系

 #include 
 #include 
 int shmdt(const void *shmaddr);

参数:
shmaddr:映射后的首地址。
返回值:成功0,失败-1;

5.删除共享内存

#include 
 #include 
 int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:
shmid:shmget()返回的操作句柄,即这块共享内存的操作句柄。
cmd:操作类型,常用IPC_RMID 表示摧毁一块被标记的内存。
buf:用于设置或捕获共享内存的信息,不使用则设置NULL即可。

关于以上shm系列的四个函数接口详细内容可以参考这里

共享内存特性:时最快的进程间通信,因为共享内存通过虚拟地址直接访问内存实现数据共享。
注意:共享内存为覆盖式写入,对共享内存的操作需要注意安全问题(同步和互斥)。

练习:利用shmget, shmat, shmdt, shmctl函数,创建一个共享内存, 支持两个进程进行通信,进程A 向共享内存当中写 “i am shm1 process ”, 进程B 从共享内存当中读出内容,并且打印到标准输出。

/shm1.c
#include 
#include 
#include 
#include 
#include 
#include 
#define IPC_KEY 0X12345600
#define PROJ_ID 0x12345678
int  main()
{
// key_t key = ftok("./",PROJ_ID);
  int shmid = shmget(IPC_KEY,4096,IPC_CREAT|0664);
  if(shmid<0){
    perror("shmget error");
    return -1;
  }
  void*shm_start = shmat(shmid,NULL,0);
  sleep(2);
  if(shm_start==(void*)-1){
    perror("shmat error");
    return -1;
  }

  while(1)
  {
    sleep(1);
    for(int i=0;i<1024;i++){
      printf("%c",((char*)shm_start)[i]);
    }
    printf("\n");
  }
    shmdt(shm_start);
    shmctl(shmid,IPC_RMID,NULL);
    return 0;
}


/shm2.c
#include 
#include 
#include     
#include     
#include     
#include     
#include 
#define IPC_KEY 0x12345600    

int main()
{

  int shmid= shmget(IPC_KEY,4096,IPC_CREAT|0664);
  if(shmid<0){
    perror("shmfet error");
    return -1;
  }


  void *shm_start =shmat(shmid,NULL,0);
  if(shm_start == (void*)-1){
    perror("shmaat error ");
    return -1;
  }
  while(1)
  {
    fgets((char*)shm_start,1024,stdin);   //fgets()获取一行输入存入char*)shm_start
    sleep(1);
  }
  shmdt(shm_start);
  shmctl(shmid,IPC_RMID,NULL);
  return  0;
}

进程间通信(管道,共享内存,消息队列,信号量)_第7张图片

可能会用到的一个命令:ipcs -m 可以查看共享内存资源

3.消息队列

消息队列本质上是内核中的一个优先级队列,多个进程通过访问同一个队列,像队列中添加或获取节点进而实现进程间通信的数据传输。
消息队列自带同步和互斥,它的生命周期随内核。
进程间通信(管道,共享内存,消息队列,信号量)_第8张图片

4.信号量

信号量的本质是内核中的一个计数器+pcb等待队列。(计数器为正表示使用资源的次数,计数器为负表示等待使用资源的进程数。)通过信号量可以实现进程间的同步和互斥(即协调进程对临界资源的访问)。

操作:
p操作:计数器-1,判断,若计数器<0则阻塞(设置为可中断休眠状态)进程。
v操作:计数器+1,唤醒一个被阻塞的进程。

通过p和v操作可实现同步和互斥:
同步的实现:通过计数器对资源计数,获取资源之前进行p操作,产生资源之后进行v操作。
互斥的实现:计数器为1时表示资源只有一个,进程访问前进行p操作,计数器变为0,阻塞其他进程访问,访问完之后进行v操作,计数器恢复为1,这样就实现了互斥,同一时间只能有一个进程对临界资源进行访问。

你可能感兴趣的:(linux,linux)