进程间通信

本文详细介绍了Linux环境下进程间通信的几种方式,包括管道(匿名管道)、FIFO(命名管道)和内存映射区。管道是基本的IPC机制,适用于有血缘关系的进程间通信,而FIFO则允许不相关进程间通信。内存映射区则通过将文件映射到内存,实现高效的数据交换。文章还提供了各种通信方式的使用示例及注意事项。

进程间通信

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

image-20211124145624773

管道

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递

image-20211124145906603

  • 管道的本质是一块内核缓冲区 ,内部使用环形队列实现
  • 由两个文件描述符引用,一个表示读端,一个表示写端
  • 规定数据从管道的写端流入管道,从读端流出
  • 当两个进程都终结的时候,管道也自动消失
  • 管道的读端和写端默认都是阻塞的
  • 默认缓冲区大小为4k,可使用ulimit -a命令获取大小
  • 实际操作过程中缓冲区会根据数据压力做适当调整
  • 一旦数据被读走,便不在管道中存在,不可反复读取
  • 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道
  • 只能在有血缘关系的进程间使用管道
    • 血缘关系指由共同的祖先

创建管道pipe

//创建一个管道

int pipe(int fd[2]);

//若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端
//成功返回0;失败返回-1,并设置errno

父子进程使用管道通信

一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信

  1. 父进程创建管道

image-20211124150710828

  1. 父进程fork出子进程

image-20211124150735742

  1. 父进程关闭fd[0],子进程关闭fd[1]

image-20211124150759220

//父进程调用wait函数完成对子进程的回收
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/wait.h>

int main()
{
    //创建管道
    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]);
        write(fd[1],"hello dd",strlen("hello dd"));
        wait(NULL);
    }
    else
    {
        //关闭写端
        close(fd[1]);
        char buf[256];
        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;

}

image-20211124151456704

管道的读写行为

读操作
  • 有数据
    • read正常读,返回读出的字节数
  • 无数据
    • 写端全部关闭
      • read解除阻塞,返回0,相当于读文件读到尾部
    • 没有全部关闭
      • read阻塞
写操作
  • 读端全部关闭
    • 管道破裂,进程终止,内核给当前进程发SIGPIPE信号
  • 读端没有全部关闭
    • 缓冲区写满了
      • write阻塞
    • 缓冲区没有满
      • 继续write

设置管道为非阻塞

int flag=fcntl(fd[0],F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd[0],F_SETFL,flag);

若是读端设置为非阻塞

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

查看管道缓冲区大小

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));

FIFO

FIFO常被称为命名管道

通过FIFO,不相关的进程也能交换数据

FIFO是Linux基础文件类型中的一种(文件类型为p)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道

进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信

image-20211124154424343

创建管道

//使用命令 mkfifo

mkfifo 管道名

//例如:mkfifo myfifo
//使用函数

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

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

内存映射区

存储映射I/O使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件

这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作

使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现

image-20211124182005580

mmap

//建立存储映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

//成功:返回创建的映射区首地址;失败:MAP_FAILED宏

//addr: 	指定映射的起始地址, 通常设为NULL, 由系统指定
//length:映射到内存的文件长度
//prot:	映射区的保护方式, 最常用的:
//读:PROT_READ
//写:PROT_WRITE
//读写:PROT_READ | PROT_WRITE
//flags:	映射区的特性, 可以是
//MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享
//MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件
//fd:由open返回的文件描述符, 代表要映射的文件。
//offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/wait.h>
#include<sys/mman.h>

int main()
{
    //使用mmap函数建立共享映射区
    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;
    }
    else if(pid>0)
    {
        memcpy(addr,"hello dd",strlen("hello dd"));
        wait(NULL);
    }
    else if(pid==0)
    {
        sleep(1);
        char* p=(char*)addr;
        printf("[%s]\n",p);
    }
    return 0;
}

munmap

//释放由mmap函数建立的存储映射区

int munmap(void *addr, size_t length);

//成功:返回0 失败:返回-1,设置errno值

//addr:调用mmap函数成功返回的映射区首地址
//length:映射区大小(mmap函数的第二个参数)

mmap注意事项

  • 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区

  • 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制

  • 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭

  • 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的

  • munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作

  • 文件偏移量必须为0或者4K的整数倍

  • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作

  • 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的

  • munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作

  • 文件偏移量必须为0或者4K的整数倍

  • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值