无缓冲文件流
带用户态缓冲区的文件流:
- 这类文件流属于语言的库函数,是对系统调用的封装。
- 它们使用用户态缓冲区来减少系统调用的次数,提高 I/O 操作的效率。
- 用户程序通过缓冲区与文件进行交互,而不需要直接进行系统调用。
用户态缓冲区的作用:
- 用户态缓冲区作为数据传输的中间层,由语言的标准库管理。
- 它的存在简化了用户程序的 I/O 操作,因为用户程序不需要关心缓冲区的内部管理。
无缓冲的文件 I/O:
- 在某些情况下,应用程序可能需要直接与内核进行数据交互,而不是通过用户态缓冲区。
- 无缓冲的 I/O 通常使用系统调用直接进行,这种 I/O 操作方式可以提供更直接的控制,但可能牺牲一些性能。
在传统的文件 I/O 操作中,通常使用带用户态缓冲区的文件流,如通过 fopen
等 C 语言库函数实现。这些函数对系统调用进行了封装,引入了用户态缓冲区,使得用户进程与内核之间的数据交互更加高效和方便。
用户态缓冲区由 C 语言标准库创建和管理,它的大小通常为 4KB 或 8KB。这种缓冲区起到了数据的“中间商”作用,内核中的数据首先流入这个缓冲区,然后再传输到用户自己申请的内存中。通过这种方式,一次系统调用可以交换大量数据,从而减少了系统调用的次数,提升了 I/O 操作的性能。
然而,在某些场景下,我们可能不希望有这个中间商的存在。例如,在需要直接与内核进行数据交互的场合,或者在对性能要求极高的应用中,用户进程希望能够绕过用户态缓冲区,直接进行系统调用。这时,就需要使用不带用户态缓冲区的文件流,也就是无缓冲 I/O。
无缓冲 I/O,或称为直接 I/O,是一种不通过用户态缓冲区进行数据交互的 I/O 操作方式。在这种方式下,用户进程直接与内核态缓冲区进行交互,绕过了用户态缓冲区。这种方式可以减少数据复制的开销,提高数据传输效率,但也要求用户进程更加谨慎地管理数据的读取和写入。
在无缓冲 I/O 中,用户进程需要自己申请内存空间用于存储从磁盘读取的数据或待写入磁盘的数据。然后,通过系统调用直接与内核态缓冲区进行数据交换,实现纯粹的内存数据交互。
带用户态缓冲区的文件流和无缓冲 I/O 各有适用场景。前者通过用户态缓冲区简化了 I/O 操作,提高了性能;后者则提供了更直接、更高效的数据交互方式,适用于对性能要求极高的应用。
在操作系统中,内核区域负责管理文件系统和磁盘设备。为了实现高效的数据交互,内核区域使用了一系列关键概念和数据结构。每个文件在内核中都由一个“文件对象”来表示,它是一个临时存储文件数据的数据结构。文件对象内部包含一个“内核缓冲区”,用于暂存从磁盘读取的数据或待写入磁盘的数据,并通过一个指针来跟踪当前的读写位置。
由于安全和设计的原因,内核缓冲区不能直接暴露给用户进程。因此,内核维护了一个“索引指针数组”,用于间接地管理文件对象。索引指针数组中的每个元素都是指向一个文件对象的指针,而数组元素的下标,即“文件描述符”,作为文件的唯一标识符,用于用户进程与内核之间的交互。
文件描述符是一个非常重要的概念,它允许用户进程通过一个整数来引用特定的文件对象。用户进程通过文件描述符与内核进行系统调用交互,实现无缓冲 I/O 操作。例如,当用户进程需要读取文件数据时,它会通过文件描述符请求内核从磁盘读取数据到内核缓冲区,然后再从内核缓冲区复制到用户空间的缓冲区。
在用户空间,应用程序通常会分配一个缓冲区用于存储与文件交互的数据。通过这种方式,用户进程可以避免直接与内核缓冲区交互,从而简化了编程模型并提高了安全性。
内核区域通过文件对象、内核缓冲区、索引指针数组和文件描述符等机制,实现了对文件的高效管理和用户进程的安全交互。用户进程通过文件描述符与内核进行无缓冲 I/O 操作,这种方式在某些场景下可以提高数据交换的效率,尤其是在对性能要求极高的应用中。
关于文件描述符:
当用户在 Linux 系统中打开一个文件时,内核会创建一个对应的文件对象。这个文件对象是内核用来临时存储和操作文件数据的数据结构。为了安全和组织的原因,内核并不直接将文件对象返回给用户进程,而是为每个文件对象分配一个唯一的编号,即文件描述符。
文件描述符的分配是通过一个索引指针数组来管理的。数组中的每个元素都是一个指针,指向一个文件对象。当用户打开文件时,内核会返回数组中新分配的元素的下标,这个下标就是文件描述符。用户进程随后使用这个文件描述符来执行系统调用级别的文件操作,如读取、写入和关闭文件。
安全性: 使用文件描述符的方式提高了安全性,因为在整个过程中,用户进程没有获得内核的任何地址信息,也没有与内核区域进行直接的数据交互。这种间接的交互方式限制了用户进程对内核的操作,从而减少了潜在的安全风险。
系统调用: 用户进程通过文件描述符与内核进行系统调用级别的交互。例如,read()
和 write()
系统调用使用文件描述符来确定要操作的文件,而 close()
系统调用使用文件描述符来关闭文件。
文件描述符的限制: 文件描述符的数量是有限的,通常由系统设置的最大文件描述符数量决定。这意味着一个进程能够同时打开的文件数量是有限的。
文件描述符是用户进程与操作系统内核交互的桥梁,它提供了一种安全、间接的方式来访问和管理文件。通过使用文件描述符,用户进程可以在不同的系统调用中指定要操作的文件,而无需直接与内核缓冲区交互。
文件流相关函数
open
打开文件系统调用
open
函数是 POSIX 标准中用于打开文件的系统调用,其原型在 fcntl.h
头文件中声明。open
函数提供了两种重载形式,一种接受两个参数,另一种接受三个参数。
第一种形式:接受两个参数
int open(const char *pathname, int flags);
参数:
pathname
:一个指向以 null 结尾的字符串的指针,指定了要打开的文件的路径。-
flags
:一个或多个标志的组合,用来指定文件打开的方式。这些标志包括:
标志 | 描述/含义 |
---|---|
O_RDONLY | 以只读的方式打开 |
O_WRONLY | 以只写的方式打开 |
O_RDWR | 以可读可写的方式打开 |
O_CREAT | 如果文件不存在,则创建文件。如果不添加此标志,那么文件不存在时,将打开失败 |
O_EXCL | 仅与 O_CREAT 连用,单独使用无意义。如果文件已存在,则 open 失败 |
O_TRUNC | 如果文件已存在且成功以写入模式打开,则将其长度截断为 0,即删除文件内容。 |
O_APPEND | 以追加模式打开文件,不能和 O_RDONLY 或者 O_TRUNC 连用。 |
返回值:
- 成功时,返回一个非负的文件描述符(FD),用于在后续操作中引用该文件。
- 失败时,返回
-1
,并设置全局变量errno
以指示错误类型。
第二种形式:接受三个参数
int open(const char *pathname, int flags, mode_t mode);
除了包含第一种形式的所有参数外,还增加了:
mode
:当创建新文件时,此参数指定文件的权限模式。mode_t
类型通常定义在 <sys/types.h>
头文件中。这个模式通常由文件系统上的 umask 值修改。
这个形式的 open
函数在指定要创建新文件的情况下非常有用,它允许你设置新文件的权限。
关于 flags
的宏标志:
当用户打开文件时,Linux 系统内核会根据提供的 flags
参数创建相应的文件对象。flags
参数是一个 32 位的整数,每一位的设置代表不同的文件打开行为。
在设置文件打开模式时,需要使用位运算符按位或(|
)来组合多个宏标志。然而,并非所有宏标志都可以组合使用。例如,O_RDONLY
、O_WRONLY
和 O_RDWR
这三个访问模式标志是互斥的,只能选择其中一个来确定文件的访问方式。它们通常使用 32 位整数的低 2 位来表示,确保了它们的互斥性。
另外,某些宏标志的组合有特定的限制。O_APPEND
标志只能与允许写入的模式(O_WRONLY
或 O_RDWR
)一起使用,因为它用于将数据追加到文件末尾。同时,O_APPEND
与 O_TRUNC
是冲突的,因为 O_TRUNC
用于截断文件,而 O_APPEND
用于在文件末尾追加数据。
fopen
函数是 C 语言中用于打开文件的标准库函数,它的行为依赖于传入的模式参数。这些模式实际上是通过组合不同的宏标志来控制文件的打开方式:
"r"
:以只读方式打开文件,等同于O_RDONLY
。"r+"
:以读写方式打开文件,等同于O_RDWR
。"w"
:以只写方式打开文件,如果文件存在则截断文件,等同于O_WRONLY | O_CREAT | O_TRUNC
。"w+"
:以读写方式打开文件,如果文件存在则截断文件,等同于O_RDWR | O_CREAT | O_TRUNC
。"a"
:以追加模式打开文件,等同于O_WRONLY | O_CREAT | O_APPEND
。"a+"
:以读写追加模式打开文件,等同于O_RDWR | O_CREAT | O_APPEND
。
close
关闭文件系统调用
close
函数是 POSIX 标准中用于关闭文件描述符(file descriptor,FD)的系统调用,其原型在 unistd.h
头文件中声明。
函数原型:
int close(int fd);
参数:fd
:文件描述符,是一个非负整数,表示要关闭的文件或设备。
功能:close
函数关闭指定的文件描述符,释放与该文件描述符相关的所有资源。
返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
使用 close
函数时,应当总是在文件操作完成后调用它,以确保及时释放系统资源。文件描述符是操作系统用来跟踪打开文件和网络连接等资源的整数。当程序不再需要这些资源时,使用 close
函数可以防止资源泄露。
read
读取文件系统调用
read
函数是 POSIX 标准中用于从指定的文件描述符(file descriptor,FD)读取数据的系统调用,其原型在 unistd.h
头文件中声明。
函数原型:
ssize_t read(int fd, void *buf, size_t count);
参数:
fd
:文件描述符,是一个非负整数,表示要读取数据的文件或设备。buf
:指向一个内存缓冲区的指针,read
函数会将读取的数据存储在这个缓冲区中。缓冲区的大小应该足够大,以容纳count
参数指定的字节数。count
:要读取的字节数。
返回值:
- 成功时,
read
函数返回实际读取的字节数,这个值通常小于或等于count
。如果已到达文件末尾(end-of-file,EOF),并且没有更多数据可读,它将返回 0。 - 如果函数执行失败,返回
-1
,并设置全局变量errno
以指示错误类型。
ssize_t
是一个可签名的整数类型,用于表示有符号的大小,它可以表示负数,这在出现错误时很有用。
read
和 fread
函数的差异:
read
和 fread
函数是两种不同的文件读取方法,它们在读取数据时的计量单位和行为上存在以下差异
- 计量单位:
read
函数读取的是字节数量,它只接受一个表示数量的参数,即count
,表示一次性读取多少个字节的数据。 - 参数差异:
fread
函数读取的是元素的数量,它接受两个表示数量的参数:size
和count
。其中size
表示每个元素的大小,而count
表示一次性读取的元素个数。 - 返回值类型:
fread
函数返回的是size_t
类型,即无符号整数,表示一次性读取到的元素个数。当函数读到文件末尾或调用出错时,返回 0。 read
函数的返回值:read
函数返回的是ssize_t
类型,即有符号整数,表示一次性读取到的字节数量。当函数读到文件末尾时返回 0,表示已经读取完毕;函数出错时返回-1。- 写入函数的差异性:类似地,
write
和fwrite
函数在写入数据时也存在类似的差异。write
函数写入字节,而fwrite
函数写入元素,它们的行为和返回值类型与read
和fread
相对应。
write
写入文件系统调用
write
函数是 POSIX 标准中用于向指定的文件描述符(file descriptor,FD)写入数据的系统调用,其原型同样在 unistd.h
头文件中声明。
函数原型:
ssize_t write(int fd, const void *buf, size_t count);
参数:
fd
:文件描述符,是一个非负整数,表示要写入数据的文件或设备。buf
:指向要写入的数据缓冲区的指针,该区域包含了要写入的数据。count
:要写入的字节数。
返回值:
- 成功时,
write
函数返回实际写入的字节数,这个值通常小于或等于count
。 - 如果函数执行失败,返回
-1
,并设置全局变量errno
以指示错误类型。
ssize_t
是一个可签名的整数类型,它用于表示写入操作的结果,可以是正数、零或负数。负数表示出现了错误。
ftruncate
改变打开文件的长度
ftruncate
函数是 POSIX 标准中用于截断或扩展文件长度的系统调用,其原型在 unistd.h
头文件中声明。
函数原型:
int ftruncate(int fd, off_t length);
参数:
fd
:文件描述符,是一个非负整数,表示要操作的文件。length
:新的文件长度,off_t
类型,是一个可签名的整数类型,表示文件应该被截断或扩展到的长度(字节为单位)。
功能:
ftruncate
函数将指定文件描述符fd
引用的文件截断到length
参数指定的长度。如果文件当前的长度大于length
,则文件将被截断,超出部分将丢失。如果文件当前的长度小于length
,则文件将被扩展,新的空间将被分配,但文件内容不确定(通常是零)。
返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
ftruncate()
函数扩展文件大小的注意事项
两个关键概念:文件大小和块的数量。文件大小是指文件中实际数据的总和,通常以字节计量。而块的数量则表示文件在磁盘上占用的块的总数,这与文件系统使用的块大小密切相关。通常情况下,文件占用的磁盘块数量所表示的大小应该大于或等于文件的实际大小,因为文件系统按照固定大小的块来分配存储空间。
当使用 ftruncate()
函数来扩展文件大小时,如果新增加的空间仅填充了空字符,那么这部分空间实际上并没有被真实数据占用。这意味着,尽管文件的大小在逻辑上增加了,但在物理存储上并没有额外占用磁盘空间。
以一个具体的例子来说明,如果一个文件原本大小为 0 字节,通过 ftruncate()
函数将其大小扩展到 10000 字节,但文件的实际内容仍然是空的,那么这个文件在磁盘上的占用可能仍然是 0 字节。这是因为新增的字节都是空字符,文件系统可能不会为这些空字符分配实际的存储空间。
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 3);
int fd = open(argv[1], O_RDWR | O_CREAT, 0666);
ERROR_CHECK(fd, -1, "open");
int len;
sscanf(argv[2], "%d", &len);
int ret = ftruncate(fd, len);
ERROR_CHECK(ret, -1, "ftruncate");
close(fd);
return 0;
}
如上图,使用 vim 编辑 file 文件后,块的数量增加到 24,也就是三个磁盘块。
文件空洞
利用 ftruncate()
函数,可以在文件系统中为文件预分配空间,这个过程称为“文件空洞”。文件空洞允许文件在逻辑上具有特定的大小,而实际上磁盘上可能还没有存储全部的数据信息。
文件空洞现象的作用:
- 文件空洞可以显著提高磁盘操作的性能。在 VFS 中提前占位是一个非常迅速的过程,而物理磁盘上的数据分配则相对缓慢。
- 这种技术在处理大文件下载时特别有用。例如,在下载大文件的过程中,由于网络和硬盘性能的限制,整个下载过程可能非常漫长。
在下载任务中的应用:
- 在下载任务开始时,使用
ftruncate()
函数预分配文件所需的空间。这样,即使磁盘空间分配速度较慢,文件的逻辑大小已经确定,可以避免因磁盘空间不足导致下载失败的情况。 - 一旦预分配完成,下载任务可以逐步将数据写入磁盘,而不必担心空间不足的问题。
实现方式:
- 在下载开始前,使用
ftruncate()
为文件分配所需的全部空间。 - 随后,下载的数据可以逐步写入已经预留的空间中。
无缓冲文件 IO 写操作
文件写入和读取的原则:
- 数据一致性:数据应当以写入时的相同形式被读取。如果以字符串的形式写入,那么在读取时也应当以字符串的形式处理。
- 类型匹配:写入文件时的数据类型应当与读取时的数据类型一致。这意味着,如果以整数形式写入,也应当以整数形式读取。
文本文件与二进制文件的比较:
- 文本文件:
- 存储字符的编码值,遵循特定的编码规则(如 UTF-8、ASCII 等)。
- 文本编辑器可以根据编码规则解码,使得文本文件可以被人类直接阅读。
- 缺点是同等大小的文件存储的数据量较少,且读取时通常需要进行类型转换。
- 二进制文件:
- 直接存储字节序列,不遵循字符编码规则,而是遵循特定的编码规则。
- 这些规则通常是为了效率和存储特定类型的数据,而不是为了人类阅读。
- 优点是同等大小的文件可以存储更多的数据,且读取时不需要进行类型转换,因为数据的类型在写入和读取时保持一致。
文本文件更适合人类可读性和便携性,而二进制文件则更适合高效的数据存储和传输。
文件复制
有缓冲 I/O 的文件复制
在有缓冲 I/O 操作中,用户进程与用户缓冲区直接进行数据交互。用户缓冲区充当数据的中间存储,然后再由用户缓冲区与内核态缓冲区进行系统调用级别的数据交互。
- 用户缓冲区的作用: 用户缓冲区通常大小固定,不会很小(一般为 4KB 或 8KB)。它的存在可以在很多情况下有效减少系统调用的次数,尤其是在进程自身的缓冲数组较小时。
- 性能提升: 通过减少系统调用次数,用户缓冲区可以显著提升 I/O 操作的整体性能。这种设计确保了 I/O 操作性能的基本下限,避免了因频繁大量的系统调用而导致的性能下降。
- 系统调用: 系统调用是用户缓冲区与内核态缓冲区之间数据交互的桥梁。用户缓冲区的引入,使得 I/O 操作性能不会因为无意义的系统调用而降低太多。
- 性能瓶颈: 如果使用不带用户缓冲区的 I/O 流,在很多情况下,I/O 操作的性能可能不如带缓冲区的情况。用户缓冲区的设计机制在一定程度上防止了性能瓶颈的出现。
- 用户空间与内核区域: 用户进程在用户空间内操作,通过 FILE* 指针和 char buff [] 等数据结构与内核区域进行交互。磁盘数据通过内核态缓冲区与用户缓冲区进行数据传输。
有缓冲 I/O 通过引入用户缓冲区,提高了文件操作的性能,减少了系统调用的频率,并且提供了一种有效的数据交换机制。这种设计对于大多数应用场景来说是有益的,尤其是在处理大量数据时,可以显著提高效率。
无缓冲区的 I/O 流
在不带用户缓冲区的 I/O 操作中,用户进程的缓冲数组 buf
直接与内核系统调用进行交互。这种直接交互方式意味着数据在用户空间和内核空间之间的传输没有中间的用户态缓冲区作为中介。
- 内核缓冲区的作用: 内核缓冲区是内核区域中用于临时存储文件数据的缓冲区。它通过指针来追踪当前的读写位置。
- 系统调用的交互: 用户进程通过系统调用直接与内核进行数据交互,这种方式在处理大量数据时可能涉及到多次系统调用。
- 性能影响: 如果用户进程的缓冲数组
buf
较小,那么在进行文件 I/O 操作时,可能会因为系统调用次数过多而导致整体 I/O 操作性能急剧下降。 - 文件对象的操作: 在无缓冲 I/O 中,用户进程可能需要直接处理文件对象,如
src
和dest
文件对象,用于从源文件读取数据并写入目标文件。 - 磁盘 I/O: 无缓冲 I/O 直接涉及到磁盘 I/O 操作。用户进程的缓冲数组
buf
直接与磁盘进行数据交换,这在某些场景下可以减少数据复制的开销。
不带用户缓冲区的 I/O 流提供了一种直接与内核系统调用交互的方式,这种方式在某些特定场景下可以提高性能,尤其是在缓冲区较小且需要频繁进行系统调用的情况下。然而,这种方式也可能导致性能下降,因为每次系统调用都可能涉及磁盘 I/O,增加了系统的负载。
有缓冲 I/O 和无缓冲 I/O 的比较
有缓冲 I/O 和无缓冲 I/O 文件复制各有其适用场景和优缺点。在许多情况下,使用无缓冲文件流可能不如使用带用户缓冲区的普通文件流效率高,特别是在没有设置合理缓冲区的情况下。
无缓冲 I/O 主要适用于以下场景:
- 实时性要求高的场景:
read()
和write()
提供了更直接的磁盘访问,允许更精确地控制数据读写时机。在需要即时处理数据的实时系统中,使用带缓冲的读写可能不合适。 - 系统编程:在 Linux 系统编程中,有些操作只支持
read
和write
。例如,读写某些设备文件、管道的进程间通信、网络通信等,这些场景通常只能使用无缓冲 I/O。
然而,在大多数普通文件操作场景中,使用带缓冲的文件流是更优选择,特别是:
- 需要跨平台的文件操作时,带缓冲的文件流可以提供更好的兼容性和便利性。
- 处理文本文件的操作时,可以使用
fgets
/fputs
这样的函数读写整行数据,这更符合人类的阅读习惯。
选择有缓冲 I/O 还是无缓冲 I/O 取决于具体的应用场景和性能需求。在需要高实时性和直接控制磁盘访问的场合,无缓冲 I/O 可能是更好的选择。而在大多数其他情况下,带缓冲的文件流因其便利性和效率优势,通常是首选。
文件映射
mmap
内存映射函数
mmap
函数是一种在 POSIX 系统上将文件或其他对象映射到内存的机制。使用 mmap
,你可以将一个文件的内容映射到进程的地址空间,实现文件与内存之间的高效数据交换。这个函数定义在 sys/mman.h
头文件中。
函数原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数:
addr
:内存映射的起始地址,通常设置为NULL
,由系统选择映射区域的地址。length
:映射区域的长度(字节为单位)。注意length
必须是大于 0 的整数值,不能等于 0,更不能是负数。prot
:期望的内存保护选项,可以是以下一个或多个标志的组合:PROT_EXEC
:允许执行内存内容。PROT_READ
:允许读取内存内容。PROT_WRITE
:允许写入内存内容。PROT_NONE
:页面不可访问。
flags
:控制映射特性的标志,常用的标志包括:MAP_SHARED
:对映射区域的修改会反映到文件上。MAP_PRIVATE
:私有的 copy-on-write 映射,对映射区域的修改不会反映到文件上。MAP_ANONYMOUS
:匿名映射,不与任何文件关联。MAP_FIXED
:将映射区域固定在addr
指定的地址。
fd
:被映射文件的文件描述符,如果是匿名映射则设置为-1
。offset
:文件映射的起始位置,通常为文件大小的整数倍。
返回值:
- 成功时,返回指向映射区域的指针。
- 失败时,返回
MAP_FAILED
(通常是(void *)-1
),并设置errno
以指示错误。
总线错误
int main(int argc, char *argv[]) {
ARGS_CHECK(argc, 2);
int fd = open(argv[1], O_RDWR);
ERROR_CHECK(fd, -1, "open");
// 从文件的开头映射 5 个字节的数据到内存中
// 假如文件是一个文本文件
char *p = (char *)mmap(NULL, 5, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
ERROR_CHECK(p, MAP_FAILED, "mmap");
// 修改一下文件数据
p[4] = 'a';
close(fd);
return 0;
}
(file 为空文件)
当 CPU 尝试访问不存在或不正确的物理地址时,可能会遇到一种称为总线错误(bus error)的硬件异常。在使用 mmap
函数进行内存映射时,这种情况尤其值得注意。因为 mmap
可能会映射一个比实际文件数据更大的区域,这就导致映射区域可能包含一些未初始化或不存在的空间。如果程序在映射完成后访问或修改这些不存在的区域,就可能触发总线错误。
为了避免这种情况,程序员在使用 mmap
函数时需要格外小心,确保对映射区域的所有操作都是正确和合理的。这包括避免映射过多的区域,以及不操作那些实际上不存在的区域。此外,还应该在程序中实现错误处理机制,以便在发生总线错误时能够妥善应对。
mmap
和 open
的关系
mmap()
函数和 open()
函数都涉及文件的读写权限,它们之间的关系很重要。
mmap()
的prot
参数:这个参数定义了映射区域的读写权限。它可以设置为PROT_READ
(只读)、PROT_WRITE
(只写)或PROT_RDWR
(读写)。open()
的flags
参数:这个参数定义了文件的打开模式,可以是O_RDONLY
(只读)、O_WRONLY
(只写)或O_RDWR
(读写)。
它们之间的关系:
- 如果
mmap()
的prot
参数设置为只读,那么open()
可以以只读模式打开文件。 - 如果
mmap()
的prot
参数设置为可写(无论是单独可写还是读写),那么open()
必须以读写模式打开文件。这是因为映射操作需要读取文件内容,如果文件无法读取,映射将无法完成。
使用 mmap()
函数时,open()
获得的文件描述符必须具有足够的权限来满足映射区域的读写需求。基本权限是读取权限,这是必须具备的,以确保映射操作可以成功执行。
munmap
撤销内存映射
munmap
函数用于撤销之前通过 mmap
函数建立的内存映射。这个函数也是 POSIX 标准的一部分,其原型定义在 sys/mman.h
头文件中。
函数原型:
int munmap(void *addr, size_t length);
参数:
addr
:要撤销映射的内存区域的起始地址。这应该是之前mmap
函数返回的地址。length
:要撤销映射的内存区域的长度(字节为单位)。这应该与创建映射时指定的长度相同。
功能:munmap
函数通知操作系统,应用程序不再需要从 addr
开始、长度为 length
的内存区域的映射。操作系统随后可以释放相关资源,并将内存区域恢复到未映射状态。
返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
mmap
系统调用的原理
在文件 I/O 操作中,文件映射(mmap)提供了一种与常规的 read/write 系统调用不同的数据交互方式。
- 页缓存(Page Cache): 页缓存是内核空间中的一片内存区域,用于存储文件数据的缓存。它在文件 I/O 操作时,直接与磁盘上的文件交互数据。
- 数据交互时机: 页缓存决定了内存和磁盘之间数据交换的时机。这个时机由内核中的一系列算法和硬件(如 DMA)共同决定和管理,程序员不需要直接考虑这些细节。
- mmap 系统调用过程: mmap 允许用户空间的程序直接在内存中映射文件。通过 mmap 申请开辟映射区域后,用户空间的程序可以直接操作映射的内存区域,而无需通过 read/write 系统调用。
- 文件映射的优势: 使用 mmap 进行文件映射可以提高性能,因为它减少了系统调用的开销,并且允许内核优化数据的读取和存储过程。
- 用户空间的 buf 和映射操作: 在用户空间,程序可以使用指针直接操作映射的内存区域(如 buf),对这些区域的修改可能直接影响到磁盘上的文件内容。
文件映射通过 mmap 系统调用,在用户空间创建了文件的内存映射,允许程序以更直接的方式访问和修改文件数据。页缓存机制使得数据交互更加高效,同时减轻了程序员管理内存和磁盘交互的负担。
mmap
函数和 read/write
函数的比较
mmap
和 read/write
是两种不同的文件 I/O 操作方式,它们各自适用于不同的应用场景。
- read/write:适用于顺序读写文件。数据流向是从硬盘上的文件,经过页缓存,再到文件对象,最后到达用户进程。这种方式在处理大量数据时,可能需要多次系统调用和数据复制,但在顺序访问时性能较好。
- mmap:适用于随机访问文件。数据流向是从硬盘上的文件到页缓存,然后直接映射到用户空间的内存区域。使用
mmap
时,用户可以直接操作这个内存区域来访问文件,减少了数据从内核空间到用户空间的复制步骤。
mmap
的优势在于减少了数据转移的过程,这在处理大型文件时尤为明显。在进行极大文件的复制操作时使用 mmap
可以提高效率,因为它允许程序以更接近硬件的方式直接访问文件数据。
然而 mmap
并不总是性能上优于 read/write
。由于顺序访问的性能通常远高于随机访问,所以在顺序读写场景下,read/write
可能仍然是更好的选择。
选择 mmap
还是 read/write
取决于具体的应用需求和场景。如果需要高效的随机访问或处理大型文件,mmap
可能是更合适的选择。而在顺序读写或对性能要求不是特别高的场景下,read/write
可能更加方便和高效。
lseek
移动文件读写位置
lseek
函数是 POSIX 标准中用于设置文件描述符 fd
的文件位置指针的系统调用。这个函数允许你移动文件的读写位置,而不影响文件的任何其他状态。lseek
函数原型定义在 unistd.h
头文件中。
函数原型:
off_t lseek(int fd, off_t offset, int whence);
参数:
fd
:文件描述符,是一个非负整数,表示要操作的文件。offset
:偏移量,表示从whence
指定的位置开始移动的字节数。这个值可以是正数或负数。- 如果是 0 表示不偏移
- 如果是负数表示向文件开头偏移
- 如果是正数表示向文件末尾偏移
whence
:指定offset
参数的解释方式,可以是以下宏之一:SEEK_SET
:设置文件位置指针到offset
字节的位置(从文件开头开始计数)。SEEK_CUR
:将文件位置指针向前(正数)或向后(负数)移动offset
字节。SEEK_END
:设置文件位置指针到文件末尾前offset
字节的位置(如果offset
为正,则从文件末尾开始计数;如果为负,则从文件开头开始计数)。
返回值:
- 成功时,
lseek
返回新的文件位置偏移量,从文件开头到当前位置的字节数。 - 如果函数执行失败,返回
(off_t)-1
,并设置全局变量errno
以指示错误类型。
off_t
是一个可签名的整数类型,用于表示文件位置的偏移量。
fstat
文件描述符获取文件信息
fstat
函数是 POSIX 标准中用于获取文件状态信息的系统调用,它类似于 stat
函数,但是专门用于通过文件描述符获取信息,而不是通过文件名。fstat
函数原型定义在 sys/stat.h
头文件中。
函数原型:
int fstat(int fd, struct stat *buf);
参数:
fd
:文件描述符,是一个非负整数,表示要获取信息的文件或文件描述符。buf
:指向struct stat
结构的指针,该结构用于接收关于文件的状态信息。
struct stat
结构包含了文件的各种属性,如文件大小、块大小、总块数、访问权限、所有者、组、访问时间、修改时间、状态改变时间等。
返回值:
- 如果函数成功执行,返回 0。
- 如果函数执行失败,返回 -1,并设置全局变量
errno
以指示错误类型。
memcpy
复制字节
memcpy
函数是 C 语言标准库中的一个函数,用于从源内存地址复制 n
个字节到目标内存地址。这个函数定义在 string.h
头文件中。
函数原型:
void *memcpy(void *dest, const void *src, size_t n);
参数:
dest
:指向目标内存的指针,memcpy
会将数据复制到这个位置。src
:指向源内存的指针,memcpy
会从这个位置复制数据。n
:要复制的字节数。
功能:memcpy
函数将 n
个字节的数据从 src
指向的源内存复制到 dest
指向的目标内存。
返回值:memcpy
返回一个指向目标内存的指针,即 dest
。
注意:
memcpy
函数不检查源和目标内存区域是否重叠。如果它们重叠,函数的行为是未定义的。如果需要在重叠的内存区域进行复制,应使用memmove
函数,后者可以正确处理重叠情况。memcpy
通常比memmove
快,因为它假设内存区域不重叠,并且可以实现更高效的优化。
重定向
标准流和重定向
在每个进程启动伊始,都会自动打开三个文件流,就是标准输入流 stdin
、标准输出流 stdout
、标准错误输出流 stderr
。
我们已经知道文件流的底层是文件描述符,那么这些标准流也是一样的,它们的底层分别对应文件描述符:
- 标准输入流
stdin
:宏 STDIN_FILENO,对应整数值 0 - 标准输出流
stdout
:宏 STDOUT_FILENO,对应整数值 1 - 标准错误流
stderr
:宏 STDERR_FILENO,对应整数值 2
我们已经知道文件流的底层结构体对象中会存储文件描述符,所以每次程序启动,这三个标准流的内存空间都固定存储这三个文件描述符。
这三个标准流由 C 标准库来管理维护,程序员只需要使用它们就可以,不需要考虑它们的内存管理问题。
close
和重定向
文件描述符的分配: 当调用 open
函数打开一个文件时,系统会为该文件分配一个文件描述符。文件描述符是一个整数,用作文件的唯一标识符。通常情况下,open
会分配当前可用的最小文件描述符。由于标准输入(0)、标准输出(1)和标准错误(2)已经被占用,所以 open
通常从文件描述符 3 开始分配。
close
函数的作用: close
函数用于关闭一个打开的文件描述符。它实际上不是直接关闭文件,而是断开索引指针数组和文件对象之间的关系。这意味着,当 close
被调用时,相应的索引指针不再指向目标文件对象,从而断开了它们之间的链接。
资源释放: 当所有的索引指针都不再指向某个文件对象时,该文件对象就变得无用。在这种情况下,系统会通过引用计数机制来释放与该文件对象相关的资源。这个过程是自动的,确保了系统资源的有效管理。
dup
复制文件描述符
dup
函数是 POSIX 标准中用于复制文件描述符的系统调用。当你想要保留一个文件描述符的副本以便在不同的执行流程中使用,或者需要为现有的文件描述符创建一个新的独立引用时,这个函数就非常有用。
函数原型:
int dup(int oldfd);
参数:oldfd
:要复制的原始文件描述符。
功能:dup
函数创建一个新的文件描述符,它与 oldfd
引用相同的文件,并具有相同的文件状态标志。
返回值:
- 成功时,返回新的文件描述符,这个值大于或等于 0。
- 失败时,返回 -1,并设置全局变量
errno
以指示错误类型。
dup2
复制文件描述符
dup2
函数是 POSIX 标准中提供的用于复制文件描述符的系统调用,与 dup
类似,但它允许你指定新文件描述符的值。如果指定的新文件描述符已经打开,它将被关闭并重新用于复制的文件描述符。
函数原型:
int dup2(int oldfd, int newfd);
参数:
oldfd
:要复制的原始文件描述符。newfd
:新文件描述符的期望值。
功能:
dup2
函数将oldfd
复制到文件描述符newfd
。如果newfd
已经打开,它将被关闭,然后复制oldfd
的属性到newfd
。
返回值:
- 成功时,返回新的文件描述符,即
newfd
。 - 失败时,返回 -1,并设置全局变量
errno
以指示错误类型。