Linux C 文件基本操作
在UNIX系统当中,“万物皆文件”是一种重要的设计思想。在传统的定义当中,我们把存储在磁盘当中的数据集合称为文件。而在UNIX的设计当中,文件这个概念得到了进一步泛化,所有满足速度较慢、容量较大和可以持久化存储中任意一个特征的数据集合都可以称为文件,包括不限于磁盘数据、输入输出设备、用于进程间通信的管道、网络等等都属于文件。
文件系统是操作系统用于管理文件的子模块。在文件系统当中,会根据文件的存在形式分为普通文件、目录文件、链接文件和设备文件等类型:
- 普通文件:也称磁盘文件,并且能够进行随机(能够自由使用lseek或者fseek定位到某一个位置)的数据存储;
- 管道:是一个从一端发送数据,另一端接收数据的数据通道;
- 目录:也称为目录文件,它包含了保存在目录中文件列表的简单文件;
- 设备:该类型的文件提供了大多数物理设备的接口。它又分为两种类型:字符设备和块设备。字符
- 设备一次只能读出和写入一个字节的数据,包括终端、打印机、声卡以及鼠标;块设备必须以一定
- 大小的块来读出或者写入数据,块设备包括CD-ROM、RAM驱动器和磁盘驱动器等。一般而言,字符设备用于传输数据,块设备用于存储数据;
- 链接:类似于Windows的快捷方式,指包含到达另一个文件路径的文件。
基于文件流的文件操作
文件流,又称为(用户态)文件缓冲区,它是由标准C库(ISO C)设计和定义的用于管理文件的数据结构。如果进程想要使用C库函数操作文件数据,就必须提前在内存中先申请创建一个文件流对象。
文件流操作基于缓冲机制,标准库会为文件流分配一个缓冲区(buffer),用于存储读取或写入的数据。这种缓冲机制可以减少对底层文件系统的访问次数,提高文件操作的效率。
文件流的创建与关闭
fopen与fclose
创建文件流使用 fopen ,关闭文件流使用 fclose 。
#include <stdio.h>
FILE* fopen(const char* path, const char* mode);//创建文件流
int fclose(FILE* stream); //关闭文件流
filename
:一个指向以 null 结尾的字符串的指针,表示要打开的文件的路径和名称。mode
:一个指向以 null 结尾的字符串的指针,表示文件的打开模式。常见的模式包括:"r"
:以只读模式打开文件。文件必须存在。"w"
:以写模式打开文件。如果文件存在,其内容会被清空;如果文件不存在,会创建一个新文件。"a"
:以追加模式打开文件。如果文件存在,写入的内容会被追加到文件末尾;如果文件不存在,会创建一个新文件。"r+"
:以读写模式打开文件。文件必须存在。"w+"
:以读写模式打开文件。如果文件存在,其内容会被清空;如果文件不存在,会创建一个新文件。"a+"
:以读写模式打开文件。如果文件存在,写入的内容会被追加到文件末尾;如果文件不存在,会创建一个新文件。
a和a+为追加模式,在此两种模式下,在一开始的时候读取文件内容是从文件起始处开始读取的,而无论文件读写点定位到何处,在写数据时都将是在文件末尾添加(写完以后读写点就移动到文件末尾了),所以比较适合于多进程写同一个文件的情况下保证数据的完整性。
成功时返回一个指向
FILE
结构的指针。失败时返回
NULL
,可以通过errno
或perror
/strerror
获取错误原因。
fclose
stream
:一个指向FILE
结构的指针,表示要关闭的文件流。成功时返回
0
。失败时返回
EOF
(通常定义为-1
),可以通过errno
或perror
/strerror
获取错误原因。
读写文件
数据块读写
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(void *ptr, size_t size, size_t nmemb, FILE *stream);
- fread 从文件流stream 中读取nmemb个元素,写到ptr指向的内存中,每个元素的大小为size个字节
- fwrite 从ptr指向的内存中读取nmemb个元素,写到文件流stream中,每个元素的大小为size个字节
- 所有的文件读写函数都从文件的当前读写点开始读写,读写完成以后,当前读写点自动往后移动size*nmemb个字节。
格式化读写
#include <stdio.h>
int printf(const char *format, ...); //格式化输出到标准输出(通常是屏幕)
//相当于fprintf(stdout,format,…);
int scanf(const char *format, …); //从标准输入(通常是键盘)读取格式化输入
int fprintf(FILE *stream, const char *format, ...);//将格式化输出写入指定文件流
int fscanf(FILE *stream, const char *format, …); //从指定文件流读取格式化输入
int sprintf(char *str, const char *format, ...); //将格式化输出写入字符串
//eg:sprintf(buf,”the string is;%s”,str);
int sscanf(char *str, const char *format, …); //从字符串读取格式化输入
stream
:指向FILE
结构的指针,表示目标文件流。const char format
:格式化字符串,用于指定输入或输出的格式。它包含普通字符和格式说明符(如%d
、%s
、%f
等)。...
:可变参数列表,根据格式说明符提供相应的值。对于以上输出类型函数 ,成功时返回输出的字符数,如果发生错误,返回负值。
对于以上输入类型函数 ,成功时返回读取的输入项数。发生错误或到达文件末尾,返回
EOF
(通常为-1
)。
单个字符读写
#include <stdio.h>
int fgetc(FILE *stream); //从指定的文件流中读取下一个字符。
int fputc(int c, FILE *stream); //将一个字符写入指定的文件流。
int getc(FILE *stream); //从指定的文件流中读取下一个字符,等同于 fgetc(FILE* stream)
int putc(int c, FILE *stream); //将一个字符写入指定的文件流。,等同于 fputc(int c, FILE* stream)
int getchar(void); //用于从标准输入(通常是键盘)读取下一个字符。,等同于 fgetc(stdin);
int putchar(int c); //用于将一个字符写入标准输出(通常是屏幕)。等同于 fputc(int c, stdout);
getc
与fgetc
功能相同,但通常比fgetc
更快,因为它可能使用了宏实现。putc
与fputc
功能相同,但通常比fputc
更快,因为它可能使用了宏实现。- 对于以上输出类型函数 ,如果成功读取字符,返回读取的字符(以
int
类型表示)。如果到达文件末尾或发生错误,返回EOF
(通常为-1
)。 - 对于以上输入类型函数 ,如果成功写入字符,返回写入的字符。如果发生错误,返回
EOF
(通常为-1
)。
字符串读写
char *fgets(char *s, int size, FILE *stream); //从指定的文件流中读取一行字符串,直到遇到换行符或达到指定的字符数。
int fputs(const char *s, FILE *stream); //将一个字符串写入指定的文件流。
int puts(const char *s); //将一个字符串写入标准输出(通常是屏幕),并在字符串末尾自动添加换行符。等同于 fputs(const char *s,stdout);
char *gets(char *s); //从标准输入(通常是键盘)读取一行字符串,直到遇到换行符或文件结束符。等同于 fgets(const char *s, int size, stdin);
s
:指向字符数组的指针,用于存储读取的字符串。size
:指定最多读取的字符数(包括换行符和终止符\0
)。stream
:指向FILE
结构的指针,表示要从中读取的文件流。- fgets 和 fputs 从文件流stream中读写一行数据;
- puts 和 gets 从标准输入输出流中读写一行数据。
- 对于以上输出类型函数 ,如果成功读取字符串,返回指向字符串的指针(即参数
s
)。如果到达文件末尾或发生错误,返回NULL
。 - 对于以上输入类型函数 ,如果成功写入字符串,返回非负值。如果发生错误,返回
EOF
(通常为-1
)。
注意事项:
fgets
会读取直到换行符\n
或达到size - 1
个字符为止,并在字符串末尾添加空字符\0
。如果读取到换行符,换行符也会被存储在字符串中。fputs
不会自动添加换行符。如果需要换行,需要在字符串末尾手动添加\n
。puts
会在字符串末尾自动添加换行符\n
。gets
是不安全的函数,因为它不会检查目标缓冲区的大小,容易导致缓冲区溢出。
文件定位
文件定位指读取或设置文件当前读写点,所有的通过文件指针读写数据的函数,都是从文件的当前读写点读写数据的。常用的函数有:
#include <stdio.h>
int feof(FILE * stream); //检查文件流的末尾是否已到达,通常的用法为while(!feof(fp))
int fseek(FILE *stream, long offset, int whence); //设置当前读写点到偏移whence 长度为offset处
long ftell(FILE *stream); //用来获得文件流当前的读写位置
void rewind(FILE *stream); //把文件流的读写位置移至文件开头 fseek(fp, 0, SEEK_SET);
stream
:指向FILE
结构的指针,表示要操作的文件流。offset
:要移动的字节数。正值表示向前移动,负值表示向后移动。whence
:指定移动的参考位置。常见的值有:feof 如果文件流的末尾已到达,返回非零值。如果文件流的末尾未到达,返回零。
fseek 如果成功移动文件指针,返回零。如果发生错误,返回非零值。
ftell 返回文件指针的当前位置(以字节为单位)。如果发生错误,返回
-1L
。
基于文件描述符的文件操作
之前所讨论的文件操作都是操作文件流,即FILE。我们把所有和FILE类型相关的文件操作(比如fopen,fread等等)称为带缓冲的IO,它们是ISO C的组成部分,它们都是库函数,其底层调用了系统调用来使用内核的功能。
POSIX标准支持另一类无缓冲的IO,这些操作都是系统调用。值得注意的是,在这里无缓冲是没有分配用户态文件缓冲区的意思。在操作文件时,进程会在内存地址空间的内核区部分里面维护一个数据结构来管理和文件相关的所有操作,这个数据结构称为打开文件或者是文件对象(file / file struct),除此以外,内核区里面还会维护一个索引数组来管理所有的文件对象,该数组的下标就被称为文件描述符(file descriptor)。
从类型来说,文件描述符是一个非负整数,它可以传递给用户。用户在获得文件描述符之后可以定位到相关联的文件对象,从而可以执行各种IO操作。
文件描述符操作不依赖缓冲区,直接通过系统调用(如read
、write
)与操作系统内核进行交互。每次读写操作都会直接触发对文件系统的访问。而带缓冲的IO文件操作,则会在用户态缓冲区填充到一定容量时才会发送给内核进行批量写入,减少了系统调用的开销。
打开、创建和关闭文件
open与close
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags); //文件名 打开方式
int open(const char *pathname, int flags, mode_t mode);//文件名 打开方式 权限
int close(int fd); //fd表示文件描述符,是先前由open或creat创建文件时的返回值。
open 可以打开一个已存在的文件或者创建一个新文件,并在内核态创建一个文件对象,返回相关的文件描述符。使用完文件以后,要记得使用 close 来关闭文件。一旦调用 close ,会使文件的打开引用计数减1,只有文件的打开引用计数变为0以后,文件才会被真正的关闭。
pathname
:指向以 null 结尾的字符串,表示要打开的文件的路径。flags
:指定文件的打开模式。常见的标志包括:O_RDONLY
:以只读模式打开文件。O_WRONLY
:以只写模式打开文件。O_RDWR
:以读写模式打开文件。O_CREAT
:如果文件不存在,则创建文件。O_TRUNC
:如果文件已存在,并且以写模式打开,则清空文件内容。O_APPEND
:写入时将数据追加到文件末尾。O_NONBLOCK,O_NDELAY :对管道、设备文件和socket使用,以非阻塞方式打开文件,无论有无数据读取或等待,都会立即返回进程之中
mode
:(可选)当使用O_CREAT
标志时,指定文件的权限模式。通常使用八进制表示,例如:0644
:所有者可读写,组用户和其他用户可读。0755
:所有者可读写执行,组用户和其他用户可读执行。
open 成功时返回一个非负的文件描述符,失败时返回
-1
,并设置errno
以指示错误原因。成功时返回
0
。失败时返回-1
,并设置errno
以指示错误原因。在使用 open 系统调用的时候,内核会按照最小可用的原则分配一个文件描述符。一般情况下, 进程一经启动就会打开3个文件对象,占用了0、1和2文件描述符,分别关联了标准输入、标准输出和标准错误输出,所以此时再打开的文件占用的文件描述符就是3。
基本的读写操作
read和write
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);//文件描述符 缓冲区 缓冲区长度上限
ssize_t write(int fd, const void *buf, size_t count);//文件描述符 缓冲区 内容长度
read
fd
:文件描述符,标识要从中读取数据的文件或设备。buf
:指向缓冲区的指针,读取的数据将存储在这个缓冲区中。count
:指定最多读取的字节数。注意这里是“最多”,意味着文件大小超出count则会先读取count个字节,长度不足count,那么本次 read 会读取文件剩余内容;成功时返回实际读取的字节数。如果读取到文件末尾(EOF),返回
0
。如果发生错误,返回
-1
,并设置errno
以指示错误原因。read 的原理是将数据从文件对象内部的内核文件缓冲区拷贝出来(这部分的数据最初是在外部设备中,通过硬件的IO操作拷贝到内存之上)到用户态的buf之中。
write
fd
:文件描述符,标识要写入数据的文件或设备。buf
:指向要写入的数据的缓冲区的指针。count
:指定要写入的字节数。注意如果写入不足count个字节则会默认写入多余空格补足成功时返回实际写入的字节数。
如果发生错误,返回
-1
,并设置errno
以指示错误原因。write 将数据从用户态的buf当中拷贝到内核区的文件对象的内核文件缓冲区,并最
终会写入到设备中。
实战:通过基本文件读写操作实现文件复制
//copy.c
int main(int argc, char const *argv[])
{ARGS_CHECK(argc, 3);int fd1 = open(argv[1], O_RDONLY);ERROR_CHECK(fd1, -1, "open fdr");int fd2 = open(argv[2], O_WRONLY|O_CREAT, 0666);ERROR_CHECK(fd2, -1, "open fdw");char buf[1024]; //这里的缓冲区设置的较小,如果要复制很大的文件,请调整大小while (1){memset(buf, 0, sizeof(buf));int ret = read(fd1, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");if(ret == 0){break;}write(fd2, buf, strlen(buf));}close(fd1);close(fd2);return 0;
}
在上述代码cp命令的实现之中,我们可以调整buf数组的长度来影响的程序执行的效率。经过测试,buf的长度越大,则整个程序的执行效率越高,其原因也很简单, read / write 是系统调用,每次执行都需要一段时间来让硬件的状态在用户态和内核态之间切换,在文件长度固定的情况下,buf长度越大,read / write 的执行次数越少,自然效率就越高。
实战:通过基本文件操作读取格式化表格数据并输出
文件偏移
lseek
系统调用 lseek 可以(内核中的)文件对象的文件读写偏移量设定到以whence为启动,偏移值为offset的位置。它的返回值是读写点距离文件开始的距离。(所以 lseek 其实可以用来获取文件的大小)
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
fd
:文件描述符,标识要操作的文件。offset
:偏移量,表示要移动的字节数。可以是正数(向前移动)、负数(向后移动)或零。whence
:指定偏移量的参考位置。常见的值包括:SEEK_SET
:文件开始位置(offset
是相对于文件开头的偏移量)。SEEK_CUR
:当前位置(offset
是相对于当前文件指针位置的偏移量)。SEEK_END
:文件末尾位置(offset
是相对于文件末尾的偏移量)。
成功时返回新的文件指针位置(从文件开头开始计算的字节数)。
如果发生错误,返回
-1
,并设置errno
以指示错误原因。
示例:
int main(int argc, char const *argv[])
{ARGS_CHECK(argc, 2);int fd = open(argv[1], O_RDONLY);ERROR_CHECK(fd, -1, "open fd error");char buf[10];int ret = read(fd, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("the first read is : %s\n",buf);ret = read(fd, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("the second read is : %s\n",buf);lseek(fd, 0, SEEK_SET);ret = read(fd, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("the lseek read is : %s\n",buf);close(fd);return 0;
}
结果如下:
the first read is : In a small
the second read is : village,
the lseek read is : In a small
截断文件
ftruncate
使用 ftruncate 函数可以截断文件,从而控制文件的大小。它可以将文件的大小截断为指定的长度,如果文件原来比指定长度大,则多余的部分会被截断;如果文件原来比指定长度小,则文件会被扩展,新扩展的部分通常会被填充为零。
#include <unistd.h>
int ftruncate(int fd, off_t length);
fd
:文件描述符,标识要调整大小的文件。length
:新的文件大小(以字节为单位)。成功时返回
0
。如果发生错误,返回
-1
,并设置errno
以指示错误原因。ftruncate 特别适合用于扩展文件或者给文件预留空间,扩展文件时会自动将多余部分填充为0,在平常进行下载操作就是事先在磁盘预留空间防止之后下载文件空间不足。
示例:
int main(int argc, char *argv[])
{ARGS_CHECK(argc,2);int fd = open(argv[1],O_WRONLY);ERROR_CHECK(fd,-1,"open");printf("fd = %d\n",fd);off_t length = 3;int ret = ftruncate(fd,length);ERROR_CHECK(ret,-1,"ftruncate");return 0;
}
假如在上述的例子,把length的长度设置得特别大(比如40960或者更大),然后我们使用stat命令来查看文件实际所分配的磁盘空间大小,会发现文件大小居然会大于分配的磁盘空间大小。这就意味着,文件已经占用了文件系统当中的空间,但是底层磁盘还没有为其分配真正的磁盘块,这就是文件空洞。
文件映射
mmap
使用 mmap 系统调用可以实现文件映射功能,也就是将一个磁盘文件直接映射到内存用户态地址空间的一片区域当中,这样的话,内存内容就和磁盘文件内容一一对应,也不再需要使用 read 和 write 系统调用就可以进行IO操作,直接读写内存数据即可。此时读写内存等价于读写磁盘。
需要注意的是, mmap 不能修改文件的大小,所以需要经常配合函数 ftruncate 来使用。
#include <sys/mman.h>
void *mmap(void *adr, size_t len, int prot, int flag, int fd, off_t off);
addr
:指定映射区域的起始地址。通常传入
NULL
,让操作系统选择合适的地址。
length
:指定映射区域的长度(以字节为单位)。
prot
:指定映射区域的保护属性。常见的值包括:
PROT_READ
:映射区域可读。PROT_WRITE
:映射区域可写。PROT_EXEC
:映射区域可执行。PROT_NONE
:映射区域不可访问。
flags
:指定映射的类型和行为。常见的值包括:
MAP_SHARED
:映射区域对其他进程可见(即修改会反映到文件中)。MAP_PRIVATE
:映射区域对其他进程不可见(即修改不会反映到文件中)。MAP_FIXED
:强制使用指定的addr
地址(不推荐,除非必要)。
默认使用
MAP_SHARED 即可
fd
:文件描述符,标识要映射的文件或设备。必须是有效的、已打开的文件描述符。
offset
:指定文件中映射的起始位置(以字节为单位)。通常必须是页面大小的倍数(如 4096 字节)。
成功时返回映射区域的起始地址。注意这里返回值 void * 是一个万能指针,接受时必须进行强制转换。
如果发生错误,返回
(void *)-1
,并设置errno
以指示错误原因。
注意事项
页面对齐:
offset
必须是页面大小的倍数(通常是 4096 字节)。可以通过sysconf(_SC_PAGESIZE)
获取页面大小。内存保护:
prot
参数必须与文件的打开模式一致。例如,如果文件是以只读模式打开的,prot
不能包含PROT_WRITE
。取消映射:使用完映射区域后,必须调用
munmap
取消映射,以释放资源。文件大小:如果文件大小小于映射区域的长度,可能会导致未定义行为。建议在映射前确保文件大小足够。
munmap
munmap
用于取消内存映射。
int munmap(void *addr, size_t length);
addr
:映射区域的起始地址。length
:映射区域的长度。成功时返回
0
。如果发生错误,返回
-1
,并设置errno
以指示错误原因。mmap
:将文件或设备的内存映射到进程的地址空间,允许直接通过指针访问文件内容。munmap
:取消内存映射,释放资源。
示例:
int main(int argc, char *argv[]){// ./mmap file1ARGS_CHECK(argc,2);// 先 open 文件int fd = open(argv[1],O_RDWR);ERROR_CHECK(fd,-1,"open");// 建立内存和磁盘之间的映射char *p = (char *)mmap(NULL,5,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);ERROR_CHECK(p,MAP_FAILED,"mmap");//mmap失败返回不是NULLfor(int i = 0; i < 5; ++i){printf("%c", *(p+i));}printf("\n");*(p+4) = 'O';munmap(p,5);close(fd);return 0;
}
文件描述符的复制
在一些多线程和多进程情况下,不同的线程可能会同时操作同一个文件,如果线程之间的共享同一个文件描述符会导致冲突。dup和dup2通过复制文件描述符很好的解决了这个问题,复制文件描述符后,新的文件描述符与原始文件描述符是独立的,关闭其中一个不会影响另一个。
dup和dup2
所谓文件描述符的复制并不是简单地拷贝一份文件描述符的整数值,而是使用一个新的文件描述符去引用同一个文件对象。dup 返回一个新的文件描述符,该文件描述符是自动分配的,数值是没有使用的文件描述符的最小编号。该描述符与 oldfd
共享同一个文件表项。同时二者共享偏移量。
dup2 允许调用者用一个有效描述符(oldfd)和目标描述符(newfd)。函数成功返回时,目标描述符将变成旧描述符的复制品,此时两个文件描述符现在都指向同一个文件,并且是函数第一个参数(也
就是oldfd)指向的文件(如果 newfd
已经打开,它会被关闭,然后重新指向 oldfd
)。
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
如果发生错误,返回
-1
,并设置errno
以指示错误原因。
示例:
int main(int argc, char const *argv[])
{ARGS_CHECK(argc, 2);int fd1 = open(argv[1], O_RDWR);ERROR_CHECK(fd1, -1, "open fd1 error");printf("old fd = %d\n", fd1);int fd2 = dup(fd1);ERROR_CHECK(fd2, -1, "dup fd2 error");printf("new fd = %d\n", fd2);char buf[10];int ret = read(fd1, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("old fd: %s\n",buf);ret = read(fd1, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("old fd: %s\n",buf);ret = read(fd2, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("new fd: %s\n",buf);lseek(fd2, 0, SEEK_SET); //将fd2位置重置到开头ret = read(fd1, buf, sizeof(buf));ERROR_CHECK(ret, -1, "read error");printf("old fd: %s\n",buf);close(fd1);close(fd2);return 0;
}
输出结果:
old fd = 3
new fd = 4
old fd: In a small
old fd: village,
new fd: there live
old fd: In a small
可以观察到新文件描述符和旧文件描述符是共享偏移量的
通过复制文件描述符观察printf的输出结果
#include<54func.h>int main(int argc, char const *argv[])
{ARGS_CHECK(argc, 2);int fd = open(argv[1], O_RDWR); ERROR_CHECK(fd, -1, "open fd error");int tempfd = 10;int ret = dup2(STDOUT_FILENO, tempfd);//备份标准输出流ERROR_CHECK(ret, -1, "dup2 error");printf("hello 1\n");ret = dup2(fd, STDOUT_FILENO); //将文件描述符转移到标准输出中printf("hello 2\n");ret = dup2(tempfd, STDOUT_FILENO); //将备份还原printf("hello 3\n");close(tempfd);close(fd);return 0;
}
输出结果:
./file2 test1.txt
//命令行显示
hello 1
hello 3//文件中显示
hello 2
可以观察到,printf默认会向STDOUT_FILENO 中写入数据,并由操作系统输出到命令行中
从OS 底层分析 mmap 与 read/write 之间的效率
1. 数据拷贝次数
read/write
:read
和write
系统调用需要在内核空间和用户空间之间进行数据拷贝。具体来说:读操作:数据从磁盘读取到内核的页缓存(page cache),然后从页缓存拷贝到用户空间的缓冲区。
写操作:数据从用户空间的缓冲区拷贝到内核的页缓存,然后异步写入磁盘。
这种方式涉及两次数据拷贝,增加了系统调用的开销。
mmap
:mmap
将文件的某一部分直接映射到进程的地址空间,允许进程直接访问内核的页缓存。数据不需要在内核空间和用户空间之间拷贝,减少了数据传输的开销。
写操作时,数据直接写入映射的内存区域,内核会负责将修改的页面(dirty pages)异步写入磁盘。
2. 系统调用开销
read/write
:每次读写操作都需要执行系统调用,从用户态切换到内核态,完成后再切换回用户态。
多次系统调用会增加上下文切换的开销。
mmap
:只需要一次系统调用(
mmap
)来建立内存映射。一旦映射完成,后续的读写操作直接在用户空间完成,无需再次进行系统调用。
3. 内存管理与页错误
read/write
:使用内核的页缓存机制,数据存储在内核空间的缓冲区中。
不涉及复杂的内存映射和页错误处理。
mmap
:使用内核的页缓存,但通过内存映射的方式直接暴露给用户空间。
当访问未映射的页面时,会触发页错误(page fault),内核会动态加载缺失的页面。
这种按需加载的方式可以节省内存,但如果频繁触发页错误,可能会增加开销。
4. 适用场景
read/write
:适用于小数据量的文件操作,因为系统调用和数据拷贝的开销相对较小。
对于频繁的小块读写操作,效率较高。
mmap
:适用于大文件的随机访问,尤其是需要频繁读写同一文件区域的场景。
在处理大文件时,可以显著减少数据拷贝和系统调用的开销。
但需要注意,
mmap
的初始化开销较大,对于小数据量或不频繁的文件访问,可能不如read/write
高效。read/write 在顺序读写的时候性能更好,而 mmap 在随机访问的时候性能更好。
文件流和文件描述符之间的关系
fopen 函数实际在运行的过程中也获取了文件的文件描述符。使用 fileno 函数可以得到文件流的文件描述符。在使用 fopen 打开文件流之后,依然是可以使用文件描述符来执行IO的。
示例:
int main(int argc, char *argv[])
{// ./fileno file1ARGS_CHECK(argc,2);FILE * fp = fopen(argv[1],"w+");ERROR_CHECK(fp,NULL,"fopen");printf("%d\n", fileno(fp)); //可以查看对应的文件描述符write(fileno(fp),"hello",5);fclose(fp);return 0;
}
fopen 的原理: fopen 函数在执行的时候,会先调用 open 函数,打开文件并且获取文件对象的信息(通过文件描述符可以获取文件对象的具体信息),然后 fopen 函数会在用户态空间申请一片空间作为缓冲区;
fopen 的优势:因为 read 和 write 是系统调用,需要频繁地切换用户态和内核态,所以比较耗时。借助用户态缓冲区,可以减少 read 和 write 的次数。
从另一方面来说,如果需要高效地使用不带缓冲IO,为了和存储体系匹配,最好是一次读取/写入一个块大小的数据。如果获取了文件指针,就不要通过文件描述符的方式来关闭文件