掌握 Linux 文件操作函数,让你的代码高效又稳健

掌握 Linux 文件操作函数,让你的代码高效又稳健

1. 引言

1.1 Linux 文件操作相关函数简介

Linux 开发中,文件操作是最基础但又至关重要的能力。无论是读取配置文件、记录日志,还是处理数据存储,掌握 Linux 文件操作相关函数,能让你的代码更高效、更可靠。

1.2 你能获得什么

这篇文章将带你深入理解 Linux 文件操作的核心 API,包括 openreadwritecloselseekstatmmap 等,帮助你:

  • 正确打开、读取、写入和关闭文件,避免资源泄漏
  • 提高文件操作的性能,减少不必要的系统调用
  • 掌握高级文件操作,如 mmap 内存映射、文件锁等
  • 避免常见错误,写出更健壮的代码 💪

2. 文件打开与关闭

2.1 open 打开文件

在 C 语言中,使用 open() 来打开文件。它是 openat() 的封装,直接调用时,相当于 openat(AT_FDCWD, ...),在当前工作目录下操作。

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
    perror("open failed");
    return -1;
}
参数解析:
  • O_RDWR:以读写模式打开文件
  • O_CREAT如果文件不存在,则创建(必须搭配 mode 参数,如 0644
  • 0644:文件权限,所有者可读写 (rw-),组用户和其他用户可读 (r--)
⚠️ 常见问题:
  1. 文件权限不生效?

    • O_CREAT 需要 mode 参数,否则创建的文件权限不可控。
    • umask 影响,0644 可能变成 0600umask 默认 0022),可以用 umask(0); 取消影响。
  2. 文件已存在但 O_CREAT 仍失败?

    • 可能缺少 O_WRONLYO_RDWR,如果文件存在但无写权限,open() 仍然失败。
  3. 如何避免误覆盖?

    • 不要用 O_TRUNC,它会清空文件内容。

    • O_EXCL,如果文件已存在就失败:

      int fd = open("test.txt", O_RDWR | O_CREAT | O_EXCL, 0644);
      if (fd == -1) {
          perror("File exists");
      }
      

2.2 close 关闭文件

文件用完后,必须用 close(fd) 释放资源,否则可能导致文件描述符泄漏,影响系统稳定性。

if (close(fd) == -1) {
    perror("close failed");
}
⚠️ 注意事项:
  1. 不能重复关闭

    close(fd);
    close(fd); // ❌ 错误:fd 已关闭,可能导致 `EBADF` 错误
    
  2. 必须检查 close() 返回值

    • 如果 close() 失败(如 NFS 断连),fsync() 可能也失败,数据可能未写入磁盘。
    • 关闭失败可能意味着数据丢失,应立即 fsync(fd) 确保数据已落盘。
  3. 如何避免泄漏?

    • 尽早关闭:打开后立即 close(fd) 避免遗忘。
    • 使用 valgrind 检查
      valgrind --track-fds=yes ./my_program
      
    • C++ 可用 RAII 机制,用 std::unique_ptr<int, decltype(&close)> fd_guard(fd, close); 自动管理 fd

小结

  • open() 需要正确传参,O_CREAT 需搭配 modeO_EXCL 避免误覆盖。
  • close() 不能重复调用,必须检查返回值,防止数据丢失。
  • 最佳实践打开文件后尽快 close(),并用 fsync() 确保数据安全。🚀

3. 读写文件数据

3.1 read 读取文件

read() 负责从文件描述符 fd 中读取数据,并存入缓冲区 buffer

#include <unistd.h>
#include <stdio.h>

char buffer[100];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
    perror("read failed");
}
🔍 参数解析:
  • fd:文件描述符(必须是已打开的文件)
  • buffer:存储读取数据的缓冲区
  • sizeof(buffer):读取的最大字节数
📌 返回值:
  • >0:成功读取的字节数(可能小于请求的字节数)
  • 0:文件已到达末尾 (EOF)
  • -1:读取失败,需要检查 errno 进行错误处理
⚠️ 错误处理:
  1. EINTR(被信号中断)

    • read() 可能在读取过程中被信号打断,需要重新尝试:
    ssize_t bytesRead;
    do {
        bytesRead = read(fd, buffer, sizeof(buffer));
    } while (bytesRead == -1 && errno == EINTR);
    
  2. EAGAINEWOULDBLOCK(非阻塞模式下无数据可读)

    • 说明当前 fd 没有可读数据,但不会阻塞,可以稍后重试。
    • 适用于 O_NONBLOCK 方式打开的文件。
  3. EBADF(无效文件描述符)

    • 确保 fd 已正确打开,并且具有读取权限 (O_RDONLYO_RDWR)。

3.2 write 写入文件

write() 负责将数据写入文件描述符 fd,但不保证一次性写入所有数据,需要检查返回值。

#include <unistd.h>
#include <stdio.h>

ssize_t bytesWritten = write(fd, "Hello, Linux!", 13);
if (bytesWritten == -1) {
    perror("write failed");
}
📌 返回值:
  • >0:实际写入的字节数(可能小于请求的字节数)
  • -1:写入失败,需要检查 errno 进行错误处理
⚠️ write() 可能的失败情况:
  1. EINTR(被信号中断)

    • 需要循环写入直到完成:
    ssize_t totalWritten = 0;
    ssize_t bytesWritten;
    const char *data = "Hello, Linux!";
    size_t dataLen = 13;
    
    while (totalWritten < dataLen) {
        bytesWritten = write(fd, data + totalWritten, dataLen - totalWritten);
        if (bytesWritten == -1) {
            if (errno == EINTR) {
                continue;  // 继续尝试写入
            } else {
                perror("write failed");
                break;
            }
        }
        totalWritten += bytesWritten;
    }
    
  2. EAGAIN / EWOULDBLOCK(非阻塞模式下写缓冲区已满)

    • 适用于 O_NONBLOCK 文件,需等待缓冲区可用。
  3. ENOSPC(磁盘空间不足)

    • 需要检查磁盘空间 (df -h) 并清理文件。
  4. EBADF(无效文件描述符)

    • 确保 fd 已正确打开,并具有写入权限 (O_WRONLYO_RDWR)。

🎯 小结

  • read() 需要检查返回值,EINTR 需重新读取,0 表示 EOF
  • write() 不能假设一次写入完成,需要循环写入直到 totalWritten == bytesRead

这样,你的文件操作就更健壮了 💪🚀!

4. 高级文件操作

在基础的文件 openreadwrite 之外,Linux 还提供了一些高级文件操作函数,例如文件定位文件信息查询内存映射文件控制等。本章将详细介绍它们的用法及最佳实践。


4.1 lseek 文件定位

lseek() 允许你随意跳转文件位置,适用于随机读取文件扩展

📝 使用方法:
#include <unistd.h>
#include <fcntl.h>

off_t offset = lseek(fd, 10, SEEK_SET);  // 从文件开头跳过 10 个字节
if (offset == -1) {
    perror("lseek failed");
}
📌 常见模式:
  • SEEK_SET:从文件开头偏移 offset 个字节
  • SEEK_CUR:从当前位置偏移 offset 个字节
  • SEEK_END:从文件末尾偏移 offset 个字节(offset 需为负值,向前移动)
⚠️ 注意事项:
  1. lseek() 只是移动文件指针,不会修改文件内容
  2. 如果 offset 超过文件长度,文件不会立即增长,但后续 write() 会填充空洞。
  3. 用于获取文件大小
    off_t fileSize = lseek(fd, 0, SEEK_END);
    printf("文件大小: %ld 字节\n", fileSize);
    

4.2 stat 获取文件信息

stat()fstat() 用于获取文件的大小、权限、时间信息

📝 使用方法:
#include <sys/stat.h>
#include <stdio.h>

struct stat fileStat;
if (stat("test.txt", &fileStat) == -1) {
    perror("stat failed");
} else {
    printf("文件大小: %ld 字节\n", fileStat.st_size);
    printf("最后修改时间: %ld\n", fileStat.st_mtime);
}
📌 相关函数:
  • stat("file", &st):获取指定文件的信息
  • fstat(fd, &st):获取已打开文件的信息
  • lstat("file", &st):用于符号链接,不会解析真实路径
🔍 struct stat 关键字段:
  • st_size:文件大小(字节)
  • st_mode:文件类型和权限(可用 S_ISREG() 等宏判断)
  • st_mtime:最后修改时间
⚠️ 注意:
  • stat() 可能失败(如文件不存在、无权限),需检查 errno
  • st_mtimetime_t 类型,可用 ctime() 转换成人类可读格式。

4.3 mmap 内存映射文件

mmap() 将文件映射到内存,让你可以像访问数组一样操作文件,提高大文件读写效率。

📝 使用方法:
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>

int fd = open("test.txt", O_RDONLY);
struct stat st;
fstat(fd, &st);  // 获取文件大小

char *mapped = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
    perror("mmap failed");
} else {
    write(STDOUT_FILENO, mapped, st.st_size);  // 直接输出映射的内容
    munmap(mapped, st.st_size);
}
close(fd);
📌 参数解析:
  • NULL:让内核选择映射地址
  • st.st_size:映射的文件大小
  • PROT_READ只读模式(也可用 PROT_WRITE 进行写入)
  • MAP_PRIVATE私有映射,对内存的修改不会影响原文件
适用场景
  • 大文件读取(避免 read() 的内核态拷贝开销)
  • 共享内存MAP_SHARED 可实现进程间通信)
⚠️ mmap() 注意事项:
  1. mmap() 的内容会受 lseek() 影响吗?

    • 不会mmap() 直接操作内存,与 lseek() 无关。
  2. 如何正确释放 mmap() 资源?

    • 需要 munmap(mapped, size),否则可能会造成内存泄漏。

4.4 fcntl 文件控制

fcntl() 用于修改文件状态,如非阻塞模式文件锁等。

📝 设置非阻塞模式:
#include <fcntl.h>
#include <stdio.h>

int flags = fcntl(fd, F_GETFL);  // 获取当前文件状态
fcntl(fd, F_SETFL, flags | O_NONBLOCK);  // 添加 O_NONBLOCK(非阻塞模式)
📌 fcntl() 主要功能:
  • F_GETFL / F_SETFL:获取/设置文件状态,如 O_NONBLOCK
  • F_GETFD / F_SETFD:获取/设置文件描述符标志,如 FD_CLOEXEC
  • F_SETLK / F_SETLKW:文件锁(flock() 的替代方案)
🛠 fcntl() 进行文件加锁:
struct flock lock;
lock.l_type = F_WRLCK;  // 写锁
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;  // 0 代表锁定整个文件

if (fcntl(fd, F_SETLK, &lock) == -1) {
    perror("lock failed");
} else {
    printf("文件已加锁\n");
}
⚠️ fcntl() 注意事项:
  • F_SETLK 是非阻塞锁,如果文件已被锁定,会立刻返回 EBUSY
  • F_SETLKW 是阻塞锁,会一直等待直到锁可用。
  • 文件锁是advisory,不是强制锁,其他进程如果不检查锁,仍然可以写入!

🎯 小结

  • lseek() 用于文件定位,适合随机读写获取文件大小
  • stat() 能获取文件的大小、权限、修改时间,适合文件管理
  • mmap() 适用于大文件高效读写,但需要 munmap() 释放内存。
  • fcntl() 用于修改文件状态、加锁,常用于非阻塞 I/O 和进程间锁

学会这些高级操作,你的 Linux 文件操作技能就更上一层楼了!🚀💪

5. 实战案例

理论结合实践,学得更快更牢!这一章我们通过实战案例,综合运用文件操作相关函数,并用 strace 观察系统调用的实际执行情况。


5.1 综合案例:文件复制

我们实现一个简单的文件复制程序,使用 open()read()write()close() 等函数,让你更熟悉 Linux 文件操作的流程。

📝 代码实现
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define BUF_SIZE 4096  // 缓冲区大小

void copy_file(const char *src, const char *dst) {
    int src_fd = open(src, O_RDONLY);
    if (src_fd == -1) {
        perror("open source file failed");
        return;
    }

    int dst_fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (dst_fd == -1) {
        perror("open destination file failed");
        close(src_fd);
        return;
    }

    char buffer[BUF_SIZE];
    ssize_t bytes_read, bytes_written;

    while ((bytes_read = read(src_fd, buffer, BUF_SIZE)) > 0) {
        char *ptr = buffer;
        while (bytes_read > 0) {
            bytes_written = write(dst_fd, ptr, bytes_read);
            if (bytes_written == -1) {
                perror("write failed");
                close(src_fd);
                close(dst_fd);
                return;
            }
            bytes_read -= bytes_written;
            ptr += bytes_written;
        }
    }

    if (bytes_read == -1) {
        perror("read failed");
    }

    close(src_fd);
    close(dst_fd);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
        return 1;
    }

    copy_file(argv[1], argv[2]);
    return 0;
}
📌 代码解析
  1. open()

    • O_RDONLY 方式打开源文件。
    • O_WRONLY | O_CREAT | O_TRUNC 方式创建/清空目标文件。
    • 目标文件权限 0644,即所有者可读写,其他用户可读
  2. read() & write()

    • 循环读取:每次读取 BUF_SIZE(4KB)数据到 buffer
    • 循环写入:确保所有数据写入目标文件(防止 write() 只写入部分数据)。
  3. close()

    • 释放文件描述符,防止资源泄漏。
⚠️ 注意事项
  • 错误处理:每个 open()read()write()close() 调用后都要检查返回值。
  • write() 可能无法一次性写完所有数据,所以需要循环写入直到数据全部写入。
  • 大文件优化:使用 mmap() 可能会比 read() / write() 方式更高效。

5.2 用 strace 观察文件操作

想知道一个程序内部调用了哪些系统调用strace 是一个非常强大的工具,它可以跟踪系统调用,帮助你分析程序的文件操作行为。

📝 基本用法
strace -e trace=open,read,write,close ./file_copy source.txt dest.txt

💡 -e trace=open,read,write,close:只跟踪文件相关的系统调用。

📌 示例输出
open("source.txt", O_RDONLY) = 3
open("dest.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 4
read(3, "Hello, world!", 4096) = 13
write(4, "Hello, world!", 13) = 13
close(3) = 0
close(4) = 0
🔍 分析
  • open("source.txt", O_RDONLY) = 3
    • 打开源文件 source.txt,分配的文件描述符3
  • open("dest.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644) = 4
    • 创建/清空 dest.txt,分配的文件描述符是 4
  • read(3, "Hello, world!", 4096) = 13
    • 从文件 3 读取 13 字节内容(Hello, world!)。
  • write(4, "Hello, world!", 13) = 13
    • 将 13 字节写入目标文件 4
  • close(3) = 0close(4) = 0
    • 关闭文件,释放资源。

🎯 小结

  • 文件复制是一个很好的实战练习,掌握 open()read()write()close()基本用法和错误处理
  • strace 可以跟踪系统调用,帮助你分析程序的文件操作行为,是调试必备工具。

掌握这些内容后,你已经具备 Linux 文件操作的实战经验!💪🚀

6. 总结

  • 文件操作 API 核心点

    • open() / close() 负责文件打开与关闭
    • read() / write() 进行数据读写
    • lseek() 控制文件偏移
    • stat() 获取文件信息
    • mmap() 高效读写大文件
  • 最佳实践

    • 错误处理要到位,检查 openreadwrite 返回值
    • 资源必须释放,用 close(fd) 关闭文件
    • 考虑性能优化,大文件推荐 mmap()

希望这篇文章能帮助你掌握 Linux 文件操作,让你的代码更高效、更稳健!🚀

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值