菜鸟科技网

dd命令源码如何实现数据块拷贝?

下面我将为你详细解析 dd 命令的源码,包括其核心思想、关键代码片段、以及一个完整的、可编译的简化版实现。

dd命令源码如何实现数据块拷贝?-图1
(图片来源网络,侵删)

dd 的核心思想

dd 命名的由来据传是 "Data Description"(数据描述)或 "Disk Dump"(磁盘转储),它的核心功能就像一个“超级拷贝”命令,其工作流程可以概括为:

  1. 从输入源 读取数据:输入源可以是一个文件、一个设备(如磁盘、分区)、标准输入等。
  2. 对数据进行转换:根据用户指定的 conv=... 参数,对读取到的数据进行处理,例如转换大小写(ucase, lcase)、去除回车符(unblock)等。
  3. 将数据写入输出目标:输出目标可以是一个文件、一个设备、标准输出等。
  4. 统计并报告:记录并显示复制了多少个输入和输出块,以及处理了多少字节。

其行为主要由以下三个核心参数控制:

  • if=INPUT_FILE:输入文件。
  • of=OUTPUT_FILE:输出文件。
  • bs=BLOCK_SIZE:块大小。
  • count=NUMBER:总共要复制的块数量。

官方源码位置与结构

dd 命令是 GNU Coreutils 包的一部分,它的源代码可以在官方的 Git 仓库中找到:

  • Git 仓库: https://sourceware.org/git/?p=coreutils.git;a=tree;h=HEAD
  • dd 的源码目录: coreutils/src/dd.c

dd.c 这个文件非常长(超过 4000 行),因为它包含了大量的转换选项和错误处理逻辑,但其核心逻辑是清晰和简洁的。

dd命令源码如何实现数据块拷贝?-图2
(图片来源网络,侵删)

核心逻辑解析 (伪代码与关键函数)

我们来看一下 dd 命令执行流程的核心逻辑,这部分代码通常在 main 函数中。

1. 参数解析

dd 使用 getopt_long 来解析命令行参数,它不像大多数命令那样使用 开头的短选项,而是直接使用 key=value 的形式(if=/dev/zero),在代码中,这些参数被解析并存储在一个全局的结构体 struct dd 中。

// 伪代码:main 函数的开头
int main (int argc, char **argv)
{
  // ... 初始化 ...
  // 循环解析所有命令行参数
  while (1)
    {
      int c = getopt_long (argc, argv, "", longopts, NULL);
      if (c == -1)
        break;
      switch (c)
        {
          case IF: // if=...
            input_file = optarg;
            break;
          case OF: // of=...
            output_file = optarg;
            break;
          case BS: // bs=...
            block_size = atoi(optarg);
            break;
          case COUNT: // count=...
            copy_count = atoi(optarg);
            break;
          // ... 其他 conv= 等参数的处理 ...
        }
    }
  // ... 调用核心处理函数 ...
}

2. 核心循环

这是 dd 的心脏,它不断地执行“读-转换-写”的循环,直到满足退出条件(读取到文件末尾,或者达到了 count 指定的块数)。

// 伪代码:核心处理逻辑
void process_data (void)
{
  // 1. 打开输入和输出文件
  int ifd = open (input_file, O_RDONLY | O_BINARY);
  int ofd = open (output_file, O_WRONLY | O_CREAT | O_TRUNC | O_BINARY, 0666);
  // 分配一个缓冲区,大小由 bs= 参数决定
  char *buffer = (char *) malloc (block_size);
  ssize_t bytes_read;
  long blocks_processed = 0;
  // 2. 核心循环
  while (1)
    {
      // 2.1. 从输入文件读取一个块
      bytes_read = read (ifd, buffer, block_size);
      // 检查是否读取失败或到达文件末尾
      if (bytes_read < 0)
        {
          // 错误处理
          perror ("read");
          break;
        }
      if (bytes_read == 0)
        {
          // 文件结束,正常退出循环
          break;
        }
      // 2.2. (可选) 数据转换
      // if (conv_flags & CONV_UCASE) convert_to_uppercase(buffer, bytes_read);
      // ... 其他转换逻辑 ...
      // 2.3. 将数据写入输出文件
      ssize_t bytes_written = write (ofd, buffer, bytes_read);
      // 检查写入是否成功
      if (bytes_written < 0)
        {
          // 错误处理
          perror ("write");
          break;
        }
      if (bytes_written != bytes_read)
        {
          // 处理写入不完整的情况(例如磁盘已满)
          error(EXIT_FAILURE, 0, "Short write");
        }
      // 2.4. 更新统计信息
      blocks_processed++;
      total_in += bytes_read;
      total_out += bytes_written;
      // 2.5. 检查是否达到了 count= 指定的块数
      if (copy_count > 0 && blocks_processed >= copy_count)
        {
          break;
        }
    }
  // 3. 清理资源
  close (ifd);
  close (ofd);
  free (buffer);
  // 4. 打印最终的统计信息
  print_stats();
}

一个简化的、可编译的 dd 源码示例

下面是一个简化版的 dd 实现,它只支持 if=, of=, bs=, count= 这四个基本功能,并且没有数据转换,这个版本清晰地展示了 dd 的核心工作原理,你可以直接保存为 mydd.c 并编译运行。

dd命令源码如何实现数据块拷贝?-图3
(图片来源网络,侵删)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
// 打印使用说明
void usage(const char *prog_name) {
    fprintf(stderr, "Usage: %s if=<input_file> of=<output_file> [bs=<block_size>] [count=<num_blocks>]\n", prog_name);
    exit(EXIT_FAILURE);
}
// 打印统计信息
void print_stats(long total_in, long total_out, long blocks) {
    fprintf(stderr, "%ld+0 records in\n", blocks);
    fprintf(stderr, "%ld+0 records out\n", blocks);
    fprintf(stderr, "%ld bytes (%.2f MB) copied, ", total_in, (double)total_in / (1024 * 1024));
    fprintf(stderr, "%.2f s, ", 0.0); // 简化版,不计算时间
    fprintf(stderr, "%.2f MB/s\n", 0.0); // 简化版,不计算速度
}
int main(int argc, char *argv[]) {
    const char *input_file = NULL;
    const char *output_file = NULL;
    size_t block_size = 4096; // 默认块大小 4KB
    long copy_count = -1;     // -1 表示复制整个文件
    long blocks_processed = 0;
    long total_in = 0, total_out = 0;
    // 1. 解析命令行参数
    for (int i = 1; i < argc; i++) {
        if (strncmp(argv[i], "if=", 3) == 0) {
            input_file = argv[i] + 3;
        } else if (strncmp(argv[i], "of=", 3) == 0) {
            output_file = argv[i] + 3;
        } else if (strncmp(argv[i], "bs=", 3) == 0) {
            block_size = atoi(argv[i] + 3);
        } else if (strncmp(argv[i], "count=", 6) == 0) {
            copy_count = atol(argv[i] + 6);
        } else {
            usage(argv[0]);
        }
    }
    if (!input_file || !output_file) {
        usage(argv[0]);
    }
    // 2. 打开文件
    int ifd = open(input_file, O_RDONLY);
    if (ifd == -1) {
        perror("open input file");
        exit(EXIT_FAILURE);
    }
    int ofd = open(output_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
    if (ofd == -1) {
        perror("open output file");
        close(ifd);
        exit(EXIT_FAILURE);
    }
    // 3. 分配缓冲区
    char *buffer = (char *)malloc(block_size);
    if (!buffer) {
        perror("malloc");
        close(ifd);
        close(ofd);
        exit(EXIT_FAILURE);
    }
    // 4. 核心循环:读-写
    while (1) {
        ssize_t bytes_read = read(ifd, buffer, block_size);
        if (bytes_read == 0) { // 文件结束
            break;
        }
        if (bytes_read == -1) { // 读取错误
            if (errno == EINTR) continue; // 被信号中断,重试
            perror("read");
            break;
        }
        // 写入数据
        ssize_t bytes_to_write = bytes_read;
        ssize_t bytes_written = write(ofd, buffer, bytes_to_write);
        if (bytes_written == -1) {
            perror("write");
            break;
        }
        if (bytes_written != bytes_to_write) {
            fprintf(stderr, "Short write!\n");
            break;
        }
        // 更新统计
        blocks_processed++;
        total_in += bytes_read;
        total_out += bytes_written;
        // 检查是否达到 count 限制
        if (copy_count > 0 && blocks_processed >= copy_count) {
            break;
        }
    }
    // 5. 清理并打印统计
    free(buffer);
    close(ifd);
    close(ofd);
    print_stats(total_in, total_out, blocks_processed);
    return EXIT_SUCCESS;
}

如何编译和运行这个简化版 dd:

  1. 保存代码: 将上面的代码保存为 mydd.c

  2. 编译: 打开终端,使用 gcc 进行编译。

    gcc -o mydd mydd.c
  3. 运行: 创建一个测试文件,然后使用 mydd 复制它。

    # 创建一个 1MB 的测试文件
    dd if=/dev/urandom of=testfile bs=1M count=1
    # 使用我们的 mydd 复制文件,块大小为 1KB,总共复制 512 个块 (即 512KB)
    ./mydd if=testfile of=outputfile bs=1K count=512
    # 查看结果
    ls -lh testfile outputfile
    ./mydd if=testfile of=outputfile bs=1K count=512 2>&1 # 查看统计信息

高级特性:conv= 参数

官方 dd 的强大之处在于其丰富的 conv= 参数,这些参数的实现通常是在上述核心循环的“数据转换”步骤中完成的。

  • conv=ucase: 在写入输出文件之前,遍历缓冲区中的每个字节,如果它是小写字母,就转换成大写。
  • conv=lcase: 同理,转换成小写。
  • conv=notrunc: 这是一个特殊选项,它告诉 dd 如果输出文件已存在,不要截断它,这通常通过在 open 输出文件时不使用 O_TRUNC 标志来实现。
  • conv=sync: 如果读取的块大小小于 bs 指定的大小(读取到文件末尾),则用空字节(\0)填充缓冲区的剩余部分,确保每次 write 操作都写入一个完整的块。

dd 的源码虽然完整版很复杂,但其核心设计非常优雅和高效:

  1. 简单而强大的模型:一个循环,不断地执行“读-处理-写”。
  2. 直接与系统交互:直接使用 open, read, write, close 等系统调用,最小化了开销。
  3. 参数驱动:通过命令行参数灵活控制输入、输出、块大小和转换规则,使其成为一个极其通用的工具。

通过阅读和理解 dd 的源码,你可以深入掌握文件 I/O、内存管理、命令行参数处理以及错误处理等 Unix/Linux C 编程的核心概念。

分享:
扫描分享到社交APP
上一篇
下一篇