-
学习目标
- 熟练使用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]$