SpeicherpoolübersichtDer Speicherpool besteht aus einer bestimmten Anzahl gleich großer Speicherblöcke (im Allgemeinen), die vorab als Backup zugewiesen werden, bevor der Speicher tatsächlich verwendet wird. Bei neuem Speicherbedarf wird ein Teil des Speicherblocks aus dem Speicherpool allokiert. Reicht der Speicherblock nicht aus, wird neuer Speicher angefordert. Zu den Vorteilen eines Speicherpools gehören die Reduzierung des Zeitaufwands beim Beantragen und Freigeben von Speicher im System, die Lösung des Problems der Speicherfragmentierung durch häufige Speicherzuweisung, die Verbesserung der Programmleistung und die Verringerung der Aufmerksamkeit des Programmierers auf den Speicher beim Schreiben von Code. Zu den derzeit gängigen Speicherpoolimplementierungen gehören der Speicherzuweisungsbereich in STL, Nginx kapselt aus praktischen Gründen viele nützliche Datenstrukturen wie ngx_str_t, ngx_array_t, ngx_pool_t usw. Für den Speicherpool hat Nginx ein sehr ausgefeiltes Design, das es wert ist, gelernt zu werden. Dieser Artikel konzentriert sich auf die Einführung des Nginx-Speicherpool-Quellcodes und erläutert ihn anhand eines tatsächlichen Codebeispiels. 1. Nginx-Datenstruktur// Der Trennpunkt zwischen kleinen und großen SGI STL-Speicherblöcken: 128B // nginx (ordnet allen Modulen des HTTP-Servers Speicher zu) der Trennpunkt zwischen kleinen und großen Speicherblöcken: 4096B #define NGX_MAX_ALLOC_FROM_POOL (ngx_pagesize – 1) // Standardgröße des Speicherpools #define NGX_DEFAULT_POOL_SIZE (16 * 1024) // Byte-Ausrichtung des Speicherpools, SGI STL ist 8B #define NGX_POOL_ALIGNMENT 16 #define NGX_MIN_POOL_SIZE ngx_align((Größe von (ngx_pool_t) + 2 * Größe von (ngx_pool_large_t)), \ NGX_POOL_ALIGNMENT // Passen Sie den zugewiesenen Speicher auf ein ganzzahliges Vielfaches von 16 an. #define ngx_align(d, a) (((d) + (a - 1)) & ~(a - 1)) Typdefinitionsstruktur ngx_pool_s ngx_pool_t; typedef-Struktur { u_char *last; // zeigt auf die Startadresse des verfügbaren Speichers u_char *end; // zeigt auf die Endadresse des verfügbaren Speichers ngx_pool_t *next; // zeigt auf den nächsten Speicherblock ngx_uint_t failed; // die Anzahl der Male, die der aktuelle Speicherblock keinen Speicherplatz zuordnen konnte} ngx_pool_data_t; // Speicherpool-Blocktyp struct ngx_pool_s { ngx_pool_data_t d; // Header-Informationen zum Speicherpoolblock size_t max; ngx_pool_t *current; // Zeigt auf die Startadresse des Speicherblocks, der zum Zuweisen von Speicherplatz verwendet werden kann (fehlgeschlagen < 4) ngx_chain_t *chain; // Verbinde alle Speicherpoolblöcke ngx_pool_large_t *large; // Einstiegszeiger für große Speicherblöcke ngx_pool_cleanup_t *cleanup; // Bereinigungsvorgang für Speicherpoolblöcke. Benutzer können Rückruffunktionen festlegen, um Bereinigungsvorgänge auszuführen, bevor Speicherpoolblöcke freigegeben werden ngx_log_t *log; // Protokoll }; 2. nginx beantragt Speicherplatz ngx_create_pool vom Betriebssystem// Speicher entsprechend der Größe freigeben ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log){ ngx_pool_t *p; // Rufen Sie gemäß den von der Systemplattform definierten Makros und der vom Benutzer ausgeführten Größe die API verschiedener Plattformen auf, um einen Speicherpool zu öffnen p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log); wenn (p == NULL) { gibt NULL zurück; } p->d.last = (u_char *) p + sizeof(ngx_pool_t); // Zeigt auf die Startadresse des verfügbaren Speichers p->d.end = (u_char *) p + size; // Zeigt auf die Endadresse des verfügbaren Speichers p->d.next = NULL; // Zeigt auf den nächsten Speicherblock. Der Speicherblock wurde gerade angefordert, daher bleibt er leer p->d.failed = 0; // Ob der Speicherblock erfolgreich zugewiesen wurde size = size - sizeof(ngx_pool_t); // Verfügbarer Speicherplatz = Gesamtspeicherplatz - Header-Informationen // Wenn die angegebene Größe größer als eine Seite ist, verwenden Sie eine Seite, andernfalls verwenden Sie die angegebene Größe // max = min(size, 4096), max bezieht sich auf die Größe des Speicherblocks ohne die Anfangsinformationen p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL; p->current = p; // Zeigt auf die Startadresse des Speicherblocks, der zum Zuweisen von Speicherplatz verwendet werden kann p->chain = NULL; p->large = NULL; // Kleine Speicherblöcke werden direkt im Speicherblock zugewiesen, und große Speicherblöcke werden im Speicher zugewiesen, auf den large p->cleanup = NULL zeigt; p->log = log; Rückgabe p; } 3. nginx beantragt Speicherplatz aus dem SpeicherpoolLeere * ngx_palloc(ngx_pool_t *Pool, size_t Größe) { #wenn !(NGX_DEBUG_PALLOC) wenn (Größe <= Pool->max) { // Der aktuell zugewiesene Speicherplatz ist kleiner als das Maximum, kleine Speicherzuweisung return ngx_palloc_small(pool, size, 1); // Speicherausrichtung berücksichtigen} #endif returniere ngx_palloc_large(Pool, Größe); } Leere * ngx_pnalloc(ngx_pool_t *Pool, size_t Größe) { #wenn !(NGX_DEBUG_PALLOC) wenn (Größe <= Pool->max) { return ngx_palloc_small(pool, size, 0); // Speicherausrichtung wird nicht berücksichtigt} #endif returniere ngx_palloc_large(Pool, Größe); } void* ngx_pcalloc(ngx_pool_t *Pool, size_t Größe){ ungültig *p; p = ngx_palloc(Pool, Größe); // Speicherausrichtung berücksichtigen, wenn (p) { ngx_memzero(p, size); // Speicher auf 0 initialisieren } Rückgabe p; }
statisches ngx_inline void * ngx_palloc_small(ngx_pool_t *Pool, size_t Größe, ngx_uint_t ausrichten) { u_char *m; ngx_pool_t *p; // Aus dem Speicherpool zuweisen, auf den der aktuelle Zeiger des ersten Speicherblocks zeigt p = pool->current; Tun { m = p->d.last; // m zeigt auf die Startadresse des zuweisbaren Speichers if (align) { // Passe m an ein ganzzahliges Vielfaches von NGX_ALIGNMENT an m = ngx_align_ptr(m, NGX_ALIGNMENT); } //Kerncode zum Zuweisen von Speicher aus dem Speicherpoolif ((size_t) (p->d.end - m) >= size) { // Wenn der zuweisbare Speicherplatz >= der angeforderte Speicherplatz ist, // verschiebe den Zeiger d.last und zeichne die erste Adresse des freien Speicherplatzes auf p->d.last = m + size; Rückkehr m; } // Der freie Speicherplatz des aktuellen Speicherblocks reicht nicht aus, um ihn zuzuweisen. Wenn ein nächster Speicherblock vorhanden ist, wechseln Sie zum nächsten Speicherblock. // Wenn nicht, wird p auf NULL gesetzt und beendet, während p = p->d.weiter; } während (p); returniere ngx_palloc_block(Pool, Größe); } Der aktuelle Speicherpool verfügt über genügend zuzuweisende Blöcke: Der aktuelle Speicherpool verfügt nicht über genügend Blöcke zum Zuordnen:
statisches void * ngx_palloc_block(ngx_pool_t *Pool, size_t Größe){ u_char *m; Größe_t pGröße; ngx_pool_t *p, *neu; //Öffnen Sie einen Speicherblock mit der gleichen Größe wie der vorherige psize = (size_t) (pool->d.end - (u_char *) pool); // Nachdem psize auf ein ganzzahliges Vielfaches von NGX_POOL_ALIGNMENT ausgerichtet wurde, Speicherplatz vom Betriebssystem beantragen m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log); wenn (m == NULL) { gibt NULL zurück; } new = (ngx_pool_t *) m; // zeigt auf die Startadresse des neu zugewiesenen Speicherblocks new->d.end = m + psize; // zeigt auf die Endadresse des neu zugewiesenen Speicherblocks new->d.next = NULL; // die Adresse des nächsten Speicherblocks ist NULL new->d.failed = 0; // Die Anzahl der Fälle, in denen der aktuelle Speicherblock keinen Speicherplatz zuordnen konnte // Zeigt auf das Ende der Header-Informationen, während max, current, chain usw. sich nur im ersten Speicherblock befinden m += sizeof(ngx_pool_data_t); m = ngx_align_ptr(m, NGX_ALIGNMENT); new->d.last = m + size; // last zeigt auf die Startadresse des freien Speicherplatzes im aktuellen Block // Da Speicherplatz jedes Mal von pool->current zugewiesen wird // Wenn die Ausführung bis hierhin gelangt, schlägt die Zuweisung aller anderen Speicherblöcke außer dem neuen Speicherblock fehl (p = pool->current; p->d.next != NULL; p = p->d.next) { // Für alle Speicherblöcke wird failed++ hinzugefügt, bis die Anzahl der Zuordnungsfehler für den Speicherblock größer als 4 ist. // Das bedeutet, dass der verbleibende Speicherplatz des Speicherblocks sehr klein ist und kein weiterer Speicherplatz zugeordnet werden kann. // Ändern Sie dann den aktuellen Zeiger und beginnen Sie beim nächsten Mal mit der Zuordnung des Speicherplatzes vom aktuellen Zeiger aus. Bei der erneuten Zuordnung müssen Sie die vorherigen Speicherblöcke nicht durchlaufen, if (p->d.failed++ > 4) { pool->aktuell = p->d.nächster; } } p->d.next = new; // Verbinde den ersten Speicherblock des zuweisbaren Speicherplatzes und den neu geöffneten Speicherblock return m; } 4. Zuweisung und Freigabe großer SpeicherblöckeTypdefinitionsstruktur ngx_pool_large_s ngx_pool_large_t; Struktur ngx_pool_large_s { ngx_pool_large_t *next; // Die Startadresse des nächsten großen Speicherblocks void *alloc; // Die Startadresse des großen Speicherblocks }; static void * ngx_palloc_large(ngx_pool_t *Pool, size_t Größe){ ungültig *p; ngx_uint_t n; ngx_pool_large_t *groß; // Aufruf von malloc p = ngx_alloc(Größe, Pool->Log); wenn (p == NULL) { gibt NULL zurück; } n = 0; // for-Schleife durchläuft die verknüpfte Liste, in der große Blöcke mit Speicherinformationen gespeichert sind for (large = pool->large; large; large = large->next) { wenn (groß->alloc == NULL) { // Wenn ein großer Speicherblock ngx_pfree ist, ist alloc NULL // Durchlaufe die verknüpfte Liste. Wenn die erste Adresse des großen Speicherblocks leer ist, schreibe die aktuelle Malloc-Speicheradresse in Alloc. groß->zuordnen = p; Rückgabe p; } // Wenn nach viermaligem Durchlauf die entsprechenden Informationen des freigegebenen großen Speicherblocks nicht gefunden wurden // Um die Effizienz zu verbessern, beantragen Sie direkt Speicherplatz im kleinen Speicherblock, um die Informationen des großen Speicherblocks zu speichern, wenn (n++ > 3) { brechen; } } // Speicherplatz zum Speichern großer Speicherblöcke *next und *alloc im kleinen Speicherpool über den Zeigeroffset large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1); zuordnen. wenn (groß == NULL) { // Wenn die Speicherzuweisung *next und *alloc space auf dem kleinen Speicherblock fehlschlägt, kann der große Speicherblock nicht aufgezeichnet werden // Den großen Speicherblock freigeben p ngx_free(p); gibt NULL zurück; } large->alloc = p; // alloc zeigt auf die erste Adresse des großen Speicherblocks large->next = pool->large; // Diese beiden Sätze verwenden die Kopfeinfügungsmethode, um die Datensatzinformationen des neuen Speicherblocks in der verknüpften Liste mit „large“ als Kopfknoten zu speichern pool->large = large; Rückgabe p; } Freigabe großer Speicherblöcke // Den großen Speicherblock freigeben, auf den p zeigt ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p){ ngx_pool_large_t *l; für (l = Pool->groß; l; l = l->nächste) { // Durchlaufe die verknüpfte Liste mit den großen Speicherblöcken und finde den großen Speicherblock, der p entspricht, wenn (p == l->alloc) { ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, Pool->Protokoll, 0, "frei: %p", l->alloc); // Große Speicherblöcke freigeben, aber nicht den Speicherplatz freigeben, in dem Informationen gespeichert sind ngx_free(l->alloc); // frei l->alloc = NULL; // alloc wird auf NULL gesetzt und gibt NGX_OK zurück; } } gib NGX_DECLINED zurück; } 5. Über kleine Speicherblöcke, die nicht freigegeben werdenDie Tasten „Last“ und „End“ werden verwendet, um freien Speicherplatz anzuzeigen. Es ist nicht möglich, den belegten Speicherplatz sinnvoll an den Speicherpool zurückzugeben, aber der Speicherpool wird zurückgesetzt. Es speichert auch Header-Informationen, die auf den großen Speicherblock large und die Bereinigungsfunktion cleansen verweisen In Anbetracht der Effizienz von Nginx werden kleine Speicherblöcke effizient zugewiesen und der Speicher wird nicht gleichzeitig recycelt void ngx_reset_pool(ngx_pool_t *pool){ ngx_pool_t *p; ngx_pool_large_t *l; // Da der kleine Speicher zurückgesetzt werden muss und die Steuerinformationen des großen Speichers im kleinen Speicher gespeichert sind // Sie müssen also zuerst den großen Speicher freigeben und dann den kleinen Speicher zurücksetzen for (l = pool->large; l; l = l->next) { wenn (l->alloc) { ngx_free(l->alloc); } } // Durchlaufe die verknüpfte Liste kleiner Speicherblöcke und setze Verwaltungsinformationen zurück, wie etwa zuletzt, fehlgeschlagen, aktuell, Kette, groß usw. for (p = Pool; p; p = p->d.next) { // Da nur der erste Speicherblock andere Verwaltungsinformationen als ngx_pool_data_t hat, haben andere Speicherblöcke nur Informationen zu ngx_pool_data_t // Es tritt kein Fehler auf, aber es wird Speicherplatz verschwendet p->d.last = (u_char *) p + sizeof(ngx_pool_t); p->d.fehlgeschlagen = 0; } // current zeigt auf einen Speicherblock, der zum Zuweisen von Speicher verwendet werden kann pool->current = pool; Pool->Kette = NULL; Pool->groß = NULL; } Nginx ist im Wesentlichen ein HTTP-Server. Normalerweise verarbeitet er Kurzlinks und stellt Dienste indirekt bereit. Er benötigt nicht viel Speicher, sodass er den Speicher nicht recycelt und zurückgesetzt werden kann. Nachdem der Client eine Anforderung initiiert hat, gibt der Nginx-Server nach Erhalt der Anforderung eine Antwort zurück. Wenn innerhalb der Keep-Alive-Zeit keine weitere Anforderung vom Client eingeht, trennt der Nginx-Server aktiv die Verbindung und setzt den Speicherpool zurück. Wenn das nächste Mal eine Client-Anforderung eingeht, kann der Speicherpool wiederverwendet werden. Bei einer langen Verbindung können die Serverressourcen erst freigegeben werden, wenn die Systemressourcen erschöpft sind, solange der Client noch online ist. Lange Links verwenden im Allgemeinen den SGI STL-Speicherpool zum Zuweisen und Freigeben von Speicher. Allerdings ist diese Methode beim Zuweisen und Freigeben von Speicherplatz weniger effizient als nginx. 6. Zerstören und löschen Sie den SpeicherpoolNehmen Sie die folgende Situation an: // Angenommen, die Speicherausrichtung ist 4B typedef-Struktur{ Zeichen* p; Zeichendaten[508]; }stDaten; ngx_pool_t *pool = ngx_create_pool(512, log); // Erstellen Sie einen Nginx-Speicherblock mit einem Gesamtspeicherplatz von 512 B stData* data_ptr = ngx_alloc(512); // Da die tatsächlich verfügbare Speichergröße 512-sizeof(ngx_pool_t) beträgt, gehört sie zu einer großen Speicherzuweisung data_ptr->p = malloc(10); // p zeigt auf einen externen Heap-Speicher, ähnlich der Verwendung externer Ressourcen in C++-Objekten Beim Zurückfordern großer Speicherblöcke führt der Aufruf von ngx_free zu Speicherlecks Das oben genannte Speicherleckproblem kann gelöst werden, indem Speicher über eine Rückruffunktion freigegeben wird (implementiert über einen Funktionszeiger). Typdefinition void (*ngx_pool_cleanup_pt)(void *data); Typdefinitionsstruktur ngx_pool_cleanup_s ngx_pool_cleanup_t; // Auf die folgende Struktur zeigt ngx_pool_s.cleanup. Dabei handelt es sich auch um einen kleinen Speicherblock, der im Speicherpool gespeichert ist. struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt-Handler; void *data; // Auf die Ressource zeigen, die freigegeben werden muss ngx_pool_cleanup_t *next; // Funktionen, die Ressourcen freigeben, werden in eine verknüpfte Liste eingefügt und „next“ wird verwendet, um auf diese verknüpfte Liste zu zeigen}; Von nginx bereitgestellte Funktionsschnittstelle: // p stellt die Eintragsadresse des Speicherpools dar, size stellt die Größe des Zeigers p->cleanup->data dar // p->cleanup zeigt auf eine Struktur, die Informationen zur Bereinigungsfunktion enthält // ngx_pool_cleanup_add gibt einen Zeiger auf eine Struktur zurück, die Informationen zur Bereinigungsfunktion enthält ngx_pool_cleanup_t* ngx_pool_cleanup_add(ngx_pool_t *p, size_t size){ ngx_pool_cleanup_t *c; // Die Struktur der Bereinigungsfunktion ist eigentlich ein kleiner Speicherblock, der im Speicherpool gespeichert ist c = ngx_palloc(p, sizeof(ngx_pool_cleanup_t)); wenn (c == NULL) { gibt NULL zurück; } if (Größe) { // Speicherplatz der Größe für c->data beantragen c->data = ngx_palloc(p, size); wenn (c->data == NULL) { gibt NULL zurück; } } anders { c->Daten = NULL; } c->handler = NULL; //Verwenden Sie die Methode zum Einfügen des Kopfes, um die aktuelle Struktur nach Pool->Cleanup zu stringent. c->next = p->Cleanup; p->Aufräumen = c; ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, p->log, 0, "Bereinigung hinzufügen: %p", c); Rückkehr c; } Anwendung: void Freigabe(void* p){ frei(p); } ngx_pool_cleanup_t* clean_ptr = ngx_clean_cleanup_add(Pool, Größe von(char*)); clean_ptr->handler = &release; // Der Benutzer legt die Funktion fest, die vor der Zerstörung des Speicherpools aufgerufen werden soll clean_ptr->data = data_ptr->p; // Der Benutzer legt die Adresse des Speichers fest, der vor der Zerstörung des Speicherpools freigegeben werden soll ngx_destroy_pool(pool); // Der Benutzer zerstört den Speicherpool 7. Kompilieren und testen Sie die Speicherpool-Schnittstellenfunktionvoid ngx_destroy_pool(ngx_pool_t *pool) { ngx_pool_t *p, *n; ngx_pool_large_t *l; ngx_pool_cleanup_t *c; // Durchlaufe die Bereinigungsliste (gespeichert als Funktionen, die vor der Freigabe aufgerufen werden müssen), um externe Ressourcen für (c = pool->cleanup; c; c = c->next) freizugeben { wenn (c->handler) { ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, Pool->Protokoll, 0, "Bereinigung ausführen: %p", c); c->handler(c->daten); } } // Große Speicherblöcke freigeben für (l = pool->large; l; l = l->next) { wenn (l->alloc) { ngx_free(l->alloc); } } // Kleinen Speicherpool freigeben für (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { ngx_free(p); wenn (n == NULL) { brechen; } } } Führen Sie Das Makefile lautet wie folgt: Führen Sie den Befehl make aus, um mit Makefile den Quellcode zu kompilieren und #include <ngx_config.h> #include <nginx.h> #include <ngx_core.h> #include <ngx_palloc.h> #include <stdio.h> #include <stdlib.h> #include <string.h> void ngx_log_error_core(ngx_uint_t Ebene, ngx_log_t *log, ngx_err_t Fehler, const char *fmt, ...){ } Typdefinitionsstruktur Data stData; Struktur Data{ Zeichen *ptr; DATEI *pdatei; }; void funktion1(char *p){ printf("Freier PTR-Speicher!\n"); frei(p); } void func2(DATEI *pf){ printf("Datei schließen!\n"); fclose(pf); } void main(){ // max = 512 - Größe von (ngx_pool_t) // Erstellen Sie einen Nginx-Speicherblock mit einem Gesamtspeicherplatz von 512 Bytes ngx_pool_t *pool = ngx_create_pool(512, NULL); wenn(pool == NULL){ printf("ngx_create_pool fehlgeschlagen …"); zurückkehren; } // void *p1 aus kleinem Speicherpool zugewiesen = ngx_palloc(pool, 128); wenn(p1 == NULL){ printf("ngx_palloc 128 Bytes fehlgeschlagen …"); zurückkehren; } // stData *p2 aus großem Speicherpool zugewiesen = ngx_palloc(pool, 512); wenn(p2 == NULL){ printf("ngx_palloc 512 Bytes fehlgeschlagen …"); zurückkehren; } // Externen Heap-Speicher belegen p2->ptr = malloc(12); strcpy(p2->ptr, "Hallo Welt"); // Dateideskriptor p2->pfile = fopen("data.txt", "w"); ngx_pool_cleanup_t *c1 = ngx_pool_cleanup_add(Pool, Größe von (Zeichen*)); c1->handler = func1; // Rückruffunktion festlegen c1->data = p2->ptr; // Ressourcenadresse festlegen ngx_pool_cleanup_t *c2 = ngx_pool_cleanup_add(pool, sizeof(FILE*)); c2->Handler = Funktion2; c2->Daten = p2->pDatei; // 1. Rufen Sie alle voreingestellten Bereinigungsfunktionen auf. 2. Geben Sie große Speicherblöcke frei. 3. Geben Sie den gesamten Speicher im kleinen Speicherpool frei. ngx_destroy_pool(pool); zurückkehren; } Da der erstellte Bereinigungsblock durch Header-Einfügen in Zugehörige Testcodes werden an folgende Adresse gesendet: https://github.com/BugMaker-shen/nginx_sgistl_pool Dies ist das Ende dieses Artikels über die Quellcodeanalyse des Nginx-Speicherpools. Weitere relevante Inhalte zum Nginx-Speicherpool finden Sie in den vorherigen Artikeln von 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird! Das könnte Sie auch interessieren:
|
<<: Analyse des rel-Attributs in HTML
>>: So importieren und exportieren Sie Cookies und Favoriten in FireFox
Das Format ist einfach: Proxy_Pass-URL; Die URL u...
Was ist MySQL Multi-Instance Einfach ausgedrückt ...
Trennen Sie Front- und Backend und lösen Sie domä...
Beim Konfigurieren unterschiedlicher Servlet-Pfad...
Inhaltsverzeichnis Vorwort Konvertierungsbeziehun...
1. Vorbereitung vor der Installation 1. Laden Sie...
Umfeld Linux 3.10.0-693.el7.x86_64 Docker-Version...
docker-compose-monitor.yml Version: '2' N...
1. Verwenden Sie absolute Positionierung und Ränd...
Zusammengeklappte Kopfzeilen sind eine großartige...
Vorwort Als wir im vorherigen Interviewprozess na...
Inhaltsverzeichnis 1. Oberflächliche Kopie 1. Obj...
Bedarfsszenario: Der Chef bat mich, den Crawler z...
Bei unserer täglichen Arbeit führen wir manchmal ...
Inhaltsverzeichnis Vorwort 1. Schlüsselelemente e...