06-进程间通信

  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_ANONYMOUSThe mapping is not backed by any file; its contents are initialized to zero.  The fd  and  offset  argumentsare  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  issupported 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.rhkb.cn/news/153790.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vscode package.json文件开头的{总是提升警告

警告如下 Problems loading reference https://json.schemastore.org/stylelintrc.json: Unable to load schema from https://json.schemastore.org/stylelintrc.json: read ECONNRESET. 解决如下 在设置&#xff08;settings.json&#xff09;里 新增一条属性 "ht…

Mock工具之Moco使用

一、什么是Mock mock英文单词有愚弄、嘲笑、模拟的意思&#xff0c;这里主要是模拟的意思 二、什么是Moco 开源的、基于java开发的一个mock框架支持http、https、socket等协议 三、Mock的特点 只需要简单的配置request、response等即可满足要求 支持在request 中设置headers、…

【附代码】使用Shapely计算多边形外扩与收缩

文章目录 相关文献效果图代码 作者&#xff1a;小猪快跑 基础数学&计算数学&#xff0c;从事优化领域5年&#xff0c;主要研究方向&#xff1a;MIP求解器、整数规划、随机规划、智能优化算法 本文档介绍如何使用 Shapely Python 包 计算多边形外扩与收缩。 如有错误&…

sface人脸相似度检测

sface人脸相似度检测&#xff0c;基于OPENCV&#xff0c;人脸检测采用yunet&#xff0c;人脸识别采用sface&#xff0c;支持PYTHON/C开发&#xff0c;图片来自网络&#xff0c;侵权请联系本人立即删除 yunet人脸检测sface人脸识别&#xff0c;检测两张图片的人脸相似度

Redis-01基本数据结构

1、String 1.1、介绍 String 是最基本的 key-value 结构&#xff0c;key 是唯一标识&#xff0c;value 是具体的值&#xff0c;value其实不仅是字符串&#xff0c; 也可以是数字&#xff08;整数或浮点数&#xff09;&#xff0c;value 最多可以容纳的数据长度是 512M 1.2、…

基于springboot实现在线动漫信息交流分享平台项目【项目源码+论文说明】计算机毕业设计

基于springboot实现在线动漫信息交流分享平台演示 摘要 随着社会互联网技术的快速发展&#xff0c;每个行业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于在线动漫信息平台当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#x…

基于拉丁超立方法的风光场景生成与削减

代码链接&#xff1a;基于拉丁超立方法的风光场景生成与削减 摘要&#xff1a;与蒙特卡洛法不同&#xff0c;拉丁超立方采样改进了采样策略能够做到较小采样规模中获得较高的采样精度&#xff0c;属于分层抽样技术&#xff0c;设定风光出力遵从正态分布normrnd&#xff0c;从而…

【LeetCode】——链式二叉树经典OJ题详解

主页点击直达&#xff1a;个人主页 我的小仓库&#xff1a;代码仓库 C语言偷着笑&#xff1a;C语言专栏 数据结构挨打小记&#xff1a;初阶数据结构专栏 Linux被操作记&#xff1a;Linux专栏 LeetCode刷题掉发记&#xff1a;LeetCode刷题 算法头疼记&#xff1a;算法专栏…

关于 打开虚拟机出现“...由VMware产品创建,但该产品与此版VMwareWorkstateion不兼容,因此无法使用” 的解决方法

文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/133678951 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结…

网工内推 | base郑州,上市公司,最高15薪,五险一金全额缴

01 四方达 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1、负责公司数据中心&#xff08;机房&#xff09;的管理与运维工作。 2、负责公司服务器、路由器、防火墙、交换机等设备的管理、以及网络平台的运行监控和维护&#xff1b; 3、负责公司服务器运维管理工作、…

17基于matlab卡尔曼滤波的行人跟踪算法,并给出算法估计误差结果,判断算法的跟踪精确性,程序已调通,可直接运行,基于MATLAB平台,可直接拍下。

17基于matlab卡尔曼滤波的行人跟踪算法&#xff0c;并给出算法估计误差结果&#xff0c;判断算法的跟踪精确性&#xff0c;程序已调通&#xff0c;可直接运行&#xff0c;基于MATLAB平台&#xff0c;可直接拍下。 17matlab卡尔曼滤波行人跟踪 (xiaohongshu.com)

androidStudio第一次运行报错无法运行

安卓第一次运行失败 大家好&#xff0c;我使用androidStudio新建了一个测试demo第一次运行&#xff0c;结果失败了&#xff0c;显示如下图&#xff1a; 然后查了各种方法&#xff0c;都是没有用&#xff0c;最后 历经困难&#xff0c;还是找到了&#xff0c;原来是 gradle的依…

CV计算机视觉每日开源代码Paper with code速览-2023.10.9

精华置顶墙裂推荐&#xff01;小白如何1个月系统学习CV核心知识&#xff1a;链接 点击CV51&#xff0c;关注更多CV干货 论文已打包&#xff0c;点击进入—>下载界面 点击加入—>CV计算机视觉交流群 1.【基础网络架构】Entropic Score metric: Decoupling Topology and…

2023.10.8 基本 Thread 线程详解

目录 Thread 常见构造方法 Thread 常见属性 创建一个 Thread 线程 使用 jconsole 命令观察线程 中断一个 Thread 线程 等待一个 Thread 线程 休眠当前 Thread 线程 让出当前 Thread 线程的 CPU 资源 线程的状态 Thread 常见构造方法 方法说明Thread()创建线程对…

天然泉水除砷技术解析,除砷树脂

天然地下水和地表水都可能含有砷&#xff0c;地下水含砷量高于地表水。而地下水砷超标的原因一种是由于自然原因造成的&#xff0c;主要是含砷矿物风化溶解造成的地下水污染。由于含砷矿物分布广泛&#xff0c;这种污染在世界各地都有发生&#xff0c;尤其在南亚、南美等地区&a…

java基础 日期工具类

目录结构&#xff1a; DateUtils.java package dateStudy; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date;public class DateUtils {private static final String FORMAT_1"yyyy-MM-dd HH:mm:ss";//私有方法&#xf…

如何正确方便的理解双指针?力扣102 (二叉树的层序遍历)

双指针&#xff0c;顾名思义就是指针的指针。 在此之前我们需要先理解单指针 &#xff08;简称为指针&#xff09;。指针很简单&#xff0c;直接上例子&#xff1a;例&#xff1a;现有两个变量&#xff0c;a10,b20. 要求&#xff1a;交换他们的值&#xff0c;输出的结果应为a20…

Flink之Watermark源码解析

1. WaterMark源码分析 在Flink官网中介绍watermark和数据是异步处理的,通过分析源码得知这个说法不够准确或者说不够详细,这个异步处理要分为两种情况: watermark源头watermark下游 这两种情况的处理方式并不相同,在watermark的源头确实是异步处理的,但是在下游只是做的判断,这…

LLaVa大模型关键技术及在线演示

LLaVA&#xff0c;一种新的大型多模态模型&#xff0c;称为“大型语言和视觉助手”&#xff0c;旨在开发一种通用视觉助手&#xff0c;可以遵循语言和图像指令来完成各种现实世界的任务。 这个想法是将 GPT-4 等大型语言模型 (LLM) 的强大功能与 CLIP 等视觉编码器相结合&#…

【MATLAB源码-第45期】基于matlab的16APSK调制解调仿真,使用卷积编码软判决。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. 16APSK调制解调 16APSK (16-ary Amplitude Phase Shift Keying) 是一种相位调制技术&#xff0c;其基本思想是在恒定幅度的条件下&#xff0c;改变信号的相位&#xff0c;从而传送信息。 - 调制&#xff1a;在16APSK中&am…