系统:centos
语言:c++
背景:搜索引擎的索引数据很大,分布式之后,每个索引节点的索引数据还是比较大,占了大概100G左右,全放在内存中是不现实的,而且因为我们的搜索引擎不稳定,单个节点容易出现崩溃的问题,所以索引数据需要落盘到磁盘上,这样虽然会使单个搜索节点更加稳定,但是磁盘比内存的缺点就更加显露无疑了,那就是存取数据太慢,所以我们就想使用mmap的预读策略和文件的磁盘缓存来解决缺页错误导致从磁盘存取速度过慢的问题。
我们使用mmap的方式打开磁盘文件,mmap会返回一个指针,对指针的某个位置的修改就会自动通过flush线程将修改同步到磁盘文件上,不过有个小点需要注意,那就是不是修改后立刻同步,这样会造成读写磁盘频率过高,而是mmap内部有自己的算法写够多少个byte才会将内存缓存区的内容回写到磁盘上。
mmap的原型void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);大家自己通过man mmap自己体会,介绍几个注意的点。
1. 如果addr不为空,意味着自己分配好了空间带进去,如果为空,意味着让mmap自动分配内存空间。
2. flags那个参数一般有两个选项,
1.MAP_SHARED:共享的,修改文件会影响其他程序,当然其他程序也会影响你。
2.MAP_PRIVATE,随便改,没关系,这段空间就是你的了。
3. offset 必须是个4096能整除的数。
4. fd带入-1表示匿名映射,具体细节因为我没有设计所以我并不关心。
那么在说说文件缓存,有个很明显的区别,如果我们打开一个很久没有打开的文件,然后做mmap映射从头到尾去将每一个byte加起来,这种做法叫‘扫’。我们第一次扫一个大文件会花费很长的时间,第二次扫就会花费短一些的时间,这里面的差别就是文件缓存造成的,那么什么时候会缓存文件呢。分两种,一、刚刚创建好的文件会在文件缓存中,二、刚刚操作过的文件会在文件缓存中。
文件缓存的基本单位是页,一页通常是4096字节也就是4kb大小,使用sysconf(_SC_PAGE_SIZE)方法可以查看本机页大小。
每次文件缓存都是以页为单位的,举例说明,如果我第一次访问了很久的文件的第50页,那么系统会将35-55页的30页缓存起来,那么拿第50页会发生一次缺页中断然后缓存30页,如果我再访问第51页,那么因为有文件缓存所以就不会再去访问磁盘,直接从缓存中拿出第51页,所以就不会发生缺页中断。从而提高了程序的效率。
那么mmap返回的指针跟页有什么关系呢?简单的说,比方mmap返回的指针叫做ptr,那么ptr[0-4095]就是第一页,ptr[4096-8191]就是第二页。
再介绍几个工具和函数。
1.free -m shell命令行里输出即可。-m是以MB为单位输出。具体含义网上很多。我们主要关心最后一列的cache,这一列表示文件缓存的大小。
2.dd if=/dev/zero of=bean count=1024000 bs=1024也是shell命令行,作用是生成一个大文件,这个工具好像被用来制作开机启动脚本,反正很厉害的样子。我们这里大致介绍一下,if是输入文件目录,/dev/zero是一个有无限个0的文件,of是输出文件,count是多少块,bs是块大小。用这个命令就能生成一个1GB的名为bean的文件,文件内容为全0。
3./proc/pid/smaps,这是一个文件,其中pid是运行进程的进程号,smaps里面表示着所有内存映射的信息。我们不用关系一些.so库,我们只关心我们打开的文件。细节含义网上到处都是,我只说我们用到的,Size表示文件总大小,Rss表示加载到内存多大,即可。
4.mincore,这是一个系统调用,直白点就是c++函数,我在这里放一些我知道的,想了解细节请大家自己man mincore。原型是这样的。int mincore(void *addr, size_t length, unsigned char *vec);第一个参数是mmap返回出来的指针,第二个参数是文件长度,第三个参数是一个数组,数组类型是uint_8的,这个数组是返回值,mincore会统计addr开始 length长度的mmap文件所组成的页,这些页是在内存中还是磁盘上。内存中该页就为1,磁盘上该页就为0.
5. getrusage,也是一个系统调用,能拿到很多有用的系统信息,大家也去百度吧,我们只介绍我们需要的。函数原型int getrusage(int who, struct rusage *usage); 第一个参数是进程id,我们这里带入RUSAGE_SELF,就是本进程的意思。usage是个结构体,里面是带出来的所有信息,我们用到的是m_rusage.ru_majflt值,majflt以为着缺页中断的次数。如果who进程发生缺页中断,那么majflt的值就+1;
6. posix_fadvise,也是一个系统调用,他的含义是告诉磁盘缓存我不需要这个文件了,请把内存释放出来给其他应用使用,函数原型int posix_fadvise(int fd, off_t offset, off_t len, int advice);需要详细信息的自己man一下,其中advice有两个标志位POSIX_FADV_WILLNEED意味着告诉磁盘缓存我以后要用这个文件请不要换出,POSIX_FADV_DONTNEED告诉磁盘缓存这个文件我不需要了,请帮我清理出去。
7. top的shr内存占用,表明了所有使用mmap打开的文件所占用的内存,包括堆,栈,大的new,so,mmap打开的文件等等,其中需要注意的是因为栈只有一个栈针(栈顶指针)所以栈非栈顶释放其实是不会释放的。
其中,free计算的内存和mincore计算的内存一致,top和smaps计算的内存一致。free计算的内存包含预加载进来的内存。但是smaps里面只包含访问的那一页的内存,即smaps访问一页Rss就加4k,但是mincore访问一页需要在内存中加载很多页进来。但是预加载的这些页是不算在进程内存中的,所以top和smaps进程内存不会显示预加载的页面,因为top和smaps是进程级别的,而free和mincore计算的是系统级别的。
下面放入一段程序,该程序是自己写的一个小例子,很low,不过能反应问题,这个程序可以达到访问某页,遍历所有页,清空文件的文件缓存这4个功能。
#include <unistd.h>
#include <sys/mman.h>
#include <iostream>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <stdlib.h>
using namespace std;
int clear_file_cache(const char *filename)
{
struct stat st;
if(stat(filename , &st) < 0) {
fprintf(stderr , "stat localfile failed, path:%s\n",filename);
return -1;
}
int fd = open(filename, O_RDONLY);
if( fd < 0 ) {
fprintf(stderr , "open localfile failed, path:%s\n",filename);
return -1;
}
//clear cache by posix_fadvise
if( posix_fadvise(fd,0,st.st_size,POSIX_FADV_DONTNEED) != 0) {
printf("Cache FADV_DONTNEED failed\n");
}
else {
printf("Cache FADV_DONTNEED done\n");
}
return 0;
}
void GetPageInfo(char *pDat, size_t len, uint32_t PAGE_COUNT, unsigned char *pVec)
{
if (-1 == mincore(pDat, len, pVec)) {
return ;
}
uint32_t hitCnt = 0;
uint32_t i = 0;
for (; i < PAGE_COUNT; ++i) {
if (pVec[i] == 1)
{
hitCnt += 1;
}
}
struct rusage m_rusage;
getrusage(RUSAGE_SELF, &m_rusage);
cout << hitCnt << ", " << PAGE_COUNT << ", " << m_rusage.ru_majflt << endl;
}
int main(int argc, char *argv[])
{
if (argc == 3 && strcmp(argv[2], "clear") == 0)
{
clear_file_cache(argv[1]);
return -1;
}
int fd = open(argv[1], O_RDWR);
if (fd == -1)
{
cout << "open err ..." << endl;
return -1;
}
struct stat stat;
off_t size = lseek(fd, 0, SEEK_END);
char *pDat = (char *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
uint32_t PAGE_SIZE = sysconf(_SC_PAGESIZE);
uint32_t PAGE_COUNT = (size + PAGE_SIZE - 1) / PAGE_SIZE;
unsigned char *pVec = new unsigned char[PAGE_COUNT];
if (argc == 3 && strcmp(argv[2], "all") == 0)
{
int sum = 0;
for (uint32_t i = 0; i < size; ++i)
{
sum += *(pDat + i);
}
cout << sum << endl;
}
else if (argc == 3)
{
int page = atoi(argv[2]) * 4096;
if (page < size)
*(pDat + page) = 1;
}
GetPageInfo(pDat, size, PAGE_COUNT, pVec);
delete[] pVec;
sleep(1000);
return 0;
}
编译方式就是很简单的g++ main.cpp -o main,第一个参数带入文件名,第二个参数带入all表示遍历,clear表示清空,数字表示需要访问哪一个,输出为0,25600,0的格式,第一个0表明加载了多少页在内存中,25600表示总共有多少页,第三个0表示发生了多少次缺页中断。