Linux创建文件API接口详解
Linux中创建文件的API接口
在Linux中,创建文件主要通过以下系统调用实现:
1. open()
- 功能:用于打开或创建文件。
- 创建文件的标志:
O_CREAT
:当文件不存在时创建新文件。O_EXCL
:与O_CREAT
结合使用,确保文件不存在时才创建(避免竞争条件)。
- 示例:
1
int fd = open("filename", O_CREAT | O_WRONLY, 0644);
- 若文件不存在,则创建并设置权限为
0644
(受umask
影响)。 - 返回文件描述符
fd
,失败时返回-1
。
- 若文件不存在,则创建并设置权限为
2. creat()
- 功能:专用于创建文件的旧接口(现已被
open()
取代)。 - 等效行为:
creat(path, mode)
等价于open(path, O_CREAT | O_WRONLY | O_TRUNC, mode)
。 - 示例:
1
int fd = creat("filename", 0644);
- 以只写方式创建文件,若存在则截断内容。
- 返回文件描述符
fd
,失败时返回-1
。
关键区别
接口 | 灵活性 | 推荐程度 |
---|---|---|
open() |
支持多种标志(如读写模式) | 更灵活,推荐使用 |
creat() |
功能单一(仅写模式) | 过时,不推荐新代码使用 |
补充说明
- 权限控制:使用
O_CREAT
时需指定mode
参数(如0644
),最终权限为mode & ~umask
。 - 原子性操作:
O_EXCL
可避免多进程竞争(确保仅一个进程创建文件)。 - 高阶封装:C 标准库的
fopen("filename", "w")
底层调用open()
实现文件创建。
总结:优先使用 open()
+ O_CREAT
创建文件,兼具灵活性和现代性。
Linux下文件类型,包括普通文件、目录文件、链接文件,还有哪些
在 Linux 系统中,文件类型不仅限于普通文件、目录和链接文件,还包含其他特殊类型。以下是 Linux 支持的完整文件类型及其特点:
1. 普通文件(Regular File)
- 标识符:
-
(ls -l
输出的第一个字符) - 特点:存储普通数据(文本、二进制、图片等),无特殊功能。
- 示例:
1
$ touch example.txt # 创建一个普通文件
2. 目录文件(Directory File)
- 标识符:
d
- 特点:存储文件/子目录的索引(文件名与 inode 的映射)。
- 操作:通过
mkdir
创建,rmdir
删除空目录。1
$ mkdir mydir # 创建目录
3. 符号链接文件(Symbolic Link / Soft Link)
- 标识符:
l
- 特点:指向另一个文件的路径(类似 Windows 快捷方式)。
- 操作:
1
$ ln -s target.txt link.txt # 创建符号链接
4. 字符设备文件(Character Device File)
- 标识符:
c
- 特点:提供无缓冲的流式访问(逐字符操作),用于键盘、串口等设备。
- 示例:
1
2/dev/null # 空设备(丢弃所有写入数据)
/dev/ttyS0 # 串口设备
5. 块设备文件(Block Device File)
- 标识符:
b
- 特点:提供带缓冲的块级访问(按块操作),用于磁盘、SSD 等存储设备。
- 示例:
1
2/dev/sda # 第一块硬盘
/dev/nvme0 # NVMe 固态硬盘
6. 套接字文件(Socket File)
- 标识符:
s
- 特点:用于进程间网络通信(如 TCP/IP 或本地套接字)。
- 常见位置:
/tmp
、/run
目录。 - 示例:
1
$ nc -U /tmp/mysocket.sock # 创建 UNIX 域套接字
7. 命名管道文件(Named Pipe / FIFO)
- 标识符:
p
- 特点:允许无关联进程通过管道通信(先进先出)。
- 操作:
1
2
3$ mkfifo mypipe # 创建命名管道
$ echo "data" > mypipe & # 写入端
$ cat mypipe # 读取端
8. 其他特殊类型
- 未知类型(
?
):某些系统可能定义非标准类型。 - 门(Door)(Solaris 特有,Linux 中罕见):用于进程间高效通信。
如何查看文件类型?
- **
ls -l
**:观察首字符(如d
、l
、c
等)。 file
命令:1
$ file /dev/null # 输出: /dev/null: character special file
stat
命令:1
$ stat myfile # 查看详细文件类型信息
总结
类型 | 标识符 | 典型路径/用途 |
---|---|---|
普通文件 | - |
文本、二进制文件 |
目录文件 | d |
/home , /etc |
符号链接 | l |
指向其他文件的快捷方式 |
字符设备 | c |
/dev/tty , /dev/null |
块设备 | b |
/dev/sda , /dev/nvme0n1 |
套接字 | s |
/tmp/mysql.sock |
命名管道(FIFO) | p |
进程间通信管道 |
理解这些文件类型有助于深入掌握 Linux 文件系统的行为和系统管理。
Linux内核中删除文件的函数接口
在 Linux 内核中,删除文件的核心函数是 vfs_unlink()
,它属于虚拟文件系统(VFS)层。以下是完整的文件删除机制和相关接口:
核心删除函数
vfs_unlink()
- 功能:VFS 层删除文件的通用接口
- 位置:
fs/namei.c
- 原型:
1
int vfs_unlink(struct inode *dir, struct dentry *dentry, struct inode **delegated_inode)
- 参数:
dir
:父目录的 inodedentry
:要删除文件的目录项(包含文件名和 inode 指针)delegated_inode
:用于处理 NFS 等文件系统的委托
调用流程(用户空间到内核)
用户空间系统调用:
1
2
3unlink(const char *pathname); // 删除文件
unlinkat(int dirfd, const char *pathname, int flags); // 更灵活的版本
rmdir(const char *pathname); // 删除空目录内核系统调用入口:
1
2
3SYSCALL_DEFINE1(unlink, const char __user *, pathname)
SYSCALL_DEFINE3(unlinkat, int, dfd, const char __user *, pathname, int, flag)
SYSCALL_DEFINE1(rmdir, const char __user *, pathname)核心路径解析:
filename_lookup()
:解析路径获取 dentry 和 inodelookup_one_qstr_excl()
:查找目标文件
最终调用:
1
vfs_unlink(dir, dentry, NULL);
文件系统具体实现
每个文件系统需实现 inode_operations
中的 unlink
方法:
1 | struct inode_operations { |
示例文件系统实现:
Ext4 文件系统:
1
2
3
4const struct inode_operations ext4_dir_inode_operations = {
.unlink = ext4_unlink, // 具体实现
...
};- 实现位置:
fs/ext4/namei.c
中的ext4_unlink()
- 实现位置:
XFS 文件系统:
1
2
3
4const struct inode_operations xfs_dir_inode_operations = {
.unlink = xfs_unlink,
...
};
删除操作的关键步骤
权限检查:
- 调用
inode_permission(dir, MAY_WRITE | MAY_EXEC)
检查目录写权限 - 检查文件是否只读(
IS_APPEND(inode)
)
- 调用
文件类型处理:
- 普通文件:直接删除
- 目录文件:检查是否为空(
vfs_rmdir()
调用empty_dir()
) - 特殊文件:设备文件/管道等只删除目录项
元数据更新:
- 减少 inode 的链接计数(
inode->i_nlink--
) - 更新父目录的修改时间
- 若链接数归零,触发
iput()
回收 inode
- 减少 inode 的链接计数(
数据删除:
- 当 inode 引用计数归零时调用
->evict_inode()
释放磁盘块 - 具体释放逻辑在文件系统的
->delete_inode()
操作中
- 当 inode 引用计数归零时调用
删除目录的特殊处理
vfs_rmdir()
- 专用接口:
fs/namei.c
- 额外检查:
- 目标必须是目录(
S_ISDIR(inode->i_mode)
) - 目录必须为空(调用
empty_dir()
) - 不能删除 “.” 和 “..”
- 目标必须是目录(
- 专用接口:
系统调用映射:
1
rmdir() -> SYSCALL_DEFINE1(rmdir) -> vfs_rmdir()
关键内核数据结构
**
struct dentry
**:1
2
3
4
5
6struct dentry {
struct inode *d_inode; // 关联的 inode
struct qstr d_name; // 文件名
struct dentry *d_parent; // 父目录
...
};**
struct inode
**:1
2
3
4
5
6struct inode {
umode_t i_mode; // 文件类型和权限
nlink_t i_nlink; // 硬链接计数
const struct inode_operations *i_op; // 文件系统操作集
...
};
删除操作示意图
1 | graph TD |
注意事项
延迟删除:
- 若文件正在被进程打开,实际数据删除会延迟到最后一个
close()
- 内核通过
__fput()->...->iput()
触发最终删除
- 若文件正在被进程打开,实际数据删除会延迟到最后一个
硬链接处理:
- 删除文件本质是减少 inode 的硬链接计数
- 只有当
i_nlink == 0
且无进程打开时才会释放数据
安全机制:
- 权限检查:
may_delete()
- 防误删:
O_EXCL
标志保护 - 文件系统日志保护(如 Ext4 journaling)
- 权限检查:
特殊文件系统:
/proc
和/sys
:删除操作可能触发内核对象的释放- FUSE:转发到用户态文件系统处理
总结
层级 | 关键接口 | 功能 |
---|---|---|
用户空间 | unlink() , rmdir() |
系统调用入口 |
内核系统调用 | sys_unlink() |
参数校验和路径解析 |
VFS 层 | vfs_unlink() |
权限检查、通用删除逻辑 |
文件系统实现层 | inode_operations->unlink |
具体文件删除和元数据更新 |
存储层 | ->evict_inode() |
释放磁盘空间 |
删除文件的核心是 vfs_unlink()
,它协调权限检查、文件系统具体操作和元数据更新,最终通过减少链接计数和 iput()
机制实现资源的渐进式回收。
Linux内核中创建文件的接口函数
在 Linux 内核中,创建文件的核心函数是 vfs_create()
,它属于虚拟文件系统(VFS)层。以下是完整的文件创建流程和相关接口:
核心创建函数
vfs_create()
- 功能:VFS 层创建文件的通用接口
- 位置:
fs/namei.c
- 原型:
1
int vfs_create(struct inode *dir, struct dentry *dentry, umode_t mode, bool want_excl)
- 参数:
dir
:父目录的 inodedentry
:要创建文件的目录项(包含文件名)mode
:文件的权限模式(如0644
)want_excl
:是否使用独占创建(对应O_EXCL
标志)
调用流程(用户空间到内核)
用户空间系统调用:
1
2
3
4int open(const char *pathname, int flags, mode_t mode); // 通过 O_CREAT 标志触发创建
int creat(const char *pathname, mode_t mode); // 传统接口(已过时)
int mknod(const char *pathname, mode_t mode, dev_t dev); // 创建设备文件
int mkdir(const char *pathname, mode_t mode); // 创建目录内核系统调用入口:
open
系统调用:SYSCALL_DEFINE3(open, ...)
->do_sys_open()
mknod
系统调用:SYSCALL_DEFINE3(mknod, ...)
mkdir
系统调用:SYSCALL_DEFINE2(mkdir, ...)
路径解析:
filename_create()
:为创建操作准备路径查找(设置LOOKUP_CREATE
标志)path_openat()
->do_open()
:处理打开(包括创建)请求
创建普通文件:
- 在
open
系统调用中,如果指定了O_CREAT
标志,最终会调用vfs_create()
1
2
3
4if (open_flag & O_CREAT) {
error = vfs_create(dir, dentry, mode, open_flag & O_EXCL);
...
}
- 在
创建特殊文件:
mknod
->vfs_mknod()
mkdir
->vfs_mkdir()
文件系统具体实现
每个文件系统需要实现 inode_operations
中的 create
方法(用于普通文件)和 mknod
方法(用于所有文件类型):
1 | struct inode_operations { |
示例文件系统实现:
Ext4 文件系统:
1
2
3
4
5
6const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create, // 创建普通文件
.mknod = ext4_mknod, // 创建设备文件等
.mkdir = ext4_mkdir, // 创建目录
...
};- 实现位置:
fs/ext4/namei.c
- 实现位置:
XFS 文件系统:
1
2
3
4
5
6const struct inode_operations xfs_dir_inode_operations = {
.create = xfs_create,
.mknod = xfs_mknod,
.mkdir = xfs_mkdir,
...
};
创建普通文件的关键步骤(以 vfs_create()
为例)
权限检查:
- 检查父目录是否有写权限和执行权限(
inode_permission(dir, MAY_WRITE | MAY_EXEC)
) - 检查是否允许在父目录下创建文件
- 检查父目录是否有写权限和执行权限(
调用文件系统的具体方法:
- 调用
dir->i_op->create(dir, dentry, mode, want_excl)
- 若文件系统未实现
create
,则调用dir->i_op->mknod(dir, dentry, mode, 0)
(普通文件设备号为0)
- 调用
分配 inode 和初始化:
- 文件系统负责分配新的 inode(例如
ext4_new_inode()
) - 初始化 inode 属性:文件类型(
S_IFREG
)、权限、所有者等 - 建立 inode 与 dentry 的关联(
d_instantiate()
)
- 文件系统负责分配新的 inode(例如
更新父目录:
- 将新文件添加到父目录的目录项中
- 更新父目录的修改时间
创建目录的特殊处理
- **
vfs_mkdir()
**:- 与
vfs_create()
类似,但调用dir->i_op->mkdir()
- 额外步骤:
- 设置 inode 类型为
S_IFDIR
- 自动创建 “.” 和 “..” 目录项
- 初始链接计数为2(自身和父目录的 “..”)
- 设置 inode 类型为
- 与
关键内核数据结构
**
struct dentry
**:1
2
3
4
5
6struct dentry {
struct inode *d_inode; // 新创建文件的 inode(初始为 NULL)
struct qstr d_name; // 文件名
struct dentry *d_parent; // 父目录
...
};**
struct inode
**:1
2
3
4
5
6struct inode {
umode_t i_mode; // 文件类型和权限
nlink_t i_nlink; // 链接计数(文件为1,目录为2)
const struct inode_operations *i_op; // 文件系统操作集
...
};
创建操作示意图
1 | graph TD |
注意事项
原子性:
O_EXCL
标志确保文件不存在时才创建(避免竞争条件)- 文件系统需在内部锁保护下执行创建操作
权限处理:
- 最终权限 =
mode & ~current_umask()
- 受父目录的默认 ACL 影响
- 最终权限 =
安全机制:
- 检查文件名有效性(禁止非法字符)
- 防止符号链接攻击(
LOOKUP_NO_SYMLINKS
)
错误处理:
- 常见错误:
EEXIST
(文件已存在)、EACCES
(权限不足)、ENOSPC
(磁盘满)
- 常见错误:
特殊文件系统:
/proc
和/sys
:创建操作可能触发内核对象的创建- FUSE:转发到用户态文件系统处理
总结
层级 | 关键接口 | 功能 |
---|---|---|
用户空间 | open(O_CREAT) , mknod() |
系统调用入口 |
内核系统调用 | sys_open() , sys_mknod() |
参数校验和标志处理 |
VFS 层 | vfs_create() , vfs_mknod() |
通用创建逻辑和权限检查 |
文件系统实现层 | inode_operations->create |
具体文件创建和 inode 初始化 |
存储层 | 分配 inode 和数据块 | 持久化文件元数据及内容 |
创建文件的核心是 vfs_create()
,它协调权限检查、文件系统具体操作和目录项更新,最终通过文件系统的实现完成新文件的创建。
symlink函数的作用及用法
symlink()
函数是 Linux/Unix 系统中用于创建符号链接(软链接)的系统调用,其核心作用是创建一个指向文件或目录的符号链接。与硬链接不同,符号链接是一个独立的特殊文件,它包含指向目标文件的路径引用。
函数原型
1 |
|
参数说明:
参数 | 类型 | 说明 |
---|---|---|
target |
const char* |
要链接到的目标文件或目录的路径(可以是绝对路径或相对路径) |
linkpath |
const char* |
要创建的符号链接文件的路径 |
返回值:
- 成功:返回
0
- 失败:返回
-1
并设置errno
(常见错误码见下文)
核心特性
特性 | 说明 |
---|---|
跨文件系统 | 可以链接不同文件系统的文件(硬链接不能跨文件系统) |
链接目录 | 支持链接到目录(硬链接不能链接目录) |
悬空链接 | 允许目标不存在(创建后可通过 readlink 读取链接内容) |
链接内容 | 存储的是目标路径字符串(而非 inode 编号) |
文件类型 | 创建的链接文件类型为 l (ls -l 显示为 lrwxrwxrwx ) |
使用示例
1. 基础用法:创建文件链接
1 |
|
运行效果:
1 | $ ls -l current_syslog |
2. 创建目录链接
1 | symlink("/home/user/documents", "my_docs"); // 链接到目录 |
3. 使用相对路径
1 | symlink("../config/server.conf", "app_config"); // 相对目标路径 |
常见错误处理(errno
值)
错误码 | 含义 | 处理建议 |
---|---|---|
EEXIST |
linkpath 已存在 |
先删除旧链接或使用不同名称 |
EACCES |
对目录无写权限 | 检查目录权限 (ls -ld dirname ) |
ENOENT |
linkpath 的父目录不存在 |
确保父目录存在 |
ENOSPC |
磁盘空间不足 | 清理磁盘或检查文件系统配额 |
EROFS |
只读文件系统 | 挂载为可写或选择其他位置 |
EINVAL |
target 为空 |
检查目标路径是否有效 |
错误处理示例:
1 | if (symlink("target.txt", "link.txt") == -1) { |
符号链接操作相关函数
函数 | 用途 | 示例 |
---|---|---|
readlink() |
读取符号链接指向的实际路径 | readlink("link", buf, sizeof(buf)) |
lstat() |
获取链接文件本身信息(而非目标文件) | lstat("link", &stat_buf) |
unlink() |
删除符号链接文件 | unlink("old_link") |
realpath() |
解析链接链获取绝对路径 | realpath("link", resolved_path) |
符号链接 vs 硬链接
特性 | 符号链接 (symlink) | 硬链接 (link) |
---|---|---|
跨文件系统 | ✅ 支持 | ❌ 不支持 |
链接目录 | ✅ 支持 | ❌ 不支持(超级用户有例外) |
存储内容 | 目标路径字符串 | 目标文件的 inode 编号 |
目标删除后 | 成为悬空链接(失效) | 仍可访问数据(直到所有链接删除) |
创建命令 | symlink() / ln -s |
link() / ln |
文件类型 | l (链接文件) |
- (普通文件) |
最佳实践
路径规范:
- 优先使用绝对路径,避免链接移动后失效
- 如需相对路径,确保相对位置稳定
存在性检查:
1
2
3
4// 检查目标是否存在(可选)
if (access(target, F_OK) == -1) {
fprintf(stderr, "警告:目标文件不存在\n");
}原子操作:
- 先创建临时链接再重命名,避免竞态条件:
1
2symlink(target, "tmp_link.tmp");
rename("tmp_link.tmp", "final_link"); // 原子替换
- 先创建临时链接再重命名,避免竞态条件:
链接深度:
- 避免循环链接(如
a -> b -> a
),可用realpath()
检测
- 避免循环链接(如
典型应用场景
软件版本管理
symlink("/opt/app-2.1", "/opt/app-current")
配置文件切换
symlink("production.cfg", "active.cfg")
库文件兼容
symlink("libssl.so.3", "libssl.so")
快捷访问路径
symlink("/mnt/network/storage", "~/cloud")
开发环境适配
symlink("/usr/bin/python3.9", "~/bin/python")
💡 提示:在 Shell 中更常用
ln -s
命令创建符号链接,其底层正是调用symlink()
系统调用:
1 ln -s /path/to/target link_name # Shell 等价操作
通过合理使用 symlink()
,可以实现灵活的文件引用机制,是 Linux 系统管理和应用开发中的重要工具。
Linux中写文件的接口
在 Linux 系统中,写文件操作主要通过以下几个系统调用和库函数实现,涵盖了不同层次和场景的需求:
核心系统调用
1. write()
- 基础写操作
1 |
|
- 功能:将数据从缓冲区写入已打开的文件
- 参数:
fd
:文件描述符(由open()
返回)buf
:数据缓冲区指针count
:要写入的字节数
- 返回值:
- 成功:实际写入的字节数(可能小于
count
) - 失败:-1(设置
errno
)
- 成功:实际写入的字节数(可能小于
示例:
1 | int fd = open("data.txt", O_WRONLY | O_CREAT, 0644); |
2. pwrite()
- 定位写操作
1 |
|
- 特点:在指定偏移量写入,不改变文件当前位置
- 使用场景:随机访问文件
示例:
1 | // 在文件末尾追加数据 |
3. writev()
- 分散写(向量写)
1 |
|
- 功能:从多个缓冲区连续写入(Scatter-Gather I/O)
- 参数:
iov
:iovec
结构数组iovcnt
:数组元素个数
**struct iovec
**:
1 | struct iovec { |
示例:
1 | struct iovec iov[2]; |
高级文件操作接口
4. open()
- 打开/创建文件
1 |
|
- 关键标志:
O_WRONLY
:只写模式O_CREAT
:文件不存在时创建O_APPEND
:追加模式(自动定位到文件末尾)O_TRUNC
:存在则清空文件
示例:
1 | int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644); |
5. fsync()
/ fdatasync()
- 数据同步
1 |
|
- 功能:确保数据写入物理存储介质
- 使用场景:数据库、事务日志等关键数据
C标准库函数
6. fwrite()
- 缓冲写
1 |
|
- 特点:
- 带缓冲区,减少系统调用
- 适合结构化数据写入
- 与
fopen()
配合使用
示例:
1 | FILE *fp = fopen("data.bin", "wb"); |
7. fprintf()
- 格式化写
1 |
|
- 功能:格式化文本写入
- 示例:
1
2FILE *fp = fopen("report.txt", "w");
fprintf(fp, "User: %s\nLogin: %d times\n", username, logins);
特殊场景接口
8. mmap()
- 内存映射写
1 |
|
- 特点:
- 文件映射到内存空间
- 直接内存操作实现文件写入
- 适合大文件随机访问
示例:
1 | int fd = open("bigfile", O_RDWR); |
9. sendfile()
- 零拷贝传输
1 |
|
- 功能:在内核空间直接复制文件内容
- 适用场景:网络服务器发送文件(如Web服务器)
错误处理与注意事项
部分写(Partial Write):
1
2
3
4
5
6
7
8
9
10ssize_t total = 0;
while (total < len) {
ssize_t n = write(fd, buf + total, len - total);
if (n == -1) {
if (errno == EINTR) continue; // 被信号中断
perror("write failed");
break;
}
total += n;
}常见错误码:
错误码 含义 EAGAIN
非阻塞I/O,稍后重试 EBADF
无效文件描述符 EFBIG
超过文件大小限制 ENOSPC
磁盘空间不足 EIO
底层I/O错误 性能优化:
- 使用缓冲区减少系统调用
- 大块写入(建议4KB以上)
O_DIRECT
标志绕过页缓存(需对齐)- 异步I/O(
io_uring
/aio
)
接口对比表
接口 | 特点 | 适用场景 |
---|---|---|
write() |
基础系统调用 | 常规文件写入 |
pwrite() |
原子定位写 | 数据库操作 |
writev() |
分散写入 | 网络报文组装 |
fwrite() |
缓冲写入 | 结构化数据写入 |
fprintf() |
格式化文本 | 日志文件 |
mmap() |
内存映射 | 大文件随机访问 |
sendfile() |
零拷贝传输 | 网络文件传输 |
fsync() |
数据持久化保证 | 关键数据写入 |
完整写入流程示例
1 |
|
通过合理选择这些接口,可以高效地实现从简单的文本写入到高性能零拷贝传输等各种文件操作需求。
Linux内核中根据FILE *stream得到文件信息的方法及示例
在 Linux 用户空间编程中,根据 FILE *stream
获取文件信息主要通过以下方法实现。由于 FILE *
是 C 标准库(glibc)的结构,内核本身不直接处理 FILE *
,但我们可以通过用户空间接口获取底层文件信息。
核心方法
1. 获取文件描述符(int fd
)
1 |
|
- 功能:从
FILE *
获取底层文件描述符 - 返回值:成功返回文件描述符,失败返回 -1
2. 获取文件状态信息
1 |
|
- 功能:通过文件描述符获取文件元数据
- 填充结构:
struct stat
3. 获取文件流信息
1 |
|
- 功能:获取/设置文件流位置
struct stat
关键字段解析
1 | struct stat { |
完整示例代码
1 |
|
输出示例
1 | ===== File Information ===== |
注意事项
文件描述符有效性:
fileno()
仅在流已打开时有效- 对
stdin
/stdout
/stderr
始终有效(fd=0,1,2)
文件位置指针:
ftell()
返回当前位置(字节偏移)- 文本文件和二进制文件处理方式不同
非标准访问:
1
2// glibc 内部结构(不可移植)
printf("Buffer size: %d\n", stream->_IO_buf_end - stream->_IO_buf_base);- 直接访问
FILE
结构成员是不可移植的 - 可能随 glibc 版本变化而失效
- 直接访问
特殊文件处理:
- 设备文件(如
/dev/null
)可能返回特殊值 - 管道/套接字不支持
fstat
某些字段
- 设备文件(如
错误处理:
- 检查所有系统调用的返回值
- 使用
perror()
或strerror(errno)
输出错误
替代方案:文件流状态函数
标准接口:
1 |
|
glibc 扩展:
1 |
|
内核视角补充
虽然内核不直接处理 FILE *
,但文件操作路径如下:
1 | graph LR |
在内核中,实际通过文件描述符找到 struct file
:
1 | struct file { |
总结
操作 | 函数/方法 | 说明 |
---|---|---|
获取文件描述符 | fileno() |
基础转换接口 |
获取文件元数据 | fstat() |
填充 struct stat |
获取当前文件位置 | ftell() /fgetpos() |
文本/二进制位置 |
检查文件流状态 | feof() /ferror() |
标准错误检测 |
获取缓冲区信息 | __fbufsize() (glibc) |
非标准扩展 |
获取打开模式 | 解析 _flags (不可移植) |
仅用于调试 |
通过结合标准库函数和系统调用,可以全面获取与 FILE *
关联的文件信息,但应避免依赖不可移植的实现细节。
Linux中读文件的接口
在 Linux 系统中,读取文件可以通过多种接口实现,从底层的系统调用到高层的库函数,涵盖了不同场景的需求。以下是主要的读取接口及其详细说明:
1. 系统调用(System Calls)
a) read()
- 基础读操作
1 |
|
- 功能:从文件描述符
fd
读取最多count
字节到buf
- 返回值:
- 成功:返回读取的字节数(0 表示文件结尾)
- 失败:-1(设置
errno
)
示例:
1 | int fd = open("file.txt", O_RDONLY); |
b) pread()
- 定位读(不改变文件偏移量)
1 |
|
- 特点:从指定
offset
处读取,不影响原文件位置指针 - 线程安全:适用于多线程环境
示例:
1 | // 从文件第 100 字节处读取 512 字节 |
c) readv()
- 分散读(Scatter-Gather)
1 |
|
- 功能:将数据读取到多个缓冲区
- 参数:
iov
:iovec
结构数组iovcnt
:数组元素个数
**struct iovec
**:
1 | struct iovec { |
示例:
1 | struct iovec iov[2]; |
2. C 标准库函数(Buffered I/O)
a) fread()
- 缓冲读取
1 |
|
- 特点:
- 带缓冲区,减少系统调用
- 适合结构化数据读取
- 返回值:成功读取的元素个数(非字节数)
示例:
1 | FILE *fp = fopen("data.bin", "rb"); |
b) fgetc()
/ getc()
- 读取单个字符
1 |
|
- 返回值:
- 成功:返回字符(unsigned char 转 int)
- 失败/EOF:返回
EOF
c) fgets()
- 读取一行
1 |
|
- 功能:读取最多
size-1
个字符到s
,直到遇到换行符或 EOF - 保留换行符:如果读取到换行符会存入缓冲区
- 返回值:成功返回
s
,失败或 EOF 返回 NULL
示例:
1 | char line[256]; |
d) fscanf()
- 格式化读取
1 |
|
- 功能:根据格式字符串读取数据
- 返回值:成功匹配的输入项数
示例:
1 | int id; |
3. 内存映射(Memory-Mapped I/O)
mmap()
- 将文件映射到内存
1 |
|
- 特点:
- 大文件随机访问性能高
- 直接内存操作,无需
read
调用
- 步骤:
open()
打开文件mmap()
创建映射- 直接访问内存
munmap()
解除映射
示例:
1 | int fd = open("largefile.bin", O_RDONLY); |
4. 高级接口
a) getline()
- 读取整行(POSIX 2008)
1 |
|
- 特点:
- 自动分配缓冲区
- 处理任意长度行
- 返回值:读取的字节数(含换行符),失败返回 -1
示例:
1 | char *line = NULL; |
b) fread_unlocked()
- 非锁定版本
1 |
|
- 特点:
- 不带锁的
fread
(需手动控制线程安全) - 性能更高(适用于单线程或外部加锁)
- 不带锁的
错误处理通用模式
处理部分读(Partial Read)
1 | ssize_t read_all(int fd, void *buf, size_t count) { |
常见错误码
错误码 | 含义 |
---|---|
EAGAIN |
非阻塞I/O,无数据可读 |
EBADF |
无效文件描述符 |
EFAULT |
缓冲区超出可访问地址空间 |
EINTR |
被信号中断 |
EISDIR |
文件描述符指向目录 |
接口对比表
接口 | 特点 | 适用场景 |
---|---|---|
read() |
基础系统调用 | 小文件/底层开发 |
pread() |
原子定位读 | 多线程随机访问 |
readv() |
分散读取 | 网络报文处理 |
fread() |
缓冲读取 | 结构化数据/二进制文件 |
fgets() |
行读取 | 文本文件处理 |
getline() |
动态缓冲区行读取 | 未知行长度的文本 |
mmap() |
内存映射 | 大文件随机访问 |
完整示例:多种方式读取文件
1 |
|
通过合理选择这些接口,可以高效实现从简单的顺序读取到复杂的大文件随机访问等各种需求。
给一个getc和fgetc的使用示例
下面是一个详细展示 getc
和 fgetc
使用的示例程序,包含文件读取和标准输入处理,并附有详细的注释说明:
1 |
|
编译与运行示例
创建测试文件:
1
2
3echo "这是第一行
这是第二行
文件结束" > test.txt编译程序:
1
gcc -o file_reader file_reader.c
运行程序:
1
./file_reader test.txt
输出示例:
1 | === Reading 'test.txt' with fgetc === |
关键点说明:
**
fgetc
vsgetc
**:fgetc
是标准库函数getc
通常是宏实现(可能更快)- 两者都返回
int
而不是char
(为了能表示 EOF)
返回值处理:
1
2
3
4
5
6
7
8int c = fgetc(stream);
if (c == EOF) {
if (feof(stream)) {
// 文件结束处理
} else if (ferror(stream)) {
// 错误处理
}
}使用场景:
fgetc
:需要函数指针时(如传递给其他函数)getc
:性能敏感的循环中(宏展开可能更快)- 两者在功能上完全等价
重要注意事项:
- 必须使用 int 接收返回值:
char
类型无法正确表示 EOF - 错误处理:始终检查 EOF 后使用
feof()
和ferror()
- 性能差异:对于大文件,
getc
可能快 10-20%(因其宏实现)
- 必须使用 int 接收返回值:
特殊用法示例:
实现简单
cat
命令:1
2
3
4
5
6void simple_cat(FILE *fp) {
int c;
while ((c = getc(fp)) != EOF) {
putchar(c);
}
}读取直到特定字符:
1
2
3
4
5
6
7int read_until(FILE *fp, char delimiter) {
int c;
while ((c = fgetc(fp)) != EOF && c != delimiter) {
putchar(c);
}
return c;
}跳过空白字符:
1
2
3
4
5
6
7
8
9void skip_whitespace(FILE *fp) {
int c;
while ((c = getc(fp)) != EOF) {
if (!isspace(c)) {
ungetc(c, fp); // 将非空白字符放回
break;
}
}
}
这些函数提供了最基础但最高效的单字符读取能力,是文本处理的基础工具。尽管有更高级的函数如 fgets()
,但在需要精细控制字符处理时,getc/fgetc
仍然是不可替代的选择。
给一个getline的示例
getline 函数使用示例
getline
是 POSIX 标准中一个非常实用的函数,用于从文件流中读取整行内容,它会自动处理内存分配和行长度问题。以下是一个完整的使用示例:
1 |
|
编译与运行
1. 创建测试文件
1 | echo "这是第一行 |
2. 编译程序
1 | gcc -o getline_demo getline_demo.c |
3. 运行程序
示例1: 读取文件
1 | ./getline_demo test.txt |
输出示例:
1 | ===== 文件内容: test.txt ===== |
示例2: 从标准输入读取
1 | echo -e "第一行\n更长的第二行" | ./getline_demo test.txt |
输出示例:
1 | ===== 从标准输入读取 (按 Ctrl+D 结束) ===== |
示例3: 查找最长行
1 | ./getline_demo test.txt |
输出示例:
1 | ===== 最长行 ===== |
getline 关键特性
自动内存管理:
- 首次调用时,
lineptr
应为 NULL,getline
会自动分配缓冲区 - 后续调用会重用或扩展缓冲区
- 使用完后必须调用
free(lineptr)
释放内存
- 首次调用时,
缓冲区重用:
1
2
3
4
5
6
7
8char *buffer = NULL;
size_t bufsize = 0;
// 第一次读取 - 自动分配
getline(&buffer, &bufsize, fp);
// 第二次读取 - 重用缓冲区
getline(&buffer, &bufsize, fp);处理任意长度行:
- 当遇到超过当前缓冲区长度的行时
getline
会自动重新分配更大的缓冲区- 更新
lineptr
和n
的值
包含换行符:
- 读取的行包含结尾的换行符(如果存在)
- 可以使用以下代码移除:
1
2if (nread > 0 && line[nread-1] == '\n')
line[nread-1] = '\0';
返回值:
- 成功时返回读取的字节数(包括换行符)
- 文件结束时返回 -1
- 错误时返回 -1 并设置 errno
使用场景
配置文件解析:
1
2
3
4
5
6while (getline(&line, &len, fp) != -1) {
if (line[0] == '#' || line[0] == '\n') continue; // 跳过注释和空行
char *key = strtok(line, "=");
char *value = strtok(NULL, "\n");
// 处理键值对...
}日志文件处理:
1
2
3
4
5
6// 查找包含特定字符串的行
while (getline(&line, &len, fp) != -1) {
if (strstr(line, "ERROR")) {
printf("发现错误: %s", line);
}
}网络数据解析:
1
2
3
4
5
6// 从套接字读取 HTTP 头部
FILE *sockf = fdopen(sockfd, "r");
while (getline(&line, &len, sockf) != -1) {
if (strcmp(line, "\r\n") == 0) break; // 空行结束头部
process_header(line);
}
注意事项
内存泄漏:
1
2
3
4
5char *line = NULL;
size_t len = 0;
getline(&line, &len, fp); // 分配内存
// ... 使用 line ...
free(line); // 必须释放!错误处理:
1
2
3
4
5
6
7
8ssize_t n = getline(&line, &len, fp);
if (n == -1) {
if (feof(fp)) {
printf("文件结束\n");
} else {
perror("读取错误");
}
}可移植性:
getline
是 POSIX 2008 标准的一部分- 在编译时可能需要定义宏:
1
2 - 对于不支持的环境,可以使用替代实现:
1
2
3
4// 简化版 getline 实现
ssize_t portable_getline(char **lineptr, size_t *n, FILE *stream) {
// 实现细节...
}
性能考虑:
- 对于已知最大行长的情况,可以预先分配:
1
2
3size_t len = 256; // 初始缓冲区大小
char *line = malloc(len);
getline(&line, &len, fp);
- 对于已知最大行长的情况,可以预先分配:
这个示例展示了 getline
的强大功能,特别是在处理未知长度的文本行时。它简化了文件读取操作,避免了缓冲区溢出的风险,是处理文本文件的理想选择。