Linux:文件IO

时间:2024-10-25 20:10:00
1. 文件描述符

在 Linux 中,文件 I/O 的核心概念是文件描述符 (File Descriptor)。每当进程打开一个文件时,操作系统都会返回一个整数,称为文件描述符,它用于标识文件,系统中的所有文件操作都依赖于该文件描述符

常见的文件描述符:

  • 标准输入 (STDIN): 文件描述符 0
  • 标准输出 (STDOUT): 文件描述符 1
  • 标准错误 (STDERR): 文件描述符 2
2. 常用的文件操作函数

Linux 文件 I/O 提供了一系列的系统调用来进行文件的读写操作,这里是常用的几个函数:

  • open(): 打开文件
  • read(): 读取文件
  • write(): 写入文件
  • lseek(): 移动文件指针
  • close(): 关闭文件
close() 函数

close() 函数用于关闭文件描述符,防止资源泄漏

int close(int fd);
open() 函数

open() 函数用于打开文件并返回文件描述符,它的原型如下

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  • pathname: 文件路径。
  • flags: 打开模式,可以是以下几种或它们的组合:
    • O_RDONLY: 只读模式打开文件
    • O_WRONLY: 只写模式打开文件
    • O_RDWR: 读写模式打开文件
    • O_CREAT: 文件不存在时创建文件
    • O_TRUNC: 打开文件时清空文件内容
    • O_APPEND: 以追加模式打开文件。如果文件是以写模式(O_WRONLYO_RDWR)打开的,并且设置了O_APPEND标志,则所有写入都会追加到文件的末尾,而不是覆盖文件的现有内容
    • O_EXCL: 与O_CREAT一起使用时,如果文件已经存在,则open调用会失败。这通常用于创建文件时的原子性检查
    • O_SYNC: 以同步方式打开文件。每次对文件进行的写操作都会等待物理写入完成后再继续。这会影响性能,但可以保证数据的完整性
    • O_DSYNC: 类似于O_SYNC,但只同步数据,不同步元数据(如修改时间和访问时间)
    • O_NONBLOCK: 对于设备文件或FIFO(命名管道),以非阻塞方式打开
    • O_CLOEXEC: 在exec调用之后关闭文件描述符。这有助于防止文件描述符在程序执行新程序时被意外继承
  • mode_t 是一个数据类型,通常用于表示文件的权限:
    • S_IRUSR:文件所有者具有读权限
    • S_IWUSR:文件所有者具有写权限
    • S_IXUSR:文件所有者具有执行权限
    • S_IRGRP:文件所属组具有读权限
    • S_IWGRP:文件所属组具有写权限
    • S_IXGRP:文件所属组具有执行权限
    • S_IROTH:其他用户具有读权限
    • S_IWOTH:其他用户具有写权限
    • S_IXOTH:其他用户具有执行权限

这里打开或创建了一个文件 example.txt,并赋予该文件的拥有者读写权限,这里的S_IRUSR:表示文件所有者具有读权限,S_IWUSR:表示文件所有者具有写权限。

#include <fcntl.h>  // 引入文件控制选项的头文件,例如O_RDWR, O_CREAT等  
#include <unistd.h> // 引入POSIX操作系统API的头文件,例如close()函数  
#include <stdio.h>  // perror()函数定义在此  
  
int main() {  
    // 尝试以读写模式打开(如果不存在则创建)名为"example.txt"的文件  
    // 文件权限设置为只有文件所有者可以读写(S_IRUSR | S_IWUSR)  
    int fd = open("example.txt", O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);  
      
    // 检查open()函数是否成功执行  
    // 如果返回-1,表示打开(或创建)文件失败  
    if (fd == -1) {  
        // 使用perror()函数打印错误信息  
        // perror()会根据全局变量errno的值,输出对应的错误信息字符串  
        // "open"是传递给perror()的字符串,用于标识是哪个函数调用失败  
        perror("open");  
          
        // 返回1,表示程序异常终止  
        // 在许多操作系统和脚本中,非零返回值通常表示错误或异常  
        return 1;  
    }  
      
    // 此处可以添加对文件的读写操作  
    // 例如:write(fd, "Hello, World!", 13);  
    // 但由于本示例仅关注文件的打开与关闭,因此省略了具体的文件操作  
      
    // 使用close()函数关闭文件描述符  
    // 文件描述符是一个整数,用于在后续的文件操作中标识文件  
    // 当文件不再需要时,应关闭它,以释放系统资源  
    close(fd);  
      
    // 程序正常结束,返回0  
    return 0;  
}
read() 函数

read() 函数用于从文件中读取数据,其原型如下:

ssize_t read(int fd, void *buf, size_t count);
  • fd: 文件描述符
  • buf: 存放读取数据的缓冲区
  • count: 要读取的字节数

这里从 example.txt 中读取数据并输出读取的字节数

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

int main() {
    int fd = open("example.txt", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char buffer[128];
    ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
    if (bytesRead == -1) {
        perror("read");
    } else {
        printf("Read %ld bytes: %s\n", bytesRead, buffer);
    }

    close(fd);
    return 0;
}
write() 函数

write() 函数用于向文件中写入数据,其原型如下:

ssize_t write(int fd, const void *buf, size_t count);

  • fd: 文件描述符
  • buf: 要写入的数据
  • count: 要写入的字节数

这里向 example.txt 写入字符串 "Hello, Linux I/O!"

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

int main() {
    int fd = open("example.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    const char *data = "Hello, Linux I/O!";
    ssize_t bytesWritten = write(fd, data, sizeof(data));
    if (bytesWritten == -1) {
        perror("write");
    } else {
        printf("Wrote %ld bytes\n", bytesWritten);
    }

    close(fd);
    return 0;
}
lseek() 函数

lseek() 用于移动文件指针,其原型如下:

off_t lseek(int fd, off_t offset, int whence);
  • fd: 文件描述符
  • offset: 偏移量
  • whence: 指定如何使用偏移量,常见值为:
    • SEEK_SET: 从文件开头开始
    • SEEK_CUR: 从当前文件指针位置开始
    • SEEK_END: 从文件末尾开始

这里将文件指针移动到文件末尾

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

int main() {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    off_t newPos = lseek(fd, 0, SEEK_END);  // 移动到文件末尾
    if (newPos == -1) {
        perror("lseek");
    } else {
        printf("New file position: %ld\n", newPos);
    }

    close(fd);
    return 0;
}
3. 文件 I/O 的错误处理

每个系统调用在失败时通常返回 -1,并设置 errno 变量来指示具体的错误类型。可以通过 perror() 函数输出详细的错误信息

#include <errno.h>
#include <stdio.h>

if (fd == -1) {
    perror("open failed");
}
4. 阻塞与非阻塞 I/O

Linux 提供了阻塞 (blocking)非阻塞 (non-blocking) I/O 模式:

  • 阻塞 I/O: 系统调用会等待操作完成后才返回
  • 非阻塞 I/O: 系统调用会立即返回,操作可能未完成

通过设置 open() 的标志为 O_NONBLOCK,我们可以实现非阻塞 I/O

5. 文件锁定

在多进程环境下,文件锁定非常重要,Linux 提供了两种文件锁定机制:

  • flock(): 提供简便的文件锁定接口
  • fcntl(): 提供更细粒度的文件锁定控制

这里为文件 example.txt 添加独占锁,确保只有一个进程能进行写操作

#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <stdio.h>

int main() {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    if (flock(fd, LOCK_EX) == -1) {
        perror("flock");
        close(fd);
        return 1;
    }

    // 文件锁定操作
    flock(fd, LOCK_UN);  // 解锁

    close(fd);
    return 0;
}
6. 异步 I/O 与内存映射
  • 异步 I/O (AIO): 使用 libaio 或 POSIX 的 aio_* 系列函数实现异步 I/O,允许进程在 I/O 操作未完成时继续执行其他任务
  • 内存映射 (mmap): 使用 mmap() 函数将文件映射到进程的地址空间,可以直接像访问内存一样读写文件数据,提升 I/O 效率
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd = open("example.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    char *mapped = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (mapped == MAP_FAILED) {
        perror("mmap");
        close(fd);
        return 1;
    }

    // 使用映射的内存
    printf("File contents: %s\n", mapped);

    munmap(mapped, 4096);  // 解除映射
    close(fd);
    return 0;
}

 有点难,详细解释一下:

异步 I/O 的核心思想是让进程在执行 I/O 操作时不必等待操作完成,从而可以并发执行其他任务。通常的同步 I/O 操作是阻塞式的,也就是当一个 I/O 请求发出后,进程会被挂起,直到请求完成。而异步 I/O 允许程序在请求发出后立即返回,后台继续处理该请求,进程可以做其他事情

实现方式:
  • POSIX AIO: POSIX 的 aio_* 系列函数,如 aio_read() 和 aio_write() 提供异步 I/O 支持,常用于多任务系统和高并发应用。调用这些函数后,I/O 操作在后台执行,进程可继续执行其他代码。通过回调机制、信号、或轮询(polling)等方式,程序可以在 I/O 操作完成时得到通知
  • libaio: libaio 是 Linux 系统中用于提供异步 I/O 的库,直接与内核进行交互。与 POSIX AIO 相比,libaio 提供了更高效的非阻塞 I/O 处理方式。它通过提交 I/O 请求给内核,并利用内核事件完成队列(completion queue)来通知用户态程序操作的完成

内存映射 (mmap)

内存映射 (mmap) 的主要功能是将文件或设备映射到进程的虚拟地址空间,从而能够像访问内存一样对文件进行读写操作。它减少了读写文件时内核与用户态之间的上下文切换,同时避免了传统的 read()write() 系统调用的重复数据拷贝,极大地提高了文件 I/O 的效率

实现机制:
  • mmap() 函数: 使用 mmap() 函数,应用程序可以将一个文件或设备映射到其进程的虚拟地址空间。映射完成后,文件的内容就像内存中的数据一样可直接访问,无需通过额外的系统调用
  • 虚拟内存页: 操作系统将文件的部分或全部内容映射到内存页,进程通过访问这些内存页来读取或写入文件。当访问的页面不在物理内存中时,操作系统会自动将对应的页面从磁盘加载到内存(称为“页面缺失”)
  • 共享与私有映射: mmap 支持共享(MAP_SHARED)和私有(MAP_PRIVATE)两种模式。共享模式下,多个进程可以映射同一个文件,任何进程的修改都会反映到文件中;私有模式下,修改仅对当前进程可见

所以:

  • 异步 I/O (AIO) 适用于需要高并发、非阻塞 I/O 操作的场景,能够减少 I/O 操作的等待时间,充分利用 CPU 和 I/O 资源
  • 内存映射 (mmap) 则提供了一种更高效的文件 I/O 方式,减少系统调用开销和数据拷贝,提高了文件读写的性能,特别适合处理大文件