Hypercell ein ] Hypercell aus ] Zeige Navigation ] Verstecke Navigation ]
c++.net  
   

Die mobilen Seiten von c++.net:
https://m.c-plusplus.net

  
C++ Forum :: Linux/Unix ::  [Linux] Vorhandene Datei verändern (atomisch!)     Zeige alle Beiträge auf einer Seite Auf Beitrag antworten
Autor Nachricht
Biolunar
Moderator

Benutzerprofil
Anmeldungsdatum: 16.02.2010
Beiträge: 517
Beitrag Biolunar Moderator 16:56:01 26.06.2017   Titel:   [Linux] Vorhandene Datei verändern (atomisch!)            Zitieren

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 :)

C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#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
C:
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.
camper
Moderator

Benutzerprofil
Anmeldungsdatum: 06.08.2004
Beiträge: 7164
Beitrag camper Moderator 17:30:23 26.06.2017   Titel:              Zitieren

mktemp ?
Biolunar
Moderator

Benutzerprofil
Anmeldungsdatum: 16.02.2010
Beiträge: 517
Beitrag Biolunar Moderator 18:17:24 26.06.2017   Titel:              Zitieren

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.
lagalopex
Mitglied

Benutzerprofil
Anmeldungsdatum: 21.01.2008
Beiträge: 450
Beitrag lagalopex Mitglied 15:01:33 27.06.2017   Titel:              Zitieren

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!
Biolunar
Moderator

Benutzerprofil
Anmeldungsdatum: 16.02.2010
Beiträge: 517
Beitrag Biolunar Moderator 15:02:52 28.06.2017   Titel:              Zitieren

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.
Christoph
Moderator

Benutzerprofil
Anmeldungsdatum: 30.04.2001
Beiträge: 6046
Beitrag Christoph Moderator 12:16:16 13.08.2017   Titel:              Zitieren

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?).

_________________
Wenn Word für Längeres geeignet wäre, würde es nicht Word, sondern Sentence, Page oder Article heißen.
Biolunar
Moderator

Benutzerprofil
Anmeldungsdatum: 16.02.2010
Beiträge: 517
Beitrag Biolunar Moderator 19:58:56 21.08.2017   Titel:              Zitieren

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 ;))
C:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#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


Zuletzt bearbeitet von Biolunar am 09:55:10 22.08.2017, insgesamt 1-mal bearbeitet
C++ Forum :: Linux/Unix ::  [Linux] Vorhandene Datei verändern (atomisch!)   Auf Beitrag antworten

Zeige alle Beiträge auf einer Seite




Nächstes Thema anzeigen
Vorheriges Thema anzeigen
Sie können Beiträge in dieses Forum schreiben.
Sie können auf Beiträge in diesem Forum antworten.
Sie können Ihre Beiträge in diesem Forum nicht bearbeiten.
Sie können Ihre Beiträge in diesem Forum nicht löschen.
Sie können an Umfragen in diesem Forum nicht mitmachen.

Powered by phpBB © 2001, 2002 phpBB Group :: FI Theme

c++.net ist Teilnehmer des Partnerprogramms von Amazon Europe S.à.r.l. und Partner des Werbeprogramms, das zur Bereitstellung eines Mediums für Websites konzipiert wurde, mittels dessen durch die Platzierung von Werbeanzeigen und Links zu amazon.de Werbekostenerstattung verdient werden kann.

Die Vervielfältigung der auf den Seiten www.c-plusplus.de, www.c-plusplus.info und www.c-plusplus.net enthaltenen Informationen ohne eine schriftliche Genehmigung des Seitenbetreibers ist untersagt (vgl. §4 Urheberrechtsgesetz). Die Nutzung und Änderung der vorgestellten Strukturen und Verfahren in privaten und kommerziellen Softwareanwendungen ist ausdrücklich erlaubt, soweit keine Rechte Dritter verletzt werden. Der Seitenbetreiber übernimmt keine Gewähr für die Funktion einzelner Beiträge oder Programmfragmente, insbesondere übernimmt er keine Haftung für eventuelle aus dem Gebrauch entstehenden Folgeschäden.