Linux文件系统与IO编程

Linux文件系统与IO编程

简介

在Linux中,"一切皆文件"是最核心的设计哲学之一。不仅普通数据是文件,设备、目录、管道、套接字等统统以文件的形式呈现。理解Linux文件系统的运作机制和IO编程接口,是系统编程的根基。本文将从文件系统基础概念出发,详细讲解文件IO相关的系统调用,包括open、read、write、mmap、fcntl等核心接口的原理与使用方法。

一、Linux文件系统基础

1.1 文件类型

Linux下的文件是一个很广泛的概念,不仅仅指磁盘上的数据文件,还包括设备、目录等:

文件类型 标识 说明 示例
普通文件 - 纯文本、二进制、数据文件 .c, .txt, .o
目录文件 d 目录本身也是一种文件 /home, /tmp
字符设备文件 c 按字符流实时读写,无缓冲 串口、终端、控制台
块设备文件 b 按固定大小块读写,带缓冲 硬盘、U盘
链接文件 l 软链接(符号链接) 快捷方式
管道文件 p 用于进程间通信(无名管道) pipe
套接字文件 s 用于网络通信 socket
bash 复制代码
# 查看文件类型
ls -la                            # 第一列字符标识文件类型
file filename                     # 查看文件具体类型
stat filename                     # 查看文件详细状态信息

# 磁盘和分区操作
fdisk -l                          # 查看当前磁盘状态
mkfs.ext4 /dev/sda1               # 格式化分区
mount /dev/sda1 /mnt/point        # 挂载分区
df -h                             # 查看分区挂载情况

# 特殊设备文件
# /dev/null - 黑洞设备,写入的数据被丢弃,读取立即返回EOF
# /dev/zero - 零设备,读取时无限返回空字符

1.2 inode索引节点

inode(索引节点)是Linux文件系统中最重要的概念之一,它是文件在文件系统中的唯一标识。

图片占位符:inode结构示意图,展示inode与数据块的对应关系

inode包含的信息:

  • 文件大小
  • 文件所有者(UID)和所属组(GID)
  • 文件的读/写/执行权限
  • 文件的时间戳(ctime、atime、mtime)
  • 文件数据块的磁盘地址指针
  • 链接计数(有多少个硬链接指向此inode)
bash 复制代码
ls -i filename                    # 查看文件的inode号(最前面的数字)
ls -i | grep 节点号               # 查找指定inode的文件

# 硬链接的特征:
# - 硬链接与源文件的inode号相同
# - 删除源文件不影响硬链接
# - 删除硬链接也不影响源文件
# - 只影响链接计数(-rwxrwxrwx后面的一位数字)

# 软链接的特征:
# - 软链接有自己的inode号
# - 删除源文件后软链接失效

1.3 虚拟文件系统(VFS)

Linux通过虚拟文件系统(Virtual File System, VFS)将所有不同的文件系统进行抽象,对内核提供统一的系统调用接口。这意味着应用程序无需关心底层使用的是ext4、xfs还是NTFS,都可以使用相同的open/read/write接口进行操作。

二、文件IO系统调用

2.1 open() -- 打开/创建文件

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

// 打开已有文件
int open(const char *pathname, int flags);

// 打开或创建文件
int open(const char *pathname, int flags, mode_t mode);

// 创建新文件
int creat(const char *pathname, mode_t mode);

// 返回值:成功返回文件描述符(非负整数),失败返回-1

flags参数说明:

标志 说明
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
O_CREAT 文件不存在则创建(需配合mode参数)
O_APPEND 追加写入(在文件末尾添加)
O_TRUNC 文件存在且可写则清空文件内容
O_NONBLOCK 以非阻塞方式打开
O_EXCL 与O_CREAT同时使用,文件已存在则报错

注意事项:

  • O_RDONLYO_WRONLYO_RDWR 三者互斥,只能选其一
  • 使用 O_CREAT 时必须提供 mode 参数指定权限
  • pathname 路径长度不能超过1024字节,超出部分会被截断
  • 出错处理时建议使用 __LINE____func__ 等宏辅助定位

mode权限值示例:

c 复制代码
// 常用权限组合
mode_t mode = 0644;   // rw-r--r--  文件常用权限
mode_t mode = 0755;   // rwxr-xr-x 目录常用权限

// 权限还受 umask 影响
// 实际权限 = mode & ~umask

2.2 close() -- 关闭文件

c 复制代码
#include <unistd.h>

int close(int fd);
// 返回值:成功返回0,失败返回-1

关闭文件后,文件描述符被释放,可供后续open使用。程序终止时内核会自动关闭所有打开的文件。

2.3 read() -- 读取文件

c 复制代码
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
// fd:     文件描述符
// buf:    接收数据的缓冲区
// count:  期望读取的最大字节数
// 返回值: 实际读取的字节数;到达文件尾返回0;出错返回-1

关键注意点:

c 复制代码
// 正确的读取循环方式
char buf[1024];
ssize_t ret;
while ((ret = read(fd, buf, sizeof(buf))) > 0) {
    // 处理读取到的数据
    // 注意:条件不能写成 >= 0,否则到达文件尾会死循环
}

// ret == 0 表示到达文件末尾(EOF)
// ret == -1 表示出错

2.4 write() -- 写入文件

c 复制代码
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
// fd:     文件描述符
// buf:    要写入的数据缓冲区
// count:  期望写入的字节数
// 返回值: 实际写入的字节数;出错返回-1

注意事项:

  • write 并不能保证数据真正写入磁盘设备,它可能先将数据写入内核缓冲区
  • 系统会在合适的时机将缓冲区数据刷入磁盘(延迟写)
  • 如需确保数据写入磁盘设备,可调用 fsync()
c 复制代码
#include <unistd.h>

int fsync(int fd);
// 强制将文件描述符fd对应的缓冲区数据写入磁盘设备
// 成功返回0,失败返回-1

2.5 lseek() -- 文件定位

c 复制代码
#include <sys/types.h>
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
// fd:      文件描述符
// offset:  偏移量(可正可负)
// whence:  基准位置
// 返回值:  新的文件偏移量;出错返回-1

whence参数取值:

常量 说明
SEEK_SET 文件起始位置
SEEK_CUR 当前读写位置
SEEK_END 文件末尾位置

注意事项:

  • 标准输入流(stdin)不可以使用lseek定位
  • lseek定位位置可以超过文件大小,这会产生"空洞文件"
  • 空洞文件中未写入的区域用 \0 填充
c 复制代码
// 空洞文件示例
// 假设文件当前内容:hello world
lseek(fd, 20, SEEK_SET);          // 定位到第20个字节
write(fd, "C++", 3);               // 写入"C++"
// 文件内容变为:hello world\0\0\0\0\0\0\0\0C++
// 中间空余位置用\0填充

// 以十六进制查看文件内容
// od -c file

图片占位符:空洞文件原理图

三、文件描述符管理

3.1 文件描述符基础

文件描述符(File Descriptor, fd)是内核为了高效管理已打开文件而分配的非负整数索引值。

  • 文件描述符0:标准输入(stdin)
  • 文件描述符1:标准输出(stdout)
  • 文件描述符2:标准错误(stderr)

系统默认每个进程最多可打开1024个文件(可通过 ulimit -n 查看和修改)。

3.2 dup() / dup2() -- 文件描述符复制

c 复制代码
#include <unistd.h>

// dup: 复制文件描述符,返回当前系统最小的可用文件描述符
int dup(int oldfd);
// 成功返回新的文件描述符,失败返回-1
// 新旧fd指向同一个文件表项,共享文件偏移量

// dup2: 将oldfd复制为newfd
int dup2(int oldfd, int newfd);
// 如果newfd已经打开,则先关闭再复制
// 成功返回newfd,失败返回-1

常用场景:

c 复制代码
// 重定向标准错误到文件
int fd = open("error.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDERR_FILENO);           // 将fd重定向到stderr(2)
close(fd);
// 之后所有写入stderr的内容都会写入error.log文件

// C语言中也可使用 freopen
FILE *fp = freopen("error.log", "w+", stderr);
// 后面对stderr的操作就是对文件的操作

// 判断文件描述符是否为套接字
// 可利用 fstat(fd, &st) 获取描述符状态
// 再检查 (st.st_mode & S_IFMT) == S_IFSOCK

图片占位符:dup/dup2文件描述符复制原理图

四、文件状态与属性

4.1 stat系列函数 -- 获取文件信息

c 复制代码
#include <sys/stat.h>
#include <unistd.h>

// 通过文件路径获取文件状态
int stat(const char *pathname, struct stat *buf);

// 通过文件描述符获取文件状态
int fstat(int fd, struct stat *buf);

// 获取链接文件本身的状态(而非指向的文件)
int lstat(const char *pathname, struct stat *buf);

// 成功返回0,失败返回-1

struct stat结构体详解:

c 复制代码
struct stat {
    mode_t     st_mode;       // 文件类型和权限
    ino_t      st_ino;        // inode节点号
    dev_t      st_dev;        // 设备号码(文件系统ID)
    dev_t      st_rdev;       // 特殊设备号码
    nlink_t    st_nlink;      // 硬链接数
    uid_t      st_uid;        // 文件所有者UID
    gid_t      st_gid;        // 文件所有者GID
    off_t      st_size;       // 文件大小(字节数)
    time_t     st_atime;      // 最后访问时间
    time_t     st_mtime;      // 最后修改时间(内容)
    time_t     st_ctime;      // 最后状态改变时间(权限/所有者等)
    blksize_t  st_blksize;    // I/O操作的优选块大小
    blkcnt_t   st_blocks;     // 分配的块数量
};

常用宏判断文件类型:

c 复制代码
struct stat st;
stat("filename", &st);

if (S_ISREG(st.st_mode))  printf("普通文件\n");
if (S_ISDIR(st.st_mode))  printf("目录\n");
if (S_ISCHR(st.st_mode))  printf("字符设备\n");
if (S_ISBLK(st.st_mode))  printf("块设备\n");
if (S_ISFIFO(st.st_mode)) printf("管道/FIFO\n");
if (S_ISLNK(st.st_mode))  printf("符号链接\n");
if (S_ISSOCK(st.st_mode)) printf("套接字\n");

五、内存映射(mmap)

5.1 mmap() -- 文件内存映射

mmap将文件或设备映射到进程的地址空间,通过对映射后的内存地址直接操作来读写文件,省去了数据在内核空间和用户空间之间的拷贝,效率极高。

图片占位符:mmap内存映射原理图,展示文件与虚拟地址空间的映射关系

c 复制代码
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// addr:    映射的起始地址(通常设NULL,由系统自动分配)
// length:  映射的长度
// prot:    映射区域的保护方式
// flags:   映射的类型
// fd:      要映射的文件描述符
// offset:  文件中的偏移量(映射起始位置)
// 成功返回映射区的地址,失败返回 (void *)-1

prot保护方式(可组合使用):

常量 说明
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问

flags映射类型(必选其一):

常量 说明
MAP_SHARED 多个进程共享映射区域,写入会更新文件
MAP_PRIVATE 私有映射,写入产生副本(Copy-on-Write)

注意: prot的权限不能超过fd打开时的权限。例如fd以 O_RDONLY 打开,则 PROT_WRITE 会失效。

5.2 munmap() -- 解除映射

c 复制代码
int munmap(void *addr, size_t length);
// addr:    mmap返回的映射地址
// length:  映射的长度
// 成功返回0,失败返回-1

5.3 mmap编程规范

c 复制代码
// mmap使用的标准步骤:
// 1. 通过open获得文件描述符
int fd = open("file.txt", O_RDWR);

// 2. 获取文件大小
struct stat st;
fstat(fd, &st);

// 3. 对fd进行映射
void *addr = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE,
                  MAP_SHARED, fd, 0);

// 4. 通过指针对这块内存进行操作(像操作内存一样操作文件)
char *data = (char *)addr;
data[0] = 'H';  // 直接修改映射区,会同步到文件

// 5. 解除映射
munmap(addr, st.st_size);

// 6. 关闭文件描述符
close(fd);

六、fcntl -- 文件控制

fcntl(file control)用于获取或设置已打开文件的各种属性。

c 复制代码
#include <fcntl.h>
#include <unistd.h>

// 三种函数原型
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);

cmd命令说明:

命令 说明
F_DUPFD 复制文件描述符(类似dup)
F_GETFD 获取文件描述符标志
F_SETFD 设置文件描述符标志
F_GETFL 获取文件状态标志(O_RDONLY等)
F_SETFL 设置文件状态标志(常用:设置O_NONBLOCK)
F_GETOWN 获取异步IO所有权
F_SETOWN 设置异步IO所有权
F_GETLK 获取记录锁
F_SETLK 设置记录锁(非阻塞)
F_SETLKW 设置记录锁(阻塞)

6.1 设置非阻塞模式

c 复制代码
// 将文件描述符设置为非阻塞方式
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);

// 这会影响read和write的行为:
// 非阻塞模式下,如果没有数据可读或写入缓冲区满,
// read/write不会阻塞,而是立即返回-1,errno设为EAGAIN

6.2 文件记录锁

c 复制代码
struct flock {
    short l_type;     // 锁类型:F_RDLCK(读锁), F_WRLCK(写锁), F_UNLCK(解锁)
    short l_whence;   // 偏移基准:SEEK_SET, SEEK_CUR, SEEK_END
    off_t l_start;    // 相对于whence的偏移量
    off_t l_len;      // 锁定长度(0表示锁到文件末尾)
    pid_t l_pid;      // 持有锁的进程ID(F_GETLK时返回)
};

// 加写锁示例
struct flock fl;
fl.l_type = F_WRLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;          // 锁整个文件
fcntl(fd, F_SETLK, &fl);  // 非阻塞加锁
// 或 fcntl(fd, F_SETLKW, &fl);  // 阻塞加锁

七、ioctl -- 设备控制

ioctl(I/O control)是对设备文件的读写控制接口,用于执行设备特定的操作。

c 复制代码
#include <sys/ioctl.h>

int ioctl(int fd, unsigned long request, ...);
// fd:      文件描述符(通常是设备文件)
// request: 设备相关的请求码
// ...:     可选参数,取决于request
// 成功返回0,失败返回-1

ioctl常用于:

  • 网络接口配置(如获取/设置IP地址、MAC地址)
  • 串口参数配置(波特率、数据位等)
  • 终端属性设置
  • 其他设备特定的控制操作

八、目录操作

c 复制代码
#include <sys/types.h>
#include <dirent.h>

// 打开目录
DIR *opendir(const char *name);

// 读取目录项
struct dirent *readdir(DIR *dirp);
// struct dirent {
//     ino_t  d_ino;       // inode号
//     char   d_name[256]; // 文件名
// };

// 关闭目录
int closedir(DIR *dirp);

// 目录操作示例:遍历目录中的所有文件
DIR *dir = opendir(".");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
    printf("%s\n", entry->d_name);
}
closedir(dir);

总结

Linux文件IO编程是系统编程的基石,核心要点回顾:

  1. 文件类型与inode:理解"一切皆文件"的设计哲学,inode是文件的唯一标识
  2. 基本IO操作:掌握open/read/write/close/lseek的系统调用和使用细节
  3. 文件描述符管理:理解fd的分配规则,熟练使用dup/dup2进行重定向
  4. 内存映射mmap:高效的文件访问方式,适合处理大文件
  5. 文件控制fcntl:灵活管理文件属性,实现非阻塞IO和文件锁
  6. 文件属性stat:获取文件元数据,判断文件类型和权限
  7. 空洞文件:lseek可以超过文件大小,产生用\0填充的空洞

掌握这些系统调用是后续学习进程间通信、网络编程等高级主题的基础。


原始笔记来源: frasight/Liunx网络编程笔记.c(文件系统相关章节)

相关推荐
HHFQ9 小时前
在 systemd 场景下的 CPU 限制方式
linux
道清茗9 小时前
【RH294知识点汇总】第 9 章 《 自动执行 Linux 管理任务 》常见问题
linux·运维·服务器
山羊硬件Time9 小时前
自动化管理Linux的好工具:shell script
linux·嵌入式硬件·硬件工程师·基带工程·硬件开发
王老师青少年编程9 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【哈夫曼贪心】:合并果子
c++·算法·贪心·csp·信奥赛·哈夫曼贪心·合并果子
叼烟扛炮10 小时前
C++第二讲:类和对象(上)
数据结构·c++·算法·类和对象·struct·实例化
wj30558537810 小时前
Codex + Git 开发环境配置指南(WSL版)
linux·运维·git
星马梦缘10 小时前
如何切换window-ubuntu双系统【方案二】
linux·运维·ubuntu
样例过了就是过了11 小时前
LeetCode热题100 最长公共子序列
c++·算法·leetcode·动态规划
谭欣辰11 小时前
C++ 排列组合完整指南
开发语言·c++·算法