06-进程间通信

  1. 学习目标

  • 熟练使用pipe进行父子进程间通信
  • 熟练使用pipe进行兄弟进程间通信
  • 熟练使用fifo进行无血缘关系的进程间通信
  • 使用mmap进行有血缘关系的进程间通信
  • 使用mmap进行无血缘关系的进程间通信

2 进程间通信相关概念

2.1 什么是进程间通信

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

06-进程间通信_第1张图片

2.2 进程间通信的方式

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

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

3 管道-pipe

3.1管道的概念

管道就是 ***(输入到管道)  |  ***(从管道读出)    

管道是一种最基本的IPC(进程间通信)机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。

06-进程间通信_第2张图片

有如下特质:

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

3.2管道的原理

  • 管道的实质是内核缓冲区,内部使用环形队列实现。
  • 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。
[holo@holocom 0406]$ ulimit  -a
……
pipe size            (512 bytes, -p) 8
……

  • 实际操作过程中缓冲区会根据数据压力做适当调整。(边写边读 , 缓冲区一般不会满,, 数据适当多点, 缓冲区大小可以调整下, 数据很多 调整不了. 或者 读的慢 写得快, 也容易填满.

3.3管道的局限性

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

3.4创建管道-pipe函数

  • 函数作用:

创建一个管道

  • 函数原型:

int pipe(int fd[2]); //与int pipe(int *fd); 等价

  • 函数参数:

若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端

  • 返回值:
  • 成功返回0;
  • 失败返回-1,并设置errno值。

函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端, fd[1]是写端向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?

3.5父子进程使用管道通信

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

第一步:父进程创建管道(在fork之前)

06-进程间通信_第3张图片

第二步:父进程fork出子进程

06-进程间通信_第4张图片

第三步:父进程关闭fd[0](读),子进程关闭fd[1](写)

06-进程间通信_第5张图片

创建步骤总结:

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

3.6 管道练习

  • 一个进程能否使用管道完成读写操作呢? 可以,但没意义.
  • 使用管道完成父子进程间通信?
//1. 实现父子进程通信
[holo@holocom 0410]$ cat pipe.c
#include 
#include 
#include 
#include 
#include    
#include 


int main()
{
        //创建管道
        int fd[2];
        int ret = pipe(fd);  //不是pipe(fd[2]);
        if(ret < 0)
        {
                perror("pipe error");
                return -1;
        }


        //创建子进程
        pid_t child_pid = fork();
        if(child_pid < 0)
        {
                perror("fork error");
                return -1;
        }
        else if(child_pid>0)   //父进程关闭读端
        {
                close(fd[0]);
                sleep(5);    //验证read函数是阻塞的
                write(fd[1] , "hello world" , strlen("hello world"));   //write写满时阻塞


                wait(NULL);   //wait()是阻塞函数,回收子进程资源,确保子进程先退出


        }
        else if(child_pid == 0)   //子进程关闭写端
        {
                close(fd[1]);
                char buf[64];
                memset(buf , 0x00 , sizeof(buf));  //对数组初始化
                int n = read(fd[0],buf , sizeof(buf));  //read没数据时阻塞,如果没写入数据,就会等待写入.
                printf("read over , n == [%d] , buf == [%s]\n",n,buf);
        }


        return 0;
}

[holo@holocom 0410]$ ./pipe

//等待五秒

read over , n == [11] , buf == [hello world]

  • 父子进程间通信, 实现ps aux | grep bash //列出当前所有用户的所有进程,并在结果中筛选出包含关键词 "bash" 的行。

ps aux : 原来结果会写到标准输出(终端) ,更改为写到管道写端,使用dup2函数(可以指定第二个参数)(不可使用dup)

grep bash :原来从标准输入读, 从管道读端读 , 读到标准输出.

06-进程间通信_第6张图片

使用execlp函数和dup2函数

// 模拟ps aux | grep bash操作
#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
        //创建管道
        int fd[2];
        int ret = pipe(fd);
        if(ret < 0)
        {
                perror("pipe error");
                return -1;
        }


        //创建子进程
        pid_t child_pid = fork();
        if(child_pid < 0)
        {
                perror("fork error");
                return -1;
        }
        else if(child_pid>0)   //父进程关闭读端
        {
                close(fd[0]);
                dup2(fd[1],STDOUT_FILENO);
                execlp("ps" , "ps" , "aux" , NULL);
                perror("execlp error");   //异常处理,只有execlp函数执行失败后,才输出
               
                //wait(NULL);   //wait()是阻塞函数,确保子进程先退出
                //不写wait函数也可以,因为1. 即使父进程先执行结束,子进程变为了孤儿进程,会被1号进程领养,结束后会释放进程资源
                // 2. execlp执行成功后,就执行不到这里了.        
        }  
        else if(child_pid == 0)   //子进程关闭写端
        { //如果子进程先执行grep bash , 会阻塞等待
                close(fd[1]);
                dup2(fd[0],STDIN_FILENO);
                execlp("grep","grep","--color=auto","bash",NULL);  //执行execlp后,新的进程将替换数据段,代码段,栈,堆
                //并且不会执行execlp后面的代码了。
                //--color=auto :让bash变成红色,从ps aux | grep bash 的执行结果参考到的。
                perror("execlp error");




        return 0;
}

06-进程间通信_第7张图片

06-进程间通信_第8张图片

[holo@holocom 0410]$ ./pipeps_aux

root       6511  0.0  0.0 115304   960 ?        S    11:29   0:00 /bin/bash /usr/sbin/ksmtuned

holo      77697  0.0  0.0  72312   776 ?        Ss   12:14   0:00 /usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "env GNOME_SHELL_SESSION_MODE=classic gnome-session --session gnome-classic"

holo      98736  0.0  0.1 116356  2968 pts/0    Ss   12:37   0:00 -bash

holo      99139  0.0  0.0 112712   972 pts/0    S+   13:03   0:00 grep --color=auto bash

[holo@holocom 0410]$ ps aux | grep bash

root       6511  0.0  0.0 115304   960 ?        S    11:29   0:00 /bin/bash /usr/sbin/ksmtuned

holo      77697  0.0  0.0  72312   776 ?        Ss   12:14   0:00 /usr/bin/ssh-agent /bin/sh -c exec -l /bin/bash -c "env GNOME_SHELL_SESSION_MODE=classic gnome-session --session gnome-classic"

holo      98736  0.0  0.1 116356  2968 pts/0    Ss   12:37   0:00 -bash

holo      99141  0.0  0.0 112712   972 pts/0    S+   13:03   0:00 grep --color=auto bash

  • 兄弟进程间通信, 实现ps aux | grep bash

使用execlp函数和dup2函数

父进程要调用waitpid函数完成对子进程的回收

// 模拟兄弟进程间  ps aux | grep bash操作
#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
        //创建管道
        int fd[2];
        int ret = pipe(fd);
        int child_pid;


        if(ret < 0)
        {
                perror("pipe error");
                return -1;
        }


        //创建子进程
        int i=0;
        int n=2;
        for(i=0;i 0)
                        {
                                if(WIFEXITED(status))
                                {
                                        printf("子进程正常退出,status == [%d] \n",WEXITSTATUS(status));
                                }
                                else if(WIFSIGNALED(status))
                                {
                                        printf("子进程被信号[%d]杀死了",WTERMSIG(status));
                                }
                        }
                }
        }


        if(i==0)   //哥哥进程写
        {
                close(fd[0]);
                //sleep(5);             //验证read函数是阻塞的
                dup2(fd[1],STDOUT_FILENO);
                execlp("ps" , "ps" , "aux" , NULL);
                perror("execlp error");   //异常处理,只有execlp函数执行失败后,才输出
               
                close(fd[1]);
                //wait(NULL);   //wait()是阻塞函数,确保子进程先退出
                //不写wait函数也可以,因为即使父进程先执行结束,子进程变为了孤儿进程,会被1号进程领养,结束后会释放进程资源
        }
        else if(i==1)   //哥哥进程读
        {
                printf("儿子:fpid==[%d],child_pid==[%d]\n",getppid(),getpid());
                close(fd[1]); //关闭写端
                dup2(fd[0],STDIN_FILENO);
                execlp("grep","grep","--color=auto","bash",NULL);  //执行execlp后,新的进程将替换数据段,代码段,栈,堆
                //并且不会执行execlp后面的代码了。
                //--color=auto :让bash变成红色,从ps aux | grep bash 的执行结果参考到的。
                perror("execlp error");
                //char buf[64];
                //memset(buf , 0x00 , sizeof(buf));  //对数组初始化
                //int n = read(fd[0],buf , sizeof(buf));  //read没数据时阻塞,如果没写入数据,就会等待写入.
                //printf("read over , n == [%d] , buf == [%s]\n",n,buf);
                close(fd[0]);
        }


        return 0;
}

[holo@holocom 0410]$ ./pipebrother

儿子:fpid==[66948],child_pid==[66950]

root       6561  0.0  0.0 115304   964 ?        S    12:17   0:00 /bin/bash /usr/sbin/ksmtuned

holo      65714  0.0  0.1 116356  2956 pts/0    Ss   17:05   0:00 -bash

holo      65882  0.0  0.1 116356  2932 pts/1    Ss   17:05   0:00 -bash

holo      66682  0.1  0.0 113184  1620 ?        Ss   17:07   0:00 bash -c while true; do sleep 1;head -v -n 8 /proc/meminfo; head -v -n 2 /proc/stat /proc/version /proc/uptime /proc/loadavg /proc/sys/fs/file-nr /proc/sys/kernel/hostname; tail -v -n 16 /proc/net/dev;echo '==> /proc/df <==';df -l;echo '==> /proc/who <==';who;echo '==> /proc/end <==';echo '##Moba##'; done

holo      66950  0.0  0.0 112712   968 pts/0    S+   17:08   0:00 grep --color=auto bash

子进程正常退出,status == [0]

子进程正常退出,status == [0]

子进程死光了,wpid == [-1]

3.7 管道的读写行为

#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
        //创建管道
        int fd[2];
        int ret = pipe(fd);  //创建管道,成功返回0,失败返回-1,并设置error值  fd[0]读端,fd[1]写端


        if(ret == -1)
        {
                perror("pipe error");
                return -1;
        }
        else if(ret == 0) //创建管道成功
        {
                char buf[64];
                memset(buf , 0x00 ,sizeof(buf));
                //close(fd[1]); //关闭写端
                int i = 1;
                while(1)
                {
                        write(fd[1], "hello world" , strlen("hello world"));
                        if(i++%1000 == 0)
                        {
                                printf("正在写入数据--[第%d条]\n",i);
                        }
                }


                close(fd[0]);   //关闭读端
                int n = read(fd[0] , buf , sizeof(buf));
                printf("读到了[%d]个字节,内容是[%s]\n",n,buf);
        }


        return 0;
}

  • 读操作
  • 有数据

read正常读,返回读出的字节数

[holo@holocom 0410]$ ./pipe_wr
读到了[11]个字节,内容是[hello world]

  • 无数据

写端全部关闭

read解除阻塞,立刻返回0, 相当于读文件读到了尾部

[holo@holocom 0410]$ ./pipe_wr
读到了[0]个字节,内容是[]

没有全部关闭

read阻塞

[holo@holocom 0410]$ ./pipe_wr









  • 写操作

读端全部关闭

管道破裂,进程终止, 内核给当前进程发SIGPIPE(13)信号

[holo@holocom 0410]$ ./pipe_wr
读到了[-1]个字节,内容是[]   //读不到数据,read返回-1

读端没全部关闭

缓冲区写满了

write阻塞

……(省略了好多条写入数据)
正在写入数据--[第5918条]正在写入数据--[第5919条]正在写入数据--[第5920条]正在写入数据--[第5921条]正在写入数据--[第5922条]正在写入数据-
 
 
 
 (一直按回车,没反应,说明write在这里阻塞了)

缓冲区没有满

继续write

3.8 如何设置管道为非阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参

考下列三个步骤进行:

第1步: int flags = fcntl(fd[0], F_GETFL, 0);

第2步: flags |= O_NONBLOCK;

第3步: fcntl(fd[0], F_SETFL, flags);

若是读端设置为非阻塞:

  • 写端没有关闭,管道中没有数据可读,则read返回-1;
  • 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
  • 写端已经关闭,管道中有数据可读(先写后关闭),则read返回实际读到的字节数(几遍阻塞也可以读到)
  • 写端已经关闭,管道中没有数据可读,则read返回0

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

  • 命令

ulimit -a

  • 函数

long fpathconf(int fd, int name); //fd文件描述符,可以是读端或写端

printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF)); //_PC_PIPE_BUF获取管道大小的宏

printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

//利用函数查看管道缓冲区大小
#include 
#include 
#include 
#include 
#include 


int main(int argc,char * argv[])
{
        int fd[2];
        int ret = pipe(fd);
        printf("管道大小(读端):[%ld]\n",fpathconf(fd[0],_PC_PIPE_BUF));


        printf("管道大小(写端):[%ld]\n",fpathconf(fd[1],_PC_PIPE_BUF));
//fd[0]和fd[1]都指向管道,所以值应该是相同的
        return 0;
}

[holo@holocom 0410]$ ./pipesize

管道大小(读端):[4096]

管道大小(写端):[4096]

4 FIFO

4.1 FIFO介绍

FIFO常被称为命名管道,以区分管道(pipe,匿名管道)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。

FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。

利用fifo进行通信, 必须创建一个fifo文件.

有血缘关系的进程 , 使用pipe更简单 ; 没血缘关系的进程, 使用fifo

4.2 创建管道

  • 方式1-使用命令 mkfifo

命令格式: mkfifo 管道名

例如:mkfifo myfifo

06-进程间通信_第9张图片

  • 方式2-使用函数

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

参数说明和返回值可以查看man 3 mkfifo

当创建了一个FIFO,就可以使用open函数打开它,常见的文件I/O函数都可用于FIFO。如:close、read、write、unlink等。

FIFO严格遵循先进先出(first in first out),对FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。(因为这个FIFO文件是一个标签,里面没有内容,操作标签相当于操作内存缓冲区)

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

  • 使用FIFO完成两个进程通信的示意图

06-进程间通信_第10张图片

思路:

进程A先启动,进程B后启动

  • 进程A:
  • 创建一个fifo文件:myfifo(命令或者函数,在代码里使用函数)
  • 调用open函数打开myfifo文件,获得文件描述符fd
  • 调用write函数写入一个字符串如:“hello world”(其实是将数据写入到了内核缓冲区)
  • 调用close函数关闭myfifo文件

  • 进程B(A已经创建好了):
  • 调用open函数打开myfifo文件,获得fd
  • 调用read函数读取文件内容(其实就是从内核中读取数据)read(fd,buf,sizeof(buf));
  • 打印显示读取的内容
  • 调用close函数关闭myfifo文件

fifo_write.c

#include 
#include   
#include 
#include 
#include 
#include 
#include 


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


        //写fifo文件
        write(fd , "hello world" , strlen("hello world"));


        sleep(10);
//      getchar();      //相当于c++中system("pause");
        //关闭文件
        close(fd);
        return 0;
}

fifo_read.c

#include 
#include 
#include 
#include 
#include 
#include 
#include 


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


        //读fifo文件
        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();    //相当于c++中system("pause");
        return 0;
}

先在标签1执行写

[holo@holocom 0410]$ rm myfifo
[holo@holocom 0410]$ ./fifo_write

再复制一个标签2,执行读(10秒内)

[holo@holocom 0410]$ ./fifo_read
n == [11] , 读到的内容buf == [hello world]

注意:myfifo文件是在进程A中创建的,如果先启动进程B会报错。思考一下如何解决这个问题呢???

access 检测文件是否存在,也可以判断文件权限

如果不存在就创建, 如果存在就不创建

返回值 : =0存在 !=0不存在

int ret = access("./myfifo" , F_OK);

完整demo:

fifo_write.c

#include

#include

#include

#include

#include

#include

#include

#include

int main(int argc , char * argv[])

{

        int acc_ret = access("./myfifo" , F_OK);

        if(acc_ret != 0)                //没有创建

        {

                int ret = mkfifo("./myfifo" , 0777);  //创建一个

                if(ret == -1)

                {

                        perror("error");

                }

        }

        

        int fd = open("./myfifo", O_RDWR);

        if(fd < 0)

        {

                perror("open error");

                return -1;

        }

        write(fd , "hello world", strlen("hello world"));

        sleep(10);

        getchar();

        close(fd);

        return 0;

}

fifo_read.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

int main(int argc , char * argv[])

{

        int ret = access("./myfifo" , F_OK);

        if(ret != 0)  //没有创建

        {       

                int fifo_ret = mkfifo("./myfifo" , 0777);  //创建一个

                if(fifo_ret == -1)

                {

                        perror("error");

                        return -1;

                }

        }

        int fd = open("./myfifo", O_RDWR);

        if(fd < 0)

        {

                perror("open error");

                return -1;

        }

        char buf[64];

        memset(buf , 0x00 , sizeof(buf));

        read(fd , buf , sizeof(buf) );

        printf("read: [%s]\n",buf);

        close(fd);

        return 0;

}

标签1:

holo@holo:~/test/fifo$ ./fifo_write

标签2:

holo@holo:~/test/fifo$ ./fifo_read

read: [hello world]

5 内存映射区

5.1 存储映射区介绍

存储映射I/O (Memory-mapped I/O) 文件IO/设备IO 使一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。

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

操作内存快 , 相比操作文件提高了效率

从文件区到内存区的映射

06-进程间通信_第11张图片

5.2 mmap函数

  • 函数作用:

建立存储映射区

  • 函数原型

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

  • 函数返回值:
  • 成功:返回创建的映射区首地址;
  • 失败:MAP_FAILED宏
  • 参数:
    • addr: 指定映射的起始地址, 通常设为NULL, 由系统指定
    • length:映射到内存的文件长度 lseek和stat都可以获得文件大小.lseek在打开文件时使用很方便 一般填写文件大小
    • prot: 映射区的保护方式, 最常用的:
      • 读:PROT_READ
      • 写:PROT_WRITE
      • 读写:PROT_READ | PROT_WRITE
    • flags: 映射区的特性, 可以是
      • MAP_SHARED: 写入映射区的数据会写回文件, 且允许其他映射该文件的进程共享。(可以对内存区修改)
      • MAP_PRIVATE: 对映射区的写入操作会产生一个映射区的复制(copy-on-write), 对此区域所做的修改不会写回原文件。(不可以修改文件)

具体用哪个, 看实际需求, 只需要读 用第二个,

    • fd:由open返回的文件描述符, 代表要映射的文件。
    • offset:以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射。

如果一个文件有2k,可以只把其中的1k映射到内存中去.

5.3 munmap函数

  • 函数作用:

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

  • 函数原型:

int munmap(void *addr, size_t length);

  • 返回值:

成功:返回0

失败:返回-1,设置errno值

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

5.4 mmap注意事项

  • 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
  • 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
[holo@holocom 0410]$ ls -ltr test.log
-rw-rw-r--. 1 holo holo 29 Apr 15 19:19 test.log
[holo@holocom 0410]$ ./mmap1
buf = [0123456789]


[holo@holocom 0410]$ chmod u-wr test.log
[holo@holocom 0410]$ ls -ltr test.log
----rw-r--. 1 holo holo 29 Apr 15 19:19 test.log
[holo@holocom 0410]$ ./mmap1
open error: Permission denied

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

添加close(fd)测试即可,亲测可用。

[holo@holocom 0410]$ cat test.log
0123456789d66666666666666666
[holo@holocom 0410]$ make mmap
cc     mmap.c   -o mmap
[holo@holocom 0410]$ ./mmap
[hello world66666666666666666
][holo@holocom 0410]$ cat test.log
hello world66666666666666666

由于映射区已经建立,文件即使关闭,也不影响读写映射区操作,并且可以反应到文件中去。

  • 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
  • munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
  • 文件偏移量必须为0或者4K的整数倍

填222 报错

 void * addr = mmap(NULL , len , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 222);




[holo@holocom 0410]$ ./mmap
mmap error: Invalid argument

填4096 报错(因为文件大小没有超过4096,越界了)

[holo@holocom 0410]$ ./mmap
Bus error (core dumped)

一般填0就可以,如果文件大小超过4096,文件偏移量可以设为4096

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

5.5 有关mmap函数的使用总结

  • 第一个参数写成NULL
  • 第二个参数要映射的文件大小 > 0
  • 第三个参数:PROT_READ 、PROT_WRITE 注意要小于文件本身的权限
  • 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
  • 第五个参数:打开的文件对应的文件描述符
  • 第六个参数:4k的整数倍

5.6 mmap函数相关思考题

  • 可以open的时候O_CREAT一个新文件来创建映射区吗?

不可以,必须建立文件并对文件进行写操作,保证文件大小不等于0.才可以创建映射区

  • 如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?

不可以,open的权限要大于mmap的权限

  • mmap映射完成之后, 文件描述符关闭,对mmap映射有没有影响?

无影响

  • 如果文件偏移量为1000会怎样?

报错,无效参数。文件偏移量是4K的整数倍(0、4096、……)

  • 对mem越界操作会怎样?

报错

  • 如果mem++,munmap可否成功?

不会

  • mmap什么情况下会调用失败?

文件大小=0,open权限 < mmap权限,文件偏移量不是4K整数倍……

  • 如果不检测mmap的返回值,会怎样?

有可能调用mmap失败,返回map failed ,此时操作内存时会报错。

只要返回指针,就要检测返回值

5.7 mmap应用练习

  • 练习1:使用mmap完成对文件的读写操作

#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
        //共享映射区的建立在fork之前
        //使用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);             //文件大小 也可以用stat函数获取
        //需要用lseek函数获取文件大小  
        void * addr = mmap(NULL , len , PROT_READ | PROT_WRITE , MAP_SHARED , fd , 0);
        //void * addr = mmap(NULL , len , PROT_READ | PROT_WRITE , MAP_PRIVATE , fd , 0);


        //mmap函数有可能失败
        if(addr == MAP_FAILED)
        {
                perror("mmap error");
                return -1;
        }


        //创建子进程
        pid_t child_pid = fork();
        if(child_pid < 0)
        {
        }
        else if(child_pid>0)   //父进程
        {
                memcpy(addr , "hello world" , strlen("hello world"));
                wait(NULL);     //确保子进程先退出,父进程后退出
        }
        else if(child_pid == 0)  //子进程
        {
                sleep(1);       //保证父进程中的memcpy先完成
                char *p = (char *)addr;
                printf("[%s]",p);
        }


        return 0;
}

[holo@holocom 0410]$ vim test.log

[holo@holocom 0410]$ cat test.log //映射前文件大小必须大于0 , 等于0没法映射

11111

sssss66666666666666666

[holo@holocom 0410]$ ./mmap

[hello world66666666666666666

][holo@holocom 0410]$ cat test.log

hello world66666666666666666

[holo@holocom 0410]$

MAP_SHARED 文件会覆盖

MAP_PRIVATE 修改内存后不会写入文件里 适合进行读操作

  • 练习:2:使用mmap完成父子进程间通信

  • 图解说明

06-进程间通信_第12张图片

  • 思路
  • 调用mmap函数创建存储映射区,返回映射区首地址ptr
  • 调用fork函数创建子进程,子进程也拥有了映射区首地址
  • 父子进程可以通过映射区首地址指针ptr完成通信
  • 调用munmap函数释放存储映射区

父子进程可以共享共享映射区,文件描述符,不可以共享堆、栈

  • 练习3:使用mmap完成没有血缘关系的进程间通信

思路:两个进程都打开相同的文件,然后调用mmap函数建立存储映射区,这样两个进程共享同一个存储映射区。

//读


#include 
#include 
#include   
#include 
#include 
#include 
#include 


int main()
{
        //共享映射区的建立在fork之前
        //使用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);
        //void * addr = mmap(NULL , len , PROT_READ | PROT_WRITE , MAP_PRIVATE , fd , 0);


        //mmap函数有可能失败
        if(addr == MAP_FAILED)
        {
                perror("mmap error");
                return -1;
        }


        char buf[64];   //只读文件前10个
        memset(buf , 0x00 , sizeof(buf));
        memcpy(buf , addr , 10);        //拷贝10个
        printf("buf = [%s]\n",buf);


        return 0;
}

//写


#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
        //共享映射区的建立在fork之前
        //使用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);
        //void * addr = mmap(NULL , len , PROT_READ | PROT_WRITE , MAP_PRIVATE , fd , 0);


        //mmap函数有可能失败
        if(addr == MAP_FAILED)
        {
                perror("mmap error");
                return -1;
        }




        memcpy(addr , "0123456789" , 10);       //写10个


        return 0;
}

[holo@holocom 0410]$ vim mmap2.c

[holo@holocom 0410]$ make mmap2

cc     mmap2.c   -o mmap2

[holo@holocom 0410]$ vim mmap1.c

[holo@holocom 0410]$ ./mmap2

[holo@holocom 0410]$ ./mmap1

buf = [0123456789]

[holo@holocom 0410]$ cat test.log

0123456789d66666666666666666

[holo@holocom 0410]$

匿名映射(不建立文件)

使用mmap函数建立匿名映射:

mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

MAP_SHARED必须与MAP_ANONYMOUS一起使用,

匿名映射不使用文件,anonymous匿名.

文件描述符固定为 -1

匿名映射没有文件,所以只能用于有血缘关系的进程间通讯

文档
    MAP_ANONYMOUS
              The mapping is not backed by any file; its contents are initialized to zero.  The fd  and  offset  arguments
              are  ignored; however, some implementations require fd to be -1 if MAP_ANONYMOUS (or MAP_ANON) is specified,
              and portable applications should ensure this.  The use of MAP_ANONYMOUS in conjunction  with  MAP_SHARED  is
              supported on Linux only since kernel 2.4.

案例代码

//mmap匿名映射完成父子进程通讯
#include 
#include 
#include 
#include 
#include 
#include 
#include 


int main()
{
        //使用mmap建立共享映射区
        void * addr = mmap(NULL , 4096 , PROT_READ | PROT_WRITE , MAP_SHARED | MAP_ANONYMOUS, -1 , 0);
        //void * addr = mmap(NULL , len , PROT_READ | PROT_WRITE , MAP_PRIVATE , fd , 0);




        //创建子进程
        pid_t child_pid = fork();
        if(child_pid < 0)
        {
        }
        else if(child_pid>0)   //父进程
        {
                memcpy(addr , "hello world" , strlen("hello world"));
                wait(NULL);     //确保子进程先退出,父进程后退出
        }
        else if(child_pid == 0)  //子进程
        {
                sleep(1);       //保证父进程中的memcpy先完成
                char *p = (char *)addr;
                printf("[%s]",p);
        }


        return 0;
}




[holo@holocom 0410]$ make mmap_anonymous
cc     mmap_anonymous.c   -o mmap_anonymous
[holo@holocom 0410]$ ./mmap_anonymous
[hello world][holo@holocom 0410]$

你可能感兴趣的:(linux,linux,java,服务器)