Linux 文件访问-系统调用
通过系统调用来访问文件是最直接的方式。系统调用函数直接作用于操作系统内核的设备驱动程序从而实现文件访问。
2.1 文件描述符
在系统中需要处理的文件(读、写操作)需要一个标识,以便在其它地方能识别出这个文件,于是就产生了文件描述符。文件描述符是一些小值整数,简单的说就是
一个文件ID用于在系统中唯一的标识文件。文件描述符的总数也就是系统可以打开文件的最多个数,这取决于系统的配置情况。
当开始运行程序时,也就是系统开始运行时,它一般会有三个已经打开的文件描述符。他们是:
- 0:标准输入
- 1:标准输出
- 2:标准错误
其它文件的文件描述符,在调用文件打开函数open时返回。这就是说,每个设备对应着一个文件描述符。文件描述符由操作系统分配,每次分配最小的。
write系统调用
write,就是把缓冲区的数据写入文件中。注意,这里的文件时广泛意义的文件,比如写入磁盘、写入打印机等等。
函数原型:
*size_t write(int fildes, const void buf, size_t nbytes);
参数说明:
fildes:文件描述符,标识了要写入的目标文件。例如:fildes的值为1,就像标准输出写数据,也就是在显示屏上显示数据;如果为 2 ,则想标注错误写数据。
*buf:待写入的文件,是一个字符串指针。
nbytes:要写入的字符数。
函数返回值:size_t 返回成功写入文件的字符数。需要指出的是,write可能会报告说他写入的字节比你所要求的少。这并不一定是个错误。在程序中,你需要检查error已发现错误,然后再次调用write写入剩余的数据。
read系统调用
系统调用read是从文件中读出数据。要读取的文件用文件描述符标识,数据读入一个事先定义好的缓冲区。他返回实际读入的字节数。
Linux中read的函数原型:
size_t read(int fildes, void *buf, size_t nbytes);
参数说明:
fildes:文件描述符,标识要读取的文件。如果为0,则从标准输入读数据。类似于scanf()的功能。
*buf:缓冲区,用来存储读入的数据。
nbytes:要读取的字符数。
返回值:size_t返回成功读取的字符数,它可能会小于请求的字节数。
open系统调用
系统调用open的作用是打开一个文件,并返回这个文件的描述符。
简单地说,open建立了一条到文件或设备的访问路径。如果操作成功,它将返回一个文件描述符,read和write等系统调用使用该文件描述符对文件或设备进行操作。这个文件描述符是唯一的,他不会和任何其他运行中的进程共享。如果两个程序同时打开一个文件,会得到两个不同的文件描述符。如果同时对两个文件进行操作,他们各自操作,互补影响,彼此相互覆盖(后写入的覆盖先写入的)为了防止文件按读写冲突,可以使用文件锁的功能。这不是本次重点,以后介绍。
Linux中open的函数原型有两个:
*int open(const char path, int oflags);
*int open(const char path, int oflags, mode_t mode );
O_RDONLY :只读
O_WRONLY:只写
O_REWR:读写方式
参数说明。
path:准备打开的文件或设备名字。
oflags:指出要打开文件的访问模式。open调用必须指定如下所示的文件访问模式之一:
open调用哈可以在oflags参数中包括下列可选模式的组合(用”按位或“操作):
- O_APPEDN: 把写入数据追加在文件的末尾。
- O_TRUNC: 把文件长度设为零,丢弃以后的内容。
- O_CREAT: 如果需要,就按参数mode中给出的访问模式创建文件。
- O_EXCL: 与O_CREAT一起调用,确保调用者创建出文件。使用这个模式可防止两个程序同时创建一个文件,如果文件已经存在,open调用将失败。
关于其他可能出现的oflags值,请看考open的调用手册。
mode:
当使用哦、O_CREAT标志的open来创建文件时,我们必须使用三个参数格式的open调用。第三个参数mode 是几个标志按位OR后得到的。他们是:
- S_IRUSR: 读权限,文件属主。
- S_IWUSR:写权限,文件属主。
- S_IXUSR:执行权限,文件属主。
- S_IRGRP:读权限,文件所属组。
- S_IWGRP:写权限,文件所属组。
close
close系统调用用于“关闭”一个文件,close调用终止一个文件描述符fildes以其文件之间的关联。文件描述符被释放,并能够重新使用。
close成功返回1,出错返回-1.
#Include<unistd.h>
int close(int fildes);
ioctl系统调用
ioctl提供了一个用于控制设备及其描述符行为和配置底层服务的接口。终端、文件描述符、甚至磁带机都可以又为他们定义的ioctl,具体
细节可以参考特定设备的使用手册。
下面是ioctl 的函数原型
#include<unistd.h>
int ioctl(int fildes, int cmd,);
ioctl对描述符fildes指定的对象执行cmd 参数中所给出的操作。
lseek
补充知识点:文件数据中的空洞。
在文件系统中,文件数据(File Data)是指实际存储在文件中的内容,它包含有意义的字节数据。当文件中存在实际存储的数据时,文件系统会为这些数据分配存储空间。
而空洞(Hole)是文件中的一种特殊情况,它表示文件中的部分区域没有实际存储的数据。空洞是一系列连续的零字节(通常表示为’\0’),这些字节在底层存储中没有分配空间。文件系统不会为空洞分配磁盘空间,因为它们不包含有意义的数据。
文件中的空洞通常出现在以下情况下:
- 文件中间插入了一段空白区域,但并未实际写入任何数据。
- 文件的末尾存在未分配的空间,即文件大小大于实际存储的数据量。
空洞的存在可以使文件系统节省存储空间,尤其对于具有稀疏特性的文件非常有用。某些应用程序,如文件备份工具,可以利用空洞来减少备份文件的大小并节省存储空间。但需要注意的是,文件系统不一定会显式地报告空洞的存在,因此应用程序需要有相应的机制来检测和利用空洞。
名称
lseek - 重新定位读/写文件偏移量
概要
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
lseek() 根据指令 whence 将与文件描述符 fd 关联的打开文件描述符的文件偏移量重新定位为参数 offset 指定的位置。以下是根据指令 whence 的不同取值所进行的操作:
SEEK_SET
//文件偏移量设置为 offset 字节。
SEEK_CUR
//文件偏移量设置为当前位置加上 offset 字节。
SEEK_END
//文件偏移量设置为文件大小加上 offset 字节。
经常使用此函数进行文件的大小的获取
off_t size = lseek(fd, 0, SEEK_END);
// 别忘了移到初始位置
lseek(fd, 0, SEEK_SET);
fstat、stat、lstat
-
fstat
-
函数原型:
int fstat(int fd, struct stat *buf);
-
功能:获取与文件描述符(file descriptor)相关联的文件的元数据。
-
参数:
fd
:文件描述符,表示已打开文件的标识符。buf
:指向struct stat
结构体的指针,用于存储文件的元数据。
-
返回值:成功时返回 0,失败时返回 -1,并设置适当的错误码(通过
errno
获取)。 -
示例:
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = open("file.txt", O_RDONLY); struct stat fileStat; if (fstat(fd, &fileStat) == 0) { // 使用 fileStat 结构体中的元数据 } else { // 获取元数据失败 } close(fd); return 0; }
-
-
stat
-
函数原型:
int stat(const char *path, struct stat *buf);
-
功能:获取指定路径的文件的元数据。
-
参数:
path
:文件路径的字符串。buf
:指向struct stat
结构体的指针,用于存储文件的元数据。
-
返回值:成功时返回 0,失败时返回 -1,并设置适当的错误码(通过
errno
获取)。 -
示例:
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> int main() { struct stat fileStat; if (stat("file.txt", &fileStat) == 0) { // 使用 fileStat 结构体中的元数据 } else { // 获取元数据失败 } return 0; }
lstat
-
函数原型:
int lstat(const char *path, struct stat *buf);
-
功能:获取指定路径的文件的元数据,但是对于符号链接文件,返回链接文件本身的属性而不是链接所指向的文件的属性。
-
参数:
path
:文件路径的字符串。buf
:指向struct stat
结构体的指针,用于存储文件的元数据。
-
返回值:成功时返回 0,失败时返回 -1,并设置适当的错误码(通过
errno
获取)。 -
示例:
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> int main() { struct stat fileStat; if (lstat("symlink.txt", &fileStat) == 0) { // 使用 fileStat 结构体中的元数据 } else { // 获取元数据失败 } return 0; }
这些函数在不同的情况下使用,根据你的需求选择合适的函数。
fstat
用于已打开文件描述符,stat
用于指定路径的文件,而lstat
用于获取符号链接文件的属性。结构体
stat
的结构:struct stat { dev_t st_dev; // 文件所在设备的 ID ino_t st_ino; // 文件的 inode 号 mode_t st_mode; // 文件的访问权限和文件类型 nlink_t st_nlink; // 文件的硬链接数目 uid_t st_uid; // 文件的所有者用户 ID gid_t st_gid; // 文件的所有者组 ID dev_t st_rdev; // 如果文件是特殊文件,则为其设备 ID off_t st_size; // 文件的大小(以字节为单位) blksize_t st_blksize; // 文件系统 I/O 缓冲区大小 blkcnt_t st_blocks; // 文件所占用的磁盘块数 struct timespec st_atim; // 文件的最后访问时间 struct timespec st_mtim; // 文件的最后修改时间 struct timespec st_ctim; // 文件的最后状态更改时间 };
库函数
在输入、输出操作中,直接使用系统调用效率会非常底。具体原因有二:
- 系统调用会影响系统性能。与函数调用相比,系统调用的开销大。因为在执行系统调用的时候,要切换到内核代码区执行,然后再返回用户代码。这必然就需要大量的时间开支。一种解决办法是:尽量减少系统调用的次数,让每次系统调用完成尽可能多的 任务。例如每次系统调用写入大量的字符而不是单个字符。
- 硬件会对系统调用一次能读写的数据块做一定的限制。例如,磁带机通常的写操作数据块长度是10k,如果缩写数据不是10k的整数倍,磁带机还是会以10k为单位绕磁带,这就在磁带上留下空隙。
为了提高文件访问操作的效率,并且使得文件操作变得更方便,Linux发行版提供了一系列的标准函数库。他们是一些由函数构成的集合,你可以在自己的程序方便的中使用它们,
去操作文件。提供输出缓冲功能的标准I/O库就是这样的例子。你可以高效的写任意长度的数据块,库函数则在需要的时候安排底层函数调用(系统调用)
也就是说,库函数在用户和系统之间,增加了一个中间层。如下图所示:
库函数是根据实际需要而包装好的系统调用,用户可在程序中方便的使用库函数,如标准I O库(稍后会讲)。
fopen函数
fopen函数类似于系统调用中的open函数。和open一样,它返回文件的标识符,只是这里叫做流(stream),在库函数里实现为一个指向文件的指针。
如果需要对设备的行为进行明确的控制,最好使用底层系统调用,因为这可以避免使用库函数带来的一些非预期的副作用,如输入/输出缓冲。
函数原型:
#include<stdio.h>
FILE *fopen(const char *filename, const char *mode);
参数说明:
*filename:打开文件的文件名
*mode:打开的方式
r 以只读方式打开文件,该文件必须存在。
r+ 以可读写方式打开文件,该文件必须存在。
rb+ 读写打开一个二进制文件,允许读数据。
rw+ 读写打开一个文本文件,允许读和写。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
fopen在成功是返回一个非空的FILE *指针。失败返回NULL
fread/fwrite函数
fread
和 fwrite
是 C 语言标准库中用于进行文件输入输出的函数。它们可以用于读取和写入二进制数据到文件中。下面是它们的基本用法:
-
fread
函数:- 函数原型:
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
- 功能:从文件中读取二进制数据,并将其存储到指定的内存区域中。
- 参数:
ptr
:指向存储读取数据的内存区域的指针。size
:每个数据元素的大小(以字节为单位)。count
:要读取的数据元素的个数。stream
:指向要读取的文件的指针。
- 返回值:成功时返回实际读取的元素个数,如果出现错误或到达文件末尾,则返回一个小于
count
的值。 - 示例:
#include <stdio.h> int main() { FILE *file = fopen("data.bin", "rb"); if (file != NULL) { int data[10]; size_t elements_read = fread(data, sizeof(int), 10, file); printf("Read %zu elements from file.\n", elements_read); fclose(file); } else { printf("Failed to open file.\n"); } return 0; }
- 函数原型:
-
fwrite
函数:- 函数原型:
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
- 功能:将内存中的二进制数据写入文件。
- 参数:
ptr
:指向要写入文件的数据的指针。size
:每个数据元素的大小(以字节为单位)。count
:要写入的数据元素的个数。stream
:指向要写入的文件的指针。
- 返回值:成功时返回实际写入的元素个数,如果出现错误,则返回一个小于
count
的值。 - 示例:
#include <stdio.h> int main() { FILE *file = fopen("data.bin", "wb"); if (file != NULL) { int data[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; size_t elements_written = fwrite(data, sizeof(int), 10, file); printf("Written %zu elements to file.\n", elements_written); fclose(file); } else { printf("Failed to open file.\n"); } return 0; }
- 函数原型:
在使用 fread
和 fwrite
函数时,需要注意以下几点:
- 打开文件时需要使用适当的模式,如
"rb"
表示以二进制读取方式打开文件,"wb"
表示以二进制写入方式打开文件。 - 读取和写入的数据元素大小 (
size
) 必须与实际数据类型的大小一致。 - 返回值可以用于检查读取或写入是否成功,但并不一定需要完全匹配
count
的值,因为可能会遇到文件末尾或错误。 - 在读取或写入完成后,需要通过调用
fclose
函数关闭文件。
请注意,fread
和 fwrite
是以二进制方式进行读写操作,适用于处理二进制数据,如结构体、数组等。如果需要读取或写入文本数据,可以使用 fgets
和 fputs
函数等。
fputs/fgets
fgets
和 fputs
是 C 语言标准库中用于进行文本文件输入输出的函数。它们可以用于读取和写入文本数据到文件中。下面是它们的基本用法:
-
fgets
函数:- 函数原型:
char *fgets(char *str, int n, FILE *stream);
- 功能:从文件中读取一行文本,并将其存储到指定的字符串中。
- 参数:
str
:指向存储读取文本的字符数组(字符串)的指针。n
:要读取的最大字符数(包括终止符\0
在内)。stream
:指向要读取的文件的指针。
- 返回值:成功时返回
str
,如果到达文件末尾或出现错误,则返回NULL
。 - 示例:
#include <stdio.h> int main() { FILE *file = fopen("data.txt", "r"); if (file != NULL) { char line[100]; while (fgets(line, sizeof(line), file) != NULL) { printf("%s", line); // 输出读取的文本行 } fclose(file); } else { printf("Failed to open file.\n"); } return 0; }
- 函数原型:
-
fputs
函数:- 函数原型:
int fputs(const char *str, FILE *stream);
- 功能:将指定的字符串写入文件。
- 参数:
str
:要写入文件的字符串。stream
:指向要写入的文件的指针。
- 返回值:成功时返回非负值,如果出现错误,则返回
EOF
。 - 示例:
#include <stdio.h> int main() { FILE *file = fopen("data.txt", "w"); if (file != NULL) { const char *text = "Hello, World!"; if (fputs(text, file) != EOF) { printf("Text written to file.\n"); } else { printf("Failed to write to file.\n"); } fclose(file); } else { printf("Failed to open file.\n"); } return 0; }
- 函数原型:
在使用 fgets
和 fputs
函数时,需要注意以下几点:
- 打开文件时需要使用适当的模式,如
"r"
表示以读取方式打开文件,"w"
表示以写入方式打开文件。 fgets
函数会读取一行文本,包括换行符\n
,并将其存储到指定的字符串中,所以需要确保提供的字符数组足够大以容纳读取的文本。fgets
函数在成功读取一行文本时,会在末尾自动添加终止符\0
。fgets
函数会保留文本中的换行符,如果不需要换行符,可以使用字符串处理函数(如strtok
或strcspn
)去除它。fputs
函数会将指定的字符串写入文件,不会自动添加换行符。如果需要换行,需要手动在字符串末尾添加换行符\n
。
请注意,fgets
和 fputs
是以文本方式进行读写操作,适用于处理文本数据。如果需要读取或写入二进制数据,可以使用 fread
和 fwrite
函数等。
fclose函数
fclose函数关闭指定的文件流stream,这个操作会使所有未写出的数据都写出。因为stdio库函数会对数据进行缓冲,所有调用fclose函数是很重要的。如果程序需要确保数据已经全部写出,就应该调用fclose函数。虽然程序正常结束时,也会自动的调用fclose函数,但这样就不能检测出调用fclose所产生的错误了。
函数原型如下:
#include<stdio,h>
int fclose(FILE *stream);
fflush函数
fflush函数的作用是把文件流中所有未写出的数据全部写出。 处于效率考虑,在使用库函数的时候会使用数据缓冲区,当缓冲区满的时候才进行写操作。使用fflush函数
可以将缓冲区的数据全部写出,而不关心缓冲区是否满。fclose的执行隐含调用了fflush函数,所以不必再fclose执行之前调用fflush。
函数原型:
#include<stdio.h>
int fflush(FILE *stream);
/proc文件系统
/dev目录中的文件使用底层系统调用这样一种特殊方式来访问硬件。
/proc文件系统,可以看做是一个特殊的文件系统,在这个系统中,每个文件都对应一个独立的硬件,所以用户可以通过proc文件系统像访问文件一样来访问硬件设备。该文件系统通常表现为/proc 目录。该目录中包含了许多特殊文件以允许对驱动和内核信息进行高层访问。
socket-send-recv
在基于套接字(socket)的网络编程中,send
和 recv
是两个常用的函数,用于发送和接收数据。它们是在 BSD socket API 中定义的函数,用于在网络上进行数据传输。
-
send
函数:- 函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 功能:将数据从发送缓冲区发送到已连接的套接字。
- 参数:
sockfd
:套接字描述符,表示要发送数据的套接字。buf
:指向要发送数据的缓冲区的指针。len
:要发送的数据的大小(以字节为单位)。flags
:可选的标志参数,用于控制发送操作的行为。通常可置为 0。
- 返回值:成功时返回实际发送的字节数,出错时返回 -1。
- 示例:
#include <sys/socket.h> int main() { int sockfd; // 假设已创建并连接好的套接字 const char *message = "Hello, server!"; ssize_t bytes_sent = send(sockfd, message, strlen(message), 0); if (bytes_sent == -1) { printf("Failed to send data.\n"); } else { printf("Sent %zd bytes.\n", bytes_sent); } return 0; }
- 函数原型:
-
recv
函数:- 函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 功能:从已连接的套接字接收数据,并将其存储到接收缓冲区。
- 参数:
sockfd
:套接字描述符,表示要接收数据的套接字。buf
:指向接收数据的缓冲区的指针。len
:接收缓冲区的大小(以字节为单位)。flags
:可选的标志参数,用于控制接收操作的行为。通常可置为 0。
- 返回值:成功时返回实际接收的字节数,如果连接已关闭或出错,则返回 0 或 -1。
- 示例:
#include <sys/socket.h> int main() { int sockfd; // 假设已创建并连接好的套接字 char buffer[1024]; ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer), 0); if (bytes_received == -1) { printf("Failed to receive data.\n"); } else if (bytes_received == 0) { printf("Connection closed by remote host.\n"); } else { buffer[bytes_received] = '\0'; // 在数据末尾添加终止符 printf("Received: %s\n", buffer); } return 0; }
- 函数原型:
在使用 send
和 recv
函数时,需要注意以下几点:
send
函数会将数据从应用程序的发送缓冲区复制到操作系统的内核发送缓冲区,并通过网络发送给对端。recv
函数会从操作系统的内核接收缓冲区接收数据,并将其复制到应用程序指定的接收缓冲区。len
参数表示要发送或接收的数据的大小。建议在调用时确保缓冲区足够大以容纳数据。send
和recv
函数返回的字节数可能小于请求的大小,需要根据返回值判断是否需要进行多次调用以完成发送或接收。flags
参数通常可以置为 0,但也可以使用一些可选的标志进行特定操作,如MSG_DONTWAIT
(非阻塞操作)和MSG_WAITALL
(保证接收完整数据)等。- 在发送和接收数据之前,需要创建并连接好套接字,通常使用
socket
、connect
等函数完成。
请注意,以上示例仅展示了 send
和 recv
函数的基本用法,实际使用时还需要进行错误处理和适当的调用顺序。此外,这些示例假设已经创建并连接好了套接字,实际应用中可能需要使用其他函数来创建和建立连接。
C/C++从终端读取信息的函数
其中只介绍getline
-
getline
函数(C++):-
头文件:
#include <string>
-
函数原型:
std::istream& getline(std::istream& is, std::string& str);
-
功能:从输入流(标准输入)读取一行文本。
-
参数:
is
:输入流对象,一般为std::cin
。str
:接收读取文本的字符串对象。
-
返回值:输入流对象的引用。
-
示例:
#include <iostream> #include <string> int main() { std::string name; std::cout << "Enter your name: "; std::getline(std::cin, name); std::cout << "Hello, " << name << std::endl; return 0; }
-
技巧一
#include <iostream>
#include <string>
int main() {
std::string line;
std::cout << "Enter multiple lines of text (Press Enter twice to stop):\n";
while (std::getline(std::cin, line)) {
if (line == "\r") {
// 遇到回车换行符作为结束标志
break;
}
// 处理每行输入
std::cout << "Line: " << line << std::endl;
}
return 0;
}
技巧二
#include <iostream>
#include <sstream>
#include <string>
#include <vector>
int main() {
std::string input = "Hello,World,OpenAI,Chatbot";
std::stringstream ss(input);
std::string token;
std::vector<std::string> tokens;
while (std::getline(ss, token, ',')) {
tokens.push_back(token);
}
// 打印分割后的结果
for (const auto& token : tokens) {
std::cout << token << std::endl;
}
return 0;
}
// 遇到回车换行符作为结束标志
break;
}
// 处理每行输入
std::cout << "Line: " << line << std::endl;
}
return 0;
}