linux应用:文件描述符、lseek
文件描述符
概念
在 Linux 系统中,当一个程序打开一个文件或者建立一个网络连接等 I/O 操作时,内核会为其分配一个非负整数,这个整数就是文件描述符(File Descriptor)。可以简单将其理解为一个索引值,指向内核中一个代表该打开文件或 I/O 资源的结构体。每个进程都有自己独立的文件描述符表,记录着该进程所打开的所有文件描述符及其对应的文件信息。
作用
文件操作标识:在进行文件的读写、关闭等操作时,需要使用文件描述符来指定具体操作的对象。例如,使用read函数读取文件内容,第一个参数就是文件描述符,它明确了从哪个文件读取数据。
资源管理:内核通过文件描述符来管理系统中的各种 I/O 资源。通过文件描述符,内核能够追踪每个打开资源的状态,确保资源的正确使用和释放,避免资源泄漏等问题。
实现 I/O 复用:在网络编程等场景中,常常需要同时处理多个 I/O 操作。文件描述符使得可以利用诸如select、poll、epoll等 I/O 复用机制,通过监控一组文件描述符的状态变化,实现高效地处理多个并发的 I/O 事件,提升系统的并发处理能力。
常见文件描述符
标准输入(STDIN_FILENO):其值通常为 0,代表键盘输入。程序通过这个文件描述符从标准输入设备读取数据。例如,scanf函数默认从标准输入读取用户输入的数据,底层实际上就是通过标准输入文件描述符进行操作。
标准输出(STDOUT_FILENO):值通常为 1,用于将程序的输出发送到屏幕等标准输出设备。像printf函数输出的内容就是通过标准输出文件描述符显示在终端上。
标准错误(STDERR_FILENO):值通常为 2,专门用于输出程序运行过程中产生的错误信息到标准错误设备,一般也是显示在终端上。这样可以将正常输出和错误输出分开,便于调试和查看程序运行状态。
文件描述符的使用
打开文件获取文件描述符:使用open函数可以打开一个文件并返回一个新的文件描述符。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd;
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
// 后续可使用fd进行文件操作
close(fd);
return 0;
}
open函数尝试以只读方式打开名为test.txt的文件,如果成功则返回一个文件描述符fd,否则返回 -1 并通过perror打印错误信息。
文件读写操作:
有了文件描述符后,可以使用read和write函数进行文件读写。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFFER_SIZE 1024
int main() {
int fd, n;
char buffer[BUFFER_SIZE];
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
n = read(fd, buffer, BUFFER_SIZE);
if (n == -1) {
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
write(STDOUT_FILENO, buffer, n);
close(fd);
return 0;
}
read函数从文件描述符fd对应的文件中读取最多BUFFER_SIZE字节的数据到buffer数组中,write函数将读取到的数据通过标准输出文件描述符STDOUT_FILENO输出到屏幕上。
3. 关闭文件描述符:使用完文件描述符后,要及时通过close函数关闭,以释放系统资源。如上述代码中的close(fd)操作,防止文件描述符泄漏导致系统资源浪费。
文件描述符复制(dup 函数)
dup函数用于复制一个现有的文件描述符,返回一个新的文件描述符,这个新的文件描述符与原文件描述符指向相同的文件或 I/O 资源,并且共享文件偏移量等状态信息。
#include <unistd.h>
int dup(int oldfd);
oldfd是要被复制的现有文件描述符。如果复制成功,dup函数返回一个新的文件描述符,该文件描述符是当前进程未使用的最小整数值。如果失败,返回 -1 并设置errno以指示错误原因。
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd, new_fd;
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
new_fd = dup(fd);
if (new_fd == -1) {
perror("dup");
close(fd);
exit(EXIT_FAILURE);
}
// 现在fd和new_fd都指向同一个文件
close(fd);
close(new_fd);
return 0;
}
通过dup函数复制了fd文件描述符得到new_fd,它们都指向test.txt文件。之后可以通过new_fd继续对该文件进行操作,就如同使用fd一样,并且关闭其中一个文件描述符不会影响另一个对文件的访问,直到两个文件描述符都被关闭,对应的文件资源才会真正被释放。
fcntl 函数介绍:
fcntl函数是一个功能强大的文件控制函数,它可以对已打开的文件描述符进行各种操作,包括复制文件描述符、改变文件状态标志、设置和获取文件的访问控制权限等。
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fd:需要操作的文件描述符。
cmd:指定要执行的操作命令,常见的命令如下:
F_DUPFD:复制文件描述符fd,返回一个新的文件描述符,新描述符是当前进程中未使用的最小整数值。与dup函数类似,但fcntl通过F_DUPFD提供了更多控制选项。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd, new_fd;
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
new_fd = fcntl(fd, F_DUPFD, 0);
if (new_fd == -1) {
perror("fcntl");
close(fd);
exit(EXIT_FAILURE);
}
// 现在fd和new_fd都指向同一个文件
close(fd);
close(new_fd);
return 0;
}
fcntl函数使用F_DUPFD命令复制了文件描述符fd,第三个参数 0 表示新文件描述符的最小取值为 0(通常使用 0,由系统选择未使用的最小整数值)。
F_GETFL:获取文件描述符fd的状态标志,返回值是open函数中设置的文件状态标志,如O_RDONLY、O_WRONLY、O_RDWR、O_APPEND等的按位或结果。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd, flags;
fd = open("test.txt", O_RDONLY | O_APPEND);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl");
close(fd);
exit(EXIT_FAILURE);
}
if (flags & O_APPEND) {
printf("文件以追加模式打开\n");
}
close(fd);
return 0;
}
通过fcntl函数的F_GETFL命令获取了文件描述符fd的状态标志,并检查是否设置了O_APPEND标志。
F_SETFL:设置文件描述符fd的状态标志。可以修改文件的打开模式,如添加或移除O_APPEND、O_NONBLOCK等标志,但不能修改O_RDONLY、O_WRONLY、O_RDWR这些初始打开模式。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
int fd, flags;
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl");
close(fd);
exit(EXIT_FAILURE);
}
flags |= O_NONBLOCK;
if (fcntl(fd, F_SETFL, flags) == -1) {
perror("fcntl");
close(fd);
exit(EXIT_FAILURE);
}
// 文件描述符fd现在处于非阻塞模式
close(fd);
return 0;
}
先获取文件描述符fd的当前状态标志,然后通过按位或操作添加O_NONBLOCK标志,最后使用F_SETFL命令将修改后的标志重新设置到文件描述符上。
总结
文件描述符是 Linux 系统中进行 I/O 操作的核心概念,它为进程与系统资源之间的交互提供了统一的接口。熟练掌握文件描述符的使用,对于编写高效、稳定的 Linux 应用程序,特别是涉及文件处理和网络编程等方面的程序,具有至关重要的意义
lseek
函数定义与头文件
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
#include <unistd.h>是包含lseek函数声明的头文件。在使用lseek函数前,必须确保包含该头文件,否则编译器将无法识别lseek函数。
fd参数是文件描述符,它是一个非负整数,用于标识一个已打开的文件。文件描述符通常由open、creat等函数返回,通过它,操作系统能够准确地定位到对应的文件,进而对文件执行各种操作。
offset参数表示偏移量,它是一个有符号整数(类型为off_t)。偏移量的具体含义取决于whence参数的值。当whence为SEEK_SET时,offset表示从文件开头开始的偏移量;当whence为SEEK_CUR时,offset表示从当前文件偏移量位置开始的偏移量;当whence为SEEK_END时,offset表示从文件末尾开始的偏移量。
whence参数用于指定偏移的基准位置,它有三个预定义的常量值:
SEEK_SET:表示偏移量从文件开头开始计算。
SEEK_CUR:表示偏移量从当前文件指针位置开始计算。
SEEK_END:表示偏移量从文件末尾开始计算。
lseek函数返回一个off_t类型的值,它表示文件指针的新位置。如果lseek函数执行成功,返回的是从文件开头到新位置的字节偏移量。如果执行失败,lseek函数将返回-1,并且会设置全局变量errno来指示错误原因。常见的错误原因包括无效的文件描述符、偏移量超出文件系统支持的范围等。
实现文件的随机读写
通过lseek函数将文件指针移动到指定位置,然后使用read或write函数进行读写操作,从而实现文件的随机读写。例如,在一个包含学生信息的文件中,每个学生信息占用固定的字节数,通过lseek计算出第 n 个学生信息在文件中的位置,然后读取或修改该学生的信息
// 假设每个学生信息占100字节,读取第5个学生的信息
off_t studentOffset = lseek(fd, 4 * 100, SEEK_SET);
if (studentOffset!= -1) {
char studentData[100];
ssize_t bytesRead = read(fd, studentData, sizeof(studentData));
if (bytesRead == -1) {
perror("read");
}
}
获取文件大小
通过将文件指针移动到文件末尾(SEEK_END),然后获取此时的偏移量,即可得到文件的大小
off_t fileSize = lseek(fd, 0, SEEK_END);
if (fileSize!= -1) {
printf("The size of the file is %ld bytes.\n", fileSize);
}
创建空洞文件
空洞文件是指文件在磁盘上占用的空间大于其实际存储的数据量。通过lseek函数将文件指针移动到一个较大的偏移量位置,然后执行write操作,就可以创建一个空洞文件。
// 创建一个10MB的空洞文件
off_t largeOffset = lseek(fd, 10 * 1024 * 1024, SEEK_SET);
if (largeOffset!= -1) {
char data = 'A';
ssize_t bytesWritten = write(fd, &data, 1);
if (bytesWritten == -1) {
perror("write");
}
}
注意
文件打开方式的影响:lseek函数的行为可能会受到文件打开方式的影响。例如,以追加模式(O_APPEND)打开的文件,每次调用lseek函数设置文件指针位置后,实际的写入操作仍会从文件末尾开始,因为O_APPEND标志会强制文件指针在每次写入前移动到文件末尾。
设备文件的支持:并非所有的设备文件都支持lseek函数。例如,像字符设备(如终端设备)通常不支持随机访问,调用lseek函数可能会返回错误。在对设备文件使用lseek函数前,需要了解设备的特性和支持的操作。
文件系统的限制:不同的文件系统对文件的最大偏移量、文件大小等有不同的限制。在使用lseek函数时,需要确保设置的偏移量在文件系统支持的范围内,否则可能会导致未定义行为或错误。