[Linux] Vorhandene Datei verändern (atomisch!)



  • Ich bekomme von der Kommandozeile einige Dateinamen und möchte diese Dateien verändern ohne sie zu korruptieren, falls irgendwas schiefläuft. Daher meine Idee:

    • Temporäre Datei erstellen
    • Die zu ändernde Datei einlesen
    • Alle Änderungen der Datei in die temp. Datei schreiben
    • temp. Datei zu der alten Datei umbenennen

    Soweit die Theorie 🙂

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <linux/limits.h>
    #include <linux/fs.h>
    
    static void write_all(int const fd, void const* const buf, size_t const count)
    {
    	char const* const dest = buf;
    	size_t written = 0;
    	while (written != count)
    	{
    		ssize_t const ret = write(fd, dest + written, count - written);
    		if (ret == -1)
    		{
    			if (errno == EINTR)
    				continue;
    
    			perror("write");
    			exit(EXIT_FAILURE);
    		}
    
    		written += (size_t)ret;
    	}
    }
    
    static void test_rename(char const* const filename)
    {
    	int const fd1 = open(filename, O_RDONLY, 0);
    	if (fd1 == -1)
    	{
    		perror("open 1");
    		exit(EXIT_FAILURE);
    	}
    	int const fd2 = open("/tmp", O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    	if (fd2 == -1)
    	{
    		perror("open 2");
    		exit(EXIT_FAILURE);
    	}
    
    	// TODO: Analyze fd1 and write the important stuff into fd2.
    	char const buf[] = "test\n";
    	write_all(fd2, buf, sizeof buf);
    
    	char path[PATH_MAX] = {0};
    	snprintf(path, sizeof path, "/proc/self/fd/%d", fd2);
    	printf("path: %s\n", path);
    	//if (syscall(SYS_renameat2, AT_FDCWD, path, AT_FDCWD, filename, RENAME_EXCHANGE) < 0)
    	if (renameat(AT_FDCWD, path, AT_FDCWD, filename) == -1)
    	{
    		printf("error: %d\n", errno);
    		perror("renameat");
    		exit(EXIT_FAILURE);
    	}
    
    	close(fd2);
    	close(fd1);
    }
    
    int main(int argc, char* argv[])
    {
    	test_rename("file");
    
    	return EXIT_SUCCESS;
    }
    

    Das Problem ist, dass /tmp nicht notwendigerweise auf der selben Partition liegt. Bei mir ist es z.B. ein tempfs und somit bricht renameat in Zeile 56 mit EXDEV als Fehler ab. Ergibt für mich Sinn. Mit renameat2 in Zeile 55 habe ich es auch probiert; selber Fehler.
    Okay. Ich sollte die temp. Datei also im selben Verzeichnis wie die erste Datei erstellen. Nur wie mache ich das geschickt? Mit

    int const fd2 = openat(fd1, ".", O_RDWR | O_TMPFILE, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    

    bemängelt es, dass es sich nicht um ein Verzeichnis handelt.

    Meine Frage ist also: wie erstelle ich eine temporäre Datei in dem selben Verzeichnis wie die erste Datei?
    Eine Analyse des Pfades im filename String würde ich auch nur ungern machen, keine Ahnung auf welche Sonderheiten ich alles Acht geben muss. Im Codebeispiel ist "file" ja auch relativ; da müsste ich ja am String herumdoktoren, damit open(at) nicht fehlschlägt.
    Das snprintf in Zeile 53 gefällt mir auch nicht. Kann ich mein Vorhaben auch irgendwie nur mit den file descriptoren erreichen? Ich möchte bei dem ganzen am Ende ohne libc auskommen und nur direkt die Syscalls verwenden. sendfile(2) würde hier ja funktionieren, sogar wenn die temp. Datei in /tmp liegt. Allerdings würde ich dann insgesamt ja zwei mal schreiben, was nicht akzeptabel ist.



  • mktemp ?



  • mktemp ist furchtbar. Eine race condition zwischen dem Rückgabewert und dem Erstellen der temporären Datei ist hier unvermeidbar!
    mkstemp ist schon ein Schritt in die richtige Richtung, mich würde aber eine Lösung nur mit Syscalls interessieren. Wenn ich eine Reihe von Dateien bearbeite und SIGKILL gesendet bekomme, soll auch kein Überrest im Dateisystem bestehenbleiben, was sich mit mkstemp aber nicht vermeiden lässt. Das O_TMPFILE flag von open ist hier sehr nützlich.

    Ich neige fast schon dazu den Pfad der alten Datei zu analysieren um das konkrete Verzeichnis zu erhalten. So viele Sonderfälle gibt es ja gar nicht:
    Absolute Pfade und relative Pfade (mit '/') kann ich beim letzen '/' abtrennen. Bei relativen Pfaden (ohne '/') nehme ich das current working directory.



  • Etwas googlen zeigt, dass es wohl (noch) nicht möglich ist. O_TMPFILE wurde zu unbedacht implementiert, so dass sich der effektive Nutzen stark in Grenzen hält. Auch dieser Umweg über /proc/self/fd/... sieht sehr unüberlegt aus.

    Zu deinem Bisherigen:
    fd1 ist eine Datei, kein Verzeichnis. Daher der Fehler bzgl "kein Verzeichnis". (Mit einem Deskriptor von open(dirname(filename)... geht es.)

    /proc/self/fd/... ist ein Symlink. Für linkat kann man angeben, dass dereferenziert werden soll. Bei renameat geht das nicht. (Daher würde der Symlink umbenannt werden, was wohl fehlschlägt, da /proc/ im Allgemeinen ein eigener Mountpoint ist.)

    Für linkat wird AT_EMPTY_PATH angeboten um Dateideskriptoren ohne Pfad zu verwenden. Das klappt aber nur mit entsprechenden Rechten (root bzw CAP_DAC_READ_SEARCH), da damit Dateisystemrechte umgangen werden.

    Wenn du alles in einem O_TMPFILE vorbereitest, dann ist die Zeitspanne für linkat mit einem temp. Namen gefolgt von renameat sehr klein. (Nicht sauber, nicht elegant, ...)

    Wenn es eine bessere (oder gar gute 😉 ) Lösung gibt, würde es mich auch interessieren!



  • Ahh ich habe die Funktionsweise von openat missverstanden! Ich nahm an, dass der erste Parameter ein file descriptor einer Datei ist und in Abhängigkeit dieser das Verzeichnis bestimmt wird. Richtig ist: der file descriptor soll dieses Verzeichnis darstellen.

    Mein zweiter Fehler: ob die temp. Datei nun in /tmp oder in "." liegt spielt keine Rolle, sondern renameat kann keine symlinks umbenennen. Dafür braucht man linkat .

    Folglich muss ich also alle mir möglichen Signale blocken, ein linkat gefolgt von einem renameat machen und wieder alle Signale zulassen. Nur bei einem SIGKILL , das zwischen dem linkat und renameat auftritt würde dann eine temporäre Datei im Dateisystem zurück bleiben. Ich glaube damit kann ich leben 😉
    Nachteil ist: ich muss selbst einen einzigartigen Namen für die temp. Datei finden, damit keine Kollisionen auftreten.

    Oder ich mache mir das Leben einfach und verwende mkstemp . Nachteil hierbei ist, dass die temp. Datei länger als nötig im Dateisystem hängt, man kaum Kontrolle über den Namen dieser temp. Datei hat und dass nicht garantiert ist, dass auch ein einzigartiger Name gefunden wird.



  • Soweit ich weiß garantiert POSIX atomares Umbenennen nur dann, wenn der alte und neue Dateiname im selben Dateisystem liegt. Bei Dateien in /tmp ist aber eher unwahrscheinlich, dass die im selben Dateisystem wie die Zieldatei liegen. Das hilft also nicht, wenn atomares Ersetzen dein Ziel ist.

    Ich denke die temporäre Datei in "." zu erstellen ist schon der richtige Weg.

    Besser wäre vielleicht, eine library zu suchen, die das alles macht, denn auch da gibt es wieder Randfälle. Du könntest z.B. Schreibrechte auf die Datei haben, aber nicht aufs directory, und es gibt bestimmt noch mehr zu beachten (brauchst du ein fsync? wenn ja, auf die Datei, aufs Directory, auf beides?).



  • Ziele waren mir:

    • Keine Fehler schlucken; alles melden.
    • Temporäre Datei nur so kurz wie möglich im Dateisystem lassen.
    • Auch verwendbar, wenn es bisher keine Datei zum Ersetzen gibt. Sprich eine neue Datei wird angelegt.
    • Bei irgendwelchen Vorfällen (Stromausfall, Betriebsystemabsturz, etc) die original Datei unverändert lassen (falls vorhanden).

    Ist ein bisschen abstrakter als ich zu Anfang brauchte, aber tut seinen Dienst (zumindest bei mir ;))

    #include <stdio.h>
    #include <stdlib.h>
    #include <stdbool.h>
    #include <signal.h>
    #include <errno.h>
    #include <string.h>
    #include <assert.h>
    #include <limits.h>
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    
    enum SaveAtomicError
    {
    	SAE_SUCCESS,
    	SAE_ERROR_RESOLVE_DIR,
    	SAE_ERROR_OPEN_DIR,
    	SAE_ERROR_OPEN_TMPFILE,
    	SAE_ERROR_FSYNC,
    	SAE_ERROR_NAME_TOO_LONG,
    	SAE_ERROR_SIGPROCMASK,
    	SAE_ERROR_LINK,
    	SAE_ERROR_RENAME,
    	SAE_ERROR_CLOSE,
    };
    
    struct SaveAtomic
    {
    	char const* filename;
    	int dir;
    	int src;
    	int dest;
    };
    
    // Returns the absolute path for the directory.
    static bool get_file_directory(char directory[const static PATH_MAX], char const* const filename)
    {
    	directory[0] = 0;
    
    	if (filename[0] != '/') // a relative path
    	{
    		if (!getcwd(directory, PATH_MAX))
    			return false;
    		strcat(directory, "/");
    	}
    
    	strcat(directory, filename);
    	char* const last_slash = strrchr(directory, '/');
    	if (last_slash)
    		*last_slash = 0;
    
    	return true;
    }
    
    // It is not an error if there is no file with the name 'filename' yet. But if there is, it will be opened for reading.
    // ASSUME: filename must be valid until either save_atomic_finalize or save_atomic_cancel has been called.
    static enum SaveAtomicError save_atomic_begin(struct SaveAtomic* const sa, char const* const filename)
    {
    	assert(sa);
    	assert(filename);
    
    	mode_t const mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
    
    	// Initialize the struct.
    	sa->filename = filename;
    	sa->dir = sa->src = sa->dest = -1;
    
    	// Get the (absolute) directory path where all the work will take place.
    	char dir[PATH_MAX];
    	if (!get_file_directory(dir, filename))
    		return SAE_ERROR_RESOLVE_DIR;
    
    	// Open a file descriptor to our working directory. This is needed in case something happens (renamed, moved, ...)
    	// to the directory while we are working on the files inside.
    	sa->dir = open(dir, O_DIRECTORY, 0);
    	if (sa->dir == -1)
    		return SAE_ERROR_OPEN_DIR;
    
    	// If there is already a file with the supplied filename, open it for further use by the user.
    	sa->src = openat(sa->dir, filename, O_RDONLY, 0);
    
    	// Open the temporary file.
    	sa->dest = openat(sa->dir, ".", O_WRONLY | O_TMPFILE, mode);
    	if (sa->dest == -1)
    	{
    		if (sa->src != -1)
    			close(sa->src);
    		close(sa->dir);
    		return SAE_ERROR_OPEN_TMPFILE;
    	}
    
    	return SAE_SUCCESS;
    }
    
    static void save_atomic_cancel(struct SaveAtomic const* const sa)
    {
    	assert(sa);
    
    	if (sa->dest != -1)
    		close(sa->dest);
    	if (sa->src != -1)
    		close(sa->src);
    	if (sa->dir != -1)
    		close(sa->dir);
    }
    
    static enum SaveAtomicError save_atomic_finalize(struct SaveAtomic const* const sa)
    {
    	assert(sa);
    
    	enum SaveAtomicError ret = SAE_SUCCESS;
    
    	// Make sure everything is written to disk.
    	if (fsync(sa->dest) == -1)
    	{
    		ret = SAE_ERROR_FSYNC;
    		goto out;
    	}
    
    	// Create the temporary filename.
    	char path[PATH_MAX];
    	int const name_len = snprintf(path, sizeof path, "%s~", sa->filename);
    	assert(name_len >= 0);
    	if ((size_t)name_len > (sizeof path - 1))
    	{
    		ret = SAE_ERROR_NAME_TOO_LONG;
    		goto out;
    	}
    
    	// Create the path from where to get the temporary file.
    	char proc_path[PATH_MAX]; // TODO: Reduce the array size. Maybe to 14+(MAX_NUM_INT_DIGITS)+1.
    	int const proc_path_len = snprintf(proc_path, sizeof proc_path, "/proc/self/fd/%d", sa->dest);
    	assert(proc_path_len >= 0);
    	assert((size_t)proc_path_len < (sizeof proc_path - 1));
    
    	// Block all signals so that the following linkat and renameat won't get interrupted by a signal. Unfortunately
    	// SIGKILL will still terminate us and leave the temporary file behind.
    	sigset_t set;
    	sigfillset(&set);
    	if (sigprocmask(SIG_BLOCK, &set, 0) == -1)
    	{
    		ret = SAE_ERROR_SIGPROCMASK;
    		goto out;
    	}
    
    	// Link the temporary file into the filesystem with the name we created above.
    	if (linkat(AT_FDCWD, proc_path, sa->dir, path, AT_SYMLINK_FOLLOW) == -1)
    	{
    		ret = SAE_ERROR_LINK;
    		sigprocmask(SIG_UNBLOCK, &set, 0); // Restore the signal mask before exiting.
    		goto out;
    	}
    
    	// Overwrite the old file with our temporary file.
    	if (renameat(sa->dir, path, sa->dir, sa->filename) == -1)
    	{
    		ret = SAE_ERROR_RENAME;
    		sigprocmask(SIG_UNBLOCK, &set, 0); // Restore the signal mask before exiting.
    		goto out;
    	}
    
    	// Restore the signal mask.
    	if (sigprocmask(SIG_UNBLOCK, &set, 0) == -1)
    		ret = SAE_ERROR_SIGPROCMASK;
    
    out:
    	if (sa->dest != -1)
    	{
    		// Check for error or SILENT data loss is possible.
    		if (close(sa->dest) == -1)
    			ret = SAE_ERROR_CLOSE;
    	}
    	if (sa->src != -1)
    		close(sa->src);
    	assert(sa->dir != -1);
    	close(sa->dir);
    
    	return ret;
    }
    
    int main(int argc, char* argv[])
    {
    	if (argc < 2)
    		return EXIT_FAILURE;
    
    	struct SaveAtomic sa;
    	if (save_atomic_begin(&sa, argv[1]))
    	{
    		perror("save_atomic_begin");
    		return EXIT_FAILURE;
    	}
    
    	char const str[] = "Hallo Welt!\n";
    	if (write(sa.dest, str, sizeof str))
    	{
    		perror("write");
    		save_atomic_cancel(&sa);
    		return EXIT_FAILURE;
    	}
    
    	if (save_atomic_finalize(&sa))
    	{
    		perror("save_atomic_finalize");
    		return EXIT_FAILURE;
    	}
    
    	return EXIT_SUCCESS;
    }
    

    TL;DR:

    • Verzeichnis der Datei ermitteln
    • File descriptor zu diesem Verzeichnis offen halten
    • Datei öffnen, relativ zum Verzeichnis
    • Temporäre Datei öffnen, relativ zum Verzeichnis
    • Irgendwas in die temporäre Datei schreiben
    • Daten auf die Platte schreiben
    • Alle Signale blocken
    • Temporäre Datei ins Dateisystem einhängen, relativ zum Verzeichnis
    • Originale Datei durch temporäre Datei ersetzen, relativ zum Verzeichnis
    • Signale wieder herstellen
    • Alle file descriptoren wieder schließen

Anmelden zum Antworten