Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Synchronisation

3 views
Skip to first unread message

Hilko Bengen

unread,
Aug 21, 2003, 9:32:59 AM8/21/03
to
Gegeben sei ein Prozess, der in regelmaessigen Abstaenden mehrere
Kindprozesse wegfork()t, damit diese Logdaten auf Platte schreiben.

Nachdem das Kind weggefork()t ist, merkt sich der Elternprozess dessen
PID in einer Variable und faehrt mit seiner eigentlichen Arbeit fort.
Wenn das Kind stirbt, bekommt der Elter ein SIGCHLD und kann anhand
der PID das Kind identifizieren.

Nun habe ich zwischen fork() und Merken der PID eine Race Condition,
denn es kann vorkommen, dass das Kind schon mit seiner Arbeit fertig
ist, bevor sich der Elter seine PID gemerkt hat. Also muss ich
irgendeinen Synchronisationsmechanismus implementieren, und meine
Frage ist jetzt: Wie?

Im Prinzip muss ja nur der Elter dem Kind signalisieren, dass es sich
beenden darf. Was ist dafuer am elegantesten? Mir fallen ad-hoc eine
Pipe oder irgendeine Schweinerei im Dateisystem, z.B. ein Hard link
auf das Logfile, ein.

Was ist hier zu empfehlen?

-Hilko

Patrick Schaaf

unread,
Aug 21, 2003, 9:57:36 AM8/21/03
to
Hilko Bengen <bengen+us...@hilluzination.de> writes:

>Nun habe ich zwischen fork() und Merken der PID eine Race Condition,

Niemand zwingt den Vater, zwischen fork() und dem Merken der PID
einen der wait-syscalls aufzurufen. Niemand zwingt Dich, im
Signalhandler selbst wait aufzurufen. Geht es vielleicht Anders?

Wenn nicht, tendiere ich zu der pipe, die Du schon erwaehnt hast,
und zwar in der Form: Kind schliesst Schreibseite, und liest ein
Byte von der Readseite, schliesst diese dann, bevor es weitermacht,
und ggf. stirbt. Papa schliesst die Readseite, merkt sich die PID,
schreibt auf die Writeseite ein Byte, und schliesst die dann auch.

Gruss
Patrick

Hilko Bengen

unread,
Aug 21, 2003, 10:12:10 AM8/21/03
to
mailer...@bof.de (Patrick Schaaf) writes:

> Hilko Bengen <bengen+us...@hilluzination.de> writes:
>
>>Nun habe ich zwischen fork() und Merken der PID eine Race Condition,
>
> Niemand zwingt den Vater, zwischen fork() und dem Merken der PID
> einen der wait-syscalls aufzurufen.

Schuppen von Augen. Danke. Ja, es geht anders.

Bisher hatte ich den wait im Signalhandler, was ja ohnehin bad
practice ist. Man soll die Dinger ja schlank halten.

Zu meiner Ehrenrettung muss ich sagen: Der Code war geerbt.

Noch ein Disclaimer: Nicht von SCO. ;-)

-Hilko

Patrick Schaaf

unread,
Aug 21, 2003, 10:34:37 AM8/21/03
to
Hilko Bengen <bengen+us...@hilluzination.de> writes:

>mailer...@bof.de (Patrick Schaaf) writes:

Du, zur Ehrenrettung reicht es voellig, es dann geloest zu haben.
Auch bei eigenem Code hat oefter mal Bretter vor'm Kopf, was man
lernen muss, ist, an ihnen vorbei zu kucken. :)

Freut mich, geholfen zu haben.

Gruss
Patrick

Bodo Thiesen

unread,
Aug 22, 2003, 5:09:40 AM8/22/03
to
Hilko Bengen <bengen+us...@hilluzination.de> wrote:

Wenn wir im Deutschen die richtigen Begriffe für
Verwandtschaftsverhältnisse verwenden, haben wir auch nicht so große
Probleme, ein Elternteil zu bezeichen:

>Gegeben sei ein Prozess, der in regelmaessigen Abstaenden mehrere
>Kindprozesse

Tochterprozesse

>wegfork()t, damit diese Logdaten auf Platte schreiben.

Schonmal an Nonblocking IO gedacht?

>Nachdem das Kind

Nachdem die Tochter

>weggefork()t ist, merkt sich der Elternprozess

Mutterprozess

>dessen

ihre

>PID in einer Variable und faehrt mit seiner eigentlichen Arbeit fort.
>Wenn das Kind

die Tochter

>stirbt, bekommt der Elter

Und das meinte ich in der Einleitung. Mein Duden kennt das Wort Elter
nicht. Kein Wunder, denn Du meinst ja auch "die Mutter".

>ein SIGCHLD und kann anhand
>der PID das Kind

die Tochter

>identifizieren.
>
>Nun habe ich zwischen fork() und Merken der PID eine Race Condition,
>denn es kann vorkommen, dass das Kind

die Tochter

>schon mit seiner

ihrer

>Arbeit fertig
>ist, bevor sich der Elter seine

die Mutter ihre

>PID gemerkt hat. Also muss ich
>irgendeinen Synchronisationsmechanismus implementieren, und meine
>Frage ist jetzt: Wie?

Indem sich der Tochterprozess selbst mit dem Signal SIGSTOP anhält wohl
kaum, denn das verlagert das Race nur auf die Frage "Na hat sie sich schon
angehalten, oder wird sie es noch tun...".

>Im Prinzip muss ja nur der Elter dem Kind

die Mutter der Tochter

>signalisieren, dass es

sie

>sich
>beenden darf.

Eventuell nicht fork()en sondern einen thread anlegen? (Da gibt es dann
die conds und mutexe usw.)

>Was ist dafuer am elegantesten?

Nonblocking IO, oder davon ausgehen, daß die Log-Zeile schnell genug
weggeschrieben wird. Ich kenne Deinen Hintergrund ja nicht.

>Mir fallen ad-hoc eine
>Pipe

könnte man machen, ja ...

>oder irgendeine Schweinerei im Dateisystem, z.B. ein Hard link
>auf das Logfile, ein.

Naja, Overhead ^ 10.

>Was ist hier zu empfehlen?

Da du Das Problem inzwischen ja gelöst hast, brauchst Du
glücklicherweise keine Tipps mehr. Viel glück noch.

Gruß, Bodo

PS: fup2 desd, da es mir mehr um die Sprache Deutsch, als ums eigentliche
Problem ging. x-post habe ich mir mal gespart, also bitte mit
sinnvollem Subject posten, und das alte nicht vergessen :)
--
MS Outlook Express?->[DE: http://piology.org/ILOVEYOU-Signature-FAQ.html]

@@@@@ GEGEN TCG aka. TCPA: @@@@@ [DE: http://www.againsttcpa.com]
Probleme mit Spam? [EN: http://www.spamhaus.org/globalremove.html]

Bastian Blank

unread,
Aug 22, 2003, 8:38:52 AM8/22/03
to
Hilko Bengen <bengen+us...@hilluzination.de> wrote:
> Nachdem das Kind weggefork()t ist, merkt sich der Elternprozess dessen
> PID in einer Variable und faehrt mit seiner eigentlichen Arbeit fort.
> Wenn das Kind stirbt, bekommt der Elter ein SIGCHLD und kann anhand
> der PID das Kind identifizieren.

Es merkt sie sich direkt aus der Rueckgabe von fork in einer Variable
des Types pid_t. Es ist sichergestellt, das diese Variable richtig
geschrieben ist, bevor einer der beiden Prozesse weiterlaeuft.

Bastian

Stefan Reuther

unread,
Aug 22, 2003, 8:09:39 AM8/22/03
to
Hallo,

Hilko Bengen <bengen+us...@hilluzination.de> wrote:
> Nun habe ich zwischen fork() und Merken der PID eine Race Condition,
> denn es kann vorkommen, dass das Kind schon mit seiner Arbeit fertig
> ist, bevor sich der Elter seine PID gemerkt hat. Also muss ich
> irgendeinen Synchronisationsmechanismus implementieren, und meine
> Frage ist jetzt: Wie?

Da gibt's eine Menge praktischer Funktionen, die mit 'sig'
anfangen. Zum Beispiel so:
sigset_t sig;
pid_t pid;

sigemptyset(&sig);
sigaddset(&sig, SIGCHLD);
sigprocmask(SIG_BLOCK, &sig, 0);
pid = fork();
sigprocmask(SIG_UNBLOCK, &sig, 0);
// pid auswerten...

Stefan

Patrick Schaaf

unread,
Aug 22, 2003, 8:18:08 AM8/22/03
to
Bastian Blank <bast...@gmx.de> writes:

Das ist falsch, alleine schon deshalb, weil eine Variable im Vaterprozess
NUR vom Vaterprozess selbst geschrieben werden kann, und der MUSS dazu
logischerweise schon am Weiterlaufen sein. Das Kind kann in der originalen
Situation von Hilko zu dem Zeitpunkt schon laengst tot und beerdigt sein.

Gruss
Patrick

Hilko Bengen

unread,
Aug 22, 2003, 8:55:12 AM8/22/03
to
Bastian Blank <bast...@gmx.de> writes:

> Hilko Bengen <bengen+us...@hilluzination.de> wrote:
>> Nachdem das Kind weggefork()t ist, merkt sich der Elternprozess dessen
>> PID in einer Variable und faehrt mit seiner eigentlichen Arbeit fort.
>> Wenn das Kind stirbt, bekommt der Elter ein SIGCHLD und kann anhand
>> der PID das Kind identifizieren.
>
> Es merkt sie sich direkt aus der Rueckgabe von fork in einer Variable
> des Types pid_t.

Ja, das war auch bisher so:

while( (writepid = fork()) < 0) sleep(1);

> Es ist sichergestellt, das diese Variable richtig geschrieben ist,
> bevor einer der beiden Prozesse weiterlaeuft.

Ich habe beobachtet, dass dies offensichtlich nicht so ist: Kind wird
gefork()t, writepid=x. Signalhandler wird gerufen, ruft wait() auf,
bekommt x, guckt in writepid nach, stellt fest, dass die nicht mit dem
Rueckgabewert uebereinstimmt, schreit. Dass zweimal geforkt wurde,
bevor der Signalhandler gerufen wurde, ist ausgeschlossen.

Bist Du sicher, dass die Rueckkehr aus fork() mit dem Speichern des
Rueckgabewertes eine atomare Operation ist? Wenn ja, warum?

-Hilko

Hilko Bengen

unread,
Aug 22, 2003, 9:08:20 AM8/22/03
to
F'up ignoriert, weil ich momentan fuer Diskussion ueber die deutsche
Sprache weder viel Zeit noch daran gesteigertes Interesse habe. Ein
paar technische Kommentare gibt es denn doch.

Bodo Thiesen <bot...@gmx.de> writes:

> Hilko Bengen <bengen+us...@hilluzination.de> wrote:
>
> Wenn wir im Deutschen die richtigen Begriffe für
> Verwandtschaftsverhältnisse verwenden, haben wir auch nicht so große
> Probleme, ein Elternteil zu bezeichen:

Jaja, ist recht.

> Schonmal an Nonblocking IO gedacht?

Ich rechne damit, dass in festen Abstaenden viel geschrieben werden
muss, daher wuerde mir die Komplexitaet zu hoch werden.

[...]


> Und das meinte ich in der Einleitung. Mein Duden kennt das Wort Elter
> nicht. Kein Wunder, denn Du meinst ja auch "die Mutter".

Komisch. Jeder andere hat es verstanden. Und ich erinnere mich genau
daran, dass es diesen Begriff im Biounterricht in der Vererbungslehre
durchaus gab. So, genug davon.
[...]

> Indem sich der Tochterprozess selbst mit dem Signal SIGSTOP anhält
> wohl kaum, denn das verlagert das Race nur auf die Frage "Na hat sie
> sich schon angehalten, oder wird sie es noch tun...".

Ja, das war mir auch klar.

> Eventuell nicht fork()en sondern einen thread anlegen? (Da gibt es
> dann die conds und mutexe usw.)

Neenee, ich habe keine gesteigerte Lust, mich mit den Untiefen von
Threads zu beschaeftigen. Ausserdem muesste ich dann meine gesamten
Datenstrukturen umbauen bzw. deren Verwaltung synchronisieren.

>>Was ist dafuer am elegantesten?
>
> Nonblocking IO, oder davon ausgehen, daß die Log-Zeile schnell genug
> weggeschrieben wird. Ich kenne Deinen Hintergrund ja nicht.

Der Hintergrund ist der: Parent sammelt IP-Accountingdaten und
aggregiert die in Records, die per (achtung! Verwandtschaft zu anderem
Thread!) Hashtable und verketteten Listen verwaltet werden. In
regelmaessigen Abstaenden werden Log-Prozesse gefork()t, die die
Records in ein Log schreiben.

Dadurch, dass ich getrennte Prozesse verwende, spare ich mir das
explizite Umkopieren der Records. Ich werfe "einfach" alles, was ich
im Child wegschreibe, im Parent nach dem fork() mit free() weg.

-Hilko

Patrick Schaaf

unread,
Aug 22, 2003, 10:31:02 AM8/22/03
to
Hilko Bengen <bengen+us...@hilluzination.de> writes:

>Dadurch, dass ich getrennte Prozesse verwende, spare ich mir das
>explizite Umkopieren der Records. Ich werfe "einfach" alles, was ich
>im Child wegschreibe, im Parent nach dem fork() mit free() weg.

Mit dem wegwerfen solltest Du vielleicht warten, bis das Kind signalisiert,
dass es mit Schreiben fertig ist (zB. durch seinen Tod)

Warum? Nun, fork kopiert nur die pagetables und setzt in Vater wie Kind
die entsprechenden Pages auf "readonly / copy-on-write". Uebliche malloc-
Implementationen schreiben bei free() aber in/um die freigegebenen Speicher-
Bereiche, so dass ohne die empfohlene Synchronisation ein Freigeben im Vater
waehrend des Laufs des Kindes, ziemlich sicher zu einem kompletten duplizieren
der Pages fuehren wird.

Du kannst Dir den Effekt zur Laufzeit durch gelegentliche Aufrufe von
getrusage() selbst vorfuehren: achte auf die minflt-Zaehler.

Gruss
Patrick

Hilko Bengen

unread,
Aug 23, 2003, 8:32:53 AM8/23/03
to
mailer...@bof.de (Patrick Schaaf) writes:

> Mit dem wegwerfen solltest Du vielleicht warten, bis das Kind
> signalisiert, dass es mit Schreiben fertig ist (zB. durch seinen
> Tod)
>
> Warum? Nun, fork kopiert nur die pagetables und setzt in Vater wie
> Kind die entsprechenden Pages auf "readonly / copy-on-write".
> Uebliche malloc- Implementationen schreiben bei free() aber in/um
> die freigegebenen Speicher- Bereiche, so dass ohne die empfohlene
> Synchronisation ein Freigeben im Vater waehrend des Laufs des
> Kindes, ziemlich sicher zu einem kompletten duplizieren der Pages
> fuehren wird.

Danke, guter Punkt. Das hatte ich gar nicht bedacht. Mal sehen,
vielleicht läßt sich da noch mehr optimieren.

Ich lerne diese Gruppe gerade sehr zu schätzen!

Dsa Programm, um das es geht, heißt übrigens ulog-acctd, stammt von
net-acct ab, steht wie dieses unter der GPL und ist unter
<http://savannah.nongnu.org/projects/ulog-acctd/> zu finden.

Gruß,
-Hilko

Bodo Thiesen

unread,
Aug 25, 2003, 5:10:11 AM8/25/03
to
Hilko Bengen <bengen+us...@hilluzination.de> wrote:

>> Eventuell nicht fork()en sondern einen thread anlegen? (Da gibt es
>> dann die conds und mutexe usw.)
>
>Neenee, ich habe keine gesteigerte Lust, mich mit den Untiefen von
>Threads zu beschaeftigen. Ausserdem muesste ich dann meine gesamten
>Datenstrukturen umbauen bzw. deren Verwaltung synchronisieren.
>

>> Nonblocking IO, oder davon ausgehen, daß die Log-Zeile schnell genug
>> weggeschrieben wird. Ich kenne Deinen Hintergrund ja nicht.
>
>Der Hintergrund ist der: Parent sammelt IP-Accountingdaten und
>aggregiert die in Records, die per (achtung! Verwandtschaft zu anderem
>Thread!) Hashtable und verketteten Listen verwaltet werden. In
>regelmaessigen Abstaenden werden Log-Prozesse gefork()t, die die
>Records in ein Log schreiben.
>
>Dadurch, dass ich getrennte Prozesse verwende, spare ich mir das
>explizite Umkopieren der Records. Ich werfe "einfach" alles, was ich
>im Child wegschreibe, im Parent nach dem fork() mit free() weg.

Wenn Du mit Threas arbeiten würdest, könntest Du den Tochter-Thread
einfach arbeiten lassen. Dieser geht dann bei, und free()t auch die
Speicherbereiche, sobald er sie weggeloggt hat, und der Mutterthread muß
sich um mehr oder weniger nichts kümmern. Weiterhin könntest Du sogar noch
bei gehen, und den Tochter-Thread permanent laufen lassen (in einem cond
wartend), und sobald eine neue Log-Zeile da ist -> Thread aufwecken. Und
falls Du nur alle X Zeilen Loggen möchstest (um Kontextwechsel zu sparen
z.B.), dann machst Du das halt...

Oder du bleibst halt beim fork() mit den sig* funktionen und dem free()
nach SIGCHLD. BTW: Warum testest Du im Sighandler überhaupt die Pid? Wenn
Du eh zu jedem Zeitpunkt nur einen Prozess laufen hast, könntest Du Dir
das auch sparen, denn dann kann das Signal ja nur von diesem einen her
kommen.

Gruß, Bodo

Bodo Thiesen

unread,
Aug 25, 2003, 5:05:00 AM8/25/03
to
mailer...@bof.de (Patrick Schaaf) wrote:

Mit anderen worten, der Prototyp von fork() müsste so aussehen:

int fork(pid_t * pid);

und dann würde der Rückgabewert (0 oder -1) nur über den Erfolg berichten,
und in pid wird 0 bzw. die Pid der Tochter geschrieben. Dann wäre der
Aufrufer dafür Verantwortlich, sicherzustellen, daß die Variablen pid
korrekt gesetzt sind, bevor einer der beiden Prozesse weiter läuft (darum
müsste sich dann im schlimmsten Fall die Library kümmern, besser jedoch,
wenn es das OS selber kann...)

Hilko Bengen

unread,
Aug 25, 2003, 7:24:05 PM8/25/03
to
Bodo Thiesen <bot...@gmx.de> writes:

> Hilko Bengen <bengen+us...@hilluzination.de> wrote:
>
>>Dadurch, dass ich getrennte Prozesse verwende, spare ich mir das
>>explizite Umkopieren der Records. Ich werfe "einfach" alles, was ich
>>im Child wegschreibe, im Parent nach dem fork() mit free() weg.
>
> Wenn Du mit Threas arbeiten würdest, könntest Du den Tochter-Thread
> einfach arbeiten lassen. Dieser geht dann bei, und free()t auch die
> Speicherbereiche, sobald er sie weggeloggt hat, und der Mutterthread
> muß sich um mehr oder weniger nichts kümmern.

Dann habe ich aber genau dort ein Synchronisationsproblem, denn
während das Kind schreibt, sammelt die Mutter weiterhin Daten und
schreibt sie in die Datenstrukturen. Kein Vorteil durch Threads.

[...]

> Oder du bleibst halt beim fork() mit den sig* funktionen und dem
> free() nach SIGCHLD. BTW: Warum testest Du im Sighandler überhaupt
> die Pid?

Das habe ich abgestellt. Im Sighandler wird nunmehr nur ein Flag
gesetzt.

> Wenn Du eh zu jedem Zeitpunkt nur einen Prozess laufen hast,

Habe ich nicht. Das Loggen der Accountingdaten läuft parallel zum
Sammeln neuer Daten. Außerdem gibt es noch ein Dumpfile.

> könntest Du Dir das auch sparen, denn dann kann das Signal ja nur
> von diesem einen her kommen.

Paranoia, defensives Programmieren. :-)

-Hilko

Bodo Thiesen

unread,
Sep 2, 2003, 1:10:40 PM9/2/03
to
Hilko Bengen <bengen+us...@hilluzination.de> wrote:

> Bodo Thiesen <bot...@gmx.de> writes:
>
>> Hilko Bengen <bengen+us...@hilluzination.de> wrote:
>>
>>> Dadurch, dass ich getrennte Prozesse verwende, spare ich mir das
>>> explizite Umkopieren der Records. Ich werfe "einfach" alles, was ich
>>> im Child wegschreibe, im Parent nach dem fork() mit free() weg.
>>
>> Wenn Du mit Threas arbeiten würdest, könntest Du den Tochter-Thread
>> einfach arbeiten lassen. Dieser geht dann bei, und free()t auch die
>> Speicherbereiche, sobald er sie weggeloggt hat, und der Mutterthread
>> muß sich um mehr oder weniger nichts kümmern.
>
>Dann habe ich aber genau dort ein Synchronisationsproblem, denn
>während das Kind schreibt, sammelt die Mutter weiterhin Daten und
>schreibt sie in die Datenstrukturen. Kein Vorteil durch Threads.

Ich weiß ja nicht, wie Du das realisiert hast, ich würde das jetzt
ungefähr folgendermaßen machen:

struct msg_node {
char * msg;
struct msg_node * next;
};

volatile struct msg_node * first=NULL;
volatile struct msg_node * last=NULL;
MUTEX msg_node=MUTEX_UNLOCKED;

Neue log-Botschaft:

int add_log_line(char * string) {
struct msg_node * msg=malloc(sizeof(struct msg_node));
if (!msg) return -1;
msg->msg=string; // Eventuell strdup
msg->next=NULL;
if (last) {
LOCK(msg_node);
last->next=msg;
last=msg;
UNLOCK(msg_node);
} else {
last=msg;
first=msg;
}
return 0;
}

Und der Thread macht den hier:

void log_line_dispatcher() {
struct msg_node * msg;
for (;;) {
while (!first) { sleep(1); }
fprintf(logfile,first->msg);
free((msg=first)->msg);
if (first==last) {
LOCK(msg_node);
first=last=NULL;
UNLOCK(msg_node);
} else {
first=first->next;
}
free(msg);
}
}

Der Haupt-thread würde auf diese Weise nur dann angehalten, wenn der
Log-Thread gerade dabei ist, die Zeiger first und last auf NULL zu setzen,
was nur dann geschieht, wenn gerade die letzte Log-Zeile geschrieben
wurde. Da der Log-Thread aber nach dem Blocking-IO erst gerade die CPU
wiederbekommen hat, dürfte an dieser Stelle normalerweise überhaupt kein
Context Switch stattfinden, so daß es praktisch nie zu einer Blockierung
des Haupt-Threads kommt. Und solange noch einige Log-Messages da sind,
wird es auch nicht zu einer Blockierung des Haupt-Threads kommen, dann
solange wird der Log-Thread überhaupt kein Mutex brauchen.

Ansonsten könnte ich mir noch vorstellen, zwei Listen anzulegen, und es
dem Haupt-Thread zu überlassen, die Listen zu wechseln. Etwa so:

struct msg_node {
char * msg;
struct msg_node * next;
};

struct msg_list {
struct msg_node * first=NULL;
struct msg_node * last=NULL;
};

volatile struct msg_list liste[2]={{NULL,NULL},{NULL,NULL}};
volatile struct msg_list * ilist=liste;
volatile struct msg_list * olist=NULL;

Neue log-Botschaft:

int add_log_line(char * string) {
struct msg_node * msg=malloc(sizeof(struct msg_node));
if (!msg) return -1;
msg->msg=string; // Eventuell strdup
msg->next=NULL;
if (!olist) {
olist=ilist;
if (ilist==liste) {
ilist=liste+1;
} else {
ilist=liste;
}
}
if (!ilist->first) {
ilist->first=ilist->last=msg;
} else {
ilist->last->next=msg;
ilist->last=msg;
}
return 0;
}

Und dann macht der Log-Thread den hier:

void log_line_dispatcher() {
struct msg_node * msg;
for (;;) {
while (!olist) { sleep(1); }
fprintf(logfile,olist->first->msg);
free(olist->first->msg);
// Das nur, wenn die Stings, die an add_log_line() übergeben
// werden, via free() freigegeben werden müssen.
msg=olist->first;
olist->first=msg->next;
free(msg);
if (!olist->first) {
olist->last=NULL;
olist=NULL;
}
}
}

Der Log-Thread arbeitet ausschließlich mit olist, und der Hauptthread
füllt ilist, solange bis olist=NULL ist, tauscht diese dann, und macht
normal weiter. Sollte Race-Frei sein, und braucht überhaupt keine weiteren
Aktionen hinsichtlich Mutexe o.ä.

Note: Alles ungetestet, und ich behalte mir das Recht vor, beliebig viele
Fehler in meine Quellen einzubauen.

0 new messages