Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

Squashfs: add SEEK_DATA/SEEK_HOLE support

Add support for SEEK_DATA and SEEK_HOLE lseek() whence values.

These allow much faster searches for holes and data in sparse files, which
can significantly speed up file copying, e.g.

before (GNU coreutils, Debian 13):

cp --sparse=always big-file /

took real 11m58s, user 0m5.764s, sys 11m48s

after:

real 0.047s, user 0.000s, sys 0.027s

Where big-file has a 256 GB hole followed by 47 KB of data.

Link: https://lkml.kernel.org/r/20250923220652.568416-3-phillip@squashfs.org.uk
Signed-off-by: Phillip Lougher <phillip@squashfs.org.uk>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>

authored by

Phillip Lougher and committed by
Andrew Morton
dec91e7a 9ee94bfb

+130 -13
+126 -11
fs/squashfs/file.c
··· 307 307 all_done: 308 308 *index_block = cur_index_block; 309 309 *index_offset = cur_offset; 310 - *data_block = cur_data_block; 310 + if (data_block) 311 + *data_block = cur_data_block; 311 312 312 313 /* 313 314 * Scale cache index (cache slot entry) to index ··· 325 324 * Get the on-disk location and compressed size of the datablock 326 325 * specified by index. Fill_meta_index() does most of the work. 327 326 */ 328 - static int read_blocklist(struct inode *inode, int index, u64 *block) 327 + static int read_blocklist_ptrs(struct inode *inode, int index, u64 *start, 328 + int *offset, u64 *block) 329 329 { 330 - u64 start; 331 330 long long blks; 332 - int offset; 333 331 __le32 size; 334 - int res = fill_meta_index(inode, index, &start, &offset, block); 332 + int res = fill_meta_index(inode, index, start, offset, block); 335 333 336 - TRACE("read_blocklist: res %d, index %d, start 0x%llx, offset" 337 - " 0x%x, block 0x%llx\n", res, index, start, offset, 338 - *block); 334 + TRACE("read_blocklist: res %d, index %d, start 0x%llx, offset 0x%x, block 0x%llx\n", 335 + res, index, *start, *offset, block ? *block : 0); 339 336 340 337 if (res < 0) 341 338 return res; ··· 345 346 * extra block indexes needed. 346 347 */ 347 348 if (res < index) { 348 - blks = read_indexes(inode->i_sb, index - res, &start, &offset); 349 + blks = read_indexes(inode->i_sb, index - res, start, offset); 349 350 if (blks < 0) 350 351 return (int) blks; 351 - *block += blks; 352 + if (block) 353 + *block += blks; 352 354 } 353 355 354 356 /* 355 357 * Read length of block specified by index. 356 358 */ 357 - res = squashfs_read_metadata(inode->i_sb, &size, &start, &offset, 359 + res = squashfs_read_metadata(inode->i_sb, &size, start, offset, 358 360 sizeof(size)); 359 361 if (res < 0) 360 362 return res; 361 363 return squashfs_block_size(size); 364 + } 365 + 366 + static inline int read_blocklist(struct inode *inode, int index, u64 *block) 367 + { 368 + u64 start; 369 + int offset; 370 + 371 + return read_blocklist_ptrs(inode, index, &start, &offset, block); 362 372 } 363 373 364 374 static bool squashfs_fill_page(struct folio *folio, ··· 666 658 kfree(pages); 667 659 } 668 660 661 + static loff_t seek_hole_data(struct file *file, loff_t offset, int whence) 662 + { 663 + struct inode *inode = file->f_mapping->host; 664 + struct super_block *sb = inode->i_sb; 665 + struct squashfs_sb_info *msblk = sb->s_fs_info; 666 + u64 start, index = offset >> msblk->block_log; 667 + u64 file_end = (i_size_read(inode) + msblk->block_size - 1) >> msblk->block_log; 668 + int s_offset, length; 669 + __le32 *blist = NULL; 670 + 671 + /* reject offset if negative or beyond file end */ 672 + if ((unsigned long long)offset >= i_size_read(inode)) 673 + return -ENXIO; 674 + 675 + /* is offset within tailend and is tailend packed into a fragment? */ 676 + if (index + 1 == file_end && 677 + squashfs_i(inode)->fragment_block != SQUASHFS_INVALID_BLK) { 678 + if (whence == SEEK_DATA) 679 + return offset; 680 + 681 + /* there is an implicit hole at the end of any file */ 682 + return i_size_read(inode); 683 + } 684 + 685 + length = read_blocklist_ptrs(inode, index, &start, &s_offset, NULL); 686 + if (length < 0) 687 + return length; 688 + 689 + /* nothing more to do if offset matches desired whence value */ 690 + if ((length == 0 && whence == SEEK_HOLE) || 691 + (length && whence == SEEK_DATA)) 692 + return offset; 693 + 694 + /* skip scanning forwards if we're at file end */ 695 + if (++ index == file_end) 696 + goto not_found; 697 + 698 + blist = kmalloc(SQUASHFS_SCAN_INDEXES << 2, GFP_KERNEL); 699 + if (blist == NULL) { 700 + ERROR("%s: Failed to allocate block_list\n", __func__); 701 + return -ENOMEM; 702 + } 703 + 704 + while (index < file_end) { 705 + int i, indexes = min(file_end - index, SQUASHFS_SCAN_INDEXES); 706 + 707 + offset = squashfs_read_metadata(sb, blist, &start, &s_offset, indexes << 2); 708 + if (offset < 0) 709 + goto finished; 710 + 711 + for (i = 0; i < indexes; i++) { 712 + length = squashfs_block_size(blist[i]); 713 + if (length < 0) { 714 + offset = length; 715 + goto finished; 716 + } 717 + 718 + /* does this block match desired whence value? */ 719 + if ((length == 0 && whence == SEEK_HOLE) || 720 + (length && whence == SEEK_DATA)) { 721 + offset = (index + i) << msblk->block_log; 722 + goto finished; 723 + } 724 + } 725 + 726 + index += indexes; 727 + } 728 + 729 + not_found: 730 + /* whence value determines what happens */ 731 + if (whence == SEEK_DATA) 732 + offset = -ENXIO; 733 + else 734 + /* there is an implicit hole at the end of any file */ 735 + offset = i_size_read(inode); 736 + 737 + finished: 738 + kfree(blist); 739 + return offset; 740 + } 741 + 742 + static loff_t squashfs_llseek(struct file *file, loff_t offset, int whence) 743 + { 744 + struct inode *inode = file->f_mapping->host; 745 + 746 + switch (whence) { 747 + default: 748 + return generic_file_llseek(file, offset, whence); 749 + case SEEK_DATA: 750 + case SEEK_HOLE: 751 + offset = seek_hole_data(file, offset, whence); 752 + break; 753 + } 754 + 755 + if (offset < 0) 756 + return offset; 757 + 758 + return vfs_setpos(file, offset, inode->i_sb->s_maxbytes); 759 + } 760 + 669 761 const struct address_space_operations squashfs_aops = { 670 762 .read_folio = squashfs_read_folio, 671 763 .readahead = squashfs_readahead 764 + }; 765 + 766 + const struct file_operations squashfs_file_operations = { 767 + .llseek = squashfs_llseek, 768 + .read_iter = generic_file_read_iter, 769 + .mmap_prepare = generic_file_readonly_mmap_prepare, 770 + .splice_read = filemap_splice_read 672 771 };
+2 -2
fs/squashfs/inode.c
··· 168 168 } 169 169 170 170 set_nlink(inode, 1); 171 - inode->i_fop = &generic_ro_fops; 171 + inode->i_fop = &squashfs_file_operations; 172 172 inode->i_mode |= S_IFREG; 173 173 inode->i_blocks = ((inode->i_size - 1) >> 9) + 1; 174 174 squashfs_i(inode)->fragment_block = frag_blk; ··· 222 222 xattr_id = le32_to_cpu(sqsh_ino->xattr); 223 223 set_nlink(inode, le32_to_cpu(sqsh_ino->nlink)); 224 224 inode->i_op = &squashfs_inode_ops; 225 - inode->i_fop = &generic_ro_fops; 225 + inode->i_fop = &squashfs_file_operations; 226 226 inode->i_mode |= S_IFREG; 227 227 inode->i_blocks = (inode->i_size - 228 228 le64_to_cpu(sqsh_ino->sparse) + 511) >> 9;
+1
fs/squashfs/squashfs.h
··· 107 107 108 108 /* inode.c */ 109 109 extern const struct inode_operations squashfs_inode_ops; 110 + extern const struct file_operations squashfs_file_operations; 110 111 111 112 /* namei.c */ 112 113 extern const struct inode_operations squashfs_dir_inode_ops;
+1
fs/squashfs/squashfs_fs.h
··· 208 208 #define SQUASHFS_META_INDEXES (SQUASHFS_METADATA_SIZE / sizeof(unsigned int)) 209 209 #define SQUASHFS_META_ENTRIES 127 210 210 #define SQUASHFS_META_SLOTS 8 211 + #define SQUASHFS_SCAN_INDEXES 1024 211 212 212 213 struct meta_entry { 213 214 u64 data_block;