虚拟内存操作
虚拟内存操作
- 一、改变内存保护:mprotect()
- 二、内存锁:mlock()和mlockatt()
- 三、给一个进程占据的所有内存加锁和解锁
- 四、确定内存驻留性:mincore()
- 五、建议后续的内存使用模式:madvise()
一、改变内存保护:mprotect()
此函数用于修改起始位置为addr
长度为length
字节的虚拟内存区域中分页上的保护。
#include<sys/mman.h>
int mprotect(void *addr,size_t length,int port);
//成功返回0,错误-1
- addr值必须是系统分页大小(
sysconf(_SC_PAGESIZE)
的返回值)的整数倍,且是分页对齐的。 - prot位掩码,指定内存区域上的新保护,取值是
PROT_NONE或PROT_READ、PROT_ERITE以及PROT_EXEC
这三个值中的一个或多个取OR。 - 进程访问内存区域时违背了内存保护,内核向进程发送
SIGSEGV
信号。
用途:修改原先通过mmap函数设置的映射内存区域上的保护。
例:使用mprotct修改内存保护。
#include<sys/mman.h>
#define LEN (1024 * 1024)
#define SHELL_FMT "cat /proc/%ld/maps | grep zero"
#define CMD_SIZE (sizeof(SHELL_FMT) + 20)
int main(int argc, char *argv[])
{
char cmd[CMD_SIZE];
char *addr;
/* 创建拒绝所有访问的匿名映射 */
addr = mmap(NULL, LEN, PROT_NONE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");
/* 显示与映射对应的/proc/self/maps中的行 */
printf("Before mprotect()\n");
snprintf(cmd, CMD_SIZE, SHELL_FMT, (long) getpid());
system(cmd);
/*更改内存保护以允许读写访问*/
if (mprotect(addr, LEN, PROT_READ | PROT_WRITE) == -1)
errExit("mprotect");
printf("After mprotect()\n");
system(cmd); /* 通过/proc/self/map查看保护*/
exit(EXIT_SUCCESS);
}
二、内存锁:mlock()和mlockatt()
在应用程序中将进程的虚拟内存的部分或全部锁进内存确保永远不会因为分页故障而发生延迟,且确保安全性。
只有特权进程(CAP_IPC_LOCK
)才能给内存加锁,RLIMIT_MEMLOCK
软资源限制为一个特权进程能够锁住的字节数设定一个上限。以后允许非特权进程给一小段内存进行加锁,确保一小部分敏感信息不会被写入到磁盘上的交换空间定位应用程序。
软和硬 RLIMIT_MEMLOCK
限制的默认值都是 8 个分页(即在 x86-32 上是 32768 字节)。
RLIMIT_MEMLOCK
限制影响:
-
mlock()和mlockall()
。 -
mmap() MAP_LOCKED
标记,该标记用来在映射被创建时将内存映射锁进内存。 -
shmctl() SHM_LOCK
操作,该操作用来给 System V 共享内存段加锁。
由于虚拟内存的管理单位是分页,因此内存加锁会应用于整个分页。在执行权限检测时,RLIMIT_MEMLOCK
限制会被向下舍入到最近的系统分页大小的整数倍。
尽管这个资源限制只有一个值,但实际上定义了两个单独的限制:
- 对于
mlock()、mlockall()以及mmap() MAP_LOCKED
操作,RLIMIT_MEMLOCK
定义了一个进程级别的限制,限制字节数。 - 对于
shmctl()SHM_LCOK
操作,RLIMIT_MEMLOCK
定义了一个用户级别的限制,限制真实用户ID在共享内存段中能够锁住的字节数。
#include <sys/mman.h>
int mlock(const void *addr, size_t len);//内存区域加锁
//锁住虚拟地址空间中起始地址为addr(无需分页大小的整数倍)长度为length字节的区域中的所有分页。
int munlock(const void *addr, size_t len);//内存区域解锁
//成功返回0,错误-1
由于加锁操作的单位是分页,因此被锁住的区域的结束位置为大于 length 加 addr 的下一个分页边界。例如,在一个分页大小为 4096 字节的系统上,mlock(2000, 4000)
调用会将 0 到8191 之间的字节锁住
mlock函数调用成功后就能确保指定区域中的分页会被锁住并驻留在物理内存中,当没有足够的物理内存锁住所有请求的分页或违背RLIMIT_MEMLOCK
软资源限制mlock函数调用将失败。
munlock()即删除之前由调用进程创建的内存锁。给一组分页解锁并不能确保它们就不会驻留在内存中了:只有在其他进程请求内存的时候才会从RAM 中删除分页。
除了显式地使用 munlock()之外,内存锁在下列情况下会被自动删除:
- 在进程终止时。
- 当被锁住的分页通过
munmap()
被解除映射时。 - 当被锁住的分页被使用
mmap() MAP_FIXED
标记的映射覆盖时。
存锁不会被通过 fork()创建的子进程继承,也不会在 exec()执行期间被保留。
-
当多个进程共享一组分页时(如
MAP_SHARED
映射),只要还存在一个进程持有着这些分页上的内存锁,那么这些分页就会保持被锁进内存的状态。-
内存锁不在单个进程上叠加。如果一个进程重复地在一个特定虚拟地址区域上调用mlock(),那么只会建立一个锁,并且只需要通过一个 munlock()调用就能够删除这个锁。
- 若使用 mmap()将同一组分页(即同样的文件)映射到单个进程中的几个不同的位置,然后分别给所有这些映射加锁,那么这些分页会保持被锁进 RAM 的状态直到所有的映射都被解锁为止。
-
shmctl() SHM_LOCK
操作的语义与 mlock()和 mlockall()
的语义是不同的:
-
SHM_LOCK
操作之后,分页只有在因后续引用而发生故障时才会被锁进内存。与之相反的是,mlock()和 mlockall()
调用在返回之前会将所有分页锁进内存。 -
SHM_LOCK
操作会设置共享内存段的一个属性,而不是进程的属性,意味着分页一旦因故障被锁进了内存,那么即使所有进程都与这个共享内存段分离了,分页还是会保持驻留在内存中的状态。与之相反的是,使用 mlock()(或 mlockall())锁进内存的区域只有在还存在进程持有该区域上的锁时才会保持被锁进内存的状态。
三、给一个进程占据的所有内存加锁和解锁
进程可以使用下列两个函数给占据的所有内存加锁和解锁。
#include<sys/mman.h>
int mlockall(int flags);
int munlockall(void);
//成功返回0,出错返回-1
mlockall调用根据flags的取值将一个进程的虚拟地址空间中当前所有映射的分页或将来所有映射的分页或两者锁进内存,其中flags参数的取值为下列常量一个或多个其OR:
-
MCL_CURRENT
:将调用进程的虚拟地址空间中当前映射的分页锁进内存,包括当前为程序文本段、数据段、内存映射以及栈分配的所有分页 -
MCL_FUTURE
:将后继映射进调用进程的虚拟地址空间的所有分页锁进内存。指定MCL_FUTURE
标记的结果是后续的内存分配操作(如mmap()、sbrk()或 malloc()
)可能会失败,或者栈增长可能会产生SIGSEGV 信
号,当然前提是系统已经没有 RAM 分配给进程或者已经达到了RLIMIT_MEMLOCK
软资源限制
通过 mlock()
创建的内存锁上有关约束、生命周期以及继承性方面的规则同样也适用于通过 mlockall()
创建的内存锁。
munlockall()
系统调用将调用进程的所有分页解锁并撤销之前的 mlockall(MCL_ FUTURE)
调用所产生的结果。与 munlock()
一样,这个调用无法保证会从 RAM
中删除被解锁的分页。
四、确定内存驻留性:mincore()
mincore()系统调用是内存加锁系统调用的补充,它报告在一个虚拟地址范围中哪些分页当前驻留在RAM中,因此在访问这些分页时也不会导致分页故障。
#include<sys/mman.h>
int mincore(void *addr,size_t length,unsigned char *vec);
//成功返回0出错返回-1
-
此函数调用返回起始地址为addr长度为length字节的虚拟地址范围中分页的内存驻留信息。
-
adr地址必须是分页对齐的,并且由于返回的信息是有关整个分页的,length分页大小的下一个整数倍。
-
内存驻留相关信息通过vec返回,vec是数组,大小为
(length+PAGE_SIZE-1)/PAGE_SIZE
字节。
例:使用mlock和mincore函数。
static void displayMincore(char *addr, size_t length)
{
unsigned char *vec;
long pageSize, numPages, j;
pageSize = sysconf(_SC_PAGESIZE);
numPages = (length + pageSize - 1) / pageSize;
vec = malloc(numPages);
if (vec == NULL)
errExit("malloc");
if (mincore(addr, length, vec) == -1)
errExit("mincore");
for (j = 0; j < numPages; j++) {
if (j % 64 == 0)
printf("%s%10p: ", (j == 0) ? "" : "\n", addr + (j * pageSize));
printf("%c", (vec[j] & 1) ? '*' : '.');
}
printf("\n");
free(vec);
}
int main(int argc, char *argv[])
{
char *addr;
size_t len, lockLen;
long pageSize, stepSize, j;
if (argc != 4 || strcmp(argv[1], "--help") == 0)
usageErr("%s num-pages lock-page-step lock-page-len\n", argv[0]);
pageSize = sysconf(_SC_PAGESIZE);
if (pageSize == -1)
errExit("sysconf(_SC_PAGESIZE)");
len = getInt(argv[1], GN_GT_0, "num-pages") * pageSize;
stepSize = getInt(argv[2], GN_GT_0, "lock-page-step") * pageSize;
lockLen = getInt(argv[3], GN_GT_0, "lock-page-len") * pageSize;
addr = mmap(NULL, len, PROT_READ, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
errExit("mmap");
printf("Allocated %ld (%#lx) bytes starting at %p\n",
(long) len, (unsigned long) len, addr);
printf("Before mlock:\n");
displayMincore(addr, len);
/* 将命令行参数指定的页面锁定到内存中 */
for (j = 0; j + lockLen <= len; j += stepSize)
if (mlock(addr + j, lockLen) == -1)
errExit("mlock");
printf("After mlock:\n");
displayMincore(addr, len);
exit(EXIT_SUCCESS);
}
五、建议后续的内存使用模式:madvise()
此函数调用通过通知内核调用进程对起始地址为addr长度为length字节的范围之内分页的可能的使用情况来提升应用程序的性能。内核可能会使用这种信息来提升在分页之下的文件映射上执行的I/O效率。
#include<sys/mman.h>
int madvise(void *addr,size_t length,int advice);
//成功返回0出错返回-1
addr
中值必须是分页对齐的,length
会被向上舍入到系统分页大小的下一个整数倍。advice
参数取值如下:
-
MADV_NORMAL
:这是默认行为。分页是以簇的形式(较小的一个系统分页大小的整数倍)传输的。这个值会导致一些预先读和事后读。 -
MADV_RANDOM
:这个区域中的分页会被随机访问,这样预先读将不会带来任何好处,因此内核在每次读取时所取出的数据量应该尽可能少。 -
MADV_SEQUENTIAL
:在这个范围中的分页只会被访问一次,并且是顺序访问,因此内核可以激进地预先读,并且分页在被访问之后就可以将其释放了。 -
MADV_WILLNEED
:预先读取这个区域中的分页以备将来的访问之需。MADV_WILLNEED
操作的效果与 Linux特有的readahead()
系统调用和posix_fadvise() POSIX_FADV_WILLNEED
操作的效果类似。 -
MADV_DONTNEED
:调用进程不再要求这个区域中的分页驻留在内存中。这个标记的精确效果在不同 UNIX 实现上是不同的。
SUSv3 使用了一个不同的名称来标准化了这个 API,即 posix_madvise()
,并且在相应的 advice
常量上加上了一个前缀字符串 POSIX_
。、常量值变成了POSIX_MADV_NORMAL、POSIX_MADV_RANDOM、POSIX_MADV_SEQUENTIAL、POSIX_MADV_WILLNEED
等等。