06-进程间通信

news2024/9/23 15:22:43

  1. 学习目标

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

2 进程间通信相关概念

2.1 什么是进程间通信

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

2.2 进程间通信的方式

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

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

3 管道-pipe

3.1管道的概念

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

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

有如下特质:

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

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之前)

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

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

创建步骤总结:

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

3.6 管道练习

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


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 :原来从标准输入读, 从管道读端读 , 读到标准输出.

使用execlp函数和dup2函数

// 模拟ps aux | grep bash操作
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>


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

[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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>


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<n;i++)
        {
                //创建子进程
                child_pid = fork();
                if(child_pid < 0)
                {
                        perror("fork error");
                        return -1;
                }
                else if(child_pid == 0)
                {
                        break;
                }
        }


        if(i==n)
        {
                close(fd[0]);
                close(fd[1]);
                pid_t wpid;
                int status;


                while(1)
                {
                        wpid = waitpid(-1,&status,WNOHANG);
                        if(wpid == 0)  //目前咩有紫禁城退出
                        {
                                sleep(1);
                                continue;
                        }
                        else if(wpid == -1)     //子进程全部死光
                        {
                                printf("子进程死光了,wpid == [%d]\n",wpid);
                                exit(0);
                        }
                        else if(wpid > 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>


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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>


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

  • 方式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完成两个进程通信的示意图

思路:

进程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 <stdio.h>
#include <stdlib.h>  
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.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;
        }


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


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

fifo_read.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.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;
        }


        //读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 <stdio.h>

#include <errno.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/stat.h>

#include <fcntl.h>

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 <stdio.h>

#include <errno.h>

#include <stdlib.h>

#include <string.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

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函数来实现。

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

从文件区到内存区的映射

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>


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完成父子进程间通信

  • 图解说明

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

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

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

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

//读


#include <stdio.h>
#include <stdlib.h>
#include <string.h>  
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>


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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>


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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>


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]$

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1075257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA设计时序约束四、多周期约束

目录 一、背景 二、set_multicycle_path a)Targets界面 b)options界面 c)setup与hold关系 三、多周期约束场景 3.1 单时钟域的多周期约束 3.2 多周期路径与时钟相移 3.3 慢时钟到快时钟的多周期约束 3.4 快时钟到慢时钟的多周期约束 四、工程示例 五、参考 一、背景…

广州华锐互动:VR互动教学平台如何赋能职业院校?

随着科技的发展&#xff0c;我们的教育方式也在不断进步。其中&#xff0c;虚拟现实&#xff08;VR&#xff09;技术的出现为我们提供了一种全新的教学方式。特别是在职业学校中&#xff0c;VR互动教学平台已经成为一种重要的教学工具。 VR互动教学平台是一种利用虚拟现实技术创…

游戏软件开发与应用软件开发有什么不同呢?

游戏软件开发和应用软件开发是两种不同类型的软件开发&#xff0c;它们在许多方面都有不同之处。以下是它们之间的一些主要区别&#xff1a; 目标用户群体&#xff1a; 游戏软件开发的主要目标是提供娱乐和休闲体验&#xff0c;通常面向广大的游戏玩家群体。游戏软件的设计和开…

【java基础学习】之DOS命令

#java基础学习 1.常用的DOS命令&#xff1a; dir:列出当前目录下的文件以及文件夹 md: 创建目录 rd:删除目录cd:进入指定目录 cd.. :退回到上级目录 cd\ : 退回到根目录 del:删除文件 exit:退出dos命令行 1.dir:列出当前目录下的文件以及文件夹 2.md: 创建目录 …

Spring实例化源码解析之Custom Events上集(八)

Events使用介绍 在ApplicationContext中&#xff0c;事件处理通过ApplicationEvent类和ApplicationListener接口提供。如果将实现ApplicationListener接口的bean部署到上下文中&#xff0c;每当一个ApplicationEvent被发布到ApplicationContext时&#xff0c;该bean将被通知。…

Android Studio版本升级后的问题 gradle降级、jdk升级

Cannot use TaskAction annotation on method IncrementalTask.taskAction$gradle_core() because interface org.gradle.api.tasks.incremental.IncrementalTaskInputs is not a valid parameter to an action method. 修改下面两处地方分别为7.0.3、7.3.3Android Gradle plu…

Nginx的跨域问题解决

同源策略 浏览器的同源策略&#xff1a;是一种约定&#xff0c;是浏览器最核心也是最基本的安全功能&#xff0c;如果浏览器少了同源策略&#xff0c;则浏览器的正常功能可能都会受到影响。 同源: 协议、域名(IP)、端口相同即为同源 跨域问题 有两台服务器分别为A,B,如果从…

网络安全(自学黑客技术)——黑客学习方法

如果你想自学网络安全&#xff0c;首先你必须了解什么是网络安全&#xff01;&#xff0c;什么是黑客&#xff01;&#xff01; 1.无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面性&#xff0c;例如 Web 安全技术&#xff0c;既有 Web 渗透2.也有 Web 防…

MySQL运维—从零到放弃

1. 日志 1.1 错误日志 错误日志是 MySQL 中最重要的日志之一&#xff0c;它记录了当 mysqld 启动和停止时&#xff0c;以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时&#xff0c;建议首先查看此日志。 该日志是默认开启的…

JVM完整图文学习笔记 (含拓展知识广度学习) 第三章: 类加载与字节码技术

目录 编译期处理——语法糖 默认构造器 自动拆装箱 泛型集合取值 可变参数 foreach循环 switch 字符串 switch 枚举 枚举类 try-with-resources 方法重写时的桥接方法 匿名内部类 类加载阶段&#xff08;重点&#xff01;&#xff09; 加载 链接 &#xff08;1&#xff09;验证…

pip 清华镜像

python -m pip install --upgrade pip pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple pypi | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror

大数据—数据透析表常见使用(手把手详解)

我的个人主页&#xff1a;☆光之梦☆_C语言基础语法&#xff08;超详细&#xff09;,【java入门】语法总结-CSDN博客 创作不易&#xff0c;如果能帮到你就好 注&#xff1a;你的 &#x1f44d;点赞 ⭐收藏 &#x1f4dd;评论 是对博主最大的支持与鼓励喔 目录 一、创建数据透…

CentOS系统/root根目录扩容(扩展逻辑卷)

具体操作步骤 1、查看本机磁盘环境挂载情况 2、添加磁盘分区 3、开始扩容 4、同步到文件系统 1、查看本机磁盘环境挂载情况 [rooticon ~]# df -lh 可以看到/dev/mapper/centos-root 路径下容量为50G&#xff0c;我们要给这个路径下的容量扩容&#xff1a;[rooticon ~]# lsblk…

el-table 边框颜色修改 简单有效!

废话不多说&#xff0c;直接上图 &#xff08;1&#xff09;修改前的图如下&#xff1a; 以上是elementUI原组件自带的样式 &#xff08;2&#xff09;下面是修改后的边框图如下&#xff1a; 源码如下&#xff1a; <el-table :data"jctableData" border size…

mysql 逻辑备份 bin-log日志恢复

一、逻辑备份 逻辑备份&#xff1a;备份的是建表&#xff0c;建库&#xff0c;插入数据等操作所执行SQL语句&#xff0c;适用于中小型数据库&#xff0c;效率相对较低&#xff0c;提供三种级别的备份&#xff0c;表级&#xff0c;库级和全库级。 本质&#xff1a;导出的是SQL语…

基于Springboot实现疫情网课管理系统项目【项目源码+论文说明】分享

基于Springboot实现疫情网课管理系统演示 摘要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于疫情网课管理系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了疫情…

麻省理工学院与Meta AI共同开发StreamingLLM框架,实现语言模型无限处理长度

&#x1f989; AI新闻 &#x1f680; 麻省理工学院与Meta AI共同开发StreamingLLM框架&#xff0c;实现语言模型无限处理长度 摘要&#xff1a;麻省理工学院与Meta AI的研究人员联合研发了一款名为StreamingLLM的框架&#xff0c;解决了大语言模型在RAM与泛化问题上的挑战&am…

一用就会的手机配音软件,效果好到你震惊~

作为当今社交媒体时代的一员&#xff0c;我们经常需要在各种场合中使用配音软件&#xff0c;无论是自制视频内容还是进行个人创作&#xff0c;一款好用的配音软件真的很重要。所以经过小编这几天的努力&#xff0c;终于选出了一款备受好评的免费配音软件&#xff0c;不仅功能强…

高效数据传输:Java通过绑定快速将数据导出至Excel

摘要&#xff1a;本文由葡萄城技术团队原创并首发。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 前言 把数据导出至 Excel 是很常见的需求&#xff0c;而数据的持久化&#xff0c;往往又放在…

为什么选择HubSpot电子邮件营销?

在数字时代&#xff0c;电子邮件营销是企业吸引、互动和保留客户的关键工具之一。然而&#xff0c;要在竞争激烈的市场中脱颖而出&#xff0c;并确保你的消息被准确地传达给目标受众&#xff0c;需要一款强大而智能的电子邮件营销平台。这就是HubSpot电子邮件营销背后的核心理念…