日志标签 ‘mmap’

与虚拟内存区域有关的操作(2)-合并内存区域

2011年12月16日

合并内存区域

Linux的内存管理模块以虚拟内存区域为单位管理进程的虚拟内存,一个进程的所有内存区域分别以链表和树形结构进行组织。当一个新建的区域加入进程时,内核会试图将这个新区域与已存在的区域进行合并。区域合并的首要条件就是检查新区域之前的prve区域终止地址是否与新区域起始地址重合,或新区域的结束地址是否与其之后的next区域起始地址重合;接着再检查将要合并的区域是否有相同的标志。如果合并区域均映射了磁盘文件,则还要检查其映射文件是否相同,以及文件内的偏移量是否连续。

上述提及的一系列条件检查是在vma_merge()中完成的,根据新区域和它前后两个内存区域之间的位置关系,区域合并分为8种情况。由于合并条件的复杂性,该函数包含好几个参数,分别对上述说明的合并条件进行检测。

mm描述要添加新区域进程的内存空间,prev指向当前区域之前的一个内存区域,addr表示新区域的起始地址,end为新区域的结束地址,vm_flags表示该区域的标志。如果该新区域映射了一个磁盘文件,则file结构表示该文件,pgoff表示该文件映射的偏移量。

struct vm_area_struct *vma_merge(struct mm_struct *mm,
                        struct vm_area_struct *prev, unsigned long addr,
                        unsigned long end, unsigned long vm_flags,
                        struct anon_vma *anon_vma, struct file *file,
                        pgoff_t pgoff, struct mempolicy *policy)

合并函数首先通过起始地址和终止地址计算新区域的长度,接下来判断新区域是否设置了VM_SPECIAL,这个标志指定了该区域不能和其他区域合并,因此立即返回NULL。

        pgoff_t pglen = (end - addr) >> PAGE_SHIFT;
        struct vm_area_struct *area, *next;
        int err;

        if (vm_flags & VM_SPECIAL)
                return NULL;

        if (prev)
                next = prev->vm_next;
        else
                next = mm->mmap;
        area = next;
        if (next && next->vm_end == end)                /* cases 6, 7, 8 */
                next = next->vm_next;

接着,通过prev获得下一个内存区域的描述符next,如图1。如果新区域的终止地址与next区域的终止地址重合,则next再向前移动一个区域,如图2。为了便于说明,在图2所示的情况下,next‘表示紧邻prev的那个区域,而next表示紧邻next‘的区域。图中每个不同的内存区域都使用不同的颜色标示。

接下来开始真正的合并工作,合并分为两大类,第一大类为新区域的起始地址和prev区域的终止地址重合,第二种情况为新区域的终止地址和next区域的起始地址重合。

我们首先分析第一种情况,即便addr和prev的终止地址重合还不足以将其合并,还应通过can_vma_merge_after()判断两者的标志和映射文件等是否相同。如果都满足条件,那么合并此时就应该可以开始了。不过合并函数力求每次合并程度达到最大化,它再继续检查end是否恰好与next区域的起始地址重合。

在这样的判断条件下,会出现5种不同的合并情况(分别为case1,6,2,5,7)。每种合并情况最终都会调用vma_adjust(),它通过修改vma结构中的字段对区域进行适当调整,也就是说真正的合并是在这个函数中完成的。可以看出vma_merge()本质上是一个“分流”函数,它将区域合并细化,根据不同的合并情况向vma_adjust()传递不同的参数。

        if (prev && prev->vm_end == addr &&
                        mpol_equal(vma_policy(prev), policy) &&
                        can_vma_merge_after(prev, vm_flags,
                                                anon_vma, file, pgoff)) {
                if (next && end == next->vm_start &&
                                mpol_equal(policy, vma_policy(next)) &&
                                can_vma_merge_before(next, vm_flags,
                                        anon_vma, file, pgoff+pglen) &&
                                is_mergeable_anon_vma(prev->anon_vma,
                                                      next->anon_vma)) {
                                                        /* cases 1, 6 */
                        err = vma_adjust(prev, prev->vm_start,
                                next->vm_end, prev->vm_pgoff, NULL);
                } else                                  /* cases 2, 5, 7 */
                        err = vma_adjust(prev, prev->vm_start,
                                end, prev->vm_pgoff, NULL);
                if (err)
                        return NULL;
                return prev;
        }

从上面的分析以及源码可以看出,case1和case6既满足addr与prev终止地址重合,又满足end与next起始地址重合,但是他们的next(如图1,2)却指向不同的区域。case1可参考下图,它实际上是填充了prev和next之间的“空洞”,也就是说三个区域合为一个区域,vma_adjust()会删除next区域同时扩大prev区域。

case6也可以看作是填充prev和next区域之间的空洞,不过它会将next‘区域进行“覆盖”。通过代码可以发现,next‘和next区域事实上是连续的,不过由于其他原因,比如标志不同,造成它们是两个不同的区域。尽管地址连续,但是组织的时候仍然通过链表链接。这里为了定量的表示区域之间的关系,省去了next‘和next之间的链接箭头。

如果end与next的起始地址不重合,那么会出现case2,case5,case7三种情况。这三种情况的差异会在vma_adjust()中被进一步区分,而在当前函数中,它们被看作是一种情况,即addr与prev终止地址重合而end与next起始地址不重合。

如果end小于next区域的起始地址,则为case2。

 

如果end大于next区域的起始地址,则为case5。在这种情况下,next会被一分为二,一部分加入prev,而另一部分则继续保留在原始区域中。

case7也是一种扩大prev的情况,它会将next‘覆盖,而next则保持不变。

当上述情况都不符合时,进入第二大类合并,即end与next的起始地址重合。在这种情形下涉及三种合并模型,它们的图示分别如下所示。

        if (next && end == next->vm_start &&
                        mpol_equal(policy, vma_policy(next)) &&
                        can_vma_merge_before(next, vm_flags,
                                        anon_vma, file, pgoff+pglen)) {
                if (prev && addr < prev->vm_end)        /* case 4 */
                        err = vma_adjust(prev, prev->vm_start,
                                addr, prev->vm_pgoff, NULL);
                else                                    /* cases 3, 8 */
                        err = vma_adjust(area, addr, next->vm_end,
                                next->vm_pgoff - pglen, NULL);
                if (err)
                        return NULL;
                return area;
        }

        return NULL;

如果addr大于prev的终止地址,则属于case4。这种情况下缩小prev,扩充next。

如果addr小于prev区域的终止地址,则属于case3。这种情况下prev不做改变,扩充next。

case8与case3比较类似,不过它会覆盖已存在的next‘。

从源码的条件判断语句可以看出,上述8种合并情况可以规划为四类,通过向vma_adjust()传递不同的参数可以为每种情况调节内存区域。

存储映射I/O

2011年10月23日

一个进程拥有独立并且连续虚拟地址空间,在32位体系结构中进程的地址空间是4G。不过,内核在管理进程的地址空间时是以内存区域为单位。内存区域是进程整个地址空间中一个独立的内存范围,它在内核中使用vm_area_struct数据结构来描述。每个内存区域都有自己访问权限以及操作函数,因此进程只能对有效范围的内存地址进行访问。

存储映射I/O是一种基于内存区域的高级I/O操作,它将磁盘文件与进程地址空间中的一个内存区域相映射。当从这段内存中读数据时,就相当于读磁盘文件中的数据,将数据写入这段内存时,则相当于将数据直接写入磁盘文件。这样就可以在不使用基本I/O操作函数read和write的情况下执行I/O操作。

1.基本实现方法

实现存储映射I/O的核心操作是通过mmap系统调用将一个给定的磁盘文件映射到一个存储区域中。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

关于该函数定义中各个参数说明Linux上的man手册已经解释的很清楚,在此不再赘述。这里需要特别说明的是prot和flags参数。prot用来指定对映射区域的保护要求,但是它的保护范围不能超过文件open时指定的打开权限。比如以只读(PROT_READ)方式打开一个文件,那么以读写(PROT_READ|PROT_WRITE)方式保护内存区域是不合法的。flags用来指定内存区域的多种属性,两个典型的取值是MAP_SHARED和MAP_PRIVATE。MAP_SHARED标志指定了进程对内存区域的修改会影响到映射文件。而当对flags指定MAP_PRIVATE时,进程会为该映射内存区域创建一个私有副本,对该内存区的所有操作都是在这个副本上进行的,此时对内存区域的修改并不会影响到映射文件。

下面列出一个简单的示例程序,它将磁盘文件映射到一个内存区域中,通过mmap返回的指针先读文件,再写文件。可以看到对文件的读和写操作都是通过内存映射I/O的方式完成的。

int main()
{
	int fd;
	char *buf = NULL;
	int i;

	//打开一个文件
	if (-1 == (fd = open("./mapping_file.txt", O_RDWR))) {
		printf("open file error!\n");
		exit(1);
	}

	//将文件映射到进程的一个内存区域
	buf = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (!buf) {
		printf("mmap error!\n");
		exit(1);
	}

	//对映射内存读数据
	for (i = 0; i < 100; i++)
	printf("%c", buf[i]);

	//对映射内存写数据
	if (buf[0] == 'H')
		buf[0] = 'h';
	else
		buf[0] = 'H';

	system("cat ./mapping_file.txt");
	return 0;
}

2.使用内存映射I/O进行文件拷贝

使用基本I/O操作函数如何实现一个类似cp命令的程序?比如我们要将A文件复制到B文件,那么程序的基本框架是这样的:

1.open()文件A和文件B

2.将A文件的内容read()到buffer

3.将buffer中的数据write()到文件B

4.close()文件A和文件B

如果使用内存映射I/O来实现cp命令,那么它的基本框架是这样的:

1.open()文件A和文件B

2.mmap()文件A和文件B,其中src和dest分别为两个文件映射到内存的地址

3.将以src为起始的len长字节数据memcpy()到dest

4.close()文件A和文件B

示例程序如下:

int main()
{
	int srcfd, destfd;
	struct stat statbuf;
	char *src = NULL, *dest = NULL;

	//打开两个文件
	if (-1 == (srcfd = open("./src.txt", O_RDONLY))) {
		printf("open src file error!\n");
		exit(1);
	}

	if (-1 == (destfd = open("./dest.txt", O_RDWR | O_CREAT | O_TRUNC))) {
		printf("open dest file error!\n");
		exit(1);
	}

	//获取原始文件的长度
	if (-1 == fstat(srcfd, &statbuf)) {
		printf("fstat src file error!\n");
		exit(1);
	}

	//设置输出文件的大小
	if (-1 == lseek(destfd, statbuf.st_size - 1, SEEK_SET)) {
		printf("lseek error!\n");
		exit(1);
	}
	if (-1 == write(destfd, "", 1)) {
		printf("write error!\n");
		exit(1);
	}

	if ((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, srcfd, 0)) == MAP_FAILED) {
		printf("mmaping src file error!\n");
		exit(1);
	}

	if ((dest = mmap(0, statbuf.st_size + 2, PROT_READ | PROT_WRITE, MAP_SHARED, destfd, 0)) == MAP_FAILED) {
		printf("mmaping dest file error!\n");
		exit(1);
	}

	memcpy(dest, src, statbuf.st_size);

	printf("src file:\n");
	system("cat ./src.txt");
	printf("dest file:\n");
	system("cat ./dest.txt");

	close(srcfd);
	close(destfd);

	return 0;
}

按照上述列出的基本框架,该程序首先打开两个文件,通过fstat()获得源文件的长度。因为在mmap两个文件以及设置目的文件长度时都需要源文件的长度。设置目的文件通过lseek()即可完成,如果没有设置目的文件的长度,那么将会产生总线错误(引发信号SIGBUS)。然后分别mmap()两个文件到进程的地址空间,最后调用memcpy()将源文件内存区的数据拷贝到目的文件内存区。

通过基本I/O和内存映射I/O均可以进行文件拷贝,那么两者的效率谁更高一些?这其实是个很难回答的问题。不管是使用基本I/O操作函数还是mmap方式,操作系统都会在内存中进行缓存(cache),而且在不同的应用场景、不同的平台下结果都会收到影响。但是抛开这些因素单从对文件操作这个方面来说,内存映射方式比read和write方式要快。

如果使用read/write方式进行文件拷贝,首先将数据从用内核缓冲区复制到用户空间缓冲区,这是read的过程;再将数据从用户空间缓冲区复制到内核缓冲区,这是write过程。如果是内存映射方式,则直接是用户空间中数据的拷贝,也就是将源文件所映射内存中的数据拷贝到目的文件所映射的内存中。这样就避免了用户空间和内核空间之间数据的来回拷贝。

但是内存映射方式并不是完美的,它所映射的文件只能是固定大小,因为文件所映射的内存区域大小在mmap时通过len已经指定。另外,文件映射的内存区域的大小必须以页大小为单位。比如系统页大小为4096字节,假定映射文件的大小为20字节,那么该页剩余的4076字节全部被填充为0。虽然通过映射地址可以访问并修改剩余字节,但是任何变动都不会在映射文件中反应出来。由此可见,使用内存映射进行大数据量的拷贝比较有效。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key