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  # 创建目录

  • 标识符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 中罕见):用于进程间高效通信。

如何查看文件类型?

  1. **ls -l**:观察首字符(如 dlc 等)。
  2. file 命令
    1
    $ file /dev/null  # 输出: /dev/null: character special file
  3. 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)层。以下是完整的文件删除机制和相关接口:


核心删除函数

  1. vfs_unlink()
    • 功能:VFS 层删除文件的通用接口
    • 位置fs/namei.c
    • 原型
      1
      int vfs_unlink(struct inode *dir, struct dentry *dentry, struct inode **delegated_inode)
    • 参数
      • dir:父目录的 inode
      • dentry:要删除文件的目录项(包含文件名和 inode 指针)
      • delegated_inode:用于处理 NFS 等文件系统的委托

调用流程(用户空间到内核)

  1. 用户空间系统调用

    1
    2
    3
    unlink(const char *pathname);         // 删除文件
    unlinkat(int dirfd, const char *pathname, int flags); // 更灵活的版本
    rmdir(const char *pathname); // 删除空目录
  2. 内核系统调用入口

    1
    2
    3
    SYSCALL_DEFINE1(unlink, const char __user *, pathname)
    SYSCALL_DEFINE3(unlinkat, int, dfd, const char __user *, pathname, int, flag)
    SYSCALL_DEFINE1(rmdir, const char __user *, pathname)
  3. 核心路径解析

    • filename_lookup():解析路径获取 dentry 和 inode
    • lookup_one_qstr_excl():查找目标文件
  4. 最终调用

    1
    vfs_unlink(dir, dentry, NULL);

文件系统具体实现

每个文件系统需实现 inode_operations 中的 unlink 方法:

1
2
3
4
struct inode_operations {
int (*unlink) (struct inode *dir, struct dentry *dentry);
// 其他方法...
};

示例文件系统实现

  1. Ext4 文件系统

    1
    2
    3
    4
    const struct inode_operations ext4_dir_inode_operations = {
    .unlink = ext4_unlink, // 具体实现
    ...
    };
    • 实现位置:fs/ext4/namei.c 中的 ext4_unlink()
  2. XFS 文件系统

    1
    2
    3
    4
    const struct inode_operations xfs_dir_inode_operations = {
    .unlink = xfs_unlink,
    ...
    };

删除操作的关键步骤

  1. 权限检查

    • 调用 inode_permission(dir, MAY_WRITE | MAY_EXEC) 检查目录写权限
    • 检查文件是否只读(IS_APPEND(inode)
  2. 文件类型处理

    • 普通文件:直接删除
    • 目录文件:检查是否为空(vfs_rmdir() 调用 empty_dir()
    • 特殊文件:设备文件/管道等只删除目录项
  3. 元数据更新

    • 减少 inode 的链接计数(inode->i_nlink--
    • 更新父目录的修改时间
    • 若链接数归零,触发 iput() 回收 inode
  4. 数据删除

    • 当 inode 引用计数归零时调用 ->evict_inode() 释放磁盘块
    • 具体释放逻辑在文件系统的 ->delete_inode() 操作中

删除目录的特殊处理

  1. vfs_rmdir()

    • 专用接口:fs/namei.c
    • 额外检查:
      • 目标必须是目录(S_ISDIR(inode->i_mode)
      • 目录必须为空(调用 empty_dir()
      • 不能删除 “.” 和 “..”
  2. 系统调用映射

    1
    rmdir() -> SYSCALL_DEFINE1(rmdir) -> vfs_rmdir()

关键内核数据结构

  1. **struct dentry**:

    1
    2
    3
    4
    5
    6
    struct dentry {
    struct inode *d_inode; // 关联的 inode
    struct qstr d_name; // 文件名
    struct dentry *d_parent; // 父目录
    ...
    };
  2. **struct inode**:

    1
    2
    3
    4
    5
    6
    struct inode {
    umode_t i_mode; // 文件类型和权限
    nlink_t i_nlink; // 硬链接计数
    const struct inode_operations *i_op; // 文件系统操作集
    ...
    };

删除操作示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
graph TD
A[用户空间] -->|unlink('/tmp/file')| B[系统调用入口]
B --> C[sys_unlink()]
C --> D[filename_lookup]
D --> E{解析路径获取<br>父目录dentry和文件dentry}
E --> F[vfs_unlink]
F --> G[检查权限]
F --> H[调用具体文件系统的<br>inode_operations->unlink]
H --> I[ext4_unlink / xfs_unlink等]
I --> J[更新元数据:<br>减少i_nlink]
J --> K{链接数归零?}
K -->|是| L[iput触发inode回收]
K -->|否| M[仅删除目录项]

注意事项

  1. 延迟删除

    • 若文件正在被进程打开,实际数据删除会延迟到最后一个 close()
    • 内核通过 __fput()->...->iput() 触发最终删除
  2. 硬链接处理

    • 删除文件本质是减少 inode 的硬链接计数
    • 只有当 i_nlink == 0 且无进程打开时才会释放数据
  3. 安全机制

    • 权限检查:may_delete()
    • 防误删:O_EXCL 标志保护
    • 文件系统日志保护(如 Ext4 journaling)
  4. 特殊文件系统

    • /proc/sys:删除操作可能触发内核对象的释放
    • FUSE:转发到用户态文件系统处理

总结

层级 关键接口 功能
用户空间 unlink(), rmdir() 系统调用入口
内核系统调用 sys_unlink() 参数校验和路径解析
VFS 层 vfs_unlink() 权限检查、通用删除逻辑
文件系统实现层 inode_operations->unlink 具体文件删除和元数据更新
存储层 ->evict_inode() 释放磁盘空间

删除文件的核心是 vfs_unlink(),它协调权限检查、文件系统具体操作和元数据更新,最终通过减少链接计数和 iput() 机制实现资源的渐进式回收。

Linux内核中创建文件的接口函数

在 Linux 内核中,创建文件的核心函数是 vfs_create(),它属于虚拟文件系统(VFS)层。以下是完整的文件创建流程和相关接口:


核心创建函数

  1. vfs_create()
    • 功能:VFS 层创建文件的通用接口
    • 位置fs/namei.c
    • 原型
      1
      int vfs_create(struct inode *dir, struct dentry *dentry, umode_t mode, bool want_excl)
    • 参数
      • dir:父目录的 inode
      • dentry:要创建文件的目录项(包含文件名)
      • mode:文件的权限模式(如 0644
      • want_excl:是否使用独占创建(对应 O_EXCL 标志)

调用流程(用户空间到内核)

  1. 用户空间系统调用

    1
    2
    3
    4
    int 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); // 创建目录
  2. 内核系统调用入口

    • open 系统调用:SYSCALL_DEFINE3(open, ...) -> do_sys_open()
    • mknod 系统调用:SYSCALL_DEFINE3(mknod, ...)
    • mkdir 系统调用:SYSCALL_DEFINE2(mkdir, ...)
  3. 路径解析

    • filename_create():为创建操作准备路径查找(设置 LOOKUP_CREATE 标志)
    • path_openat() -> do_open():处理打开(包括创建)请求
  4. 创建普通文件

    • open 系统调用中,如果指定了 O_CREAT 标志,最终会调用 vfs_create()
      1
      2
      3
      4
      if (open_flag & O_CREAT) {
      error = vfs_create(dir, dentry, mode, open_flag & O_EXCL);
      ...
      }
  5. 创建特殊文件

    • mknod -> vfs_mknod()
    • mkdir -> vfs_mkdir()

文件系统具体实现

每个文件系统需要实现 inode_operations 中的 create 方法(用于普通文件)和 mknod 方法(用于所有文件类型):

1
2
3
4
5
6
struct inode_operations {
int (*create) (struct inode *dir, struct dentry *dentry, umode_t mode, bool excl);
int (*mknod) (struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev);
int (*mkdir) (struct inode *dir, struct dentry *dentry, umode_t mode);
// 其他方法...
};

示例文件系统实现

  1. Ext4 文件系统

    1
    2
    3
    4
    5
    6
    const struct inode_operations ext4_dir_inode_operations = {
    .create = ext4_create, // 创建普通文件
    .mknod = ext4_mknod, // 创建设备文件等
    .mkdir = ext4_mkdir, // 创建目录
    ...
    };
    • 实现位置:fs/ext4/namei.c
  2. XFS 文件系统

    1
    2
    3
    4
    5
    6
    const struct inode_operations xfs_dir_inode_operations = {
    .create = xfs_create,
    .mknod = xfs_mknod,
    .mkdir = xfs_mkdir,
    ...
    };

创建普通文件的关键步骤(以 vfs_create() 为例)

  1. 权限检查

    • 检查父目录是否有写权限和执行权限(inode_permission(dir, MAY_WRITE | MAY_EXEC)
    • 检查是否允许在父目录下创建文件
  2. 调用文件系统的具体方法

    • 调用 dir->i_op->create(dir, dentry, mode, want_excl)
    • 若文件系统未实现 create,则调用 dir->i_op->mknod(dir, dentry, mode, 0)(普通文件设备号为0)
  3. 分配 inode 和初始化

    • 文件系统负责分配新的 inode(例如 ext4_new_inode()
    • 初始化 inode 属性:文件类型(S_IFREG)、权限、所有者等
    • 建立 inode 与 dentry 的关联(d_instantiate()
  4. 更新父目录

    • 将新文件添加到父目录的目录项中
    • 更新父目录的修改时间

创建目录的特殊处理

  1. **vfs_mkdir()**:
    • vfs_create() 类似,但调用 dir->i_op->mkdir()
    • 额外步骤:
      • 设置 inode 类型为 S_IFDIR
      • 自动创建 “.” 和 “..” 目录项
      • 初始链接计数为2(自身和父目录的 “..”)

关键内核数据结构

  1. **struct dentry**:

    1
    2
    3
    4
    5
    6
    struct dentry {
    struct inode *d_inode; // 新创建文件的 inode(初始为 NULL)
    struct qstr d_name; // 文件名
    struct dentry *d_parent; // 父目录
    ...
    };
  2. **struct inode**:

    1
    2
    3
    4
    5
    6
    struct inode {
    umode_t i_mode; // 文件类型和权限
    nlink_t i_nlink; // 链接计数(文件为1,目录为2)
    const struct inode_operations *i_op; // 文件系统操作集
    ...
    };

创建操作示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
graph TD
A[用户空间] -->|open('newfile', O_CREAT, 0644)| B[系统调用入口]
B --> C[sys_open()]
C --> D[do_sys_open]
D --> E[getname] --> F[get_unused_fd_flags] --> G[do_filp_open]
G --> H[path_openat]
H --> I{路径查找}
I -->|LOOKUP_CREATE| J[filename_create]
J --> K[创建dentry对象]
K --> L{文件存在?}
L -->|否| M[vfs_create]
M --> N[调用具体文件系统的<br>inode_operations->create]
N --> O[ext4_create等]
O --> P[分配inode<br>初始化文件]
P --> Q[更新父目录]
Q --> R[关联dentry和inode]
R --> S[打开文件]

注意事项

  1. 原子性

    • O_EXCL 标志确保文件不存在时才创建(避免竞争条件)
    • 文件系统需在内部锁保护下执行创建操作
  2. 权限处理

    • 最终权限 = mode & ~current_umask()
    • 受父目录的默认 ACL 影响
  3. 安全机制

    • 检查文件名有效性(禁止非法字符)
    • 防止符号链接攻击(LOOKUP_NO_SYMLINKS
  4. 错误处理

    • 常见错误:EEXIST(文件已存在)、EACCES(权限不足)、ENOSPC(磁盘满)
  5. 特殊文件系统

    • /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
2
3
#include <unistd.h>

int symlink(const char *target, const char *linkpath);

参数说明:

参数 类型 说明
target const char* 要链接到的目标文件或目录的路径(可以是绝对路径或相对路径)
linkpath const char* 要创建的符号链接文件的路径

返回值:

  • 成功:返回 0
  • 失败:返回 -1 并设置 errno(常见错误码见下文)

核心特性

特性 说明
跨文件系统 可以链接不同文件系统的文件(硬链接不能跨文件系统)
链接目录 支持链接到目录(硬链接不能链接目录)
悬空链接 允许目标不存在(创建后可通过 readlink 读取链接内容)
链接内容 存储的是目标路径字符串(而非 inode 编号)
文件类型 创建的链接文件类型为 lls -l 显示为 lrwxrwxrwx

使用示例

1. 基础用法:创建文件链接

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <stdio.h>

int main() {
if (symlink("/var/log/syslog", "current_syslog") == 0) {
printf("符号链接创建成功\n");
} else {
perror("创建失败");
}
return 0;
}

运行效果:

1
2
$ ls -l current_syslog
lrwxrwxrwx 1 user group 15 Jun 10 10:00 current_syslog -> /var/log/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
2
3
4
5
6
7
8
9
10
11
12
13
if (symlink("target.txt", "link.txt") == -1) {
switch(errno) {
case EEXIST:
fprintf(stderr, "错误:链接文件已存在\n");
break;
case EACCES:
fprintf(stderr, "错误:无权在此目录创建链接\n");
break;
default:
perror("未知错误");
}
exit(EXIT_FAILURE);
}

符号链接操作相关函数

函数 用途 示例
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. 存在性检查

    1
    2
    3
    4
    // 检查目标是否存在(可选)
    if (access(target, F_OK) == -1) {
    fprintf(stderr, "警告:目标文件不存在\n");
    }
  3. 原子操作

    • 先创建临时链接再重命名,避免竞态条件:
      1
      2
      symlink(target, "tmp_link.tmp");
      rename("tmp_link.tmp", "final_link"); // 原子替换
  4. 链接深度

    • 避免循环链接(如 a -> b -> a),可用 realpath() 检测

典型应用场景

  1. 软件版本管理
    symlink("/opt/app-2.1", "/opt/app-current")

  2. 配置文件切换
    symlink("production.cfg", "active.cfg")

  3. 库文件兼容
    symlink("libssl.so.3", "libssl.so")

  4. 快捷访问路径
    symlink("/mnt/network/storage", "~/cloud")

  5. 开发环境适配
    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
2
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  • 功能:将数据从缓冲区写入已打开的文件
  • 参数
    • fd:文件描述符(由 open() 返回)
    • buf:数据缓冲区指针
    • count:要写入的字节数
  • 返回值
    • 成功:实际写入的字节数(可能小于 count
    • 失败:-1(设置 errno

示例

1
2
3
4
int fd = open("data.txt", O_WRONLY | O_CREAT, 0644);
char data[] = "Hello, Linux!";
ssize_t bytes_written = write(fd, data, strlen(data));
close(fd);

2. pwrite() - 定位写操作

1
2
#include <unistd.h>
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
  • 特点:在指定偏移量写入,不改变文件当前位置
  • 使用场景:随机访问文件

示例

1
2
3
// 在文件末尾追加数据
off_t end = lseek(fd, 0, SEEK_END);
pwrite(fd, "\nAppended", 9, end);

3. writev() - 分散写(向量写)

1
2
#include <sys/uio.h>
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
  • 功能:从多个缓冲区连续写入(Scatter-Gather I/O)
  • 参数
    • ioviovec 结构数组
    • iovcnt:数组元素个数

**struct iovec**:

1
2
3
4
struct iovec {
void *iov_base; // 缓冲区地址
size_t iov_len; // 缓冲区长度
};

示例

1
2
3
4
5
6
7
8
9
10
struct iovec iov[2];
char header[] = "[HEADER]";
char body[] = "Message body";

iov[0].iov_base = header;
iov[0].iov_len = strlen(header);
iov[1].iov_base = body;
iov[1].iov_len = strlen(body);

writev(fd, iov, 2);

高级文件操作接口

4. open() - 打开/创建文件

1
2
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
  • 关键标志
    • 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
2
3
#include <unistd.h>
int fsync(int fd); // 同步元数据+数据
int fdatasync(int fd); // 仅同步数据(更快)
  • 功能:确保数据写入物理存储介质
  • 使用场景:数据库、事务日志等关键数据

C标准库函数

6. fwrite() - 缓冲写

1
2
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 特点
    • 带缓冲区,减少系统调用
    • 适合结构化数据写入
    • fopen() 配合使用

示例

1
2
3
4
FILE *fp = fopen("data.bin", "wb");
int data[100] = {...};
fwrite(data, sizeof(int), 100, fp);
fclose(fp);

7. fprintf() - 格式化写

1
2
#include <stdio.h>
int fprintf(FILE *stream, const char *format, ...);
  • 功能:格式化文本写入
  • 示例
    1
    2
    FILE *fp = fopen("report.txt", "w");
    fprintf(fp, "User: %s\nLogin: %d times\n", username, logins);

特殊场景接口

8. mmap() - 内存映射写

1
2
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 特点
    • 文件映射到内存空间
    • 直接内存操作实现文件写入
    • 适合大文件随机访问

示例

1
2
3
4
5
int fd = open("bigfile", O_RDWR);
char *map = mmap(NULL, file_size, PROT_WRITE, MAP_SHARED, fd, 0);
strcpy(map + offset, "New data"); // 直接写入内存
msync(map, file_size, MS_SYNC); // 同步到磁盘
munmap(map, file_size);

9. sendfile() - 零拷贝传输

1
2
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
  • 功能:在内核空间直接复制文件内容
  • 适用场景:网络服务器发送文件(如Web服务器)

错误处理与注意事项

  1. 部分写(Partial Write)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ssize_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;
    }
  2. 常见错误码

    错误码 含义
    EAGAIN 非阻塞I/O,稍后重试
    EBADF 无效文件描述符
    EFBIG 超过文件大小限制
    ENOSPC 磁盘空间不足
    EIO 底层I/O错误
  3. 性能优化

    • 使用缓冲区减少系统调用
    • 大块写入(建议4KB以上)
    • O_DIRECT 标志绕过页缓存(需对齐)
    • 异步I/O(io_uring / aio

接口对比表

接口 特点 适用场景
write() 基础系统调用 常规文件写入
pwrite() 原子定位写 数据库操作
writev() 分散写入 网络报文组装
fwrite() 缓冲写入 结构化数据写入
fprintf() 格式化文本 日志文件
mmap() 内存映射 大文件随机访问
sendfile() 零拷贝传输 网络文件传输
fsync() 数据持久化保证 关键数据写入

完整写入流程示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
// 1. 打开文件(追加模式)
int fd = open("example.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}

// 2. 准备数据
const char *msg = "[INFO] New log entry\n";
size_t len = strlen(msg);
ssize_t total = 0;

// 3. 处理部分写
while (total < len) {
ssize_t n = write(fd, msg + total, len - total);
if (n == -1) {
if (errno == EINTR) continue; // 被信号中断
perror("write error");
break;
}
total += n;
}

// 4. 确保数据落盘
if (fsync(fd) == -1) {
perror("fsync failed");
}

// 5. 关闭文件
close(fd);
return 0;
}

通过合理选择这些接口,可以高效地实现从简单的文本写入到高性能零拷贝传输等各种文件操作需求。

Linux内核中根据FILE *stream得到文件信息的方法及示例

在 Linux 用户空间编程中,根据 FILE *stream 获取文件信息主要通过以下方法实现。由于 FILE * 是 C 标准库(glibc)的结构,内核本身不直接处理 FILE *,但我们可以通过用户空间接口获取底层文件信息。


核心方法

1. 获取文件描述符(int fd

1
2
#include <stdio.h>
int fileno(FILE *stream);
  • 功能:从 FILE * 获取底层文件描述符
  • 返回值:成功返回文件描述符,失败返回 -1

2. 获取文件状态信息

1
2
#include <sys/stat.h>
int fstat(int fd, struct stat *buf);
  • 功能:通过文件描述符获取文件元数据
  • 填充结构struct stat

3. 获取文件流信息

1
2
3
4
5
6
#include <stdio.h>
int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);
  • 功能:获取/设置文件流位置

struct stat 关键字段解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* 文件所在设备ID */
ino_t st_ino; /* inode号 */
mode_t st_mode; /* 文件类型和权限 */
nlink_t st_nlink; /* 硬链接数 */
uid_t st_uid; /* 所有者UID */
gid_t st_gid; /* 所属组GID */
dev_t st_rdev; /* 特殊文件设备ID */
off_t st_size; /* 文件大小(字节) */
blksize_t st_blksize; /* 文件系统I/O块大小 */
blkcnt_t st_blocks; /* 分配的512B块数量 */
time_t st_atime; /* 最后访问时间 */
time_t st_mtime; /* 最后修改时间 */
time_t st_ctime; /* 最后状态变更时间 */
};

完整示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <string.h>

void print_file_info(FILE *stream) {
// 1. 获取文件描述符
int fd = fileno(stream);
if (fd == -1) {
perror("fileno failed");
return;
}
printf("File Descriptor: %d\n", fd);

// 2. 获取文件状态
struct stat st;
if (fstat(fd, &st) == -1) {
perror("fstat failed");
return;
}

// 3. 打印基本信息
printf("File Size: %ld bytes\n", (long)st.st_size);
printf("Inode: %lu\n", (unsigned long)st.st_ino);
printf("Links: %lu\n", (unsigned long)st.st_nlink);

// 4. 解析文件类型
char *type = "Unknown";
if (S_ISREG(st.st_mode)) type = "Regular File";
else if (S_ISDIR(st.st_mode)) type = "Directory";
else if (S_ISCHR(st.st_mode)) type = "Character Device";
else if (S_ISBLK(st.st_mode)) type = "Block Device";
else if (S_ISFIFO(st.st_mode)) type = "FIFO/Pipe";
else if (S_ISLNK(st.st_mode)) type = "Symbolic Link";
else if (S_ISSOCK(st.st_mode)) type = "Socket";
printf("File Type: %s\n", type);

// 5. 格式化时间
char atime_buf[80], mtime_buf[80];
strftime(atime_buf, sizeof(atime_buf), "%Y-%m-%d %H:%M:%S", localtime(&st.st_atime));
strftime(mtime_buf, sizeof(mtime_buf), "%Y-%m-%d %H:%M:%S", localtime(&st.st_mtime));
printf("Last Access: %s\n", atime_buf);
printf("Last Modification: %s\n", mtime_buf);

// 6. 获取当前位置
long pos = ftell(stream);
if (pos == -1) {
perror("ftell failed");
} else {
printf("Current Position: %ld\n", pos);
}

// 7. 获取打开模式(非标准方法)
const char *mode = "Unknown";
if (stream->_flags & _IO_READ_ONLY) mode = "Read-Only";
else if (stream->_flags & _IO_WRITE_ONLY) mode = "Write-Only";
else if (stream->_flags & _IO_RDWR) mode = "Read-Write";
printf("Open Mode: %s\n", mode);
}

int main() {
// 示例1:普通文件
FILE *fp = fopen("example.txt", "w+");
if (!fp) {
perror("fopen failed");
return 1;
}
fprintf(fp, "Test data for file info\n");
fflush(fp); // 确保数据写入

printf("\n===== File Information =====\n");
print_file_info(fp);
fclose(fp);

// 示例2:标准输入
printf("\n===== stdin Information =====\n");
print_file_info(stdin);

return 0;
}

输出示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
===== File Information =====
File Descriptor: 3
File Size: 22 bytes
Inode: 1234567
Links: 1
File Type: Regular File
Last Access: 2023-06-15 14:30:25
Last Modification: 2023-06-15 14:30:25
Current Position: 22
Open Mode: Read-Write

===== stdin Information =====
File Descriptor: 0
File Size: 0 bytes
Inode: 0
Links: 1
File Type: Character Device
Last Access: 2023-06-15 14:30:25
Last Modification: 2023-06-15 14:30:25
Current Position: 0
Open Mode: Read-Only

注意事项

  1. 文件描述符有效性

    • fileno() 仅在流已打开时有效
    • stdin/stdout/stderr 始终有效(fd=0,1,2)
  2. 文件位置指针

    • ftell() 返回当前位置(字节偏移)
    • 文本文件和二进制文件处理方式不同
  3. 非标准访问

    1
    2
    // glibc 内部结构(不可移植)
    printf("Buffer size: %d\n", stream->_IO_buf_end - stream->_IO_buf_base);
    • 直接访问 FILE 结构成员是不可移植
    • 可能随 glibc 版本变化而失效
  4. 特殊文件处理

    • 设备文件(如 /dev/null)可能返回特殊值
    • 管道/套接字不支持 fstat 某些字段
  5. 错误处理

    • 检查所有系统调用的返回值
    • 使用 perror()strerror(errno) 输出错误

替代方案:文件流状态函数

标准接口:

1
2
3
4
#include <stdio.h>
int ferror(FILE *stream); // 检查错误指示器
int feof(FILE *stream); // 检查文件结束指示器
void clearerr(FILE *stream); // 清除错误标志

glibc 扩展:

1
2
3
4
5
#include <stdio_ext.h>
size_t __fbufsize(FILE *stream); // 获取缓冲区大小
size_t __fpending(FILE *stream); // 输出缓冲区待写字节数
int __freadable(FILE *stream); // 是否可读
int __fwritable(FILE *stream); // 是否可写

内核视角补充

虽然内核不直接处理 FILE *,但文件操作路径如下:

1
2
3
4
5
6
graph LR
A[FILE *stream] --> B[fileno]
B --> C[int fd]
C --> D[系统调用 read/write]
D --> E[VFS层 vfs_read/vfs_write]
E --> F[文件系统具体实现]

在内核中,实际通过文件描述符找到 struct file

1
2
3
4
5
6
7
struct file {
struct path f_path; // 文件路径
struct inode *f_inode; // inode指针
const struct file_operations *f_op; // 文件操作集
loff_t f_pos; // 当前文件位置
// ...
};

总结

操作 函数/方法 说明
获取文件描述符 fileno() 基础转换接口
获取文件元数据 fstat() 填充 struct stat
获取当前文件位置 ftell()/fgetpos() 文本/二进制位置
检查文件流状态 feof()/ferror() 标准错误检测
获取缓冲区信息 __fbufsize() (glibc) 非标准扩展
获取打开模式 解析 _flags (不可移植) 仅用于调试

通过结合标准库函数和系统调用,可以全面获取与 FILE * 关联的文件信息,但应避免依赖不可移植的实现细节。

Linux中读文件的接口

在 Linux 系统中,读取文件可以通过多种接口实现,从底层的系统调用到高层的库函数,涵盖了不同场景的需求。以下是主要的读取接口及其详细说明:


1. 系统调用(System Calls)

a) read() - 基础读操作

1
2
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  • 功能:从文件描述符 fd 读取最多 count 字节到 buf
  • 返回值
    • 成功:返回读取的字节数(0 表示文件结尾)
    • 失败:-1(设置 errno

示例

1
2
3
int fd = open("file.txt", O_RDONLY);
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));

b) pread() - 定位读(不改变文件偏移量)

1
2
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
  • 特点:从指定 offset 处读取,不影响原文件位置指针
  • 线程安全:适用于多线程环境

示例

1
2
// 从文件第 100 字节处读取 512 字节
ssize_t n = pread(fd, buf, 512, 100);

c) readv() - 分散读(Scatter-Gather)

1
2
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
  • 功能:将数据读取到多个缓冲区
  • 参数
    • ioviovec 结构数组
    • iovcnt:数组元素个数

**struct iovec**:

1
2
3
4
struct iovec {
void *iov_base; // 缓冲区起始地址
size_t iov_len; // 缓冲区长度
};

示例

1
2
3
4
5
6
7
8
9
10
struct iovec iov[2];
char header[128];
char body[1024];

iov[0].iov_base = header;
iov[0].iov_len = sizeof(header);
iov[1].iov_base = body;
iov[1].iov_len = sizeof(body);

ssize_t n = readv(fd, iov, 2);

2. C 标准库函数(Buffered I/O)

a) fread() - 缓冲读取

1
2
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • 特点
    • 带缓冲区,减少系统调用
    • 适合结构化数据读取
  • 返回值:成功读取的元素个数(非字节数)

示例

1
2
3
FILE *fp = fopen("data.bin", "rb");
double data[100];
size_t num_read = fread(data, sizeof(double), 100, fp);

b) fgetc() / getc() - 读取单个字符

1
2
3
#include <stdio.h>
int fgetc(FILE *stream); // 函数版本
int getc(FILE *stream); // 宏版本(可能更快)
  • 返回值
    • 成功:返回字符(unsigned char 转 int)
    • 失败/EOF:返回 EOF

c) fgets() - 读取一行

1
2
#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
  • 功能:读取最多 size-1 个字符到 s,直到遇到换行符或 EOF
  • 保留换行符:如果读取到换行符会存入缓冲区
  • 返回值:成功返回 s,失败或 EOF 返回 NULL

示例

1
2
3
4
char line[256];
while (fgets(line, sizeof(line), stdin)) {
printf("Read: %s", line);
}

d) fscanf() - 格式化读取

1
2
#include <stdio.h>
int fscanf(FILE *stream, const char *format, ...);
  • 功能:根据格式字符串读取数据
  • 返回值:成功匹配的输入项数

示例

1
2
3
4
int id;
char name[32];
float score;
fscanf(fp, "%d %31s %f", &id, name, &score);

3. 内存映射(Memory-Mapped I/O)

mmap() - 将文件映射到内存

1
2
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 特点
    • 大文件随机访问性能高
    • 直接内存操作,无需 read 调用
  • 步骤
    1. open() 打开文件
    2. mmap() 创建映射
    3. 直接访问内存
    4. munmap() 解除映射

示例

1
2
3
4
5
6
7
8
9
10
int fd = open("largefile.bin", O_RDONLY);
size_t file_size = lseek(fd, 0, SEEK_END);
void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0);

// 直接读取内存
char *data = (char*)addr;
printf("Header: %.4s\n", data);

munmap(addr, file_size);
close(fd);

4. 高级接口

a) getline() - 读取整行(POSIX 2008)

1
2
#include <stdio.h>
ssize_t getline(char **lineptr, size_t *n, FILE *stream);
  • 特点
    • 自动分配缓冲区
    • 处理任意长度行
  • 返回值:读取的字节数(含换行符),失败返回 -1

示例

1
2
3
4
5
6
7
char *line = NULL;
size_t len = 0;
ssize_t read;
while ((read = getline(&line, &len, fp)) != -1) {
printf("Line: %s", line);
}
free(line);

b) fread_unlocked() - 非锁定版本

1
2
#include <stdio.h>
size_t fread_unlocked(void *ptr, size_t size, size_t n, FILE *fp);
  • 特点
    • 不带锁的 fread(需手动控制线程安全)
    • 性能更高(适用于单线程或外部加锁)

错误处理通用模式

处理部分读(Partial Read)

1
2
3
4
5
6
7
8
9
10
11
12
13
ssize_t read_all(int fd, void *buf, size_t count) {
size_t total = 0;
while (total < count) {
ssize_t n = read(fd, (char*)buf + total, count - total);
if (n == 0) break; // EOF
if (n == -1) {
if (errno == EINTR) continue; // 被信号中断
return -1; // 其他错误
}
total += n;
}
return total;
}

常见错误码

错误码 含义
EAGAIN 非阻塞I/O,无数据可读
EBADF 无效文件描述符
EFAULT 缓冲区超出可访问地址空间
EINTR 被信号中断
EISDIR 文件描述符指向目录

接口对比表

接口 特点 适用场景
read() 基础系统调用 小文件/底层开发
pread() 原子定位读 多线程随机访问
readv() 分散读取 网络报文处理
fread() 缓冲读取 结构化数据/二进制文件
fgets() 行读取 文本文件处理
getline() 动态缓冲区行读取 未知行长度的文本
mmap() 内存映射 大文件随机访问

完整示例:多种方式读取文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/mman.h>

// 方法1: 使用 read 逐块读取
void read_by_chunk(const char *filename) {
int fd = open(filename, O_RDONLY);
if (fd == -1) { perror("open"); return; }

char buf[4096];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) {
if (n == -1) {
if (errno == EINTR) continue;
perror("read");
break;
}
// 处理数据 (此处简单打印大小)
printf("Read chunk: %zd bytes\n", n);
}
close(fd);
}

// 方法2: 使用 mmap 映射整个文件
void read_by_mmap(const char *filename) {
int fd = open(filename, O_RDONLY);
if (fd == -1) { perror("open"); return; }

struct stat st;
if (fstat(fd, &st)) { perror("fstat"); close(fd); return; }

void *addr = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) { perror("mmap"); close(fd); return; }

// 直接访问整个文件内存
printf("File size via mmap: %ld bytes\n", st.st_size);

munmap(addr, st.st_size);
close(fd);
}

// 方法3: 使用标准库按行读取
void read_by_line(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) { perror("fopen"); return; }

char *line = NULL;
size_t len = 0;
ssize_t read;

while ((read = getline(&line, &len, fp)) != -1) {
printf("Line length: %zu\n", read);
// 注意:line 包含换行符
}

free(line);
fclose(fp);
}

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

printf("===== Chunk Read =====\n");
read_by_chunk(argv[1]);

printf("\n===== MMap Read =====\n");
read_by_mmap(argv[1]);

printf("\n===== Line Read =====\n");
read_by_line(argv[1]);

return 0;
}

通过合理选择这些接口,可以高效实现从简单的顺序读取到复杂的大文件随机访问等各种需求。

给一个getc和fgetc的使用示例

下面是一个详细展示 getcfgetc 使用的示例程序,包含文件读取和标准输入处理,并附有详细的注释说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

// 使用 fgetc 读取文件内容
void read_with_fgetc(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) {
fprintf(stderr, "fopen failed: %s\n", strerror(errno));
return;
}

printf("=== Reading '%s' with fgetc ===\n", filename);

int c;
int count = 0;
while ((c = fgetc(fp)) != EOF) { // 使用 fgetc 读取字符
putchar(c); // 输出到控制台
count++;
}

// 检查是否因错误结束
if (ferror(fp)) {
perror("读取错误");
} else if (feof(fp)) {
printf("\n\n成功读取文件结尾\n");
}

printf("总字符数: %d\n\n", count);
fclose(fp);
}

// 使用 getc 读取标准输入
void read_stdin_with_getc() {
printf("=== 使用 getc 读取标准输入 ===\n");
printf("请输入文本 (按 Ctrl+D 结束):\n");

int c;
int line_count = 1;
printf("%3d: ", line_count);

while ((c = getc(stdin)) != EOF) { // 使用 getc 读取字符
putchar(c);

// 行号计数
if (c == '\n') {
line_count++;
printf("%3d: ", line_count);
}
}

// 处理文件结束情况
if (feof(stdin)) {
printf("\n\n检测到输入结束 (EOF)\n");
}
if (ferror(stdin)) {
perror("读取错误");
}
}

// 比较 getc 和 fgetc 的性能差异
void compare_performance(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) {
perror("fopen failed");
return;
}

// 测试 fgetc 性能
clock_t start = clock();
rewind(fp);
while (fgetc(fp) != EOF) {} // 空循环
clock_t fgetc_time = clock() - start;

// 测试 getc 性能
start = clock();
rewind(fp);
while (getc(fp) != EOF) {} // 空循环
clock_t getc_time = clock() - start;

fclose(fp);

printf("\n=== 性能比较 ===\n");
printf("fgetc 耗时: %.4f 秒\n", (double)fgetc_time / CLOCKS_PER_SEC);
printf("getc 耗时: %.4f 秒\n", (double)getc_time / CLOCKS_PER_SEC);
printf("注意: getc 通常更快,因为它是宏实现\n");
}

int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "用法: %s <文件名> [性能比较]\n", argv[0]);
fprintf(stderr, "示例: %s test.txt\n", argv[0]);
return 1;
}

// 1. 使用 fgetc 读取文件
read_with_fgetc(argv[1]);

// 2. 使用 getc 读取标准输入
read_stdin_with_getc();

// 3. 性能比较(可选)
if (argc > 2 && strcmp(argv[2], "perf") == 0) {
compare_performance(argv[1]);
}

return 0;
}

编译与运行示例

  1. 创建测试文件

    1
    2
    3
    echo "这是第一行
    这是第二行
    文件结束" > test.txt
  2. 编译程序

    1
    gcc -o file_reader file_reader.c
  3. 运行程序

    1
    ./file_reader test.txt

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
=== Reading 'test.txt' with fgetc ===
这是第一行
这是第二行
文件结束

成功读取文件结尾
总字符数: 36

=== 使用 getc 读取标准输入 ===
请输入文本 (按 Ctrl+D 结束):
1: Hello, getc! # 用户输入
Hello, getc! # 程序回显
2: This is line 2
This is line 2
3: ^D # 用户按下 Ctrl+D

检测到输入结束 (EOF)

关键点说明:

  1. **fgetc vs getc**:

    • fgetc 是标准库函数
    • getc 通常是宏实现(可能更快)
    • 两者都返回 int 而不是 char(为了能表示 EOF)
  2. 返回值处理

    1
    2
    3
    4
    5
    6
    7
    8
    int c = fgetc(stream);
    if (c == EOF) {
    if (feof(stream)) {
    // 文件结束处理
    } else if (ferror(stream)) {
    // 错误处理
    }
    }
  3. 使用场景

    • fgetc:需要函数指针时(如传递给其他函数)
    • getc:性能敏感的循环中(宏展开可能更快)
    • 两者在功能上完全等价
  4. 重要注意事项

    • 必须使用 int 接收返回值char 类型无法正确表示 EOF
    • 错误处理:始终检查 EOF 后使用 feof()ferror()
    • 性能差异:对于大文件,getc 可能快 10-20%(因其宏实现)

特殊用法示例:

  1. 实现简单 cat 命令

    1
    2
    3
    4
    5
    6
    void simple_cat(FILE *fp) {
    int c;
    while ((c = getc(fp)) != EOF) {
    putchar(c);
    }
    }
  2. 读取直到特定字符

    1
    2
    3
    4
    5
    6
    7
    int read_until(FILE *fp, char delimiter) {
    int c;
    while ((c = fgetc(fp)) != EOF && c != delimiter) {
    putchar(c);
    }
    return c;
    }
  3. 跳过空白字符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

// 使用 getline 读取文件并显示行号
void read_file_with_line_numbers(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) {
fprintf(stderr, "错误: 无法打开文件 '%s': %s\n", filename, strerror(errno));
return;
}

printf("===== 文件内容: %s =====\n", filename);

char *line = NULL; // getline 会自动分配缓冲区
size_t len = 0; // 缓冲区长度
ssize_t read; // 实际读取的字符数
int line_number = 1;

// 逐行读取文件
while ((read = getline(&line, &len, fp)) != -1) {
// 移除行尾的换行符(如果有)
if (read > 0 && line[read - 1] == '\n') {
line[read - 1] = '\0';
read--;
}

// 打印行号和内容
printf("%4d: %s [长度: %zu]\n", line_number++, line, read);
}

// 检查是否因错误退出
if (ferror(fp)) {
perror("读取文件时发生错误");
}

// 释放 getline 分配的缓冲区
if (line) {
free(line);
}

fclose(fp);
}

// 使用 getline 从标准输入读取
void read_from_stdin() {
printf("\n===== 从标准输入读取 (按 Ctrl+D 结束) =====\n");
printf("请输入多行文本:\n");

char *line = NULL;
size_t len = 0;
ssize_t read;
int count = 0;

while ((read = getline(&line, &len, stdin)) != -1) {
// 移除换行符
if (read > 0 && line[read - 1] == '\n') {
line[read - 1] = '\0';
}

printf("接收: %s\n", line);
count++;

// 动态内存使用信息
printf("(缓冲区大小: %zu, 内容长度: %zu)\n\n", len, read);
}

printf("\n共读取 %d 行\n", count);

if (line) {
free(line);
}
}

// 使用 getline 进行文本处理 - 查找最长行
void find_longest_line(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) {
perror("fopen failed");
return;
}

char *longest_line = NULL;
size_t longest_len = 0;
size_t max_buffer = 0;

char *current_line = NULL;
size_t current_len = 0;
ssize_t read;

while ((read = getline(&current_line, &current_len, fp)) != -1) {
// 移除换行符计算实际长度
size_t actual_len = read;
if (actual_len > 0 && current_line[actual_len - 1] == '\n') {
actual_len--;
}

if (actual_len > longest_len) {
longest_len = actual_len;

// 为最长行分配新空间
if (longest_line) free(longest_line);
longest_line = strdup(current_line);
if (!longest_line) {
perror("strdup failed");
break;
}
longest_line[actual_len] = '\0'; // 确保正确终止

max_buffer = current_len;
}
}

if (longest_line) {
printf("\n===== 最长行 =====\n");
printf("长度: %zu 字符\n", longest_len);
printf("分配缓冲区: %zu 字节\n", max_buffer);
printf("内容: \"%s\"\n", longest_line);
free(longest_line);
} else {
printf("文件为空\n");
}

if (current_line) free(current_line);
fclose(fp);
}

int main(int argc, char *argv[]) {
// 示例1: 读取文件并显示行号
if (argc > 1) {
read_file_with_line_numbers(argv[1]);
} else {
printf("用法: %s <文件名> [其他选项]\n", argv[0]);
printf("示例: %s test.txt\n", argv[0]);
}

// 示例2: 从标准输入读取
read_from_stdin();

// 示例3: 查找文件中最长的行
if (argc > 1) {
find_longest_line(argv[1]);
}

return 0;
}

编译与运行

1. 创建测试文件

1
2
3
4
5
echo "这是第一行
这是一个更长的第二行
短行
这一行特别长,用于演示getline如何处理不同长度的行内容
最后一行" > test.txt

2. 编译程序

1
gcc -o getline_demo getline_demo.c

3. 运行程序

示例1: 读取文件

1
./getline_demo test.txt

输出示例:

1
2
3
4
5
6
===== 文件内容: test.txt =====
1: 这是第一行 [长度: 12]
2: 这是一个更长的第二行 [长度: 21]
3: 短行 [长度: 4]
4: 这一行特别长,用于演示getline如何处理不同长度的行内容 [长度: 51]
5: 最后一行 [长度: 9]

示例2: 从标准输入读取

1
echo -e "第一行\n更长的第二行" | ./getline_demo test.txt

输出示例:

1
2
3
4
5
6
7
8
9
===== 从标准输入读取 (按 Ctrl+D 结束) =====
请输入多行文本:
接收: 第一行
(缓冲区大小: 120, 内容长度: 8)

接收: 更长的第二行
(缓冲区大小: 120, 内容长度: 14)

共读取 2 行

示例3: 查找最长行

1
./getline_demo test.txt

输出示例:

1
2
3
4
===== 最长行 =====
长度: 51 字符
分配缓冲区: 120 字节
内容: "这一行特别长,用于演示getline如何处理不同长度的行内容"

getline 关键特性

  1. 自动内存管理:

    • 首次调用时,lineptr 应为 NULL,getline 会自动分配缓冲区
    • 后续调用会重用或扩展缓冲区
    • 使用完后必须调用 free(lineptr) 释放内存
  2. 缓冲区重用:

    1
    2
    3
    4
    5
    6
    7
    8
    char *buffer = NULL;
    size_t bufsize = 0;

    // 第一次读取 - 自动分配
    getline(&buffer, &bufsize, fp);

    // 第二次读取 - 重用缓冲区
    getline(&buffer, &bufsize, fp);
  3. 处理任意长度行:

    • 当遇到超过当前缓冲区长度的行时
    • getline 会自动重新分配更大的缓冲区
    • 更新 lineptrn 的值
  4. 包含换行符:

    • 读取的行包含结尾的换行符(如果存在)
    • 可以使用以下代码移除:
      1
      2
      if (nread > 0 && line[nread-1] == '\n')
      line[nread-1] = '\0';
  5. 返回值:

    • 成功时返回读取的字节数(包括换行符)
    • 文件结束时返回 -1
    • 错误时返回 -1 并设置 errno

使用场景

  1. 配置文件解析:

    1
    2
    3
    4
    5
    6
    while (getline(&line, &len, fp) != -1) {
    if (line[0] == '#' || line[0] == '\n') continue; // 跳过注释和空行
    char *key = strtok(line, "=");
    char *value = strtok(NULL, "\n");
    // 处理键值对...
    }
  2. 日志文件处理:

    1
    2
    3
    4
    5
    6
    // 查找包含特定字符串的行
    while (getline(&line, &len, fp) != -1) {
    if (strstr(line, "ERROR")) {
    printf("发现错误: %s", line);
    }
    }
  3. 网络数据解析:

    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. 内存泄漏:

    1
    2
    3
    4
    5
    char *line = NULL;
    size_t len = 0;
    getline(&line, &len, fp); // 分配内存
    // ... 使用 line ...
    free(line); // 必须释放!
  2. 错误处理:

    1
    2
    3
    4
    5
    6
    7
    8
    ssize_t n = getline(&line, &len, fp);
    if (n == -1) {
    if (feof(fp)) {
    printf("文件结束\n");
    } else {
    perror("读取错误");
    }
    }
  3. 可移植性:

    • getline 是 POSIX 2008 标准的一部分
    • 在编译时可能需要定义宏:
      1
      2
      #define _POSIX_C_SOURCE 200809L
      #include <stdio.h>
    • 对于不支持的环境,可以使用替代实现:
      1
      2
      3
      4
      // 简化版 getline 实现
      ssize_t portable_getline(char **lineptr, size_t *n, FILE *stream) {
      // 实现细节...
      }
  4. 性能考虑:

    • 对于已知最大行长的情况,可以预先分配:
      1
      2
      3
      size_t len = 256; // 初始缓冲区大小
      char *line = malloc(len);
      getline(&line, &len, fp);

这个示例展示了 getline 的强大功能,特别是在处理未知长度的文本行时。它简化了文件读取操作,避免了缓冲区溢出的风险,是处理文本文件的理想选择。