OSTEP学习 | 持久化的一个普通例子

持久化的一个例子

代码

#include <stdio.h> 
#include <unistd.h> 
#include <assert.h> 
#include <fcntl.h> 
#include <sys/types.h> 
int main(int argc, char *argv[]) 
{ 
    int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU); 
    assert(fd > -1); 
    int rc = write(fd, "hello world\n", 13); 
    assert(rc == 13); 
    close(fd); 
    return 0; 
}

解释

好的,这个程序是一个非常典型的 C 语言文件操作的例子,它演示了如何在 Linux/Unix-like 系统上创建一个文件,并向其中写入内容。

我们来一步一步地详细解释它:

程序的总体目标

这个程序的主要目标是:

  1. /tmp 目录下创建一个名为 file 的新文件。
  2. 向这个文件中写入字符串 "hello world\n"。
  3. 关闭文件。
  4. 如果在这个过程中发生任何错误(比如无法创建文件或写入失败),程序会立即终止并报错。

逐行解释

第 1-6 行:包含头文件

1 #include <stdio.h>       // 标准输入/输出库,虽然本程序没有直接使用printf/fprintf,但它是C程序常用库
2 #include <unistd.h>      // 提供了对 POSIX 操作系统 API 的访问,包括 write() 和 close()
3 #include <assert.h>      // 提供了 assert() 宏,用于调试和断言检查
4 #include <fcntl.h>       // 提供了文件控制选项,包括 open() 函数的标志位 (O_WRONLY, O_CREAT, O_TRUNC)
5 #include <sys/types.h>   // 提供了定义系统数据类型的头文件,例如 off_t(文件偏移量),虽然本例没有直接使用,但常与文件操作一起包含
  • #include <stdio.h>: 提供了像 printf, fprintf 等标准输入输出函数。虽然在这个程序中没有直接调用这些函数,但它是 C 语言程序中非常常用的一个库。
  • #include <unistd.h>: 这个头文件包含了许多 UNIX 标准的系统调用函数,比如 write() (写入文件) 和 close() (关闭文件)。
  • #include <assert.h>: 包含了 assert() 宏。assert(expression) 会检查 expression 是否为真。如果 expression 为假(即 0),程序会终止并打印一条错误消息,通常还会显示是哪个文件哪一行代码出了问题。这在调试时非常有用。
  • #include <fcntl.h>: 这个头文件包含了文件控制(file control)相关的函数和常量。特别是,它定义了 open() 函数的各种标志位(例如 O_WRONLY, O_CREAT, O_TRUNC)。
  • #include <sys/types.h>: 包含了定义各种系统数据类型(如 pid_t 进程ID, off_t 文件偏移量)的头文件。尽管本例中没有直接使用这些类型,但在进行系统编程(尤其是文件和进程操作)时,常常会包含它。

第 8-16 行:main 函数

8 int
9 main(int argc, char *argv[])
10 {
11  int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
12  assert(fd > -1);
13  int rc = write(fd, "hello world\n", 13);
14  assert(rc == 13);
15  close(fd);
16  return 0;
17 }
  • 第 9 行:main(int argc, char *argv[])

    • 这是 C 程序的入口点。
    • argc (argument count) 表示命令行参数的数量。
    • argv (argument vector) 是一个字符串数组,包含了命令行参数本身。
    • 在这个程序中,argcargv 并没有被使用,但它们是 main 函数的标准签名。
  • 第 10 行:int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);

    • 这是核心的文件打开操作。
    • open() 是一个系统调用,用于打开或创建文件。
    • /tmp/file: 这是要操作的文件路径和名称。/tmp 是一个通常用于存放临时文件的目录。
    • O_WRONLY | O_CREAT | O_TRUNC: 这是文件的打开模式或标志位,它们通过 | (位或) 操作符组合在一起。
      • O_WRONLY: 以只写模式打开文件。你不能从这个文件中读取内容。
      • O_CREAT: 如果文件不存在,就创建它。
      • O_TRUNC: 如果文件已经存在,并且是以可写方式打开的(O_WRONLYO_RDWR),那么文件的内容会被截断(清空)为 0 字节。
    • S_IRWXU: 这是新文件(如果 O_CREAT 生效并创建了文件)的权限位。它定义了文件所有者的读(R)、写(W)、执行(X)权限。
      • S_IRWXU 通常表示 用户(Owner) 具有读、写、执行权限。
    • 返回值 fd: open() 函数成功时返回一个非负整数,这个整数被称为文件描述符 (File Descriptor)。文件描述符是操作系统用来识别你打开的文件的整数 ID。后续所有对这个文件的操作(如读、写、关闭)都需要用到这个文件描述符。如果 open() 失败,它会返回 -1
  • 第 11 行:assert(fd > -1);

    • 这一行使用 assert() 来检查 open() 函数是否成功。
    • 如果 fd 的值小于或等于 -1(即 open 失败),assert() 就会触发,导致程序异常终止,并打印错误信息。这有助于发现文件打开失败的问题。
  • 第 12 行:int rc = write(fd, "hello world\n", 13);

    • 这是向文件写入数据的操作。
    • write() 是一个系统调用,用于将数据写入到文件描述符引用的文件中。
    • fd: 之前 open() 返回的文件描述符,表示我们要向哪个文件写入。
    • "hello world\n": 这是一个字符串字面量,是我们想要写入文件的内容。\n 是一个换行符。
    • 13: 这是要写入的字节数。字符串 "hello world\n" 包含 12 个字符和一个换行符,总共 13 个字节。
    • 返回值 rc: write() 函数成功时返回实际写入的字节数。如果失败,它返回 -1
  • 第 13 行:assert(rc == 13);

    • 这一行检查 write() 函数是否成功地写入了所有预期的字节(13个)。
    • 如果 rc 不等于 13(例如,只写入了一部分或者写入失败),assert() 将终止程序。
  • 第 14 行:close(fd);

    • 这是关闭文件的操作。
    • close() 是一个系统调用,用于释放由文件描述符 fd 关联的文件资源。
    • 关闭文件描述符是非常重要的,它释放了系统资源,并确保所有缓存的数据都被写入到磁盘。如果不关闭文件,可能会导致数据丢失或资源泄露。
  • 第 15 行:return 0;

    • main 函数返回 0 表示程序成功执行并正常退出。非零值通常表示程序执行过程中发生了错误。

程序执行后会发生什么?

当你编译并运行这个程序后:

  1. 它会尝试在你的 /tmp/ 目录下创建一个名为 file 的文件。
  2. 如果 /tmp/file 已经存在,它的内容会被清空。
  3. 然后,字符串 "hello world" 和一个换行符会被写入到这个 file 中。
  4. 程序随后会关闭这个文件。

你可以通过在终端中运行以下命令来验证:

# 编译程序 (假设你保存为 file_write.c)
gcc file_write.c -o file_write

# 运行程序
./file_write

# 查看文件内容
cat /tmp/file

cat /tmp/file 的输出将是:

hello world

这是一个非常基础但关键的文件 I/O (输入/输出) 示例,它展示了如何在 UNIX/Linux 系统中直接使用系统调用来操作文件,而不是使用像 fopen, fprintf, fclose 这样的 C 标准库函数(这些标准库函数通常内部也会调用 open, write, close 等系统调用)。

用c的标准库的写法

好的,如果你想使用 C 标准库中的文件操作函数来完成相同的任务,程序会变得稍微简洁一些,并且通常更易于移植(在不同操作系统上编译和运行)。

C 标准库的函数是对底层系统调用的封装,它们提供了更高级别的抽象,例如缓冲机制。

以下是使用 C 标准标准库函数实现相同功能的程序:

#include <stdio.h>   // 包含标准输入/输出库,提供了 fopen, fprintf, fclose 等函数
#include <stdlib.h>  // 包含标准库,提供了 exit 函数

int
main(int argc, char *argv[])
{
    // 使用 fopen 打开文件
    // "w" 模式表示:
    // - 以写入模式打开文件
    // - 如果文件不存在,则创建它
    // - 如果文件已存在,则截断(清空)其内容
    FILE *fp = fopen("/tmp/file", "w");

    // 检查文件是否成功打开
    if (fp == NULL) {
        fprintf(stderr, "Error: Could not open /tmp/file for writing.\n");
        // perror("fopen"); // 也可以使用 perror 打印更详细的系统错误信息
        exit(EXIT_FAILURE); // 退出程序,表示失败
    }

    // 使用 fprintf 写入字符串到文件
    // 写入 "hello world\n"
    int result = fprintf(fp, "hello world\n");

    // 检查写入是否成功
    // fprintf 返回写入的字符数,或者在出错时返回负值 (EOF)
    // "hello world\n" 共有 13 个字符
    if (result < 0) { // 或者直接 if (result != 13) 也可以
        fprintf(stderr, "Error: Could not write to /tmp/file.\n");
        fclose(fp); // 尝试关闭文件,即使写入失败
        exit(EXIT_FAILURE); // 退出程序,表示失败
    }

    // 关闭文件
    // fclose 刷新所有缓冲区并将数据写入磁盘,然后释放文件资源
    if (fclose(fp) != 0) { // fclose 成功返回 0,失败返回 EOF (非零)
        fprintf(stderr, "Error: Could not close /tmp/file.\n");
        exit(EXIT_FAILURE); // 退出程序,表示失败
    }

    return 0; // 程序成功执行
}

详细解释和与之前程序的对比:

  1. 头文件:

    • #include <stdio.h>:这是最重要的变化。C 标准库的文件 I/O 函数都在这个头文件中声明。
    • #include <stdlib.h>:用于 exit()EXIT_FAILURE
    • 不再需要 unistd.h, fcntl.h, sys/types.h, assert.h:因为我们不再直接使用底层的系统调用或 assert 宏(这里我们用 if 语句和 fprintf 进行错误检查,这对于初学者来说可能更直观)。
  2. 打开文件:fopen()

    • 之前int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
      • 返回一个整数文件描述符 (fd)。
      • 需要手动组合各种标志位来指定模式和行为。
      • 权限 (S_IRWXU) 是作为单独的参数传递的。
    • 现在FILE *fp = fopen("/tmp/file", "w");
      • 返回一个指向 FILE 结构体的指针 (fp)。FILE 是一个更高级的数据结构,由标准库维护,包含了文件描述符、缓冲区等信息。
      • 使用一个简单的模式字符串 "w" 来指定写入模式。
        • "w":如果文件不存在,则创建。如果文件已存在,则清空其内容(截断)。
        • 其他常见模式:"r" (只读), "a" (追加), "wb" (写入二进制), "rb" (读取二进制) 等。
      • 权限问题fopen 不直接接受权限参数。新创建的文件的权限将由操作系统的 umask 设置和文件系统的默认权限决定。如果你需要非常精确的权限控制,你可能仍然需要使用底层的 open 函数,或者在 fopen 之后使用 chmod 函数来修改权限。对于这个简单的例子,"w" 模式通常会创建一个用户可读写的文本文件。
  3. 错误检查:if (fp == NULL)

    • 之前assert(fd > -1);
    • 现在fopen 在失败时返回 NULL。我们通过检查 fp 是否为 NULL 来判断文件是否成功打开。如果失败,打印错误信息并使用 exit(EXIT_FAILURE) 终止程序。这比 assert 更友好,因为它允许你自定义错误消息并进行更优雅的退出。
  4. 写入数据:fprintf()

    • 之前int rc = write(fd, "hello world\n", 13);
      • 直接写入原始字节。
      • 需要显式指定要写入的字节数。
    • 现在int result = fprintf(fp, "hello world\n");
      • fprintf 类似于 printf,但它将输出写入到指定的文件流 (fp) 中。
      • 它会自动处理字符串的长度,直到遇到字符串的终止符 \0(尽管这里我们传入的是一个字符串字面量,它自带 \0)。
      • fprintf 提供了格式化输出的能力,例如 fprintf(fp, "The number is: %d\n", 42);
      • fprintf 返回写入的字符数,如果发生错误则返回负值(EOF)。
  5. 关闭文件:fclose()

    • 之前close(fd);
      • 直接关闭文件描述符。
    • 现在if (fclose(fp) != 0)
      • fclose 关闭 FILE 指针关联的文件流。
      • 它会先刷新(flush)任何在缓冲区中尚未写入磁盘的数据,然后释放所有相关的资源。
      • fclose 在成功时返回 0,在失败时返回 EOF(一个非零值)。通常,在简单的程序中,对 fclose 的返回值检查不如对 fopenfprintf 那么严格,但在健壮的程序中是推荐的。

总结:

使用 C 标准库函数 (fopen, fprintf, fclose) 进行文件操作通常更简单,更安全,因为它处理了底层的细节,例如文件缓冲。对于大多数常见的文件操作任务,它们是首选。只有在需要更精细控制(如特定权限、非阻塞 I/O)或极致性能时,才可能直接使用底层的系统调用 (open, read, write, close)。

posted @ 2025-07-24 09:24  粉色奶龙东京阿诺  阅读(17)  评论(0)    收藏  举报