这次再次讨论文件的操作,只是对象换成了大型文件。
之所以讨论大型文件,是因为无论当今的热点“大数据处理”,还是老牌的搜索引擎,都会涉及到大型文件的处理效率。
比如前一张我们生成了50000行的日志,那么如果是300万行,甚至更多的时候,我们前一张提到的函数是否还能达到我们能够接受的效率呢。
统计日志行数,分别用LinuxAPI, C标准库实现,可以看到直接调用API,耗时72秒,用C标准库用时20,因为标准库使用了缓存,所以速度上快了三倍。
当我们换成最后一种方法的时候,文件内存映射,速度再次提高了一个级别,不到1秒。相当惊艳的成绩。
头文件:
<sys/mman.h>
void*mmap( void *addr, size_t length, intprot, int flag, intfd, off_t offset );
第一个参数addr,表示自己制定的映射后的内存地址,一般不指定,除非你想制定确定的内存地址。
第二个参数,内存映射的大小,这里传入的是文件的大小,因为文件不是很大,所以全部大小,如果文件太大,可以分次分批映射。
第三个参数代表权限,PROT_READ/PROT_WRITE/ PROT_EXEC
第四个参数flag, 代表映射的方式,MAP_PRIVATE/ MAP_SHARED 这个参数主要是多个进程映射时使用,主要是对写入进行保护。PRIVATE代表自己又自己的备份。
最后一个参数代表文件的偏移,和第二个参数搭配使用可以分阶段映射大文件。
文件映射之所以快,是因为采用了系统中的虚拟内存技术。我们的内存一般都比硬盘小,当内存不足的时候,系统会使用硬盘来做交换分区,对页进行换入换出。虚拟内存既然可以使用硬盘来作为内存的备份,这就为文件映射提供了很好的实现方式。只需要把文件本身当作我们的交换分区,那么随时通过交换分区的算法(页中断算法)靠硬件来完成对文件的读取操作。
通过strace统计系统调用的时候,经常可以看到mmap()与mmap2()。系统调用mmap()可以将某文件映射至内存(进程空间),如此可以把对文件的操作转为对内存的操作,以此避免更多的lseek()与read()、write()操作,这点对于大文件或者频繁访问的文件而言尤其受益。
但有一点必须清楚:mmap的addr与offset必须对齐一个内存页面大小的边界,即内存映射往往是页面大小的整数倍,否则maaped_file_size%page_size内存空间将被闲置浪费。
你只是要记住:mmap内存映射文件之后,操作内存即是操作文件,可以省去不少系统内核调用(lseek, read,write)。
1. <P>#include <stdio.h>
2. #include <time.h>
3. #include <fcntl.h>
4. /*
5. [chenndao@localhost log]$ gcc -o logreadapi logreadapi.c
6. [chenndao@localhost log]$ ./logreadapi 3000000.txt
7. Finished, total 3000000 lines
8. Cost time: 72 sec
9. [chenndao@localhost log]$
10.*/</P><P>int main( int argc, char *argv[] )
11.{
12. //Caculate how many lines in the log file.
13. if ( argc != 2 )
14. {
15. printf("usage: \n readlog log.txt\n");
16. return 0;
17. }
18. int lineNum = 0;
19. char buff[256] = { 0 };
20. time_t
21. char *data = NULL;
22. int fd = open( argv[1], O_RDONLY );
23. if ( fd > 0 )
24. {
25. int len = 0;
26. do
27. {
28. len = read( fd, buff, 256 );
29. if ( len > 0 )
30. {
31. int i = 0;
32. for( ; i<len; i++)
33. {
34. if ( buff[i] == '\n' )
35. {
36. lineNum++;
37. }
38. }
39. }
40. }
41. while ( len > 0 );
42. printf("Finished, total %d lines\n", lineNum);
43. printf("Cost time: %d sec\n", time(NULL)-timeStart);
44. }
45. else
46. {
47. printf("Open file %s error\n", argv[1]);
48. }</P><P> return 1;
49.}
50.</P>
#include <stdio.h>
#include <time.h>
#include <fcntl.h>
/*
[chenndao@localhost log]$ gcc -o logreadapi logreadapi.c
[chenndao@localhost log]$ ./logreadapi 3000000.txt
Finished, total 3000000 lines
Cost time: 72 sec
[chenndao@localhost log]$
*/
int main( int argc, char *argv[] )
{
//Caculate how many lines in the log file.
if ( argc != 2 )
{
printf("usage: \nreadlog log.txt\n");
return 0;
}
int lineNum = 0;
char buff[256] = { 0 };
time_t timeStart = time( NULL );
char *data = NULL;
int fd = open( argv[1], O_RDONLY );
if ( fd > 0 )
{
int len = 0;
do
{
len = read( fd, buff, 256 );
if ( len > 0 )
{
int i = 0;
for( ; i<len;i++)
{
if ( buff[i] == '\n' )
{
lineNum++;
}
}
}
}
while ( len > 0 );
printf("Finished,total %d lines\n", lineNum);
printf("Cost time: %dsec\n", time(NULL)-timeStart);
}
else
{
printf("Open file %serror\n", argv[1]);
}
return 1;
}
[cpp]fgets
1. <P>#include <stdio.h>
2. #include <time.h>
3. /*
4. [chenndao@localhost log]$ ./logread 3000000.txt
5. Finished, total 3000000 lines
6. Cost time: 20 sec
7. */</P><P>int main( int argc, char *argv[] )
8. {
9. //Caculate how many lines in the log file.
10. if ( argc != 2 )
11. {
12. printf("usage: \n readlog log.txt\n");
13. return 0;
14. }
15. int lineNum = 0;
16. char buff[256] = { 0 };
17. time_t timeStart = time( NULL );
18. char *data = NULL;
19. FILE *fd = fopen( argv[1], "r" );
20. if ( fd != NULL )
21. {
22. int len = 0;
23. do
24. {
25. data = fgets( buff, 256, fd );
26. if ( data != NULL )
27. {
28. int i = 0;
29. for( ; i<256; i++)
30. {
31. if ( buff[i] == '\n' )
32. {
33. lineNum++;
34. break;
35. }
36. }
37. }
38. }
39. while ( data );
40. printf("Finished, total %d lines\n", lineNum);
41. printf("Cost time: %d sec\n", time(NULL)-timeStart);
42. }
43. else
44. {
45. printf("Open file %s error\n", argv[1]);
46. }</P><P> return 1;
47.}
48.</P>
#include <stdio.h>
#include <time.h>
/*
[chenndao@localhost log]$ ./logread 3000000.txt
Finished, total 3000000 lines
Cost time: 20 sec
*/
int main( int argc, char *argv[] )
{
//Caculate how many lines in the log file.
if ( argc != 2 )
{
printf("usage: \nreadlog log.txt\n");
return 0;
}
int lineNum = 0;
char buff[256] = { 0 };
time_t timeStart = time( NULL );
char *data = NULL;
FILE *fd = fopen( argv[1], "r" );
if ( fd != NULL )
{
int len = 0;
do
{
data = fgets( buff, 256, fd );
if ( data != NULL )
{
int i = 0;
for( ; i<256;i++)
{
if ( buff[i] == '\n' )
{
lineNum++;
break;
}
}
}
}
while ( data );
printf("Finished,total %d lines\n", lineNum);
printf("Cost time: %dsec\n", time(NULL)-timeStart);
}
else
{
printf("Open file %serror\n", argv[1]);
}
return 1;
}
1. #include <stdio.h>
2. #include <fcntl.h>
3. #include <time.h>
4. #include <sys/mman.h>
5.
6. /*
7. [chenndao@localhost log]$ ./maplogread 3000000.txt
8. Finished, total lines is 3000000
9. total costed time 0 sec
10.[chenndao@localhost log]$ ./maplogread 3000000.txt
11.Finished, total lines is 3000000
12.total costed time 1 secs
13.*/
14.
15.int main( int argc, char *argv[] )
16.{
17. if ( argc != 2 )
18. {
19. printf("usage: \n maplogread 50000.txt\n");
20. return 0;
21. }
22.
23. char *memory = NULL;
24. int file_length = 0;
25. char *start_address = 0;
26. int line_num = 0;
27. int time_start = time(NULL);
28. int fd = open( argv[1], O_RDONLY );
29. if ( fd > 0 )
30. {
31. file_length = lseek(fd, 1, SEEK_END);
32. memory = mmap( start_address, file_length, PROT_READ, MAP_SHARED, fd, 0 );
33.
34. int i=0;
35. for ( ; i<file_length; i++ )
36. {
37. if ( memory[i] == '\n' )
38. {
39. ++line_num;
40. }
41. }
42. close( fd );
43. munmap( memory, file_length );
44. printf("Finished, total lines is %d \n", line_num);
45. printf("total costed time %d sec\n", time(NULL) - time_start);
46. }
47. return 0;
48.}
-------------------------------------------------------------------------------------------------------------------------
linuxmmap内存映射
mmap()vs read()/write()/lseek()
通过strace统计系统调用的时候,经常可以看到mmap()与mmap2()。系统调用mmap()可以将某文件映射至内存(进程空间),如此可以把对文件的操作转为对内存的操作,以此避免更多的lseek()与read()、write()操作,这点对于大文件或者频繁访问的文件而言尤其受益。但有一点必须清楚:mmap的addr与offset必须对齐一个内存页面大小的边界,即内存映射往往是页面大小的整数倍,否则maaped_file_size%page_size内存空间将被闲置浪费。
演示一下,将文件/tmp/file_mmap中的字符转成大写,分别使用mmap与read/write二种方法实现。
/*
* @file:t_mmap.c
*/
#include <stdio.h>
#include<ctype.h>
#include<sys/mman.h> /*mmapmunmap*/
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main(intargc, char *argv[])
{
intfd;
char *buf;
off_tlen;
struct stat sb;
char *fname = "/tmp/file_mmap";
fd = open(fname, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
if(fd == -1)
{
perror("open");
return1;
}
if(fstat(fd, &sb) == -1)
{
perror("fstat");
return1;
}
buf = mmap(0, sb.st_size, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
if(buf == MAP_FAILED)
{
perror("mmap");
return1;
}
if(close(fd) == -1)
{
perror("close");
return1;
}
for(len = 0; len<sb.st_size; ++len)
{
buf[len] = toupper(buf[len]);
/*putchar(buf[len]);*/
}
if(munmap(buf, sb.st_size) == -1)
{
perror("munmap");
return1;
}
return0;
}
#gcc –ot_mmapt_mmap.c
#strace./t_mmap
open("/tmp/file_mmap", O_RDWR|O_CREAT, 0600) = 3//open,返回fd=3
fstat64(3, {st_mode=S_IFREG|0644, st_size=18, ...}) = 0//fstat, 即文件大小18
mmap2(NULL, 18, PROT_READ|PROT_WRITE, MAP_SHARED,3, 0) = 0xb7867000 //mmap文件fd=3
close(3) = 0//close文件fd=3
munmap(0xb7867000, 18) =0//munmap,移除0xb7867000这里的内存映射
虽然没有看到read/write写文件操作,但此时文件/tmp/file_mmap中的内容已由www.perfgeeks.com改变成了WWW.PERFGEEKS.COM.这里mmap的addr是0(NULL),offset是18,并不是一个内存页的整数倍,即有4078bytes(4kb-18)内存空间被闲置浪费了。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<ctype.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
int main(intargc, char *argv[])
{
intfd, len;
char *buf;
char *fname = "/tmp/file_mmap";
ssize_tret;
struct stat sb;
fd = open(fname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
if(fd == -1)
{
perror("open");
return1;
}
if(fstat(fd, &sb) == -1)
{
perror("stat");
return1;
}
buf =malloc(sb.st_size);
if(buf == NULL)
{
perror("malloc");
return1;
}
ret = read(fd, buf, sb.st_size);
for(len = 0; len<sb.st_size; ++len)
{
buf[len] = toupper(buf[len]);
/*putchar(buf[len]);*/
}
lseek(fd, 0, SEEK_SET);
ret =write(fd, buf, sb.st_size);
if(ret == -1)
{
perror("error");
return1;
}
if(close(fd) == -1)
{
perror("close");
return1;
}
free(buf);
return0;
}
#gcc –ot_rwt_rw.c
open("/tmp/file_mmap", O_RDWR|O_CREAT, 0600) = 3//open, fd=3
fstat64(3, {st_mode=S_IFREG|0644, st_size=18, ...}) = 0//fstat, 其中文件大小18
brk(0) = 0x9845000 //brk, 返回当前中断点
brk(0x9866000) = 0x9866000 //malloc分配内存,堆当前最后地址
read(3, "www.perfgeeks.com\n", 18) = 18//read
lseek(3, 0, SEEK_SET) = 0//lseek
write(3, "WWW.PERFGEEKS.COM\n", 18) = 18//write
close(3) = 0//close
这里通过read()读取文件内容,toupper()后,调用write()写回文件。因为文件太小,体现不出read()/write()的缺点:频繁访问大文件,需要多个lseek()来确定位置。每次编辑read()/write(),在物理内存中的双份数据。当然,不可以忽略创建与维护mmap()数据结构的成本。需要注意:并没有具体测试mmapvsread/write,即不能一语断言谁孰谁劣,具体应用场景具体评测分析。你只是要记住:mmap内存映射文件之后,操作内存即是操作文件,可以省去不少系统内核调用(lseek,read, write)。
mmap()vsmalloc()
使用strace调试的时候,通常可以看到通过mmap()创建匿名内存映射的身影。比如启用dl(‘apc.so’)的时候,就可以看到如下语句。
mmap2(NULL, 31457280, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) =0xb5ce7000 //30M
通常使用mmap()进行匿名内存映射,以此来获取内存,满足一些特别需求。所谓匿名内存映射,是指mmap()的时候,设置了一个特殊的标志MAP_ANONYMOUS,且fd可以忽略(-1)。某些操作系统(像FreeBSD),不支持标志MAP_ANONYMOUS,可以映射至设备文件/dev/zero来实现匿名内存映射。使用mmap()分配内存的好处是页面已经填满了0,而malloc()分配内存后,并没有初始化,需要通过memset()初始化这块内存。另外,malloc()分配内存的时候,可能调用brk(),也可能调用mmap2()。即分配一块小型内存(小于或等于128kb),malloc()会调用brk()调高断点,分配的内存在堆区域,当分配一块大型内存(大于128kb),malloc()会调用mmap2()分配一块内存,与堆无关,在堆之外。同样的,free()内存映射方式分配的内存之后,内存马上会被系统收回,free()堆中的一块内存,并不会马上被系统回收,glibc会保留它以供下一次malloc()使用。
这里演示一下malloc()使用brk()和mmap2()。
/*
*file:t_malloc.c
*/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(intargc, char *argv)
{
char *brk_mm, *mmap_mm;
printf("-----------------------\n");
brk_mm = (char *)malloc(100);
memset(brk_mm, '\0', 100);
mmap_mm = (char *)malloc(500 * 1024);
memset(mmap_mm, '\0', 500*1024);
free(brk_mm);
free(mmap_mm);
printf("-----------------------\n");
return1;
}
#gcc –ot_malloct_malloc.c
#strace./t_malloc
write(1, "-----------------------\n", 24-----------------------) = 24
brk(0) = 0x85ee000
brk(0x860f000) = 0x860f000 //malloc(100)
mmap2(NULL, 516096, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7702000 //malloc(5kb)
munmap(0xb7702000, 516096) = 0//free(), 5kb
write(1, "-----------------------\n", 24-----------------------) = 24
通过malloc()分别分配100bytes和5kb的内存,可以看出其实分别调用了brk()和mmap2(),相应的free()也是不回收内存和通过munmap()系统回收内存。
mmap()共享内存,进程通信
内存映射mmap()的另一个外常见的用法是,进程通信。相较于管道、消息队列方式而言,这种通过内存映射的方式效率明显更高,它不需要任务数据拷贝。这里,我们通过一个例子来说明mmap()在进程通信方面的应用。我们编写二个程序,分别是master和slave,slave根据master不同指令进行不同的操作。Master与slave就是通过映射同一个普通文件进行通信的。
/*
*@file master.c
*/
root@liaowq:/data/tmp# cat master.c
#include<stdio.h>
#include <time.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
void listen();
int main(intargc, char *argv[])
{
listen();
return0;
}
void listen()
{
intfd;
char *buf;
char *fname = "/tmp/shm_command";
char command;
time_tnow;
fd = open(fname, O_CREAT|O_RDWR, S_IRUSR|S_IWUSR);
if(fd == -1)
{
perror("open");
exit(1);
}
buf = mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0);
if(buf == MAP_FAILED)
{
perror("mmap");
exit(1);
}
if(close(fd) == -1)
{
perror("close");
exit(1);
}
*buf = '0';
sleep(2);
for(;;)
{
if(*buf == '1' || *buf == '3' || *buf == '5' || *buf == '7')
{
if(*buf>'1')
printf("%ld\tgood job [%c]\n", (long)time(&now), *buf);
(*buf)++;
}
if(*buf == '9')
{
break;
}
sleep(1);
}
if(munmap(buf, 4096) == -1)
{
perror("munmap");
exit(1);
}
}
/*
*@file slave.c
*/
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include <sys/mman.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
void ready(unsignedint t);
voidjob_hello();
voidjob_smile();
voidjob_bye();
charget_command(char *buf);
void wait();
int main(intargc, char *argv[])
{
wait();
return0;
}
void ready(unsignedint t)
{
sleep(t);
}
/* command 2*/
voidjob_hello()
{
time_tnow;
printf("%ld\thello world\n", (long)time(&now));
}
/* command 4*/
voidjob_simle()
{
time_tnow;
printf("%ld\t^_^\n", (long)time(&now));
}
/* command 6*/
voidjob_bye()
{
time_tnow;
printf("%ld\t|<--\n", (long)time(&now));
}
charget_command(char *buf)
{
char *p;
if(buf != NULL)
{
p = buf;
}
else
{
return'0';
}
return *p;
}
void wait()
{
intfd;
char *buf;
char *fname = "/tmp/shm_command";
char command;
time_tnow;
fd = open(fname, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
if(fd == -1)
{
perror("open");
exit(1);
}
buf = mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0);
if(buf == MAP_FAILED)
{
perror("mmap");
exit(1);
}
if(close(fd) == -1)
{
perror("close");
exit(1);
}
for(;;)
{
command =get_command(buf);
/*printf("%c\n",command);*/
switch(command)
{
case'0':
printf("%ld\tslave is ready...\n", (long)time(&now));
ready(3);
*buf = '1';
break;
case'2':
job_hello();
*buf = '3';
break;
case'4':
job_simle();
*buf = '5';
break;
case'6':
job_bye();
*buf = '7';
break;
default:
break;
}
if(*buf == '8')
{
*buf = '9';
if(munmap(buf, 4096) == -1)
{
perror("munmap");
exit(1);
}
return;
}
sleep(1);
}
if(munmap(buf, 4096) == -1)
{
perror("munmap");
exit(1);
}
}
执行master与slave,输出如下
root@liaowq:/data/tmp# echo “0″ > /tmp/shm_command
root@liaowq:/data/tmp# ./master
1320939445 good job [3]
1320939446 good job [5]
1320939447 good job [7]
root@liaowq:/data/tmp# ./slave
1320939440 slave is ready…
1320939444 hello world
1320939445 ^_^
1320939446 |<--
master向slave发出job指令2,4,6。slave收到指令后,执行相关逻辑操作,完成后告诉master,master知道slave完成工作后,打印good job并且发送一下job指令。master与slave通信,是通过mmap()共享内存实现的。
总结
1、 Linux采用了投机取巧的分配策略,用到时,才分配物理内存。也就是说进程调用brk()或mmap()时,只是占用了虚拟地址空间,并没有真正占用物理内存。这也正是free–m中used并不意味着消耗的全都是物理内存。
2、mmap()通过指定标志(flag)MAP_ANONYMOUS来表明该映射是匿名内存映射,此时可以忽略fd,可将它设置为-1。如果不支持MAP_ANONYMOUS标志的类unix系统,可以映射至特殊设备文件/dev/zero实现匿名内存映射。
3、调用mmap()时就决定了映射大小,不能再增加。换句话说,映射不能改变文件的大小。反过来,由文件被映射部分,而不是由文件大小来决定进程可访问内存空间范围(映射时,指定offset最好是内存页面大小的整数倍)。
4、通常使用mmap()的三种情况.提高I/O效率、匿名内存映射、共享内存进程通信。