Diese Folge beschäftigt sich mit Internas zur Implementation von fork und dem Zwilling clone, die linuxspezifische Variante zur Realisation von Threads. Dies war schon des öfteren Thema dieser Zeitschrift, heute sollen lediglich die Grundlagen der linuxspezifischen Machart besprochen werden. Leider sind zur Darstellung der Sachverhalte Studien der Kernelquellen unumgänglich, ich werde mich auf das notwendige Maß beschränken. Vielleicht können diese Anmerkungen jedoch auch noch ein klein wenig die Realisation von Systemcalls erhellen.
Fork ist der Systemcall, der einen Prozeß dupliziert, an sich
eine aufwendige Angelegenheit, da umfangreiche Kopierarbeiten erforderlich
sind. Fork wird eingeleitet aus dem User-Level über die Bibliotheksfunktion
fork(), die durch das Makro _syscall0 realisiert ist:
| Listing 1 |
#include <linux/unistd.h> #include <asm/ptrace.h> _syscall0(int,fork); |
Dieses parametrisierte Makro erwartet einen Datentyp (int) und die Nummer des Systemcalls (fork ist in der Datei asm/unistd.h mit #define __NR_fork 2 definiert) und führt durch die Übersetzung des Preprozessors zu
| Listing 2 |
int fork (void)
{
long __res;
__asm__ volatile ("int $0x80" : "=a" (__res) : "0" (2 ));
if (__res >= 0)
return ( int ) __res;
errno = -__res;
return -1;
}
|
Die Zeile __asm__ verwendet inline-Assembler, der durch den Preprozessor zu dem Op-Code int 0x80 mit dem Funtionswert 2 in eax generiert wird, die intelspezifische Variante des Software-Interrupts, der durch einen Systemcall ausgelöst wird. Viele Systemcalls sind über die _syscallN Makros implementiert, wobei N für die Anzahl der übergebenen Argumente steht.
Dieser Source wird normal zu fork.o compiliert und kann dann in die Bibliothek gestellt werden. Aufgerufen wird über den int 0x80 das Modul entry.S, das den Funktionswert 2 als offset in einer Tabelle mit Zeigern auf Funktionen benutzt. Das führt konkret zum Aufruf des Moduls
| Listing 3 |
/usr/src/linux/arch/i386/process.c
asmlinkage int sys_fork(struct pt_regs regs)
{
return do_fork(SIGCHLD, regs.esp, ®s);
}
|
das seinerseits nach korrekter Übergabe der Parameter das Modul do_fork aus /usr/src/linux/kernel/fork.c aufruft.
| Listing 4 |
int do_fork(unsigned long clone_flags, unsigned long usp, struct pt_regs *regs)
{
int nr;
int error = -ENOMEM;
unsigned long new_stack;
struct task_struct *p;
p = (struct task_struct *) kmalloc(sizeof(*p), GFP_KERNEL); // 1.
new_stack = alloc_kernel_stack(); // 2.
nr = find_empty_process(); // 3.
*p = *current; // 4.
.
.
|
Jetzt sind wir beim eigentlichen fork gelandet, den wir uns etwas verkürzt näher betrachten.
Jeder Prozeß erhält eine Variable vom Typ struct task_struct, die alle relevanten Informationen des Prozesses aufnimmt, diese wird mit 1. erzeugt, mit 2. erhält der Prozeß einen Kernelstack, mit 3. einen freien Slot in der Task-Tabelle und mit 4. wird über die Wertzuweisung die Identität hergestellt, denn current ist der Zeiger auf den aktuellen Prozeß, der fork aufgerufen hat. Nach einer Reihe von Initialisierungsschritten geht es weiter mit:
| Listing 5 - setzt Listing 4 fort |
if (copy_files(clone_flags, p)) goto bad_fork_cleanup; if (copy_fs(clone_flags, p)) goto bad_fork_cleanup_files; if (copy_sighand(clone_flags, p)) goto bad_fork_cleanup_fs; if (copy_mm(clone_flags, p)) goto bad_fork_cleanup_sighand; copy_thread(nr, clone_flags, usp, p, regs); . . } |
Die copy_ - Funktionen sind für die Kopierarbeiten zuständig, übergeben werden die clone_flags und der Zeiger auf die struct task_struct. An clone_flags stehen zur Vefügung:
| Listing 6 |
#define CLONE_VM 0x00000100 /* gemeinsamer Speicher */ #define CLONE_FS 0x00000200 /* gemeinsames Dateiensystem */ #define CLONE_FILES 0x00000400 /* gemeinsame Dateien */ |
Übergeben wurde lediglich SIGCHLD, das übliche Signal (wird später behandelt), das beim Ende eines Kindsprozesses an den Elternprozeß abgegeben wird, die Cloneflags entfallen damit, da fork nichts "cloned".
Um die Kopierarbeiten auf das erforderliche Maß zu beschränken, verwendet der Kernel beim fork eine copy_on_write Technik, das bedeutet, daß Original und Kopie sich zunächst alle relevanten Seiten teilen, erst bei einem schreibenden Zugriff werden neue Seiten alloziert, was an der Eigenständigkeit des neuen Prozesses nichts ändert.
Unix (und damit auch Linux) ist ein prozeßorientiertes Betriebssystem, Prozesse werden als tragende Einheiten aller Aktivitäten interpretiert, die um die Ressource Prozessor konkurrieren. Mit dem Aufkommen gekoppelter Systeme gibt es die Ressource Prozessor mehrfach, es besteht damit die Chance zur Parallelisierung von Aufgaben, die nicht sequentiell abgearbeitet werden müssen. Für jeden Handlungsstrang jedoch einen Prozeß via fork zu kreieren, impliziert einen gewaltigen overhead, der die Vorteile der Parallelisierung konterkarieren würde.
Der nächste logische Schritt ist daher die Implementierung von Threads, die mit dem Systemcall clone, der leider in keiner mir bekannten Standardbibliothek existiert, realisiert werden. Der Systemcall clone benutzt in der Zeigertabelle aus entry.S den Offset 120, der zu
| Listing 7 |
asmlinkage int sys_clone(struct pt_regs regs)
{
unsigned long clone_flags;
unsigned long newsp;
clone_flags = regs.ebx;
newsp = regs.ecx;
if (!newsp)
newsp = regs.esp;
return do_fork(clone_flags, newsp, ®s);
}
|
führt. Dieses Modul ruft ebenfalls das oben ausgeführte do_fork aus, wobei vorher die clone_flags explizit übernommen werden und, falls gesetzt, ein neuer Stack für den Thread übergeben wird.
Sinnvollerweise sollte ein Thread einen eigenständigen Handlungsstrang ausführen, programmtechnisch also eine Prozedur, die als Parameter bei der Kreation des Threads übergeben wird, dies ist mit dem Systemcall in der Variante
int clone(newstack,clone_flags);
leider nicht zu erreichen, daher muß die Angelegenheit leicht modifiziert werden.
Nachfolgend nun die Implementierung des Systemcalls clone.c mit dem Prototyp
int clone(int (*fn)(void *,...),void **stack,int flags,int argc,void *data,...)
fn = Funktion, die als thread gestartet werden soll
stack = neuer Stack für die Funktion
flags = clone_flags
argc = Anzahl der Argumente für die Funktion
data = Startparameter für fn
der im vollen Umgang wiedergegeben wird. Der Assembler-Teil (wiederum inline-Assembler) wurde von L.T. geschrieben, das drumherum von mir.
| Listing 8 |
/* with much help from Linus,
thanks
Wolfgang
*/
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <linux/unistd.h>
#define CSIGNAL 0x000000ff /* signal mask to be sent at exit */
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
#define CLONE_SIGHAND 0x00000800 /* set if signal handlers shared */
int clone(int (*fn)(void *,...), void **stack,int flags,int argc,void'* data,...)
{
long retval;
void **start=&data + (argc-1); // 1.
int i;
for(i = 0; i<argc;++i) // 2.
*--stack = *start--;
__asm__ __volatile__( // 3.
"int $0x80\n\t" // 4.
"testl %0,%0\n\t" // 5.
"jne 1f\n\t" // 6.
"call *%3\n\t" // 7.
"movl %2,%0\n\t" // 8.
"int $0x80\n" // 9.
"1:\t"
:"=a" (retval)
:"0" (__NR_clone),"i" (__NR_exit),
"r" (fn),
"b" (flags),
"c" (stack));
if (retval < 0) { // 10.
errno = -retval;
retval = -1;
}
return retval;
}
|
Ich erläutere die Vorgehensweise Schritt für Schritt:
1. definiert einen Rückgabewert und einen Zeiger auf die Adresse des ersten übergebenen Parameters ( die Anzahl ist Variable und steht in argc). Parameter werden bekanntlich von rechts nach links übergeben, zur Verfügung steht die Adresse von data, man benötigt aber die Adresse des ersten übergebenen Parameters: für den Aufruf von clone ergibt sich folgendes Bild:
clone(fn,stack,flags,argc,op1,op2);
stack - alt
op2
________________
op1
im Prototpy Position von data
__________________
argc
__________________
flags
__________________
stack
__________________
fn
=============== hier
beginnt die frame der neuen Funktion
bp - alt
Für den Aufruf von fn muß sich folgende Situation ergeben:
stack - neu
op2
___________________
op1
================ hier beginnt
die frame der neuen Funktion fn
bp - alt
2. stellt die Parameter wie oben skizziert für die Funktion fn korrekt auf den Stack, die Parameter wurden als Zeiger auf void übergeben, stack ist Zeiger auf Zeiger auf void, damit können die übergebenen Parameter vom Typ her frei interpretiert und trotzdem über den Weg der Indirektion auf dem Stack abgelegt werden
3. geht in den inline-Assembler über
4. führt den clone aus
5. testet den Rückgabewert für child - parent
6. parent springt zum Label 1, das zur Fortsetzung in der Zeile if(retval < 0) führt
7. child ruft die Funktion auf
8. Funktionswert exit (2) setzen
9. exit ausführen
10. und weiter mit C
Eine kleine Anwendung soll nun die Zusammenhänge darstellen. Wir schreiben ein Programm, das einen Thread erzeugt, der eine Datei auf eine andere kopiert, übergeben werden als Parameter die Namen der Dateien, die clone_flags und ein neuer Stack:
| Listing 9: thread.C |
#include <fstream.h>
#include <fcntl.h>
char buffer[512];
int copy(char *,char *);
#define CLONE_VM 0x00000100 /* set if VM shared between processes */
#define CLONE_FS 0x00000200 /* set if fs info shared between processes */
#define CLONE_FILES 0x00000400 /* set if open files shared between processes */
int main(int argc, char **argv)
{
int in,out;
void ** new_stack = (void **) malloc(4096); //1.
new_stack = (void **) ((char *)new_stack + 4096); //2.
clone(copy,new_stack, CLONE_VM|CLONE_FS|
CLONE_FILES,2, argv[1], argv[2]); //3.
}
int copy(char * inn, char * outn )
{
int count;
int in,out;
in = open(inn,O_RDONLY);
out = open(outn,O_CREAT|O_WRONLY,0644);
while(count = read(in,buffer,512))
write(out,buffer,count);
}
|
In 1. wird ein neuer stack erzeugt, die Startadresse des Stacks wird in 2. auf die höchstwertigste Adresse gesetzt, da der Intel-Stack von den höherwertigen zu den niederwertigen Adressen wächst, die Größe 4096 ist rein willkürlich, in 3. wird der Thread erzeugt, die Funktion copy dürfte selbsterklärend sein.
Problematisch ist die Verwendung der globalen Variablen buffer in copy, da das mehrfache Erzeugen von Threads diese in allen Threads ungeschützt zur Verfügung stellt, ich werde in einer späteren Folge Möglichkeiten zeigen, diese über Semaphore zu schützen, die Bibliothek pthread erfüllt bereits alle Wünsche.
Für diese Folge sei das genug Stoff, die nächste beschäftigt sich mit Systemcalls rund um die Prozeß-ID pid.
| Infos |
[1] Linux-Kernel-Programmierung, Addison-Wesley, 1997 [2] Rochkind, Unix-Programmierung für Fortgeschrittene, Hanser, 1985 [3] Bentson,R.,Inside Linux,Seattle:SSC 1996 [4] http://www.redhat.com/linux-info/FAQ/Threads-FAQ - eine Reihe von Artikeln zur Thread-Thematik [5] http://www.rt66.com/ubrennan/djgpp/bgtia.html - eine kleine Einführung zum Thema Inline-Assembler |
| Der Autor |
|
Wolfgang Hetzler arbeitet seit 1985 als Dozent an einem privaten Institut für Fort- und Ausbildung im EDV-Bereich und als Lehrbeauftragter an der FH-Frankfurt im Bereich Ingenieur-Informatik. Linux ist ihm seit der Version 0.9xx bekannt und eine Quelle ständiger Auseinandersetzung. Zu erreichen ist er unter het@het.gg.eunet.de. |
Copyright © 1998 Linux-Magazin Verlag