Linux进程间通信

进程间通信

一般简称为IPC----InterProcess Communication。是指在不同进程之间传播或交换信息。

进程间通信有这么几个方式:管道(命名管道/匿名管道),共享队列,消息队列,信号量

进程间为什么要通信呢?

因为进程的独立性,因此想要通信必须能够共同访问一个相同的媒介。

进程间通信的目的:数据传输,数据共享,进程间的访问控制。

也正因为通信的目的不同,使用场景不同,因此操作系统提供了多种进程间通信方式:

管道----传输数据

共享内存----共享数据

消息队列----传输数据

信号量----进程间的访问控制

管道

管道是半双工通信,双向选择的单向通信(即数据只能在一个方向上流动),具有固定的读端和写端

它是进程间的数据资料传输。在通信过程中,先将数据放到buf中,在将数据拷贝到自己的buf中在进行操作

通信1

管道生命周期随进程,如果进程消亡了,那么通信也就结束了

匿名管道
int pipe(int pipefd[2]);
//pipefd输出型参数
//数组pipefd用于返回引用结尾的两个文件描述符。

pipefd[0] 从管道读数据
pipefd[1] 从管道写数据

如果成功,则返回 0;不成功,则返回-1

只能用于具有亲缘关系的进程间通信

#include <stdio.h>    
#include <stdlib.h>    
#include <fcntl.h>    
#include <errno.h>    
    
int main(){    
  int pipefd[2];    
  int ret = pipe(pipefd);    
  if(ret < 0){    
    perror("pipe error");    
    return -1;    
  }    
  int pid = fork();    
  if(pid < 0){    
    perror("fork error");    
    return -1;    
  }else if(pid == 0){    
    close(pipefd[1]);    
  }else{    
    sleep(1);    
    close(pipefd[1]);    
    char buf[1024] = {0};    
    int ret = read(pipefd[0], buf, 1023);    
    printf("read buf:[%d - %s]\n",ret, buf);    
  }    
  return 0;    
} 

先建立管道,之后再创建子进程。这个时候就要考虑到管道的读写特性了

若管道中没有数据,则read会阻塞,直到读到数据返回
若管道中数据写满了,则write会阻塞,直到数据被读取,管道中有空闲位置,写入数据后返回

若管道中所有的读端都被关闭,则write会触发异常----SIGPIPE(信号标志)----导致进程退出
若管道中的所有写有写端都被关闭,则read返回0----通知用户没人写了

父子进程两端都要进行关闭

所以代码中将管道中写端关闭了,所以读端返回的是0.

通信2

虽然管道提供了双向选择,但是如果我们没有用到某一端,就把这一端关闭掉

管道同步与互斥特性

当读写数据的大小<管道pipe_buf ,是保证操作原子性-----这时操作不可被打断

互斥:保证对一个临界资源(公共资源,比如全局变量)同一时间的唯一访问性(我操作的时候你不能操作)

同步:保证对一个临界资源访问的时序可控性(我操作完了你才能操作)

|管道符就是匿名访问

int main(){                               
  int pipefd[2];                          
  int ret = pipe(pipefd);                 
  if(ret < 0){                            
    perror("pipe error");                 
    return -1;                            
  }                                       
  int pid1 = fork();                      
  if(pid1 == 0){                          
    close(pipefd[0]);//关闭从管道读数据    
    dup2(pipefd[1],1);//将文件描述符表中 向管道写数据 替换 标准输出    
    execlp("ps","ps","-ef",NULL);//程序替换    
  }                                       
  int pid2 = fork();                      
  if(pid2 == 0){                          
    close(pipefd[1]);                     
    dup2(pipefd[0],0);                    
    execlp("grep","grep","ssh",NULL);     
  }                                       
  close(pipefd[0]);//不用的时候将读端和写端都关闭    
  close(pipefd[1]);    
  waitpid(pid1,NULL,0);    
  waitpid(pid2,NULL,0);    
  return 0;    
} 

grep读数据时不知道自己需要多少数据,过滤之后再次读取。

代码用图示来解答一下

通信3

命名管道(FIFO)

FIFO是一种文件类型,可以用于任意进程间通信。

可见于文件系统,因为创建命名管道会随之在文件系统中创建一个命名管道文件

类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性

因为所有的进程都能够通过打开管道文件,进而获取管道的操作句柄,因此命名管道可以用于同一主机上任意进程间通信

int mkfifo(const char *pathname, mode_t mode);
pathname:管道文件名
mode:创建权限 0664

fifo_read.c

#include <stdio.h>    
#include <stdlib.h>    
#include <errno.h>    
#include <string.h>    
#include <fcntl.h>    
#include <unistd.h>    
#include <sys/stat.h>    
      
int main(){    
  char *file = "./test.fifo";    
  umask(0);    
  int ret = mkfifo(file, 0664);    
  if(ret < 0){    
    if(errno != EEXIST){    
      perror("mkfifo errno");    
      return -1;    
    }    
  }    
  printf("open file\n");    
  int fd = open(file, O_RDWR);    
  if(fd < 0){    
    perror("open error");                
    return -1;    
  }    
  printf("open success!!\n");    
  while(1){    
    char buf[1024] = {0};    
    int ret = read(fd, buf, 1023);
    if(ret > 0){
      printf("read buf:[%s]\n", buf);
    }else if(ret == 0){
        printf("write closed~~~\n");
    }else{
      perror("read error");
    }
    printf("---------\n");
  }
  close(fd);
  return 0;
} 

fifo_write.c

int main(){
    char *file = "./test.fifo";
    umask(0);
    int ret = mkfifo(file,0664);
    if(ret < 0){
        if(ret != EEXIST){
            perror("mkfifo error");
            return -1;
        }
    }
    printf("open file\n");
    int fd = open(file, O_WRONLY);
    if(fd < 0){
        perror("open error");
        return -1;
    }
    printf("open success!!\n");
    while(1){
        char buf[1024] = {0};
        scanf("%s", buf);
        write(fd, buf, strlen(buf));
    }
    return 0;
}

通信4

这有点类似于服务器–客户端之间建立连接

fifo_write类似于客服端发送一个请求 fifo_read类似于服务器返回一个请求

命名管道利用了文件系统创建文件进行通信。当write写入了数据,将数据从自己的buf写到test.fifo文件中,之后read在将数据从test.fifo中读取到自己的buf中,最后输出。

命名管道的读写特性

若管道没有被以写的方式打开,这时如果只读打开则会阻塞,直到文件被以写的方式打开

若管道没有被以读的方式打开,这时如果只写打开则会阻塞,直到文件被以读的方式打开

若管道以读写的方式打开,则不会阻塞

匿名管道和命名管道的区别

匿名管道:速度慢,容量有限,只有父进程进程能通讯

命名管道:任何进程间都能通讯,但速度慢

共享内存

共享内存的定义:在物理上开辟一块空间,内存直接映射到虚拟内存中,如果一块内存被多个进程映射,那么多个进程访问同一块内存,则可以实现通信。最快的进程间通信。因为相较于其他进程间通信方式(将数据从用户态拷贝到内核态,用的时候,从内核态拷贝到用户态),共享内存直接将一块内存映射到用户空间,用户可以直接通过地址对内存进行操作,并反馈到其他进程,少了两步数据拷贝的过程。

共享内存使用流程

1、创建/打开共享内存

int shmget(key_t key, size_t size, int shmflg);
    key:    共享内存标识符
    size:  共享内存大小
    shmflg:打开方式/创建权限
    	IPC_CREAT 共享内存不存在则创建
    	IPC_EXCL 与IPC_CREAT同用,若存在则报错,不存在则创建
    返回值:操作句柄shmid	失败:-1
key_t ftok(const char *pathname, int proj_id);
    pathname:   文件名
    proj_id:   数字
    通过文件的 inode节点号 和 proj_id 共同得出一个key值

2、将共享内存映射到虚拟地址空间(建立映射关系)

void shmat(int shmid, const void *shmaddr, int shmflg);
	shmid:	创建共享内存返回的操作句柄
	shmaddr:用于指定映射在虚拟空间的首地址	通常置NULL
    shmflg:0----可读可写
    返回值:映射首地址(通过这个地址对共享内存进行操作)	失败:(void*)-1

3、对共享内存进行基本的内存操作,memcpy

4、解除映射关系 shmdt

int shmdt(const void *shmaddr)
	shamddr:	映射返回的首地址

5、删除共享内存 shmctl

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:	操作句柄
cmd:	IPC_RMID	删除共享内存
buf:	设置或者获取共享内存信息,用不着置NULL
共享内存并不是立即删除的,只是拒绝后续映射连接,当共享内存
映射连接数为0时,则删除共享内存

shm_read.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/shm.h>
#define IPC_KEY 0x12345678
#define PROJ_ID 12345
#define SHM_SIZE  4096
int main(){
  int shmid;    
  //1、创建共享内存    
  shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);    
  if(shmid < 0){    
    perror("shmget error");    
    return -1;    
  }    
  //2、将共享内存映射到虚拟地址空间    
  char *shm_start = (char*)shmat(shmid, NULL, 0);    
  if(shm_start == (void*)-1){    
    perror("shmat error");    
    return -1;    
  }    
  while(1){    
    printf("%s\n", shm_start);    
    sleep(1);    
  }    
  //4、解除映射    
  shmdt(shm_start);    
  //5、删除共享内存    
  shmctl(shmid, IPC_RMID, NULL); 
    return 0;
}

shm_write.c

#include <stdio.h>    
#include <unistd.h>    
#include <stdlib.h>    
#include <sys/shm.h>    
#define IPC_KEY 0x12345678    
#define PROJ_ID 12345    
#define SHM_SIZE  4096    
    
int main(){    
  int shmid;    
  //1、创建共享内存    
  shmid = shmget(IPC_KEY, SHM_SIZE, IPC_CREAT|0666);    
  if(shmid < 0){    
    perror("shmget error");    
    return -1;    
  }    
  //2、将共享内存映射到虚拟地址空间    
  char *shm_start = (char*)shmat(shmid, NULL, 0);    
  if(shm_start == (void*)-1){    
    perror("shmat error");    
    return -1;    
  }    
  int i = 0;    
  while(1){    
    sprintf(shm_start, "明天又是可以学习的一天!!!+%d\n",i++);    
    sleep(1);    
  }
  //4、解除映射
  shmdt(shm_start);
  //5、删除共享内存
  shmctl(shmid, IPC_RMID, NULL);
  return 0;
} 

通信5

共享内存双方都可以修改

共享内存没有同步与互斥

删除一块共享内存,并不会立即删除,而是判断映射连接数,若为0则删除,不为0则拒绝后续连接,直到为0删除

消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

消息队列的建立过程为

创建消息队列---->添加数据节点---->获取数据节点---->删除

msgget---->msgsnd---->msgrcv(接收数据)---->msgctl

消息队列传输的是有类型的数据块,用户可以根据自己的需要选择性的获取某些数据类型

信号量

内核中的一个计数器----具有等待队列(PCB等待队列),具有等待和唤醒功能

用于资源计数,若计数小于等于0,表示没有资源,则需要等待

若计数大于0,表示有资源,则可以获取资源,然后计数-1

如果放置了资源,则计数+1,并且唤醒等待的进程

实现进程间的同步和互斥(资源计数为0或1的时候才具有互斥)

小结

消息队列和信号量现在的使用不是特别多,了解一下就可以。重点还是共享内存和管道的学习。

在代码过程中,我们应该可以感觉到,这种通信方式有点类似于服务器和客户端之间的处理过程。但是具体的实现是不同的。

匿名管道是通过对读端和写端的关闭和开启,在buf缓存区对数据进行拷贝和使用。

而命名管道是通过文件系统的打开和关闭,将数据进行读写。读写必须同时打开,否则另一端会被阻塞。

而共享内存是在同一个地址映射的一块虚拟地址被多个进程访问,这时也就是多个进程同时访问同一个内存。此时共享内存直接映射一块内存到用户空间,用户直接通过地址对内存进行操作,并反馈到其他进程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值