Systemprogrammierung - Teil 4

fork und clone

von Wolfgang Hetzler


Systemprogrammierung gilt als schwierig, da ihre Inhalte dem Benutzer sehr oft abstrakt gegenüber treten. Daß diese Abstraktheit nicht sein muß und die Systemcalls eine wohldefinierte Schnittstelle zum Kernel darstellen, will diese Serie zeigen.

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, &regs);  
 }   

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, &regs);  
} 

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