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_RDONLY、O_WRONLY、O_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编程是系统编程的基石,核心要点回顾:
- 文件类型与inode:理解"一切皆文件"的设计哲学,inode是文件的唯一标识
- 基本IO操作:掌握open/read/write/close/lseek的系统调用和使用细节
- 文件描述符管理:理解fd的分配规则,熟练使用dup/dup2进行重定向
- 内存映射mmap:高效的文件访问方式,适合处理大文件
- 文件控制fcntl:灵活管理文件属性,实现非阻塞IO和文件锁
- 文件属性stat:获取文件元数据,判断文件类型和权限
- 空洞文件:lseek可以超过文件大小,产生用\0填充的空洞
掌握这些系统调用是后续学习进程间通信、网络编程等高级主题的基础。
原始笔记来源: frasight/Liunx网络编程笔记.c(文件系统相关章节)