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.mzph.cn/news/99818.shtml

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

相关文章

STM32F030在使用内部参考电压 (VREFINT)时与STM32G070的区别

背景&#xff1a; 之前使用过STM32G070的内部参考电压来提升ADC采集的准确度&#xff08;STM32使用内部参考电压提高ADC采集准确度&#xff09;&#xff0c;所以本次使用STM32F030的芯片时直接把之前G070的代码拿过来用了&#xff0c;但是出现了问题。 查找资料发现两者不同&am…

STM32CubeMX学习笔记-USART_DMA

STM32CubeMX学习笔记-USART_DMA 一、DMA的概念二、数据传输方式普通模式循环模式 三、以串口方式讲解串口DMA方式发送函数&#xff1a;HAL_UART_Transmit_DMA串口DMA方式接收函数&#xff1a;HAL_UART_Receive_DMA获取未传输数据个数函数&#xff1a;__HAL_DMA_GET_COUNTER关闭…

如何在Apache和Resin环境中实现HTTP到HTTPS的自动跳转:一次全面的探讨与实践

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

【MySQL】基本查询(二)

文章目录 一. 结果排序二. 筛选分页结果三. Update四. Delete五. 截断表六. 插入查询结果结束语 操作如下表 //创建表结构 mysql> create table exam_result(-> id int unsigned primary key auto_increment,-> name varchar(20) not null comment 同学姓名,-> chi…

解决yolo无法指定显卡的问题,实测v5、v7、v8有效

方法1 基本上这个就能解决了&#xff01;&#xff01;&#xff01; 在train.py的最上方加上下面这两行&#xff0c;注意是最上面&#xff0c;其次指定的就是你要使用的显卡 import os os.environ[CUDA_VISIBLE_DEVICES]6方法2&#xff1a; **问题&#xff1a;**命令行参数指…

C#:出题并判断

C#:出题并判断 //出题并判断 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms;namespace Test_…

# 解析Pikachu靶场:一个安全研究的练习场

引言 Pikachu靶场是一个非常流行的安全研究和渗透测试练习平台。这个环境包括多个安全漏洞&#xff0c;从基础的到高级的&#xff0c;供安全研究人员和渗透测试者进行实验和学习。在这篇博客中&#xff0c;我们将探讨Pikachu靶场的基本概念&#xff0c;功能&#xff0c;以及如…

使用安卓Termux+Hexo,手机也能轻松搭建个人博客网站

文章目录 前言1.安装 Hexo2.安装cpolar3.远程访问4.固定公网地址5.结语 前言 Hexo 是一个用 Nodejs 编写的快速、简洁且高效的博客框架。Hexo 使用 Markdown 解析文章&#xff0c;在几秒内&#xff0c;即可利用靓丽的主题生成静态网页。 下面介绍在Termux中安装个人hexo博客并…

stl 输入输出流

标准输入输出流 头文件 iostream 从标准输入读取流 cin >> 从标准输出写入流 cout << get 系列函数 get 无参数&#xff1a;cin.get() 从指定的输入流中提取一个字符&#xff08;包括空白字符&#xff09;&#xff0c;若读取成功&#xff0c;返回该字符的 ASC…

论文分享 | 利用单模态自监督学习实现多模态AVSR

本次分享上海交通大学发表在 ACL 2022 会议 的论文《Leveraging Unimodal Self-Supervised Learning for Multimodal AVSR》。该论文利用大规模单模态自监督学习构建多模态语音识别模型。 论文地址&#xff1a; https://aclanthology.org/2022.acl-long.308.pdf 代码仓库&am…

css 写带三角形的对话框,空心的三角形边框

首先&#xff0c;我们要会先实现一个小三角形&#xff1b; 思路&#xff1a;利用元素的 border 属性&#xff0c;将其三个方向的 border-color 值设为透明色&#xff08;或者和其父元素的背景色一致&#xff0c;形成视觉差&#xff0c;俗称障眼法&#xff09;&#xff0c;剩下…

操作系统学习笔记--进程与线程

进程 概念 不同的角度有不同的定义 进程是程序的一次执行过程进程是一个程序及其数据在处理机上顺序执行时所发生的活动进程是具有独立功能的程序在一个数据集合上运行的过程&#xff0c;它是系统进行资源分配和调度的一个独立单位 进程&#xff1a;是动态的&#xff0c;是…

ESP-07S进行TCP 通信测试

一&#xff0c;TCP Server 为 AP 模式&#xff0c;TCP Client 为 Station 模式。 这里电脑pc作为TCP Server&#xff0c;ESP-07S作为TCP Client 。 二&#xff0c;电脑端配置。 1&#xff0c;开启热点。 2&#xff0c;转到“设置”&#xff0c;编辑热点信息。 3&#xff0c;关闭…

nio 文件传输

transferto方法一次只能传输2个g的数据 文件大于2个g时

动态壁纸软件iWall mac中文特色

iWall for mac是一款动态壁纸软件&#xff0c;它可以使用任何格式的漂亮视频(无须转换)&#xff0c;音频(可视化功能)&#xff0c;图片&#xff0c;动画&#xff0c;Flash&#xff0c;gif&#xff0c;swf&#xff0c;程序&#xff0c;网页&#xff0c;网站做为您的动态壁纸&…

90后整顿秦始皇老板

我的日常就像跑步机上急速前行的仓鼠&#xff0c;使劲往前冲&#xff0c;心有余力力有限。 我在一个电商运营公司做策划和写文案&#xff0c;每天总是加不完的班&#xff0c;从来没见过下午六点钟的太阳。 我做文案吗&#xff1f;唉&#xff0c;说实话&#xff0c;我倒觉得大…

HP打印机一点击打印就出现Windows资源管理器已停止工作问题解决

本次处理的打印机型号是HP Officejet 200 移动便携式打印机&#xff0c;不过其他型号如果出现类似现象&#xff0c;解决方法应该是一致的。 在弹出Windows资源管理器已停止工作的报错提示框后&#xff0c;点击左下角的详细信息&#xff0c;看到的内容显示是KernelBase.dll崩溃…

第二证券:汽车产业链股活跃,恒勃股份、博俊科技“20cm”涨停

轿车产业链股9日盘中走势活跃&#xff0c;截至发稿&#xff0c;恒勃股份、博俊科技“20cm”涨停&#xff0c;德迈仕涨超17%&#xff0c;上声电子涨超14%&#xff0c;川环科技涨超10%&#xff0c;圣龙股份、科华控股、沪光股份、上海沿浦、日盈电子、赛力斯等均涨停。 工作方面…

Feign(替代RestTemplate)远程调用

Feign初步学习 定义 Feign 是一个基于 Java 的 HTTP 客户端库&#xff0c;它是 Spring Cloud 中的一部分&#xff0c;用于简化微服务之间的 HTTP 通信。与传统的使用 RestTemplate 来调用 RESTful 服务不同&#xff0c;Feign 提供了一种声明式、基于接口的方式来定义和调用 H…

XLSX.utils.sheet_to_json()解析excel,给空的单元格赋值为空字符串

前言 今天用到XLSX来解析excel文件&#xff0c;调用XLSX.utils.sheet_to_json(worksheet)&#xff0c;发现如果单元格为空的话&#xff0c;解析出来的结果&#xff0c;就会缺少相应的key&#xff08;如图所示&#xff09;。但是我想要单元格为空的话&#xff0c;值就默认给空字…