PHP/Tutorials/Einführung in die Interna/Threads, globale Variablen und Speicherverwaltung
Inhaltsverzeichnis
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:
#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:
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''');
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:
static int tu_was (zval *wert) {
int temp;
'''TSRMLS_FETCH();'''
// tu was
}
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);
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:
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:
// 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:
// 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:
// 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:
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:
// 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 PufferNULL
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
undsafe_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ößenmemb * 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.
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:
// emalloc aufrufen
typ_t *buf1 = pemalloc (sizeof (typ_t), 0);
// malloc aufrufen
typ_t *buf2 = pemalloc (sizeof (typ_t), 1);