Dateien parallel hashen mit Rust, Rayon und SHA-256
Um mehrere Dateien in Rust zu hashen, verarbeiten Sie jede Datei mit SHA-256 als Stream und lassen Sie Rayon verschiedene Dateien gleichzeitig verarbeiten. Der folgende Befehl gibt für jede Datei eine standardmäßige SHA-256-Prüfsumme aus, ohne ganze Dateien in den Arbeitsspeicher zu laden. Parallele Worker können den Durchsatz bei mehreren Dateien erhöhen, eine einzelne Datei wird jedoch weiterhin sequenziell verarbeitet. Zudem kann der Speicher die Beschleunigung begrenzen.
Systemvoraussetzungen
Diese Anleitung verwendet Bash unter Linux, Rust und Cargo 1.98.1, ring 0.17.14 und Rayon 1.12.0.
Sie wurde in einem Debian-12-Container mit ARM64 getestet. Installieren Sie die
Rust-Toolchain sowie einen C-Compiler und Linker zum Kompilieren von
ring; zur Überprüfung wird außerdem GNU sha256sum verwendet.
Cargo benötigt beim ersten Build Netzwerkzugriff, um die Abhängigkeiten herunterzuladen.
Projekt erstellen
Führen Sie dies in dem übergeordneten Verzeichnis aus, in dem Sie ein neues Projekt namens
file-hashing erstellen möchten:
cargo new --bin --edition 2024 --vcs none file-hashing &&
cd file-hashing &&
cargo add ring@=0.17.14 rayon@=1.12.0
Die Verkettung mit && stoppt, wenn die Projekterstellung oder der Verzeichniswechsel
fehlschlägt. Falls file-hashing bereits existiert, verweigert Cargo das Ersetzen.
Wählen Sie einen noch nicht belegten Speicherort und führen Sie den gesamten Block erneut aus.
Fahren Sie erst nach erfolgreicher Einrichtung mit den weiteren Schritten fort und bleiben Sie
im neuen Projektverzeichnis.
Die Versionsvorgaben mit = legen die beiden direkten Abhängigkeiten fest.
Bewahren Sie die generierte Datei
Cargo.lock zusammen mit dem Manifest auf,
um auch die aufgelösten Versionen ihrer Abhängigkeiten beizubehalten.
Den vom Empfänger erwarteten Prüfsummenalgorithmus verwenden
Dieses Beispiel verwendet SHA-256, damit Sie die Ausgabe direkt mit
sha256sum vergleichen können. Ein BLAKE2-Hashwert würde unabhängig von der
berechnenden Bibliothek nicht mit einer SHA-256-Referenz übereinstimmen. Wählen Sie den Algorithmus,
bevor Sie seine Implementierung optimieren.
Jede Datei streamen und mehrere Dateien parallel verarbeiten
Ersetzen Sie die generierte Datei src/main.rs durch dieses vollständige Programm.
Damit wird der von Cargo erzeugte Platzhalter-Quellcode bewusst ersetzt:
use rayon::prelude::*;
use ring::digest::{Context, SHA256};
use std::fs::File;
use std::io::{self, ErrorKind, Read};
use std::path::PathBuf;
use std::process::ExitCode;
fn sha256_digest<R: Read>(mut reader: R) -> io::Result<String> {
let mut context = Context::new(&SHA256);
let mut buffer = [0u8; 8192];
loop {
let count = match reader.read(&mut buffer) {
Ok(count) => count,
Err(error) if error.kind() == ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
};
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context
.finish()
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn main() -> ExitCode {
let files: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
if files.is_empty() {
eprintln!("Usage: file-hashing <file> [file ...]");
return ExitCode::from(2);
}
let result = files.par_iter().try_for_each(|path| -> io::Result<()> {
let digest = File::open(path).and_then(sha256_digest).map_err(|error| {
io::Error::new(error.kind(), format!("{}: {error}", path.display()))
})?;
println!("{digest} {}", path.display());
Ok(())
});
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
Jeder Worker hält einen Lesepuffer von 8 KiB und einen eigenen
ring::digest::Context vor.
Wiederholte Aufrufe von update hashen die Bytes der Datei der Reihe nach;
finish erzeugt den vollständigen Hashwert.
Die Schleife hasht nur die tatsächlich gelesenen Bytes, fährt nach kurzen Lesevorgängen fort und
wiederholt den Leseversuch bei
ErrorKind::Interrupted.
Andere Lesefehler führen zu einer Fehlermeldung statt zu einer Prüfsumme für unvollständige Daten.
Die Funktion
try_for_each
von Rayon plant die gleichzeitige Verarbeitung verschiedener Pfade. Die Ausgabereihenfolge ist
nicht festgelegt. Schlägt das Öffnen oder Lesen einer Datei fehl, meldet der Befehl den Pfad auf
stderr und beendet sich mit Status 1. Rayon versucht, die verbleibende Arbeit zu stoppen, doch
andere Worker können bereits Prüfsummen ausgegeben haben. Betrachten Sie die Verarbeitung der
Dateigruppe daher als unvollständig. Ohne Argumente gibt der Befehl Hinweise zur Verwendung aus
und beendet sich mit Status 2.
Programm ausführen und Hashwerte prüfen
Erstellen Sie zwei kleine Fixtures und führen Sie das Programm aus. Die Einstellung
set -C der Subshell verhindert das Überschreiben vorhandener Beispieldateien.
Jedes && verhindert, dass nach einem Fehler weitere Befehle ausgeführt werden:
(
set -C
printf abc > abc.txt &&
: > empty.txt &&
cargo run --release --locked -- abc.txt empty.txt
)
Erwarten Sie diese beiden Zeilen in beliebiger Reihenfolge. Die Fixture
abc.txt enthält drei Bytes ohne Zeilenumbruch:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad abc.txt
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.txt
Vergleichen Sie die Hashwerte mit einer unabhängigen Implementierung:
sha256sum abc.txt empty.txt
Verwenden Sie die Dateien erneut, um den Hashvorgang zu wiederholen:
cargo run --release --locked -- abc.txt empty.txt
Der Befehl liest ausschließlich seine Eingaben und schreibt auf stdout. Cargo kann die ausführbare
Datei in target/release/ neu erstellen; Ihre Eingabedateien werden dabei nicht ersetzt.
Geben Sie Ihre eigenen Dateipfade nach -- an und setzen Sie Pfade mit
Leerzeichen in Anführungszeichen. Die ausgegebenen Pfade dienen der Anzeige: Dieses Beispiel
implementiert weder das Escaping von Dateinamen noch den Prüfsummen-Prüfmodus von
sha256sum.
Worker-Anzahlen auf Ihrem Speicher vergleichen
Sobald der Release-Build vorliegt, messen Sie die Laufzeit der ausführbaren Datei direkt, damit
die Kompilierung nicht mitgemessen wird. Rayon 1.12 verwendet
RAYON_NUM_THREADS,
um die Anzahl der Worker festzulegen:
time env RAYON_NUM_THREADS=1 ./target/release/file-hashing abc.txt empty.txt &&
time env RAYON_NUM_THREADS=4 ./target/release/file-hashing abc.txt empty.txt
Diese winzigen Fixtures prüfen die Befehle, nicht die Leistung. Ersetzen Sie die Pfade in beiden Befehlen durch dieselbe repräsentative Dateigruppe, bevor Sie die Laufzeiten vergleichen. Wiederholen Sie die Durchläufe und wechseln Sie ihre Reihenfolge: Der Dateicache des Betriebssystems kann einen späteren Durchlauf beschleunigen. Mehr Worker können helfen, wenn das Hashen der Engpass ist. Sie können die Verarbeitung aber auch verlangsamen, wenn gleichzeitige Lesevorgänge um die Speicherbandbreite konkurrieren. Verwenden Sie die Worker-Anzahl, die Ihrer Arbeitslast zugutekommt; eine Beschleunigung ist nicht garantiert.
Mit einer vertrauenswürdigen Referenz vergleichen
Um einen Download zu überprüfen, vergleichen Sie den Hashwert mit der erwarteten SHA-256-Prüfsumme des Herausgebers aus einer vertrauenswürdigen Quelle. Eine Prüfsumme allein bestätigt nicht die Echtheit einer Datei, wenn jemand sowohl die Datei als auch ihre Referenz ersetzen kann. Beispielsweise prüft Ubuntu die Signatur seiner Prüfsummendatei, bevor es den Download überprüft.
Lassen Sie Dateien während des Hashens unverändert: Dieses Programm liest einen Stream und kann daher keine konsistente Momentaufnahme einer Datei liefern, die ein anderer Prozess gerade ändert.
