Linux进程间通信(一):管道与mmap文件-内存映射

时间:2022-08-28 21:31:09

一、无名管道、有名管道与进程间通信:

1、IPC–进程间通信与管道基本概念:

(1)、IPC(进程间通信):

所谓IPC就是两个或者多个进程之间的数据交互(在不能直接进行信息交互的两个进程间增加一个“交互媒介”以达到信息交互的目的)。为什么不能直接交互?因为我们知道在应用程序执行时(即进程运行时),其占有的用户空间只有0~3G,而用户空间不共享,不共享就无法传递信息;内核空间共享,所以要实现两个进程之间的信息交互即通信,就必须通过内核空间。

IPC的方法:
①文件;
②信号(signal);
③管道;
④共享内存;
⑤消息队列;
⑥信号量集(semaphore)(与信号无关);
⑦套接字socket

今天我们就“文件–内存”与“管道”总结一下,无论是有名管道还是无名管道、也不管是消息队列还是信号量集、套接字,其本质都是在内核中实现的,而我们只是在调用一个内核提供的接口或方法。

(2)、管道基本特性:

管道文件只是媒介,只是数据的中转站,只有读写双方均就绪时才畅通,只有一方就绪时处于阻塞状态,其大小始终为0,其基本模型 如图所示:

两个进程分别持有内核管理的管道的读端与写端的权限,并且管道是单向的,或者说是单双工的。

Linux进程间通信(一):管道与mmap文件-内存映射

简单测试(打开两个终端测试,file.pipe为一个管道文件关于其创建,之后会提到):

测试①
第一步:echo message > file.pipe(输入重定向到管道中,处于阻塞)
第二步:cat file.pipe(管道畅通,cat进程输出message)

两个步骤是两个shell创建的子进程进行通信。
如果不执行第二步(不敲回车),则shell一直处于后台,echo写端则处于阻塞状态,当第二步执行时(敲回车以后),通过管道在两个shell的子进程之间(echo和cat两个进程)传递信息。

结果如图所示:
Linux进程间通信(一):管道与mmap文件-内存映射

测试②
第一步:cat file.pipe(处于阻塞)
第二步:echo message > file.pipe(管道畅通,cat进程输出message)
与第一个测试相似,只不过测试①是开始读端未打开,写端处于阻塞状态。测试②是写端未打开,读端处于阻塞状态。

结果如图所示:
Linux进程间通信(一):管道与mmap文件-内存映射

2、pipe无名管道与FIFO有名管道:

无名管道:由内核创建,只用于fork()创建的父子进程之间的通信;
有名管道:由程序员建立管道文件,用于进程间通信(管道文件是程序员创建,但是管道依旧是内核创建并且管理),前面测试的便是有名管道。

(1)、pipe无名管道:

#include<unistd.h>
int pipe(int filedes[2]);
//创建无名管道,由内核维护,且无名管道只能用于fork创建的父子进程之间通信(作用于有血缘关系的进程间通信)。

pipe函数的参数是一个整型数组,该数组包含两个文件描述符:一个写描述符fd[0],一个读描述符fd[1]。如图所示为单进程的管道信息传递:

Linux进程间通信(一):管道与mmap文件-内存映射

pipe管道使用注意的四种情况:
①当写端关闭,读端读完管道里内容时read返回0,相当与读到文件末尾;
②写端未关闭,但是写端暂无数据,读端读完管道中数据后便阻塞;
③读端关闭,写管道的进程会收到一个SIGPIPE信号,写进程终止;
④读端未读管道数据,当写端写满数据后,再次写会阻塞。

对于fork()创建的父子进程之间的文件描述符与管道的关系如图所示:

Linux进程间通信(一):管道与mmap文件-内存映射

代码实现无名管道进程间通信:

/*父写子读,关闭父进程的fd[1]和子进程的fd[0]*/
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>

void sys_err(const char * ptr){
    perror(ptr);
    exit(EXIT_FAILURE);
}
int main(void)
{
    int fd[2]; //fd[0] 读端,fd[1] 写端
    char str[1024] = "hello world!";
    char buf[1024];

    if(pipe(fd) < 0)/*无名管道创建失败判断*/
        sys_err("pipe");
    pid_t pid = fork();
    if(pid < 0)
        sys_err("fork");

    if(pid > 0){
        close(fd[0]);//父进程里,关闭父进程的读端
        sleep(5);
        write(fd[1], str, strlen(str));
        close(fd[1]);//写完之后关闭写端
        wait(NULL);//等待子进程结束回收子进程PCB资源,防止产生僵尸进程
    }
    else if(pid == 0){
        int len, flags;
        close(fd[1]);//子进程里,关闭子进程写端

        flags = fcntl(fd[0], F_GETFL);
        flags |= O_NONBLOCK;//默认是阻塞读取,改为不阻塞
        fcntl(fd[0], F_SETFL, flags);
    tryagain:
        len = read(fd[0], buf, sizeof(buf));
        if(len == -1){//等于-1,说明父进程没有向管道中写数组,子进程由于不阻塞,因此循环执行
            if(errno == EAGAIN){//EAGAIN信号表示读取返回-1的原因是没有数据可读,要求再试一次,那么久打印try again后再试一次
                write(STDOUT_FILENO, "try again\n", 10);
                sleep(1);
                goto tryagain;
            }else//如果不是因为没有数据可读,就是读取出错了,就不同在循环读取了
                sys_err("read");
        }
        write(STDOUT_FILENO, buf, len);//将读取到的内容输入到标准输出上
        close(fd[0]);//然后关闭子进程的读端
}
    return 0;
}

测试结果如图所示:

Linux进程间通信(一):管道与mmap文件-内存映射

由于父进程睡眠5秒,而子进程循环一次睡眠一秒,所以在打印出hello world!之前会打印五次tryagain。最终我们看到,父进程可以通过无名管道给子进程发送信息。并且阻塞与否可以通过fcntl函数修改。

(2)、FIFO有名管道:

管道文件的创建:管道是通过管道文件(媒介)进行进程间信息交互的,管道文件与普通文件是有区别的,通过mkfifo(make first in first out)或者mkfifo()创建管道文件。其他方式是无法创建管道文件的,管道文件后缀是”.pipe”(类型为p)即使是touch file.pipe也不行。我们知道后缀名是无关紧要的,但是一定要使用mkpipe或者mkpipe()创建管道文件。

对于有名管道,必须先有管道文件才能进行通信。所以我们在程序中创建/使用管道文件时必须先用S_ISFIFO()判断某个文件是不是管道文件。该宏函数是用来判断stat()函数获取的struct stat{}结构体中的mode_t mode参数存储的文件类型。关于struct stat{}结构体以及stat()函数的基本形式,可参考: Linux&C编程之Linux系统命令“ls -l”的简单实现

S_ISFIFO(m) /*判断是否是管道文件,m即为mode参数*/

有名管道本质:无“血缘关系”的两个进程通过name.pipe管道文件找到内核中的pipe管道,进而实现无血缘关系的管道进程间通信。
有名管道的图解如下所示:

Linux进程间通信(一):管道与mmap文件-内存映射

代码实现有名管道进程间通信:

/**** 头文件省略,sys_err函数省略 fifo_w.c有名管道写端 ****/
int main(int argc, char *argv[])//传递管道文件
{
    int fd;
    char buf[1024] = "hello world\n";
    if(argc < 2){
        printf("./fifo_w name.pipe\n");
        exit(EXIT_FAILURE);
    }

    fd = open(argv[1], O_WRONLY);
    if(fd < 0) 
        sys_err("open");

    write(fd, buf, strlen(buf));
    close(fd);

    return 0;
}
/*** 头文件省略,sys_err函数省略 fifo_r.c有名管道读端 ***/
int main(int argc, char *argv[])
{
    int fd, len;
    char buf[1024];
    if(argc < 2) {
        printf("./fifo_r name.pipe\n");
        exit(EXIT_FAILURE);
    }

    fd = open(argv[1], O_RDONLY);
    if(fd < 0) 
        sys_err("open");

    len = read(fd, buf, sizeof(buf));
    write(STDOUT_FILENO, buf, len);

    close(fd);
    return 0;
}

测试结果:
Linux进程间通信(一):管道与mmap文件-内存映射

管道只在内核中占用一小部分内存,而管道文件不会在磁盘上占用空间(管道文件的PCB在内核中占用内存,只消耗一个inode)。如图:

Linux进程间通信(一):管道与mmap文件-内存映射

二、mmap文件-内存映射与进程间通信:

1、mmap介绍:

对于mmap不进行文件映射的操作可参考: 系统调用与内存管理(sbrk、brk、mmap、munmap)

mmap函数可以把磁盘文件的一部分直接映射进内存,这样文件的位置就有了对应的地址,对于文件的都写可以直接使用指针,而不需要read与write。

void * mmap(void * addr, size_t length, int port, int flags, int fd, off_t offset);

//addr:为映射的内存起始位置,设置为NULL操作系统自动分配;
//length:映射的长度;
//port:内存访问权限,PORT_NONE、PORT_EXEC、PORT_READ、PORT_WRITE
//flags:属性,MAP_SHARED(磁盘/内存任意一处修改同步到另外一处)、//MAP_PRIVATE(磁盘/内存任意一处修改不影响另外一处);
//fd:文件描述符(映射文件已打开),如不映射文件,只申请内存时值为-1;
//offset:偏移量,4096整数倍,一般先lseek确定位置然后置将offset为0即可
//返回值:返回系统分配的addr起始地址,失败返回MAP_FAILED。

int munmap(void * addr, size_t length);//参数与mmap对应

2、简单测试:

将文件中的内容映射到内存中,并修改内存以同步到文件中,观察文件中内容是否变化:

/**** mmap.c 头文件省略,sys_err函数省略 *****/
int main(void)
{
    int fd, len, *p;
    fd = open("hello", O_RDWR);
    if(fd < 0)
        sys_err("open");
    len = lseek(fd, 0, SEEK_END);

    p = mmap(NULL, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//进行内存映射
    if(p == MAP_FAILED)
        sys_err("mmap");
    close(fd);//释放file结构体
    p[0] = 0x30313233;

    munmap(p, len);//解除映射
    return 0;
}
//注意:close(fd);并不会解除映射,close只是将file结构体计数减1,并不会对映射关系产生影响。

测试结果:
通过p[0]四个字节的空间修改了hello文件中的前四个字符(小端存储):
Linux进程间通信(一):管道与mmap文件-内存映射

3、mmap实现进程间通信:

/*mmap.h*/
#ifndef _MMAP_H_
#define _MMAP_H_

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
/*mmap文件大小为4K*/
#define MAPLEN 0x1000 

/*发送的信息结构体*/
struct STU {
    int id;
    char name[20];
    char sex;
};
#endif
/*process_mmap_w.c*/
#include"mmap.h"
int main(int argc, char *argv[])
{
    struct STU *mm;
    int fd, i = 0;
    if (argc < 2) {
        printf("./a.out filename\n");
        exit(1);
    }
    fd = open(argv[1], O_RDWR | O_CREAT, 0777);
    if(fd < 0)
        sys_err("open");
    /*将创建的空文件扩大至4KB-1*/
    if(lseek(fd, MAPLEN-1, SEEK_SET) < 0)
        sys_err("lseek");
    /*将扩大的文件末尾写一次数据保证扩大有效*/
    if(write(fd, "\0", 1) < 0)
        sys_err("write");
    /*文件到内存的映射*/
    mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(mm == MAP_FAILED)
        sys_err("mmap", 2);
    /*映射完毕后便可以关闭fd,回收file结构体了(PCB资源)*/
    close(fd);

    while(1){
        mm->id = i;
        sprintf(mm->name, "name number:%d", i);
        if(i % 2 == 0)
            mm->sex = 'm';
        else
            mm->sex = 'w';
        i++;
        sleep(1);
    }
    munmap(mm, MAPLEN);/*解除文件到内存的映射*/
    return 0;
}
/*process_mmap_r.c*/
#include"mmap.h"
int main(int argc, char *argv[])
{
    struct STU *mm;
    int fd, i = 0;
    if(argc < 2) {
        printf("./a.out filename\n");
        exit(1);
    }
    fd = open(argv[1], O_RDWR);
    if(fd < 0)
        sys_err("open");

    mm = mmap(NULL, MAPLEN, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(mm == MAP_FAILED)
        sys_err("mmap", 2);

    close(fd);
    unlink(argv[1]);/*由于创建的用于通信的文件应当是临时文件,所以用unlink删除一个硬链接*/

    while(1){
        printf("%d\t", mm->id);
        printf("%s\t", mm->name);
        printf("%c\n", mm->sex);
        sleep(1);
    }
    munmap(mm, MAPLEN);
    return 0;
}

unlink知识点

unlink(const char * pathname);

删除一个链接:
如果是硬链接,硬链接数减1,当计数减为0时,释放数据块与inode。
如果文件链接数为0,当时有进程已经打开该文件,并且持有文件描述符,则等待该进程关闭文件时,kernel采取真正删除该文件。
利用该特性创建临时文件:先open/create创建一个文件,然后unlink()。上例中由于要先用fd映射内存,所在在映射完内存后unlink即可。

测试结果:
unlink将临时文件删除,所以ls显示无该文件。

Linux进程间通信(一):管道与mmap文件-内存映射

两个进程在各自的虚拟地址空间映射了共享文件,其映射关系如图所示:
Linux进程间通信(一):管道与mmap文件-内存映射

4、mmap进程间通信注意事项:

①open权限、文件本身权限、mmap设置权限多种权限都可能导致出错。且要进行进程间通信必须以可读可写的方式打开。
②如果mmap的内存映射实现进程间通信需要写一次就刷新,否则写的进程写的内容可能在内核缓冲区,读的进程读的时候在缓冲区读到,就直接在内核缓冲区中读取,而去共享文件中读了。
③mmap的缺点:通信双方都可以写,数据可能会出错。
④创建的用于通信的文件应当是临时文件,采用ulink()进行设置:
mmap()–>close(fd)–>ulink(path)
创建文件open(O_CREAT)与删除文件unlink()一般都放在写的进程中。
⑤用于进程间通信的数据类型一般设置为结构体。
⑥如果文件大小为0,而映射大小不为0,会有总线错误。