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

来源:互联网 发布:留学美国北大gpa算法 编辑:程序博客网 时间:2024/06/06 01:53

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

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

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

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

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

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

(2)、管道基本特性:

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

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

这里写图片描述

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

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

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

结果如图所示:
这里写图片描述

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

结果如图所示:
这里写图片描述

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

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

(1)、pipe无名管道:

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

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

这里写图片描述

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

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

这里写图片描述

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

/*父写子读,关闭父进程的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;}

测试结果如图所示:

这里写图片描述

由于父进程睡眠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管道,进而实现无血缘关系的管道进程间通信。
有名管道的图解如下所示:

这里写图片描述

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

/****头文件省略,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;}

测试结果:
这里写图片描述

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

这里写图片描述

二、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文件中的前四个字符(小端存储):
这里写图片描述

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显示无该文件。

这里写图片描述

两个进程在各自的虚拟地址空间映射了共享文件,其映射关系如图所示:
这里写图片描述

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

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

1 0