目录
IO基础
现在我们学习简单的文件IO基础:最简单的文件操作包括:
-
open():打开⼀个文件/设备
-
close():关闭⼀个文件/设备
-
write():写入字符给⼀个文件
-
read():读取⼀个文件
在Linux操作系统中,文件读写操作是与程序交互的基本方式,主要通过⼀组系统调用来实现。常用 的系统调用包括open、read、write和close。这些调用提供了⼀个⾼效且直接的方式来处理文件,允 许程序员以低级别的方式与操作系统进行交互。以下将详细介绍这些API的功能及其使用方法。 ⾸先,open函数用于打开⼀个文件。它的原型通常是这样的:
int open(const char *pathname, int flags, mode_t mode);
pathname参数指定要打开的文件路径,flags参数控制打开文件的方式,如只读、只写、创建等。mode参数通常用于创建新文件时设置文件权限。当调用成功时,open返回⼀个文件描述符,这是⼀个⾮负整数,用于后续的读写操作。如果打开失败,返回-1,并设置errno以指示错误类型。其次,read函数用于从打开的文件中读取数据。其原型为ssize_t read(int fd, void *buf, size_t count);。fd是由open返回的文件描述符,buf是指向存储读取数据的缓冲区的指针,count是要读取的字节数。调用read后,它返回实际读取的字节数。如果返回值为0,表示已到达文件末尾;如果返回-1,则表示读取过程中发⽣了错误。 接下来,write函数用于向文件写入数据。其原型为ssize_t write(int fd, const void *buf, size_tcount);。fd同样是文件描述符,buf是指向要写入数据的缓冲区的指针,count是要写入的字节数。 write的返回值是实际写入的字节数。如果返回-1,则表示写入过程中发⽣了错误。需要注意的是,如果⽬标文件没有打开为可写模式,写操作将失败。最后,close函数用于关闭⼀个打开的文件,释放相关资源。其原型为int close(int fd);。fd是需要关闭的文件描述符。成功关闭文件时,返回0;如果失败,返回-1,errno将指示错误原因。使用这些API时,通常的⼯作流程是⾸先调用open打开文件,然后使用read或write进行数据操作,最后用close关闭文件。用⼀段话快速总结⼀下:
int open(const char *pathname, int flags, mode_t mode);:用于打开文件并返回文件描述符。
ssize_t read(int fd, void *buf, size_t count);:用于从文件中读取数据。
ssize_t write(int fd, const void *buf, size_t count);:用于向文件写入数据。
int close(int fd);:用于关闭文件,释放资源。
文件描述符
我们还有一件事情没有说:这个fd我们应该如何理解呢?答案是他是文件描述符,等价于Windows下的句柄。可以说是一回事情。当调用 open 函数打开一个现有文件或创建一个新文件时,内核会向进程返回一个文件描述符,用于指代被打开的文件,所有执行 IO 操作的系统调用都是通过文件描述符来索引到对应的文件。一个进程可以打开多个文件,但是在 Linux 系统中,一个进程可以打开的文件数是有限制,并不是可以无限制打开很多的文件,大家想一想便可以知道,打开的文件是需要占用内存资源的,文件越大、打开的文件越多那占用的内存就越多,必然会对整个系统造成很大的影响,如果超过进程可打开的最大文件数限制,内核将会发送警告信号给对应的进程,然后结束进程;在 Linux 系统下,我们可以通过 ulimit 命令来查看进程可打开的最大文件数,用法如下所示:ulimit -n 一般不设置的话是1024。
我们每次给打开的文件分配文件描述符都是从最小的没有被使用的文件描述符(0~1023)开始,当之前打开的文件被关闭之后,那么它对应的文件描述符会被释放,释放之后也就成为了一个没有被使用的文件描述符了。当我们在程序中,调用 open 函数打开文件的时候,分配的文件描述符一般都是从 3 开始,这里大家可能要问了,上面不是说从 0 开始的吗,确实是如此,但是 0、1、2 这三个文件描述符已经默认被系统占用了,分别分配给了系统标准输入(0)、标准输出(1)以及标准错误(2)。
值得注意的是open的系统调用的flag参数有这些常用的值:
-
O_RDONLY标志以只读方式打开文件,这意味着只能对文件进行读取操作,不能进行写入。
-
O_WRONLY则以只写方式打开文件,仅允许写入而不允许读取。
-
O_RDWR允许文件以可读可写的方式打开,用户可以同时执行读取和写入操作。
-
O_CREAT标志用于创建新文件,如果指定的文件不存在。使用此标志时,必须传入第三个参数mode,用于设置新文件的访问权限。这个参数在使用O_CREAT或O_TMPFILE时才有效。
-
O_DIRECTORY标志确保所打开的路径是一个目录,如果不是,则open调用将失败。
-
O_EXCL通常与O_CREAT组合使用,以确保在创建文件时不覆盖已有文件。当同时使用这两个标志时,如果指定的文件已存在,open将返回错误。这种组合能够有效地测试文件是否存在,若不存在则创建新文件,存在则返回错误,从而保证操作的原子性。
-
O_NOFOLLOW标志用于防止打开符号链接,如果指定的路径是一个符号链接,open调用将失败。这些标志提供了灵活的文件打开策略,使得开发者可以精确控制文件访问方式和行为。还有这些标志可用于控制文件打开行为。
-
O_APPEND标志表示每次写入时,文件指针将自动移动到文件末尾,这对于日志文件等场景非常有用。
-
O_ASYNC允许异步I/O操作,当文件描述符有可读或可写事件发生时,内核会发送信号给进程,适用于需要非阻塞操作的程序。
-
O_DSYNC保证在写操作完成后,相关数据会同步到磁盘,从而确保数据完整性,但元数据可能不会立即更新。
-
O_NOATIME标志在读取文件时不会更新访问时间,减少文件系统的写入负担,适用于频繁读取但不需要访问时间更新的场景。
-
O_NONBLOCK用于非阻塞模式打开文件,当文件描述符的操作不能立即完成时,函数调用将返回而不阻塞进程,这在网络编程和管道操作中尤为重要。
-
O_SYNC标志确保在写入操作返回之前,数据和元数据都已成功写入磁盘,提供更强的数据安全性。
-
最后,O_TRUNC用于打开文件时截断文件内容,如果文件已存在,打开时将其内容清空,适用于需要重写文件内容的情况。mode是说明我们的文件的访问权限,只有当 flags 参数中包含 O_CREAT 或 O_TMPFILE 标志时才有效(O_TMPFILE 标志用于创建一个临时文件)。
权限
权限对于文件来说是一个很重要的属性,那么在 Linux系统中,我们可以通过 touch 命令新建一个文件,此时文件会有一个默认的权限,如果需要修改文件权限,可通过 chmod 命令对文件权限进行修改,譬如在 Linux 系统下我们可以使用"ls -l"命令来查看到文件所对应的权限。当我们调用 open 函数去新建一个文件时,也需要指定该文件的权限,而 mode 参数便用于指定此文件的权限,接下来看看我们该如何通过 mode 参数来表示文件的权限,首先 mode 参数的类型是 mode_t,这是一个 u32 无符号整形数据,我们从低位从上看,每 3 个 bit 位分为一组,分别表示:O---这 3 个 bit 位用于表示其他用户的权限;G---这 3 个 bit 位用于表示同组用户(group)的权限,即与文件所有者有相同组 ID 的所有用户;U---这 3 个 bit 位用于表示文件所属用户的权限,即文件或目录的所属者;S---这 3 个 bit 位用于表示文件的特殊权限。当然用宏更快。
-
S_IRUSR 允许文件所属者读文件
-
S_IWUSR 允许文件所属者写文件
-
S_IXUSR 允许文件所属者执行文件
-
S_IRWXU 允许文件所属者读、写、执行文件
-
S_IRGRP 允许同组用户读文件
-
S_IWGRP 允许同组用户写文件
-
S_IXGRP 允许同组用户执行文件
-
S_IRWXG 允许同组用户读、写、执行文件
-
S_IROTH 允许其他用户读文件
-
S_IWOTH 允许其他用户写文件
-
S_IXOTH 允许其他用户执行文件
-
S_IRWXO 允许其他用户读、写、执行文件
写文件: 使用 write 函数需要先包含 unistd.h 头文件。函数参数和返回值含义如下:
-
fd: 文件描述符。
-
buf: 指定写入数据对应的缓冲区。
-
count: 指定写入的字节数。
返回值方面,如果成功,write()
函数将返回写入的字节数(0 表示未写入任何字节)。如果写入字节数小于 count
参数,这并不表示错误,例如在磁盘空间已满的情况下可能会出现这种情况。如果写入发生错误,则返回 -1。
对于普通文件(我们通常操作的大部分文件,如常见的文本文件和二进制文件),一个重要的问题是从文件的哪个位置开始进行读写操作,也就是 I/O 操作所对应的当前位置偏移量。读写操作都是从当前位置偏移量处开始的。当前位置偏移量可以通过 lseek
系统调用进行设置。默认情况下,当前位置偏移量通常是 0,指向文件的起始位置。当调用 read()
或 write()
函数完成读写操作后,当前位置偏移量会向后移动相应的字节数。例如,如果当前位置偏移量为 1000 个字节,调用 write()
写入或 read()
读取 500 个字节后,当前位置偏移量将移动到 1500 个字节处。
在读取文件时,使用 read()
函数可以从当前偏移量开始读取指定字节数的数据。调用 read()
后,读取的数据将存储在提供的缓冲区中,返回值是实际读取的字节数。如果返回的字节数小于请求的字节数,可能是因为到达文件末尾。通常情况下,读取操作会自动更新当前位置偏移量,以便下次读取时从正确的位置继续。
函数参数和返回值含义如下:
-
fd: 文件描述符。与 write 函数的 fd 参数意义相同。
-
buf: 指定用于存储读取数据的缓冲区。
-
count: 指定需要读取的字节数。返回值: 如果读取成功将返回读取到的字节数,实际读取到的字节数可能会⼩于 count 参数指定的字节数,也有可能会为 0,譬如进行读操作时,当前文件位置偏移量已经到了文件末尾。 实际读取到的字节数少于要求读取的字节数,譬如在到达文件末尾之前有 30 个字节数据,⽽要求读取 100 个字节,则 read 读取成功只能返回 30;⽽下⼀次再调用 read 读,它将返回 0(文件末尾) 。
把文件关闭
先使用 close 函数需要先包含 unistd.h 头文件,当我们对文件进行 IO 操作完成之后,后续不再对文件进行操作时,需要将文件关闭。函数参数和返回值含义如下
-
fd: 文件描述符,需要关闭的文件所对应的文件描述符。
-
返回值: 如果成功返回 0,如果失败则返回-1。
除了使用 close
函数显式关闭文件之外,在 Linux 系统中,当一个进程终止时,内核会自动关闭它打开的所有文件。这意味着在程序中打开的文件如果在程序结束时没有显式关闭,内核会自动将其关闭。许多程序利用这一功能而不显式使用 close
关闭打开的文件。然而,显式关闭不再需要的文件描述符是良好的编程习惯,它能使代码在后续修改时更具可读性和可靠性。此外,文件描述符是有限资源,当不再需要时必须将其释放并归还给系统。
位移寻址方面,每个打开的文件,系统都会记录它的读写位置偏移量,称为读写偏移量。这个偏移量指示文件当前的读写位置,当调用 read()
或 write()
函数进行读写操作时,就会从当前的读写位置偏移量开始进行数据读写。读写偏移量以相对于文件头部的位置表示,文件第一个字节的数据位置偏移量为 0。当打开文件时,读写偏移量会被设置为指向文件开始位置,之后每次调用 read()
和 write()
函数时,偏移量会自动调整,指向已读或已写数据后的下一个字节。因此,连续调用 read()
和 write()
函数将使得读写操作按顺序递增。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
⾸先调用 lseek 函数需要包含<sys/types.h>和<unistd.h>两个头文件。函数参数和返回值含义如下:
-
fd: 文件描述符。
-
offset: 偏移量,以字节为单位。
-
whence: 用于定义参数
offset 偏移量对应的参考值, 该参数为下列其中⼀种(宏定义) :
-
SEEK_SET:读写偏移量将指向 offset 字节位置处(从文件头部开始算)
-
SEEK_CUR:读写偏移量将指向当前位置偏移量 + offset 字节位置处, offset 可以为正、也可以为负,如果是正数表示往后偏移,如果是负数则表示往前偏移;
-
SEEK_END:读写偏移量将指向文件末尾 + offset 字节位置处,同样 offset 可以为正、也可以为负,如果是正数表示往后偏移、如果是负数则表示往前偏移。
-
返回值: 成功将返回从文件头部开始算起的位置偏移量(字节为单位), 也就是当前的读写位置; 发⽣错误将返回-1。