Linux内存管理实践-虚拟地址转换物理地址

1 11 月, 2011 by edsionte 9 comments »

Linux内核中采用了通用的四级分页模型,这种模型不仅适合32位系统也适合64位系统。分页单元是MMU(内存管理单元)中的一部分,它将线性地址转换为物理地址。本文通过一个内核模块程序模拟内核中虚拟地址转换为物理地址的过程,有关分页机制的原理可以参见这里的文章。

static void get_pgtable_macro(void)
{
	printk("PAGE_OFFSET = 0x%lx\n", PAGE_OFFSET);
	printk("PGDIR_SHIFT = %d\n", PGDIR_SHIFT);
	printk("PUD_SHIFT = %d\n", PUD_SHIFT);
	printk("PMD_SHIFT = %d\n", PMD_SHIFT);
	printk("PAGE_SHIFT = %d\n", PAGE_SHIFT);

	printk("PTRS_PER_PGD = %d\n", PTRS_PER_PGD);
	printk("PTRS_PER_PUD = %d\n", PTRS_PER_PUD);
	printk("PTRS_PER_PMD = %d\n", PTRS_PER_PMD);
	printk("PTRS_PER_PTE = %d\n", PTRS_PER_PTE);

	printk("PAGE_MASK = 0x%lx\n", PAGE_MASK);
}

static unsigned long vaddr2paddr(unsigned long vaddr)
{
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;
	unsigned long paddr = 0;
        unsigned long page_addr = 0;
	unsigned long page_offset = 0;

	pgd = pgd_offset(current->mm, vaddr);
	printk("pgd_val = 0x%lx\n", pgd_val(*pgd));
	printk("pgd_index = %lu\n", pgd_index(vaddr));
	if (pgd_none(*pgd)) {
		printk("not mapped in pgd\n");
		return -1;
	}

	pud = pud_offset(pgd, vaddr);
	printk("pud_val = 0x%lx\n", pud_val(*pud));
	if (pud_none(*pud)) {
		printk("not mapped in pud\n");
		return -1;
	}

	pmd = pmd_offset(pud, vaddr);
	printk("pmd_val = 0x%lx\n", pmd_val(*pmd));
	printk("pmd_index = %lu\n", pmd_index(vaddr));
	if (pmd_none(*pmd)) {
		printk("not mapped in pmd\n");
		return -1;
	}

	pte = pte_offset_kernel(pmd, vaddr);
	printk("pte_val = 0x%lx\n", pte_val(*pte));
	printk("pte_index = %lu\n", pte_index(vaddr));
	if (pte_none(*pte)) {
		printk("not mapped in pte\n");
		return -1;
	}

	//页框物理地址机制 | 偏移量
	page_addr = pte_val(*pte) & PAGE_MASK;
	page_offset = vaddr & ~PAGE_MASK;
	paddr = page_addr | page_offset;
	printk("page_addr = %lx, page_offset = %lx\n", page_addr, page_offset);
        printk("vaddr = %lx, paddr = %lx\n", vaddr, paddr);

	return paddr;
}

static int __init v2p_init(void)
{
	unsigned long vaddr = 0;

	printk("vaddr to paddr module is running..\n");
	get_pgtable_macro();
	printk("\n");

	vaddr = (unsigned long)vmalloc(1000 * sizeof(char));
	if (vaddr == 0) {
		printk("vmalloc failed..\n");
		return 0;
	}
	printk("vmalloc_vaddr=0x%lx\n", vaddr);
	vaddr2paddr(vaddr);

	printk("\n\n");
	vaddr = __get_free_page(GFP_KERNEL);
	if (vaddr == 0) {
		printk("__get_free_page failed..\n");
		return 0;
	}
	printk("get_page_vaddr=0x%lx\n", vaddr);
	vaddr2paddr(vaddr);

	return 0;
}

static void __exit v2p_exit(void)
{
	printk("vaddr to paddr module is leaving..\n");
        vfree((void *)vaddr);
        free_page(vaddr);
}

整个程序的结构如下:

1.get_pgtable_macro()打印当前系统分页机制中的一些宏。

2.通过vmalloc()在内核空间中分配内存,调用vaddr2paddr()将虚拟地址转化成物理地址。

3.通过__get_free_pages()在内核空间中分配页框,调用vaddr2paddr()将虚拟地址转化成物理地址。

4.分别通过vfree()和free_page()释放申请的内存空间。

vaddr2paddr()的执行过程如下:

1.通过pgd_offset计算页全局目录项的线性地址pgd,传入的参数为内存描述符mm和线性地址vaddr。接着打印pgd所指的页全局目录项。

2.通过pud_offset计算页上级目录项的线性地址pud,传入的参数为页全局目录项的线性地址pgd和线性地址vaddr。接着打印pud所指的页上级目录项。

3.通过pmd_offset计算页中间目录项的线性地址pmd,传入的参数为页上级目录项的线性地址pud和线性地址vaddr。接着打印pmd所指的页中间目录项。

4.通过pte_offset_kernel计算页表项的线性地址pte,传入的参数为页中间目录项的线性地址pmd和线性地址vaddr。接着打印pte所指的页表项。

5.pte_val(*pte)先取出页表项,与PAGE_MASK相与的结果是得到要访问页的物理地址;vaddr&~PAGE_MASK用来得到线性地址offset字段;两者或运算得到最终的物理地址。

6.打印物理地址。

recv()中的flags参数

28 10 月, 2011 by edsionte 无评论 »

从昨天上午到10分钟之前都在调试程序中的一个BUG,问题的精简描述是这样的:

一个经典的C/S模型,客户端异步的向服务器端发送一条指令,服务器端接收到该指令后向客户端发送一个int型数据len。这个过程重复的次数是随机的,并且有时候操作频繁。

对于上述过程的实现并不困难,在本地测试C/S端也并无异常。但是进行远程测试就出现异常:服务器端发送的len正常,但是客户端接收的数据有时候会为0。

经历了长时间的调试和分析后,终将问题锁定在recv()。该函数的原型如下:

       #include < sys/types.h >
       #include < sys/socket.h >
       ssize_t recv(int sockfd, void *buf, size_t len, int flags);

这个函数前三个参数的含义与read()大致相同,只不过recv()中的sockfd代表套接字描述符。除此之外,还有flags参数,一般被指定为0。而恰恰正是这种默认赋值才导致上述问题的产生,该标志可取多个值,其中一个值为MSG_WAITALL。这个标志告知内核在未读入指定字节len大小的数据之前不要将recv()返回。当为这个flags指定了该值后,结果正常。不过,该函数还是会在下述三种情况下提前返回:

1.连接被终止或套接字发生异常

2.接收了一个与之前类型不同的数据

3.捕获了一个信号

这三种情况都属于异常情况,跟上述问题无关。

目录操作API应用之ls命令的实现

27 10 月, 2011 by edsionte 无评论 »

Linux系统下有一系列对目录操作的API,如果单一的去学习这些API那么学习过程无疑是单调的。本文以实现Linux下ls命令为例,简单说明与目录操作相关的API。

1.获取文件名

获取一个指定目录下的文件名列表是ls命令最基本的功能。对于一个普通文件而言,读文件中数据的步骤是:open()文件,read()文件,close()文件。对于目录文件而言,它包含的内容就是若干个目录项,一个目录项即对应该目录下的一个文件。因此读目录的过程与普通文件类似,不过Linux下的目录是一种特殊的文件,因此有一组针对目录操作的API,具体如下:

       #include < sys/types.h >
       #include < dirent.h >

       DIR *opendir(const char *name);
       struct dirent *readdir(DIR *dirp);
       int closedir(DIR *dirp);

读目录之前必须用opendir()打开目录,它返回一个DIR *类型的目录流,它类似于文件描述符。接着使用readdir()读取该目录流中的目录项,该函数每成功调用一次则返回一个struct dirent类型的目录项。当打开某个目录后,第一次调用该函数则返回当前目录下第一个文件的信息,第二次调用则返回第二个文件的信息,以此类推。当读完该目录下的所有文件后返回NULL。由此可见,要读取指定目录下的所有目录项,则需要一个循环的过程。描述目录项的结构体dirent描述如下:

          struct dirent {
               ino_t          d_ino;       /* inode number */
               off_t          d_off;       /* offset to the next dirent */
               unsigned short d_reclen;    /* length of this record */
               unsigned char  d_type;      /* type of file; not supported
                                              by all file system types */
               char           d_name[256]; /* filename */
           };

目录项与目录是两个完全不同的概念。比如在/home/edsionte/test/下有一个目录文件mydir和一个文本文件mytxt.txt,那么该目录下就包含两个目录项,分别描述mydir和mytxt.txt。但是通过这两个目录项并不能直接就访问到对应文件,因为目录项只用于描述当前目录下的文件名等信息。如果要访问这两个文件,还必须获得/、home/、edsionte/和test/这几个目录项。由此可见,目录项是用来方便搜索文件的结构。

当不再使用某个已打开的目录时,使用closedir()关闭该目录。通过上述三个API就可以实现获取指定目录下文件名的功能。伪代码实现方法如下:

int get_file_name(char *path)
{
	DIR *mydir;
	struct dirent *myentry;

	if ((mydir = opendir(path)) == NULL) {
		do_error();
	}

	while ((myentry = readdir(mydir)) != NULL) {
		printf("%s\t", myentry->d_name);
	}
	printf("\n");
	closedir(mydir);

	return 0;
}

按照这种方式实现的ls命令只能显示path目录下的文件名称,仅此而已。

2.获取文件列表

获取指定目录下文件名对于ls命令来说还远远不够,我们需要获取每个文件名的属性信息,也就是实现类似ls -l的功能。要实现该功能就必须使用stat族函数,该函数会将文件的信息返回到传入的参数buf中,buf是struct stat类型。

       #include < sys/types.h >
       #include < sys/stat.h >
       #include < unistd.h >

       int stat(const char *path, struct stat *buf);
       int fstat(int fd, struct stat *buf);
       int lstat(const char *path, struct stat *buf);

          struct stat {
               dev_t     st_dev;     /* ID of device containing file */
               ino_t     st_ino;     /* inode number */
               mode_t    st_mode;    /* protection */
               nlink_t   st_nlink;   /* number of hard links */
               uid_t     st_uid;     /* user ID of owner */
               gid_t     st_gid;     /* group ID of owner */
               dev_t     st_rdev;    /* device ID (if special file) */
               off_t     st_size;    /* total size, in bytes */
               blksize_t st_blksize; /* blocksize for file system I/O */
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
               time_t    st_atime;   /* time of last access */
               time_t    st_mtime;   /* time of last modification */
               time_t    st_ctime;   /* time of last status change */
           };

得到了每个文件的stat结构,就可以按照需求显示文件的属性信息。对于stat族函数,不管使用哪一个都必须获得文件的绝对路径。在本文第一部分中通过读取指定目录下的目录项,只能通过d_name字段获得目录项名字,因此使用stat函数之前必须合成每个文件的绝对路径。获取文件列表的实现方法如下:

int get_file_name(char *path)
{
	DIR *mydir;
	struct dirent *myentry;

	if ((mydir = opendir(dirpath)) == NULL) {
		do_error();
	}

	while ((myentry = readdir(mydir)) != NULL) {
		printf("%s\t", myentry->d_name);
		strcat(tmpbuf, dirpath);
		strcat(tmpbuf, myentry->d_name);
		struct stat myfstat;
		if (stat(tmpbuf, &myfstat) == -1) {
			do_error();
		}
		print_file_stat(myfstat);
		printf("\n");
		memset(tmpbuf, len);
	}
	closedir(mydir);

	return 0;
}

其中,print_file_stat函数可以按照你的具体需求显示当前文件的各种属性,比如关于文件的各种时间,文件大小等。本质上就是将dirent结构中的相关字段打印。

3. 是否可以切换目录

Linux下每个文件都有读写执行(RWX)三个权限。对于普通文件来说,R权限代表可以读文件,W权限代表可以对文件进行修改,X权限代表可以执行该文件(与是否执行成功无关)。对于目录而言,读写执行的含义如下:

R:可以读取该目录下的文件名

W:可以增加、删除、修改或移动该目录下的文件名

X:可以切换到该目录下

这里比较不易理解的是X权限,它并不是指目录具有“执行”权限,而是表示当前进程可以切换到该目录下。

如果某个用户对一个目录只有R权限,那么这个用户只能ls到该目录下的文件名,甚至连ls -l都不能成功执行。通过上述文件列表的实现过程可知,如果要获取每个文件的属性信息则必须能够成功访问到该文件的绝对路径。但是现在没有X权限,进程并不能切换到该目录下,因此也就访问失败了。与此类似,如果要修改一个目录,没有X权限也是不能成功执行的。如果某个用户对一个目录只有执行权限,那么除了可以切换到该目录外,什么也做不了。

根据此原理,我们对ls命令增加一个新的功能:判断当前执行ls命令的用户是否可以切换到该目录下子目录。我们并不依次去判断子目录的权限,通过chdir()就可以完成。

       
       #include < unistd.h >
       int chdir(const char *path);

chdir()用于将当前工作目录切换到path指定的目录,如果切换成功则返回0,否则返回-1。据此,修改后的ls命令实现方法如下:

int get_file_name(char *path)
{
	DIR *mydir;
	struct dirent *myentry;

	if ((mydir = opendir(dirpath)) == NULL) {
		do_error();
	}

	while ((myentry = readdir(mydir)) != NULL) {
		printf("%s\t", myentry->d_name);
		strcat(tmpbuf, dirpath);
		strcat(tmpbuf, myentry->d_name);
		struct stat myfstat;
		if (stat(tmpbuf, &myfstat) == -1) {
			do_error();
		}
		print_file_stat(myfstat);

		if (chdir(tmpbuf) == -1) {
			print_no_chdir();
		} else {
			print_chdir();
		}
		printf("\n");
		memset(tmpbuf, len);
	}
	closedir(mydir);

	return 0;
}

如果切换成功,通过print_no_chdir()打印可切换标志,否则打印不可切换标志。上述的ls实现方式只是基本模型,感兴趣的童鞋可以在此基础上加以改进。

Linux中的分页机制

25 10 月, 2011 by edsionte 无评论 »

Linux中采用了一种通用的四级分页机制,即页全局目录(Page Global Directory)、页上级目录(Page Upper Directory)、页中间目录(Page Middle Directory)和页表(Page Table)。在这种分页机制下,一个完整的线性地址被分为五部分:页全局目录、页上级目录、页中间目录、页表和偏移量,但是对于每个部分所占的位数则是不定的,这跟系统所在的体系架构有关。

对于x86-32(未采用PAE)架构的系统来说,线性地址中的页上级目录和页中间目录两部分占用的位数均为0,页上级目录和页中间目录中都只包含一个目录项,这两个页表项(这里称为页目录项更为确切,下文中的页表项均指某级页表中的一项,对页目录项和页表项不再作特别区分)都被映射到页全局目录中一个适当的目录项中。这种方法本质上只包含两级页表,但是它却仍然保持着四级页表模型。其他未采用四级页表模型的体系架构都采用类似的方法,因此这种四级分页机制具有通用性。

1.数据结构

Linux分别采用pgd_t、pmd_t、pud_t和pte_t四种数据结构来表示页全局目录项、页上级目录项、页中间目录项和页表项。这四种数据结构本质上都是无符号长整型,Linux为了更严格数据类型检查,将无符号长整型分别封装成四种不同的页表项。如果不采用这种方法,那么一个无符号长整型数据可以传入任何一个与四种页表相关的函数或宏中,这将大大降低程序的健壮性。下面仅列出pgd_t类型的内核源码实现,其他类型与此类似。

arch/x86/include/asm/pgtable_64_types.h
 13 typedef unsigned long   pgdval_t;
arch/x86/include/asm/pgtable_types.h
192 typedef struct { pgdval_t pgd; } pgd_t;
arch/x86/include/asm/pgtable.h
 66 #define pgd_val(x)      native_pgd_val(x)
arch/x86/include/asm/pgtable_types.h
199 static inline pgdval_t native_pgd_val(pgd_t pgd)
200 {
201         return pgd.pgd;
202 }

这里需要区别指向页表项的指针和页表项所代表的数据。如果已知一个pgd_t类型的指针pgd,那么通过pgd_val(*pgd)即可获得该页表项(也就是一个无符号长整型数据),这里利用了面向对象的思想。

2.线性地址、页表和页表项

线性地址

不管系统采用多少级分页模型,线性地址本质上都是索引+偏移量的形式,甚至你可以将整个线性地址看作N+1个索引的组合,N是系统采用的分页级数。在四级分页模型下,线性地址被分为5部分,如下图:

 

在线性地址中,每个页表索引即代表线性地址在对应级别的页表中中关联的页表项。正是这种索引与页表项的对应关系形成了整个页表映射机制。

页表

多个页表项的集合则为页表,一个页表内的所有页表项是连续存放的。页表本质上是一堆数据,因此也是以页为单位存放在主存中的。因此,在虚拟地址转化物理物理地址的过程中,每访问一级页表就会访问一次内存。

页表项

从四种页表项的数据结构可以看出,每个页表项其实就是一个无符号长整型数据。每个页表项分两大类信息:页框基地址和页的属性信息。在x86-32体系结构中,每个页表项的结构图如下:

这个图是一个通用模型,其中页表项的前20位是物理页的基地址。由于32位的系统采用4kb大小的 页,因此每个页表项的后12位均为0。内核将后12位充分利用,每个位都表示对应虚拟页的相关属性。

不管是那一级的页表,它的功能就是建立虚拟地址和物理地址之间的映射关系,一个页和一个页框之间的映射关系体现在页表项中。上图中的物理页基地址是个抽象的说明,如果当前的页表项位于页全局目录中,这个物理页基址是指页上级目录所在物理页的基地址;如果当前页表项位于页表中,这个物理页基地址是指最终要访问数据所在物理页的基地址。

 3.地址转换过程

有了上述的基本知识,就很好理解四级页表模式下如何将虚拟地址转化为逻辑地址了。基本过程如下:

1.从CR3寄存器中读取页目录所在物理页面的基址(即所谓的页目录基址),从线性地址的第一部分获取页目录项的索引,两者相加得到页目录项的物理地址。

2.第一次读取内存得到pgd_t结构的目录项,从中取出物理页基址取出(具体位数与平台相关,如果是32系统,则为20位),即页上级页目录的物理基地址。

3.从线性地址的第二部分中取出页上级目录项的索引,与页上级目录基地址相加得到页上级目录项的物理地址。

4.第二次读取内存得到pud_t结构的目录项,从中取出页中间目录的物理基地址。

5.从线性地址的第三部分中取出页中间目录项的索引,与页中间目录基址相加得到页中间目录项的物理地址。

6.第三次读取内存得到pmd_t结构的目录项,从中取出页表的物理基地址。

7.从线性地址的第四部分中取出页表项的索引,与页表基址相加得到页表项的物理地址。

8.第四次读取内存得到pte_t结构的目录项,从中取出物理页的基地址。

9.从线性地址的第五部分中取出物理页内偏移量,与物理页基址相加得到最终的物理地址。

10.第五次读取内存得到最终要访问的数据。

整个过程是比较机械的,每次转换先获取物理页基地址,再从线性地址中获取索引,合成物理地址后再访问内存。不管是页表还是要访问的数据都是以页为单位存放在主存中的,因此每次访问内存时都要先获得基址,再通过索引(或偏移)在页内访问数据,因此可以将线性地址看作是若干个索引的集合。

存储映射I/O

23 10 月, 2011 by edsionte 4 comments »

一个进程拥有独立并且连续虚拟地址空间,在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