liunx系统编程学习第六天----进程间通信

1.学习目标

  1. 熟练使用pipe进行父子进程间通信
  2. 熟练使用pipe进行兄弟进程间通信
  3. 熟练使用fifo进行无血缘关系的进程间通信
  4. 使用mmap进行有血缘关系的进程间通信
  5. 使用mmap进行无血缘关系的进程间通信

2.进程间通信相关概念

2.1 什么是进程间通信

Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
在这里插入图片描述

2.2 进程间通信的方式

在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:

  1. 管道 (使用最简单)
  2. 信号 (开销最小)
  3. 共享映射区 (无血缘关系)
  4. 本地套接字 (最稳定)

3. 管道-pipe

3.1 管道的概念

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。
在这里插入图片描述
有如下特质:

  1. 管道的本质是一块内核缓冲区
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。
  4. 当两个进程都终结的时候,管道也自动消失。
  5. 管道的读端和写端默认都是阻塞的。

3.2 管道的原理

  1. 管道的实质是内核缓冲区,内部使用环形队列实现。
  2. 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。
  3. 实际操作过程中缓冲区会根据数据压力做适当调整。

3.3 管道的局限性

  1. 数据一旦被读走,便不在管道中存在,不可反复读取。
  2. 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道。
  3. 只能在有血缘关系的进程间使用管道。

3.4 创建管道-pipe函数

  1. 函数作用:创建一个管道
  2. 函数原型:int pipe(int fd[2]);
  3. 函数参数:若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端
  4. 返回值:
    • 成功返回0
    • 失败返回-1,并设置errno值
  5. 函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端, fd[1]是写端,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区

3.5 父子进程使用管道通信

一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。
步骤:

  1. 第一步:父进程创建管道
  2. 第二步:父进程fork出子进程
  3. 第三步:父进程关闭fd[0].子进程关闭fd[1]

创建步骤总结:
4. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]和fd[1],分别指向管道的读端和写端。
5. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管。
6. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出,这样就实现了父子进程间通信。

1、利用管道完成父子进程间通信代码示例

//利用管道完成父子进程之间通信
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
int main (){
  //创建管道
  //int pipe(int pipefd[2]);
  int fd[2];
  int ret=pipe(fd);
  if(ret<0){
    perror("pipe error");
    return -1;
  }
  //创建子进程
  pid_t pid=fork();
  if(pid<0){
    perror("fork error");
    return -1;
  }
  else if(pid>0){//父进程
  //关闭读端
  close(fd[0]);
  sleep(5);//验证read是否为阻塞,结果是五秒钟之后子进程才能接收到字符串
  write(fd[1],"hello world",strlen("hello world"));
  
  wait(NULL);
  }
  else {//子进程
    //关闭写端
    close(fd[1]);
    char buf[64];
    memset(buf,0x00,sizeof(buf));
    int n=read(fd[0],buf,sizeof(buf));
    printf("read over,n==[%d],buf==[%s]\n",n,buf);
  }
  return 0;
}

执行结果:
在这里插入图片描述

2、父子进程间通信,实现ps aux | grep bash

1.原理图:
在这里插入图片描述
2.步骤总结:

  1. 创建管道pipe
  2. 创建子进程fork
  3. 在父进程中关闭读端fd[0]
  4. 在子进程中关闭写端fd[1]
  5. 在父进程中将标准输出重定向到管道的写端
  6. 在子进程中将标准输入重定向到管道的读端
  7. 在父进程中调用execl函数执行ps aux命令
  8. 在子进程中调用execl函数执行grep bash命令
  9. 在父进程中回收子进程wait函数

3.代码示例:

//使用pipe完成ps aux | grep bash操作
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
int main (){
  //创建管道
  //int pipe(int pipefd[2]);
  int fd[2];
  int ret=pipe(fd);
  if(ret<0){
    perror("pipe error");
    return -1;
  }
  //创建子进程
  pid_t pid=fork();
  if(pid<0){
    perror("fork error");
    return -1;
  }
  else if(pid>0){//父进程
  //关闭读端
  close(fd[0]);
  
  //将标准输出重定向到管道的写端
  dup2(fd[1],STDOUT_FILENO);

  //执行ps aux命令
  execlp("ps","ps","aux",NULL);

  //如果正常执行了ps命令则父进程下面的代码将不会执行
  perror("execlp error");
  }
  else {//子进程
    //关闭写端
    close(fd[1]);

    //将标准输入重定向到管道的读端
    dup2(fd[0],STDIN_FILENO);

    //执行grep bash命令
    execlp("grep","grep","--color=auto","bash",NULL);

    perror("execlp error");
  }
  return 0;
}

4.执行结果:
在这里插入图片描述

3.6 管道的读写行为

1、读操作

  1. 有数据:read正常读,返回读出的字节数
  2. 无数据:
    • 写端全部关闭:read解除阻塞,返回0;相当于读文件读到了尾部
    • 没有全部关闭:read阻塞

2、写操作

  1. 读端全部关闭:管道破裂,进程终止,内核给当前进程发SIGPIPE信号
  2. 读端没全部关闭:
  3. 缓冲区写满了:write阻塞
  4. 缓冲区没有满:继续write

3.7 如何设置管道为非阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参
考下列三个步骤进行:

  1. 第1步: int flags = fcntl(fd[0], F_GETFL, 0)
  2. 第2步: flag |= O_NONBLOCK;
  3. 第3步: fcntl(fd[0], F_SETFL, flags);

若是读端设置为非阻塞:

  1. 写端没有关闭,管道中没有数据可读,则read返回-1;
  2. 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
  3. 写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
  4. 写端已经关闭,管道中没有数据可读,则read返回0

3.8 如何查看管道缓冲区大小

  1. 命令:ulimit -a
  2. 函数:long fpathconf(int fd, int name);
    • printf(“pipe size==[%ld]\n”, fpathconf(fd[0], _PC_PIPE_BUF));
    • printf(“pipe size==[%ld]\n”, fpathconf(fd[1], _PC_PIPE_BUF));

4 FIFO

4.1 FIFO介绍

FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux 基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。

4.2 创建管道

  1. ==方式1:==使用命令:mkfifo
    命令格式:mkfifo 管道名
    例如:mkfifo myfifo
    在这里插入图片描述

  2. ==方式2:==使用函数
    int mkfifo(const char *pathname, mode_t mode);
    参数说明和返回值可以查看man 3 mkfifo

  3. 当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。
    FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

4.3 使用FIFO完成两个进程通信

1. 使用FIFO完成两个进程通信的示意图
在这里插入图片描述
2. 思路:
1. 进程A:
- 创建一个fifo文件:myfifo
- 调用open函数打开myfifo文件
- 调用write函数写入一个字符串如:“hello world”(其实是将数据写入到了内核缓冲区)
- 调用close函数关闭myfifo文件
2. 进程B:
- 调用open函数打开myfifo文件
- 调用read函数读取文件内容(其实就是从内核中读取数据)
- 打印显示读取的内容
- 调用close函数关闭myfifo文件

3.进程A代码:

//fifo完成两个进程之间通信的测试
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>

int main (){
  //创建fifo文件
  //int mkfifo(const char * pathname,mode_t mode);
  int ret= mkfifo("./myfifo",0777);
  if(ret<0){
    perror("mkfifo error");
    return -1;
  }
  //打开文件
  int fd=open("./myfifo",O_RDWR);
  if(fd<0){
    perror("open error");
    return -1;
  }
  //写文件
  write(fd,"hello world",strlen("hello world"));

  sleep(10);
  
  close(fd);
  
  getchar();
  return 0;
}

4. 进程B代码:

//fifo完成两个进程之间通信的测试
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>

int main (){
  //创建fifo文件
  //int mkfifo(const char * pathname,mode_t mode);
  //int ret= mkfifo("./myfifo",0777);
 // if(ret<0){
    //perror("mkfifo error");
    //return -1;
 // }
  //打开文件
  int fd=open("./myfifo",O_RDWR);
  if(fd<0){
    perror("open error");
    return -1;
  }
  //读文件
  //write(fd,"hello world",strlen("hello world"));

  char buf[64];
  memset(buf,0x00,sizeof(buf));
  int n=read(fd,buf,sizeof(buf));
  printf("n==[%d],buf==[%s]\n",n,buf);
  //关闭文件
  close(fd);
  
  getchar();
  return 0;
}

5. 执行结果:
在这里插入图片描述

5.内存映射区

5.1 存储映射区介绍

存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。
使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

在这里插入图片描述

5.2 mmap函数

  1. 函数作用:建立存储映射区
  2. 函数原型:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  3. 函数返回值
    • 成功:返回创建的映射区首地址
    • 失败:MAP_FAILED宏
  4. 参数:
    1. addr: 指定映射的起始地址, 通常设为NULL, 由系统指定
    2. length:映射到内存的文件长度
    3. prot: 映射区的保护方式, 最常用的:
      + 读:PROT_READ
      + 写:PROT_WRITE
      + 读写:PROT_READ | PROT_WRITE
    4. flags: 映射区的特性, 可以是
      + MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享.
      + MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。
    5. fd:由open返回的文件描述符, 代表要映射的文件
    6. offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射。

5.3 munmap函数

  1. 函数作用:释放由mmap函数建立的存储映射区
  2. 函数原型:int munmap(void *addr, size_t length);
  3. 返回值:
    1. 成功:返回0
    2. 失败:返回-1,设置errno值
  4. 函数参数:
    1. addr:调用mmap函数成功返回的映射区首地址
    2. length:映射区大小(mmap函数的第二个参数)

5.4 使用mmap完成父子间通信

1、代码

//使用mmap完成父子进程间通信
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include <sys/mman.h>
#include<fcntl.h>
#include<sys/stat.h>
int main (){
  //使用mmap函数建立共享映射区
  //void *mmap(void *addr, size_t length, int prot, int flags,
  //                int fd, off_t offset);
  int fd=open("./test.log",O_RDWR);
  if(fd<0)
  {
    perror("open error");
    return -1;
  }
  int len=lseek(fd,0,SEEK_END);

  void * addr=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
  if(addr==MAP_FAILED)
  {
    perror("mmap error");
    return -1;
  }
  //创建子进程
  pid_t pid=fork();
  if(pid<0){
    perror("fork error");
    return -1;
  }
  if(pid>0){//父进程
  memcpy(addr,"hello world",strlen("hello world"));
  wait(NULL);
  }
  else {//子进程
  sleep(1);//让父进程先终结
  char *p=(char*)addr;
  printf("[%s]",p);
  }
  return 0;
}

2、执行程序之前的文件:

sssssssssssssssssssss
aaaaaaaaaaaaaaaaaaaaa
qqqqqqqqqqqqqqqqqqqqq

3、执行程序之后的文件
在这里插入图片描述

5.5 使用mmap完成无血缘关系的进程间通信

1、执行写程序代码

//使用mmap完成两个不相干进程间通信
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include <sys/mman.h>
#include<fcntl.h>
#include<sys/stat.h>
int main (){
  //使用mmap函数建立共享映射区
  //void *mmap(void *addr, size_t length, int prot, int flags,
  //                int fd, off_t offset);
  int fd=open("./test.log",O_RDWR);
  if(fd<0)
  {
    perror("open error");
    return -1;
  }
  int len=lseek(fd,0,SEEK_END);


  void * addr=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
  if(addr==MAP_FAILED)
  {
    perror("mmap error");
    return -1;
  }
  memcpy(addr,"0123456789",10);//拷贝10个字节长度的字串
  return 0;
}

2、执行读程序代码:

//使用mmap完成两个不相干进程间通信
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>
#include <sys/mman.h>
#include<fcntl.h>
#include<sys/stat.h>
int main (){
  //使用mmap函数建立共享映射区
  //void *mmap(void *addr, size_t length, int prot, int flags,
  //                int fd, off_t offset);
  int fd=open("./test.log",O_RDWR);
  if(fd<0)
  {
    perror("open error");
    return -1;
  }
  int len=lseek(fd,0,SEEK_END);


  void * addr=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
  if(addr==MAP_FAILED)
  {
    perror("mmap error");
    return -1;
  }
  char buf[64];
  memset(buf,0x00,sizeof(buf));
  memcpy(buf,addr,10);//拷贝10个字节长度的字串
  printf("buf===[%s]\n",buf);
  return 0;
}

3、执行结果
在这里插入图片描述

5.6 mmap注意事项

  1. 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
  2. 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
  3. 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
  4. 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
  5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
  6. 文件偏移量必须为0或者4K的整数倍
  7. mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
"Failed to get D-Bus connection: Operation not permitted" 是一个常见的错误消息,它通常在 Linux 系统中出现。这个错误消息表示当前用户没有权限连接到 D-Bus 会话总线。 D-Bus(Desktop Bus)是一个用于进程间通信系统总线,它在 Linux 系统中广泛使用。许多应用程序和服务使用 D-Bus 进行通信和交互。 当出现 "Failed to get D-Bus connection: Operation not permitted" 错误时,可能有以下几个原因: 1. 权限问题:当前用户没有足够的权限连接到 D-Bus 会话总线。这可能是因为当前用户没有正确配置或设置权限。 2. D-Bus 服务未运行:D-Bus 服务可能没有正确运行或已停止。你可以尝试重新启动 D-Bus 服务来解决此问题。 3. SELinux 或 AppArmor 限制:SELinux 或 AppArmor 是一些安全模块,它们可以限制进程的访问权限。如果 SELinux 或 AppArmor 配置不正确,可能会导致无法连接到 D-Bus。 解决这个问题的方法可能因系统而异,但以下是一些常见的解决方法: 1. 检查用户权限:确保当前用户具有连接到 D-Bus 的权限。你可以尝试使用 root 用户或具有适当权限的用户来连接。 2. 重新启动 D-Bus 服务:尝试重新启动 D-Bus 服务,可以使用以下命令: ``` sudo systemctl restart dbus ``` 3. 检查 SELinux 或 AppArmor 配置:如果你的系统启用了 SELinux 或 AppArmor,请确保相关配置正确,并允许进程连接到 D-Bus。 请注意,具体的解决方法可能因系统和配置而异。如果以上方法无法解决问题,建议查阅系统文档或寻求相关技术支持。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值