PHP/Tutorials/Einführung in die Interna/Threads, globale Variablen und Speicherverwaltung

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Der Thread safe resource manager

Der TSRM ist ein Teil von PHP, der sich darum kümmert, dass man globale Resourcen für jeden Thread separat speichern kann. Er abstrahiert von verschiedenen Thread-Implementierungen in unterschiedlichen Betriebssystemen und stellt einige nützliche Funktionen und Makros zur Verfügung, die man verwenden kann.

Makros zur Übergabe und Abfrage des aktuellen Zustands

In vielen Funktionsdeklarationen und Funktionsdeklarationen tauchen immer wieder folgende Makros auf: TSRMLS_D, TSRMLS_C, TSRMLS_DC und TSRMLS_CC. Dies hat den folgenden Hintergrund: Bestimmte Informationen zum aktuellen Thread müssen bei Funktionsaufrufen immer durchgereicht werden, damit diese wenn sie benötigt werden verfügbar sind. Dafür gibt es diese Makros, die genau dies bewerkstelligen. Da es aber auch möglich ist, PHP so zu kompilieren, dass keine Threads verwendet werden, ist es nicht immer nötig, dieser Parameter mitzuschleppen. Daher sind diese Makros definiert worden, die das kapseln:

Beispiel
 #define TSRMLS_D    void ***tsrm_ls
 #define TSRMLS_DC   , TSRMLS_D
 #define TSRMLS_C    tsrm_ls
 #define TSRMLS_CC   , TSRMLS_C
 // wenn keine Threads verwendet werden:
 #define TSRMLS_D    void
 #define TSRMLS_DC
 #define TSRMLS_C
 #define TSRMLS_CC

Funktionen, die sonst keinen Parameter hätten, definiert man nun mit TSRMLS_D in den Klammern, Funktionen mit Parametern mit TSRMLS_DC am Ende der Parameterliste. Beim Aufruf verwendet man TSRMLS_C für Funktionen ohne sonstige Parameter und TSRMLS_CC für Funktionen mit anderen Parametern. Wichtig ist, dass hierbei kein Komma zur Trennung verwendet wird! Beispiel:

Beispiel
 int funktion_ohne_parameter ('''TSRMLS_D''') {
   return 42;
 }
 
 int funktion_mit_parametern (int a, int b '''TSRMLS_DC''') {
   return a + b;
 }
 
 // Aufruf einer Funktion ohne sonstige Parameter
 andere_funktion_1 ('''TSRMLS_C''');
 // Aufruf einer Funktion mit anderen Parametern
 andere_funktion_1 (9, 6, 42 '''TSRMLS_CC''');
Beachten Sie: Einige Zend- und PHP-Header deklarieren bestimmte Makros für bestimmte Funktionsaufrufe, z. B. object_init. In dem Fall darf man TSRMLS_CC nicht selbst übergeben sondern muss es weglassen, da das Makro object_init die eigentliche Funktion _object_init mit diesem Makro selbst aufruft. Dies hat historische Gründe. Man sollte jedoch immer darauf achten, ob die Funktion, die man gerade aufrufen will, diese Angabe benötigt oder nicht!

Um sicherzustellen, dass man alles richtig hinzugefügt hat (in Nicht-Thread-Umgebungen expandieren die Makros zu nichts), kann man configure den Switch --enable-maintainer-zts übergeben, um sicherzustellen, dass diese Makros immer expandieren, auch wenn keine Threads vorhanden sind.

Es ist jedoch nicht immer möglich, diese Parameter durchzuschleifen. Zum einen, wenn man zum Beispiel eine externe Bibliothek verwendet, der man eine Callback-Funktion übergeben will. In diesem Fall ist man an bestimmte Prototypen für die Callback-Funktion gebunden. Aber auch innerhalb von PHP gibt es aus historischen Gründen derartige Fälle. Für diesen Fall gibt es ein Makro namens TSRMLS_FETCH(), das man am Anfang der Funktion aber am Ende der Variablendeklaration angeben muss. Dies sorgt dafür, dass der Parameter geholt wird – dies geschieht intern, indem der TSRM die Thread-Id nutzt und dann in einer globalen Liste nachsieht, welcher Parameter für diese Thread-Id notwendig ist.

Folgendes Beispiel zeigt die Verwendung von TSRMLS_FETCH() in einer Funktion, die diese Parameter nicht in der Deklaration trägt:

Beispiel
 static int tu_was (zval *wert) {
   int temp;
   '''TSRMLS_FETCH();'''
   
   // tu was
 }
Beachten Sie: Der Aufruf von TSRMLS_FETCH() kostet Performance! Daher sollte er nur dann verwendet werden, wenn es nicht anders möglich ist!

Funktion zum Abruf von der aktuellen Thread-Id

Der TSRM definiert ferner eine Funktion tsrm_thread_id, die die aktuelle Thread-Id zurückgibt. Diese steht allerdings nur zur Verfügung, wenn Thread-Support einkompiliert wurde, in dem Fall ist ein Makro namens ZTS definiert. Sie hat folgenden Prototypen:

TSRM_API THREAD_T tsrm_thread_id(void);
Beachten Sie, dass der Datentyp THREAD_T betriebssystemspezifisch definiert ist und daher nicht zwangsweise eine Id sein muss – manchmal kann er auch ein Zeiger auf eine Datenstruktur sein.

Funktion zur Behandlung von Mutexes

Manchmal ist es nötig, Mutexes zu benutzen, um globale Ressourcen zwischen Threads zu sperren. Der TSRM bietet hierfür auch Hilfsfunktionen an, die aber ebenfalls nur zur Verfügung stehen, wenn das Makro ZTS definiert ist.

Die Funktionen haben folgende Prototypen:

TSRM_API MUTEX_T tsrm_mutex_alloc(void);
TSRM_API void tsrm_mutex_free(MUTEX_T mutexp);
TSRM_API int tsrm_mutex_lock(MUTEX_T mutexp);
TSRM_API int tsrm_mutex_unlock(MUTEX_T mutexp);

MUTEX_T ist hierbei wieder ein betriebssystemabhängiger Datentyp. Ein Mutex ist eine Sperre zwischen Threads. Am Anfang, bevor weitere Threads erzeugt wurden, wird die Mutex angelegt per tsrm_mutex_alloc. Wenn ein Thread auf bestimmte Ressourcen zugreifen will ohne von anderen Threads gestört zu werden signalisiert er das per tsrm_mutex_lock, wenn er fertig ist entfernt er die Sperre per tsrm_mutex_unlock. Am Ende der Ausführung wird die Mutex per tsrm_mutex_free wieder freigegeben.

Globale Variablen

Oftmals sind globale Variablen sehr nützlich, um Dinge für spätere Aufrufe der gleichen Funktion zwischenzuspeichern. Allerdings haben sie das große Problem, dass globale Variablen bei Threads Probleme machen. Es gibt zwar das Konzept eines sogenannten Thread Local Storage (TLS), das von verschiedenen Betriebssystemen und Compilern direkt unterstützt wird, allerdings ist die Portabilität im Moment noch problematisch und es gibt einige Einschränkungen in Bezug auf die Größe der Datenmenge, die hierfür zulässig ist.

Daher verwendet PHP ein eigenes Konzept für globale Variablen. Im einfachsten Fall, wenn PHP ohne Threads kompiliert wurde, wird direkt auf eine globale Datenstruktur zugegriffen. Ansonsten wird über den TSRM die Datenstruktur herausgesucht, die für den aktuellen Thread relevant ist.

Um globale Variablen zu verwenden benötigt es zwei Teile: Zum einen eine Deklaration einer Datenstruktur, die alle globalen Variablen enthält, die man nutzen will. Dies geschieht dann über die Makros ZEND_BEGIN_MODULE_GLOBALS und ZEND_END_MODULE_GLOBALS (definiert in zend_API.h):

ZEND_BEGIN_MODULE_GLOBALS(modulname)
    long  global_value;
ZEND_END_MODULE_GLOBALS(modulname)

Dies wird übersetzt in folgenden Datentyp:

typedef struct _zend_modulname_globals {
    long  globale_variable;
} zend_modulname_globals;

Ferner wird in der Regel noch ein Makro angelegt, das den Zugriff auf die globalen Variablen des Moduls vereinfacht:

#ifdef ZTS
# define MODULG(v) TSRMG(modulname_globals_id, zend_modulname_globals *, v)
#else
# define MODULG(v) (modulname_globals.v)
#endif

Damit kann dann auf die globalen Variablen so zugegriffen werden:

// vorher:
globale_variable = 42;
// nachher:
MODULG(globale_variable) = 42;

Dies berücksichtigt automatisch Threads, ohne dass man sich als Programmierer zu sehr darum kümmern muss.

Damit die globalen Variablen aber tatsächlich definiert werden, muss man noch ein bisschen Code hinzufügen. In einer C-Datei muss die Struktur nämlich noch deklariert werden:

ZEND_DECLARE_MODULE_GLOBALS(modulname)

Hierbei ist zu beachten, dass kein Semikolon nach der Klammer folgt. Damit ist es jedoch noch nicht getan: Die globalen Variablen müssen nämlich noch irgendwo initialisiert werden. Dies geschieht über den Makro-Aufruf:

ZEND_INIT_MODULE_GLOBALS(modulname, php_modulname_init_globals, php_modulname_destroy_globals); 

Hier wird die Funktion php_modulname_init_globals immer dann aufgerufen, wenn die globalen Variablen initialisiert werden müssen und die Funktion php_modulname_destroy_globals, wenn sie deinitialisiert werden müssen. Folgendes Beispiel zeigt, wie derartige Funktionen aussehen könnten:

Beispiel
 static void php_modulname_init_globals(zend_modulname_globals *modulname_globals TSRMLS_DC) {
     modulname_globals->globale_variable = 0; // default-wert
 }
 static void php_modulname_destroy_globals(zend_modulname_globals *modulname_globals TSRMLS_DC) {
   // hier könnte Ressourcen freigegeben werden, falls welche in den globalen
   // Variablen alloziert wurden
 }

Man kann für die Deinitialisierungsfunktion bei ZEND_INIT_MODULE_GLOBALS jedoch auch NULL angeben, wenn man es nicht braucht.

Wenn nun die globalen Variablen auch in anderen C-Dateien verwendet werden müssen, sollte man in seiner Header-Datei über das Makro ZEND_EXTERN_MODULE_GLOBALS (Verwendung wie ZEND_DECLARE_MODULE_GLOBALS) definieren.

Beispiel

Betrachten wir nun mal folgende Ausschnitte aus Code zu einer Beispiel-Erweiterung, die wir example nennen:

Beispiel
 // Header-Datei:
 ZEND_BEGIN_MODULE_GLOBALS(example)
     int meaning_of_life;
     FILE *file_pointer;
 ZEND_END_MODULE_GLOBALS(example)
 
 #ifdef ZTS
 # define EXAMPLEG(v) TSRMG(example_globals_id, zend_example_globals *, v)
 #else
 # define EXAMPLEG(v) (example_globals.v)
 #endif
 
 ZEND_EXTERN_MODULE_GLOBALS(example)
 
 // C-Datei:
 ZEND_DECLARE_MODULE_GLOBALS(example)
 
 static void php_example_init_globals(zend_example_globals *example_globals TSRMLS_DC) {
   example_globals->meaning_of_life = 42;
   example_globals->file_pointer = fopen("/etc/motd", "r");
 }
 
 static void php_example_destroy_globals(zend_example_globals *example_globals TSRMLS_DC) {
   if (example_globals->file_pointer) {
     fclose (example_globals->file_pointer);
   }
 }
 
    // in der Initialisierungsfunktion
    ZEND_INIT_MODULE_GLOBALS(example, php_example_init_globals, php_example_destroy_globals);
 
    // irgendwo im Code
    printf ("Meaning of life = %d\n", EXAMPLEG(meaning_of_life));

Expansion der Makros ohne Threads

Wir können uns nun ansehen, wozu dieser Code tatsächlich expandiert – je nachdem ob wir Threads haben (ZTS definiert) oder nicht. Hier erstmal die Expansion, wenn keine Threads verwendet werden:

Beispiel
 // Header-Datei:
 typedef struct _zend_example_globals {
     int meaning_of_life;
     FILE *file_pointer;
 } zend_example_globals;
 
 # define EXAMPLEG(v) (example_globals.v)
 
 extern zend_example_globals example_globals;
 
 // C-Datei:
 zend_example_globals example_globals;
 
 static void php_example_init_globals(zend_example_globals *example_globals TSRMLS_DC) {
   example_globals->meaning_of_life = 42;
   example_globals->file_pointer = fopen("/etc/motd", "r");
 }
 
 static void php_example_destroy_globals(zend_example_globals *example_globals TSRMLS_DC) {
   if (example_globals->file_pointer) {
     fclose (example_globals->file_pointer);
   }
 }
 
    // in der Initialisierungsfunktion
    php_example_init_globals(&example_globals);
 
    // irgendwo im Code
    printf ("Meaning of life = %d\n", (example_globals.meaning_of_life));

Expansion der Makros mit Threads

Das gleiche kann man nun betrachten, wenn man Threads aktiviert hat:

Beispiel
 // Header-Datei:
 typedef struct _zend_example_globals {
         int meaning_of_life;
         FILE *file_pointer;
 } zend_example_globals;
 
 # define EXAMPLEG(v) TSRMG(example_globals_id, zend_example_globals *, v)
 
 extern ts_rsrc_id example_globals_id;
 
 // C-Datei:
 ts_rsrc_id example_globals_id;
 
 static void php_example_init_globals(zend_example_globals *example_globals TSRMLS_DC) {
   example_globals->meaning_of_life = 42;
   example_globals->file_pointer = fopen("/etc/motd", "r");
 }
 
 static void php_example_destroy_globals(zend_example_globals *example_globals TSRMLS_DC) {
   if (example_globals->file_pointer) {
     fclose (example_globals->file_pointer);
   }
 }
 
    // in der Initialisierungsfunktion
    ts_allocate_id(&example_globals_id, sizeof(zend_example_globals),
      (ts_allocate_ctor) php_example_init_globals, (ts_allocate_dtor) php_example_destroy_globals);
 
    // irgendwo im Code
    printf ("Meaning of life = %d\n", TSRMG(example_globals_id, zend_example_globals *, meaning_of_life));

Hierbei ist noch zu erwähnen, dass die letzte Zeile im Thread-Fall zu folgendem expandiert:

Beispiel
    printf ("Meaning of life = %d\n",
      (((zend_example_globals *) (*((void ***) tsrm_ls))[TSRM_UNSHUFFLE_RSRC_ID(example_globals_id)])->meaning_of_life));

Vordefinierte Makros zum Zugriff auf bestimmte globale Strukturen

In PHP gibt es diverse vordefinierte Makros mit denen man auf vordefinierte, PHP-interne Strukturen zugreifen kann. Folgende Tabelle listet diese auf, die Zeilenangaben beziehen sich auf den Quellcode von PHP 5.3.0:

Makro Zugehöriger Datentyp Definiert in Teil von PHP, der diese nutzt
EG() struct _zend_executor_globals zend_globals.h, Zeile 164 Die Ausführungsschicht (Executor)
CG() struct _zend_compiler_globals zend_globals.h, Zeile 72 Der Compiler
LANG_SCNG() struct _zend_php_scanner_globals zend_globals.h, Zeile 282 Der Scanner/Lexer für PHP-Code
INI_SCNG() struct _zend_ini_scanner_globals zend_globals.h, Zeile 262 Der Scanner/Lexer für ini-Dateien
GC_G() zend_gc_globals zend_gc.h, Zeile 99 Der Garbage Collector (neu in PHP 5.3)
EX() struct _zend_execute_data zend_compile.h, Zeile 308 Dies fällt etwas aus dem Schema, dieses Makro selbst wird nämlich nur in zend_vm_execute.h definiert (die wiederum aus zend_vm_execute.skl und zend_vm_def.h durch zend_vm_gen.php automatisch generiert wird). Es zeigt auf den aktuell gültigen Ausführungskontext (analog in etwa zum Stack-Frame in kompilierten Sprachen) und ist nur innerhalb von Opcodes relevant. An anderer Stelle sollte EG(current_execute_data)->v statt EX(v) verwendet werden.

Speicherverwaltung

PHP stellt einige eigene Funktionen zur Speicherverwaltung zur Verfügung, die den großen Vorteil haben, dass beim Ende eines PHP-Requests jeder Speicher, der im Rahmen dieses Requests alloziert wurde, automatisch wieder freigegeben wird. Dadurch entstehen keine Speicherlecks, wenn man diese Routinen verwendet.

PHP orientiert sich hierbei von der Konvention her an den Standardfunktionen, die C zur Verfügung stehen, damit die Verwendung möglichst simpel ist. Folgende Funktionen stehen zur Verfügung:

Aktion C-Standardfunktion PHP-Funktion Makro
Einen Speicherbereich auf dem Heap allozieren malloc _emalloc emalloc
Einen Speicherbereich auf dem Heap reallozieren realloc _erealloc erealloc
Einen Speicherbereich auf dem Heap allozieren und mit 0 initialisieren calloc _ecalloc ecalloc
Einen Speicherbereich auf dem Heap freigeben free _efree efree
Eine C-Zeichenkette duplizieren strdup _estrdup estrdup
Eine C-Zeichenkette duplizieren (mit maximaler Länge) strndup _estrndup estrndup

Wichtig ist hierbei zu beachten, dass die eigentlichen PHP-Funktionen mit einem Unterstrich beginnen und als zusätzliche Parameter am Ende noch Informationen über die aktuelle Zeile und Datei übergeben bekommen wollen – dies erleichtert das Debuggen von Allozierungsproblemen. Damit man dies nicht bei jedem Allozierungsvorgang selbst angeben muss gibt es daher Makros, die das automatisch kapseln. Daher sollte man nur die Makros aufrufen, nicht jedoch die Funktionen direkt. Es gibt außerdem noch Makros, die auf _rel enden, die die Zeilen- und Dateiinformationen von der aufrufenden Funktion übergeben und nicht die der eigenen (die eigene Funktion muss diese natürlich selbst übergeben bekommen haben).

Folgende Beispiele demonstrieren die Verwendung:

Beispiel
 // Einen Puffer mit 200 Zeichen allozieren
 char *buf1, *buf2, *buf3;
 unsigned long *array;
 
 // Puffer allozieren
 buf1 = emalloc(200);
 // "Test string" hineinkopieren
 strlcpy(buf1, "Test string", 200);
 
 // Duplizieren
 buf2 = estrdup(buf);
 
 // Duplizieren mit Maximallänge
 // buf3 enthält "Test s"
 buf3 = estrndup(6);
 
 // Array allozieren und mit 0 initialisieren
 array = ecalloc(20, sizeof(unsigned long));
 
 // Oh, war doch zu wenig, vergrößern
 //     (siehe unten, warum andere Funktion besser geeignet ist)
 array = erealloc(array, 40 * sizeof(unsigned long));

„Sichere“ Allozierungsfunktionen

PHP bietet ferner noch einige zusätzliche Funktionen an, die einige Fallstricke vermeiden. Zum einen gibt es Funktionen mit einem zusätzlichen Präfix safe_, die zwei verschiedene Dinge tun:

  • Für Stringfunktionen (safe_estrdup, safe_estrndup) prüfen diese, ob der übergebene Puffer NULL ist. Sollte dies der Fall sein, wird ein neu allozierter Leerstring zurückgegeben, damit der zurückgegebene Zeiger immer gültig ist.
  • Für die Funktionen safe_emalloc und safe_erealloc ändern sich die Parameter etwas: Statt eines Parameters für die Größe werden drei Parameter übergeben: nmemb, size, offset. In diesem Fall wird Speicher der Größe nmemb * size + offset alloziert. Allerdings wird hier vorher geprüft, ob diese Allozierung zu einem Integer-Overflow führt – wenn ja, wird abgebrochen, weil das eine potentielle Sicherheitslücke darstellt.


Beispiel für die Verwendung von safe_emalloc (safe_erealloc funktioniert analog)
 // alloziert 20 * 4 + 40 bytes, stellt aber sicher,
 // dass 20 * 4 + 40 noch in den Datentyp passt
 void *ptr = safe_emalloc(20, 4, 40);

Persistente Allozierungsfunktionen

Da mancher Speicher auch über Requests hinweg alloziert werden soll (zum Beispiel für persistente Datenbankverbindungen), es aber für den Code eventuell umständlich ist, viele if-Bedingungen bei jeder Allozierung hinzuschreiben, gibt es außerdem noch Makros, die einem die Arbeit abnehmen. Sie werden mit einem zusätzlichen p gepräfixt und erhalten am Ende einen zusätzlichen Parameter. Wenn dieser 1 ist, werden die normalen Allozierungsfunktionen verwendet, ansonsten die PHP-eigenen. Beispiel:

Beispiel
 // emalloc aufrufen
 typ_t *buf1 = pemalloc (sizeof (typ_t), 0);
 // malloc aufrufen
 typ_t *buf2 = pemalloc (sizeof (typ_t), 1);