mmap()函数参数详解
author : wfs
time : 2019.4.17
功能描述
mmap将⼀个⽂件或者其它对象映射进内存。⽂件被映射到多个页上,如果⽂件的⼤⼩不是所有页的⼤⼩之和,最后⼀个页不被使⽤的空间将会清零。 munmap执⾏相反的操作,删除特定地址区域的对象映射。
基于⽂件的映射,在mmap和munmap执⾏过程的任何时刻,被映射⽂件的st_atime可能被更新。
在对映射区写⼊之后,调⽤msync()函数写回。
⽤法
  #include <sys/mman.h>
  void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);
  int munmap(void *start, size_t length);
参数:
start:映射区的开始地址。
length:映射区的长度。
prot:期望的内存保护标志,不能与⽂件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在⼀起
PROT_EXEC //页内容可以被执⾏
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写⼊
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是⼀个或者多个以下位的组合体
  MAP_FIXED //使⽤指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,
重叠部分将会被丢弃。如果指定的起始地址不可⽤,操作将会失败。并且起始地址必须落在页的边界上。
  MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写⼊,相当于输出到⽂件。直到msync()或者
munmap()被调⽤,⽂件实际上不会被更新。
  MAP_PRIVATE //建⽴⼀个写⼊时拷贝的私有映射。内存区域的写⼊不会影响到原⽂件。这个标志和以上标志是互斥的,只能使⽤其中⼀个。
  MAP_DENYWRITE //这个标志被忽略。
  MAP_EXECUTABLE //同上
  MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不⾜,对映射区的修改会引起段违例信号。
  MAP_LOCKED //锁定映射区的页⾯,从⽽防⽌页⾯被交换出内存。
  MAP_GROWSDOWN //⽤于堆栈,告诉内核VM系统,映射区可以向下扩展。
  MAP_ANONYMOUS //匿名映射,映射区不与任何⽂件关联。
  MAP_ANON //MAP_ANONYMOUS的别称,不再被使⽤。
  MAP_FILE //兼容标志,被忽略。
  MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到⽀持。
  MAP_POPULATE //为⽂件映射通过预读的⽅式准备好页表。随后对映射区的访问不会被页违例阻塞。
  MAP_NONBLOCK //仅和MAP_POPULATE⼀起使⽤时才有意义。不执⾏预读,只为已存在于内存中的页⾯建⽴页表⼊⼝。
  fd:有效的⽂件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1。
  offset:被映射对象内容的起点。
  返回说明:
  成功执⾏时,mmap()返回被映射区的指针,munmap()返回0。失败时,mmap()返回MAP_FAILED[其
值为(void *)-1],munmap 返回-1。errno被设为以下的某个值
  EACCES:访问出错
  EAGAIN:⽂件已被锁定,或者太多的内存已被锁定
  EBADF:fd不是有效的⽂件描述词
  EINVAL:⼀个或者多个参数⽆效
  ENFILE:已达到系统对打开⽂件的限制
  ENODEV:指定⽂件所在的⽂件系统不⽀持内存映射
  ENOMEM:内存不⾜,或者进程已超出最⼤内存映射数量
  EPERM:权能不⾜,操作不允许
  ETXTBSY:已写的⽅式打开⽂件,同时指定MAP_DENYWRITE标志
  SIGSEGV:试着向只读区写⼊
  SIGBUS:试着访问不属于进程的内存区
共享内存可以说是最有⽤的进程间通信⽅式,也是最快的IPC形式。两个不同进程A、B共享内存的意思是,同⼀块物理内存被映射到进程A、B各⾃的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新,反之亦然。由于多个进程共享同⼀块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
采⽤共享内存通信的⼀个显⽽易见的好处是效率⾼,因为进程可以直接读写内存,⽽不需要任何数据的拷贝。对于像管道和消息队列等通信⽅式,则需要在内核和⽤户空间进⾏四次的数据拷贝,⽽共享内存则只拷贝两次数据[1]:⼀次从输⼊⽂件到共享内存区,另⼀次从共享内存区到输出⽂件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建⽴共享内存区域。⽽是保持共享区域,直到通信完毕为⽌,这样,数据内容⼀直保存在共享内存中,并没有写回⽂件。共享内存中的内容往往是在解除映射时才写回⽂件的。因此,采⽤共享内存的通信⽅式效率是⾮常⾼的。
Linux的2.2.x 内核⽀持多种共享内存⽅式,如mmap()系统调⽤,Posix共享内存,以及系统V共享内存。linux发⾏版本如Redhat 8.0⽀持mmap()系统调⽤及系统V共享内存,但还没实现Posix共享内存,本⽂将主要介绍mmap()系统调⽤及系统V共享内存API的原理及应⽤。
⼀、内核怎样保证各个进程寻址到同⼀个共享内存区域的内存页⾯
1、 page cache及swap cache中页⾯的区分:⼀个被访问⽂件的物理页⾯都驻留在page cache或swap cache中,⼀个页⾯的所有信息由struct page来描述。struct page中有⼀个域为指针mapping ,它指向⼀个struct address_space类型结构。page cache或swap cache中的所有页⾯就是根据address_space结构以及⼀个偏移量来区分的。
2、⽂件与 address_space结构的对应:⼀个具体的⽂件在打开后,内核会在内存中为之建⽴⼀个struct inode结构,其中的i_mapping域指向⼀个address_space结构。这样,⼀个⽂件就对应⼀个address_space结构,⼀个 address_space与⼀个偏移量能够确定⼀个page cache 或swap cache中的⼀个页⾯。因此,当要寻址某个数据时,很容易根据给定的⽂件及数据在⽂件内的偏移量⽽到相应的页⾯。
3、进程调⽤mmap()时,只是在进程空间内新增了⼀块相应⼤⼩的缓冲区,并设置了相应的访问标识,但并没有建⽴进程空间到物理页⾯的映射。因此,第⼀次访问该空间时,会引发⼀个缺页异常。
4、对于共享内存映射情况,缺页异常处理程序⾸先在swap cache中寻⽬标页(符合address_space以及偏移量的物理页),如果到,则直接返回地址;如果没有到,则判断该页是否在交换区 (swap area),如果在,则执⾏⼀个换⼊操作;如果上述两种情况都不满⾜,处理程序将分配新的物理页⾯,并把它插⼊到page cache中。进程最终将更新进程页表。
注:对于映射普通⽂件情况(⾮共享映射),缺页异常处理程序⾸先会在page cache中根据address_space以及数据偏移量寻相应的页⾯。如果没有到,则说明⽂件数据还没有读⼊内存,处理程序会从磁盘读⼊相应的页⾯,并返回相应地址,同时,进程页表也会更新。
5、所有进程在映射同⼀个共享内存区域时,情况都⼀样,在建⽴线性地址与物理地址之间的映射之后,不论进程各⾃的返回地址如何,实际访问的必然是同⼀个共享内存区域对应的物理页⾯。
注:⼀个共享内存区域可以看作是特殊⽂件系统shm中的⼀个⽂件,shm的安装点在交换区上。
上⾯涉及到了⼀些数据结构,围绕数据结构理解问题会容易⼀些。
回页⾸
⼆、mmap()及其相关系统调⽤
mmap()系统调⽤使得进程之间通过映射同⼀个普通⽂件实现共享内存。普通⽂件被映射到进程地址空间后,进程可以向访问普通内存⼀样对⽂件进⾏访问,不必再调⽤read(),write()等操作。
注:实际上,mmap()系统调⽤并不是完全为了⽤于共享内存⽽设计的。它本⾝提供了不同于⼀般对普通⽂件的访问⽅式,进程可以像读写内存⼀样对普通⽂件的操作。⽽Posix或系统V的共享内存IPC则纯粹⽤于共享⽬的,当然mmap()实现共享内存也是其主要应⽤之⼀。
1、mmap()系统调⽤形式如下:
void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )
参数fd为即将映射到进程空间的⽂件描述字,⼀般由open()返回,同时,fd可以指定为-1,此时须指定flags参数中的MAP_ANON,表明进⾏的是匿名映射(不涉及具体的⽂件名,避免了⽂件的创建及打开,很显然只能⽤于具有亲缘关系的进程间通信)。len是映射到调⽤进程地址空间的字节数,它从被映射⽂件开头offset个字节开始算起。prot 参数指定共享内存的访问权限。可取如下⼏个值的或:
PROT_READ(可读) , PROT_WRITE (可写), PROT_EXEC (可执⾏), PROT_NONE(不可访问)。flags由以下⼏个常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必选其⼀,⽽MAP_FIXED则不推荐使⽤。offset参数⼀般设为0,表⽰从⽂件头开始映射。参数addr指定⽂件应被映射到进程空间的起始地址,⼀般被指定⼀个空指针,此时选择起始地址的任务留给内核来完成。函数的返回值为最后⽂件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址。这⾥不再详细介绍mmap()的参数,读者可参考mmap()⼿册页获得进⼀步的信息。
2、系统调⽤mmap()⽤于共享内存的两种⽅式:
(1)使⽤普通⽂件提供的内存映射:适⽤于任何进程之间;此时,需要打开或创建⼀个⽂件,然后再调⽤mmap();典型调⽤代码如下:    fd=open(name, flag, mode);
if(fd<0)
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信⽅式有许多特点和要注意的地⽅,我们将在范例中进⾏具体说明。
(2)使⽤特殊⽂件提供匿名内存映射:适⽤于具有亲缘关系的进程之间;由于⽗⼦进程特殊的亲缘关系,在⽗进程中先调⽤mmap(),然后调⽤fork()。那么在调⽤fork()之后,⼦进程继承⽗进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,⽗⼦进程就可以通过映射区域进⾏通信了。注意,这⾥不是⼀般的继承关系。⼀般来说,⼦进程单独维护从⽗进程继承下来的⼀些变量。⽽mmap()返回的地址,却由⽗⼦进程共同维护。
对于具有亲缘关系的进程实现共享内存最好的⽅式应该是采⽤匿名内存映射的⽅式。此时,不必指定具体的⽂件,只要设置相应的标志即可,参见范例2。
3、系统调⽤munmap()
int munmap( void * addr, size_t len )
该调⽤在进程地址空间中解除⼀个映射关系,addr是调⽤mmap()时返回的地址,len是映射区的⼤⼩。当映射关系解除后,对原来映射地址的访问将导致段错误发⽣。
4、系统调⽤msync()
int msync ( void * addr , size_t len, int flags)
⼀般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘⽂件中,往往在调⽤munmap()后才执⾏该操作。可以通过调⽤msync()实现磁盘上⽂件内容与共享内存区的内容⼀致。
回页⾸
三、mmap()范例
下⾯将给出使⽤mmap()的两个范例:范例1给出两个进程通过映射普通⽂件实现共享内存通信;范例2给出⽗⼦进程通过匿名映射实现共享内存。系统调⽤ mmap()有许多有趣的地⽅,下⾯是通过mmap()映射普通⽂件实现进程间的通信的范例,我们通过该范例来说明mmap()实现共享内存的特点及注意事项。
范例1:两个进程通过映射普通⽂件实现共享内存通信
范例1包含两个⼦程序:map_normalfile1.c及map_normalfile2.c。编译两个程序,可执⾏⽂件分别为 map_normalfile1及
map_normalfile2。两个程序通过命令⾏参数指定同⼀个⽂件来实现共享内存⽅式的进程间通信。 map_normalfile2试图打开命令⾏参数指定的⼀个普通⽂件,把该⽂件映射到进程的地址空间,并对映射后的地址空间进⾏写操作。 map_normalfile1把命令⾏参数指定的⽂件映射到进程地址空间,然后对映射后的地址空间执⾏读操作。这样,两个进程通过命令⾏参数指定同⼀个⽂件来实现共享内存⽅式的进程间通信。
下⾯是两个程序代码:
/-------------map_normalfile1.c-----------/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
char name[4];
mmap格式怎么打开int age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
int fd,i;
people *p_map;
char temp;
fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
lseek(fd,sizeof(people)*5-1,SEEK_SET);
write(fd,"",1);
p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0 );
close( fd );
temp = 'a';
for(i=0; i<10; i++)
{
temp += 1;
memcpy( ( *(p_map+i) ).name, &temp,2 );
( *(p_map+i) ).age = 20+i;
}
printf(" initialize over /n ");
sleep(10);
munmap( p_map, sizeof(people)*10 );
printf( "umap ok /n" );
}
/-------------map_normalfile2.c-----------/
#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
typedef struct{
char name[4];
int age;
}people;
main(int argc, char** argv) // map a normal file as shared mem:
{
int fd,i;
people p_map;
fd=open( argv[1],O_CREAT|O_RDWR,00777 );
p_map = (people)mmap(NULL,sizeof(people)10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
for(i = 0;i<10;i++)
{
printf( “name: %s age %d;/n”,((p_map+i)).name, (*(p_map+i)).age );
}
munmap( p_map,sizeof(people)*10 );
}
map_normalfile1.c ⾸先定义了⼀个people数据结构,(在这⾥采⽤数据结构的⽅式是因为,共享内存区的数据往往是有固定格式的,这由通信的各个进程决定,采⽤结构的⽅式有普遍代表性)。map_normfile1⾸先打开或创建⼀个⽂件,并把⽂件的长度设置为5个people 结构⼤⼩。然后从mmap()的返回地址开始,设置了10个people结构。然后,进程睡眠10秒钟,等待其他进程映射同⼀个⽂件,最后解除映射。
map_normfile2.c只是简单的映射⼀个⽂件,并以people数据结构的格式从mmap()返回的地址处读取10个people结构,并输出读取的值,然后解除映射。
分别把两个程序编译成可执⾏⽂件map_normalfile1和map_normalfile2后,在⼀个终端上先运⾏./map_normalfile2 /tmp/test_shm,程序输出结果如下:
initialize over
umap ok

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。