Linux/C下拷贝文件时裁剪末尾零填充空洞的实现方案
解决方案:裁剪tmpfs空洞文件至实际数据大小
优先方案:系统调用直接定位实际数据末尾
方法1:利用lseek的SEEK_DATA/SEEK_HOLE快速定位
Linux的lseek支持特殊定位标志,可直接跳过空洞或数据块,从文件末尾往前找到最后一个有数据的位置,这是最轻量化的实现:
#include <unistd.h> #include <fcntl.h> #include <errno.h> off_t get_actual_data_size(int fd) { off_t total_size = lseek(fd, 0, SEEK_END); if (total_size == -1) return -1; off_t current_pos = total_size; while (current_pos > 0) { // 从当前位置往前找第一个数据块 current_pos = lseek(fd, current_pos - 1, SEEK_DATA); if (current_pos != -1) { // 实际数据大小为最后一个数据字节的下一位 return current_pos + 1; } if (errno != ENXIO) { // 非空洞导致的错误,直接返回失败 return -1; } // 当前位置是空洞,跳转到空洞的起始位置继续往前找 current_pos = lseek(fd, current_pos, SEEK_HOLE); if (current_pos == -1) return -1; } // 文件全为空洞,返回0 return 0; }
使用流程:
- 以
O_RDONLY模式打开目标tmpfs文件 - 调用该函数获取实际数据大小
- 对下载副本执行
ftruncate裁剪,或拷贝时仅读取到该偏移量
方法2:结合FICLONE快速拷贝+裁剪
如果需要生成离线副本,可先利用ioctl(FICLONE)快速克隆文件(tmpfs下克隆几乎无开销),再用上述方法获取实际大小后裁剪:
#include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/fs.h> #include <stdio.h> #include <errno.h> #include <unistd.h> int copy_and_trim(const char* src_path, const char* dst_path) { int src_fd = open(src_path, O_RDONLY); if (src_fd == -1) return -1; int dst_fd = open(dst_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (dst_fd == -1) { close(src_fd); return -1; } // 获取源文件总大小并初始化目标文件 off_t total_size = lseek(src_fd, 0, SEEK_END); if (total_size == -1) goto cleanup; if (ftruncate(dst_fd, total_size) == -1) goto cleanup; // 尝试快速克隆,失败则回退到普通拷贝 if (ioctl(dst_fd, FICLONE, src_fd) == -1) { lseek(src_fd, 0, SEEK_SET); char buf[4096]; ssize_t n; while ((n = read(src_fd, buf, sizeof(buf))) > 0) { if (write(dst_fd, buf, n) != n) goto cleanup; } if (n == -1) goto cleanup; } // 获取实际数据大小并裁剪目标文件 off_t actual_size = get_actual_data_size(src_fd); if (actual_size == -1) goto cleanup; if (ftruncate(dst_fd, actual_size) == -1) goto cleanup; close(src_fd); close(dst_fd); return 0; cleanup: close(src_fd); close(dst_fd); unlink(dst_path); return -1; }
备选方案:通过fiemap解析文件区间
利用ioctl(FS_IOC_FIEMAP)获取文件的所有已分配区间,遍历找到最大的已使用偏移量:
#include <linux/fs.h> #include <linux/fiemap.h> #include <sys/ioctl.h> #include <unistd.h> #include <fcntl.h> off_t get_actual_size_via_fiemap(int fd) { struct fiemap fm = {0}; fm.fm_length = ~0ULL; // 覆盖整个文件范围 if (ioctl(fd, FS_IOC_FIEMAP, &fm) == -1) { return -1; } off_t max_end = 0; for (int i = 0; i < fm.fm_mapped_extents; i++) { struct fiemap_extent* fe = &fm.fm_extents[i]; // 跳过空洞区间 if (!(fe->fe_flags & FIEMAP_EXTENT_HOLE)) { off_t extent_end = fe->fe_logical + fe->fe_length; if (extent_end > max_end) { max_end = extent_end; } } } return max_end; }
注意:tmpfs完全支持fiemap,但需确保调用进程有足够权限。
最不推荐方案:调用系统工具(无system())
若上述系统调用不可用,可通过execvp调用fallocate工具裁剪空洞,避免使用system():
#include <unistd.h> #include <fcntl.h> #include <stdio.h> int trim_file_via_fallocate(const char* path) { int fd = open(path, O_RDONLY); if (fd == -1) return -1; off_t actual_size = get_actual_data_size(fd); off_t total_size = lseek(fd, 0, SEEK_END); close(fd); if (actual_size == -1 || total_size == -1) return -1; char offset_str[32], length_str[32]; snprintf(offset_str, sizeof(offset_str), "%lld", (long long)actual_size); snprintf(length_str, sizeof(length_str), "%lld", (long long)(total_size - actual_size)); char* args[] = {"fallocate", "--collapse-range", "--offset", offset_str, "--length", length_str, path, NULL}; execvp(args[0], args); // execvp仅在失败时返回 return -1; }
该方法依赖系统预装fallocate,可靠性不如系统调用。
内容的提问来源于stack exchange,提问作者ABu
相关产品推荐
相关产品推荐

