Linux的mmap函数与动态库的加载
在文件的网络传输、共享内存、动态库的加载等系统底层的关键功能上有重要的应用。
在x64上,mmap的系统调用号是9,定义在如
mmap()函数是Linux的一个系统调用,用于实现文件到内存的映射。 在文件的网络传输、共享内存、动态库的加载等系统底层的关键功能上有重要的应用。 在x64上,mmap的系统调用号是9,定义在如下的头文件里: /usr/include/x86_64-linux-gnu/asm/unistd_64.h 可以看到,read()的调用号是0,write()的是1,open()的是2。 Linux的系统调用号,都是以__NR_开始的宏常量。 系统调用,是Linux内核与用户程序的唯一接口。 实际代码中,一般不会直接使用系统调用,而是使用glibc库对它的封装函数。 Linux的man手册,就是C库封装函数的说明书。其中关于mmap的部分如下: 它是参数最多的常用函数,6个参数。加上系统调用号,就是7个。 mmap()的最后一个参数offset,是文件的起始偏移量。从这个偏移量开始映射内存,它必须是一个内存页大小的整数倍(Linux的是4096字节)。 如果offset不按4096对齐,mmap()函数会失败,并设置错误码是EINVAL,即无效的参数。 Linux内核是通过分页来管理内存的,内存权限管理的最小单位是页。Linux并不能为小于1页的多少字节单独设置权限。 更细的管理,意味着索引数据要占据更多的内存,挤压有效数据的可用空间。 第1个参数void* addr,通常传NULL,让Linux内核自己去选要映射到哪个内存地址。 Linus肯定比咱更懂进程的内存布局,让他选(笑)。 第2个参数length,是要映射的文件长度,它与最后一个参数offset一起确定映射的文件区域。 对于比较小的文件,一般length设置为文件的总长度,offset设置为0,即映射整个文件。 第3个参数prot,是映射的内存权限,分为读、写、执行三种。大多数情况下,只使用读写权限,但是在加载动态库的时候,也会使用执行权限。 第4个参数flags,用于确定映射的内存在多个进程之间的共享权限。 如果要用于进程间通信,那么flags要设置为MAP_SHARED。 否则,flags设置为MAP_PRIVATE,即只有当前进程自己用。 第5个参数fd,是打开的文件描述符,在mmap()之前要先打开文件,并且获取文件的长度(fstat()函数)。 Linux系统上的内存可以粗略分为两类,物理内存和虚拟内存。 物理内存,是可以实际读写的内存。 虚拟内存,只是个内存范围的占位符。在用户程序实际使用(这块内存)之前,Linux系统并不为它分配物理内存。 所以,在mmap()调用的时候,Linux内核要选的只是进程的一块虚拟内存空间。 这块空间的起始地址要按4096字节对齐,这样在使用的时候正好在一个页的边界上,而不至于出现横跨两个页的情况。 同时,这块空间的大小要能放下需要映射的文件区域。因为mmap()之后就可以把文件当内存读写了,memcpy()函数只会连续拷贝linux动态库,所以这块区域必须是连续的。 选好这块虚拟内存空间之后,Linux只需要记录文件和这块内存的对应关系,并不需要把文件的数据读到内存里。 在用户实际要用的时候再读文件数据,这就是需求加载。 每个进程都有自己的虚拟内存空间,它们通过各自的页表实现虚拟内存与物理内存的对应关系。 如果一个物理内存页,同时对应到了多个进程的虚拟内存空间,就是共享内存。 两个进程打开同一个文件,使用MAP_SHARED方式映射相同的文件区域,就能以这块共享内存实现进程间通信了。Linux有一个专门的函数shm_open(),打开共享内存的文件描述符,专门用于这种情况。 如果两个进程共享的内存特别多,除了栈之外都共享,那么它们就是线程。 线程是可以并发运行的,它们有各自的栈,所以函数的局部变量并不会受到多线程的影响。 但malloc()分配的堆内存,以及全局变量、静态变量,必须使用锁保护,它们在线程之间是共享的。 如果是动态库,它也可以在多个进程之间共享。只要把动态库所在的物理内存页,同时映射到多个进程的虚拟空间就行。 与普通文件的区别是,动态库的代码段在用mmap()时要添加执行权限,PROT_EXEC。 如图,这个例子用的是.o文件,在mmap()时设置了读和执行权限,然后把代码段所在的地址当作一个函数指针来调用。 从readelf -a的信息可以看出,代码段.text的偏移量是0x40,即函数add()从这个elf文件的0x40字节开始。 函数的内容很简单,就是把两个整数相加,并返回。 两个输入参数在rdi和rsi,输出在rax。 运行的结果如上图。 f(1, 2)的返回值是3,打印出来的二进制码也与objdump的输出一致。 如果mmap()没有选择执行权限,在把add当作函数调用时,Linux就会报段错误,导致进程core dump。 动态库也是使用mmap()加载的,与这个加载.o文件的例子类似。 只不过动态库的内容更复杂,要通过分析它的elf文件来确定它包含哪些函数,依赖哪些(其他)动态库。 如果它还依赖其他动态库,那么在加载时还要做动态连接。 (编辑:晋中站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |