Esploriamo il motore VoIP Mediastreamer2. Parte 12

Il materiale dell'articolo è tratto dal mio canale zen.

Esploriamo il motore VoIP Mediastreamer2. Parte 12

Nell'articolo precedente abbiamo chiarito il corretto completamento dei programmi che utilizzano il mediastreamer., ho promesso di considerare la questione della valutazione del carico sul ticker e dei metodi per affrontare il carico computazionale eccessivo nello streaming multimediale. Ma ho deciso che sarebbe più logico trattare le questioni di debug dei filtri artigianali, legate al movimento dei dati, e poi esaminare le questioni di ottimizzazione delle prestazioni.

Debug dei filtri artigianali

Dopo aver esaminato nel precedente articolo il meccanismo di movimento dei dati nello streaming multimediale, sarà logico parlare dei pericoli nascosti in esso. Una delle caratteristiche del principio 'data flow' è che l'allocazione della memoria dall'heap avviene nei filtri che si trovano all'inizio del flusso di dati, mentre il rilascio della memoria con la restituzione all'heap viene effettuato dai filtri situati alla fine del percorso del flusso. Inoltre, la creazione di nuovi dati e la loro distruzione possono avvenire in punti intermedi. In generale, il rilascio della memoria non viene svolto dallo stesso filtro che ha creato il blocco di dati.

Dal punto di vista di un monitoraggio trasparente della memoria, sarebbe ragionevole che il filtro, ricevendo un blocco di input, dopo averlo elaborato lo distruggesse immediatamente liberando la memoria, e in output fornisse un nuovo blocco con i dati in uscita. In questo caso, una perdita di memoria nel filtro sarebbe facilmente tracciabile: se l'analizzatore rileva una perdita nel filtro, significa che il filtro successivo non sta distruggendo i blocchi in ingresso in modo appropriato, quindi l'errore risiede in esso. Ma dal punto di vista del mantenimento di alte prestazioni, questo approccio al lavoro con i blocchi di dati non è produttivo: porta a un elevato numero di operazioni di allocazione/rilascio della memoria per i blocchi di dati senza alcun output utile.

Per questo motivo, i filtri del media streamer, per non rallentare l'elaborazione dei dati, utilizzano funzioni che creano copie leggere durante la copia dei messaggi (ne abbiamo parlato nell'articolo precedente). Queste funzioni creano solo una nuova istanza dell'intestazione del messaggio "agganciando" ad essa un blocco di dati dal "vecchio" messaggio copiato. Di conseguenza, a un blocco di dati sono collegati due intestazioni e viene incrementato il contatore di riferimenti nel blocco di dati. Tuttavia, questo apparirà come due messaggi. Possono esserci più messaggi con tale blocco di dati "condiviso"; ad esempio, il filtro MS_TEE genera immediatamente una dozzina di queste copie leggere, distribuendole tra le sue uscite. Se tutti i filtri nella catena funzionano correttamente, alla fine della linea di elaborazione questo contatore di riferimenti dovrebbe raggiungere zero e verrà chiamata la funzione di liberazione della memoria: ms_free(). Se non si verifica la chiamata, significa che questo blocco di memoria non tornerà più nel'heap, cioè "fuoriesce". Il prezzo da pagare per l'uso di copie leggere è la perdita della possibilità di determinare facilmente (come sarebbe nel caso di usare copie normali) in quale filtro si perde memoria.

Poiché la responsabilità della ricerca delle perdite di memoria nei filtri "nativi" grava sugli sviluppatori del media streamer, probabilmente non dovrai fare il debugging. Tuttavia, con il tuo filtro personalizzato — sei tu il fabbro della tua fortuna e il tempo che trascorrerai nella ricerca delle perdite nel tuo codice dipenderà dalla tua attenzione. Per ridurre il tuo tempo di tormento con il debugging, dobbiamo esaminare le tecniche di localizzazione delle perdite durante lo sviluppo dei filtri. Inoltre, può succedere che la perdita si manifesti solo quando il filtro viene utilizzato in un sistema reale, dove il numero di "sospetti" potrebbe risultare enorme e il tempo per il debugging limitato.

Come si manifesta una perdita di memoria?

È logico presumere che nell'output del programma top verrà mostrato un aumento della percentuale di memoria occupata dalla tua applicazione.

L'esternazione si manifesterà nel fatto che a un certo punto il sistema inizierà a rispondere lentamente ai movimenti del mouse, a ridisegnare lo schermo con lentezza. Potrebbe anche crescere il registro di sistema, occupando spazio sul disco rigido. In questo caso, la tua applicazione inizierà a comportarsi in modo strano, smettendo di rispondere ai comandi, non riuscendo ad aprire file, ecc.

Per identificare il fatto che si è verificata una perdita di memoria utilizzeremo un analizzatore di memoria (d'ora in poi analizzatore). Questo può essere Valgrind (buono su Habr. a riguardo) o integrato nel compilatore gcc MemorySanitizer o qualsiasi altra cosa. Se l'analizzatore mostra che la perdita si verifica in uno dei filtri del grafo, significa che è tempo di applicare uno dei metodi descritti di seguito.

Metodo delle tre pigne

Come già detto, in caso di perdita di memoria, l'analizzatore indicherà il filtro che ha richiesto l'allocazione di memoria dalla heap. Ma non indicherà il filtro che "ha dimenticato" di restituirla, che è in effetti il colpevole. Di conseguenza, l'analizzatore può solo confermare le nostre preoccupazioni, ma non indicare la loro origine.

Per determinare la posizione del "cattivo" filtro nel grafo, è possibile ridurre il grafo al numero minimo di nodi, in cui l'analizzatore rileva ancora la perdita e localizzare il filtro problematico nei tre rimanenti.

Ma potrebbe accadere che riducendo il numero di filtri nel grafo tu violi il normale flusso di interazione dei filtri con altri elementi del tuo sistema e la perdita smetta di manifestarsi. In questo caso, sarà necessario lavorare con il grafo di dimensioni complete e utilizzare l'approccio descritto di seguito.

Metodo del separatore mobile

Per semplificare l'esposizione, utilizzeremo un grafo composto da una catena di filtri. È rappresentato nell'illustrazione.

Esploriamo il motore VoIP Mediastreamer2. Parte 12

Un grafo normale, in cui insieme ai filtri pronti dello stream media sono applicati quattro filtri artigianali F1…F4, di quattro tipi diversi, realizzati tempo fa e di cui non metti in dubbio la correttezza. Tuttavia, poniamo che in alcuni di essi ci sia una perdita di memoria. Eseguendo il nostro programma di monitoraggio dell'analizzatore, dalla sua relazione scopriremo che un certo filtro ha richiesto una certa quantità di memoria e non l'ha restituita nel heap N volte. Si può facilmente dedurre che ci sarà un riferimento a funzioni interne di un filtro di tipo MS_VOID_SOURCE. Il suo compito è quello di prelevare memoria dal heap. Devono essere altri filtri a restituirla. Quindi, scopriremo il fatto della perdita.

Per determinare in quale parte della catena ci sia stata inattività che ha portato a una perdita di memoria, si propone di introdurre un filtro aggiuntivo che semplicemente trasferisce i messaggi dall'ingresso all'uscita, ma creando una copia non leggera, ma "pesante" del messaggio in ingresso, per poi eliminare completamente il messaggio ricevuto all'ingresso. Questo filtro verrà chiamato isolatore. Si presume che, poiché il filtro è semplice, la perdita in esso sia esclusa. Un'altra caratteristica positiva è che se lo aggiungiamo in qualsiasi punto del nostro grafo, non influenzerà il funzionamento dello schema. Rappresenteremo il filtro isolatore come un cerchio con doppio contorno.

Attiviamo l'isolatore subito dopo il filtro voidsource:
Esploriamo il motore VoIP Mediastreamer2. Parte 12

Eseguiamo di nuovo il programma con l'analizzatore e vediamo che questa volta l'analizzatore attribuirà la colpa all'isolatore. Infatti, è lui a creare blocchi di dati che poi vengono persi da un filtro (o filtri) negligente e sconosciuto. Come prossimo passo, spostiamo l'isolatore lungo la catena a destra, di un filtro, e rilanciamo l'analisi. Così, muovendo l'isolatore a destra passo dopo passo, otterremo una situazione in cui nel successivo rapporto dell'analizzatore la quantità di blocchi di memoria "persi" diminuirà. Questo significa che in questo passaggio l'isolatore si trovava nella catena subito dopo il filtro problematico. Se il "cattivo" filtro era solo uno, la perdita scomparirà del tutto. In questo modo abbiamo localizzato il filtro problematico (o uno dei diversi). Dopo aver "riparato" il filtro, possiamo continuare a muovere l'isolatore a destra nella catena fino alla completa vittoria sulle perdite di memoria.

Implementazione del filtro isolatore

L'implementazione dell'isolatore appare come un normale filtro. Intestazione:

/* Файл iso_filter.h  Описание изолирующего фильтра. */

#ifndef iso_filter_h
#define iso_filter_h

/* Задаем идентификатор фильтра. */
#include <mediastreamer2/msfilter.h>

#define MY_ISO_FILTER_ID 1024

extern MSFilterDesc iso_filter_desc;

#endif

Il filtro stesso:

/* Файл iso_filter.c  Описание изолирующего фильтра. */

#include "iso_filter.h"

    static void
iso_init (MSFilter * f)
{
}
    static void
iso_uninit (MSFilter * f)
{
}

    static void
iso_process (MSFilter * f)
{
    mblk_t *im;

    while ((im = ms_queue_get (f->inputs[0])) != NULL)
    {
        ms_queue_put (f->outputs[0], copymsg (im));
        freemsg (im);
    }
}

static MSFilterMethod iso_methods[] = {
    {0, NULL}
};

MSFilterDesc iso_filter_desc = {
    MY_ISO_FILTER_ID,
    "iso_filter",
    "A filter that reads from input and copy to its output.",
    MS_FILTER_OTHER,
    NULL,
    1,
    1,
    iso_init,
    NULL,
    iso_process,
    NULL,
    iso_uninit,
    iso_methods
};

MS_FILTER_DESC_EXPORT (iso_desc)

Metodo di sostituzione delle funzioni di gestione della memoria

Per ricerche più approfondite, nel mediastreamer è prevista la possibilità di sostituire le funzioni di accesso alla memoria con le proprie, che oltre al lavoro principale registreranno "Chi, dove e perché". Tre funzioni vengono sostituite. Questo avviene nel seguente modo:

OrtpMemoryFunctions reserv;
OrtpMemoryFunctions my;

reserv.malloc_fun = ortp_malloc;
reserv.realloc_fun = ortp_realloc;
reserv.free_fun = ortp_free;

my.malloc_fun = &my_malloc;
my.realloc_fun = &my_realloc;
my.free_fun = &my_free;

ortp_set_memory_functions(&my);

Questa possibilità è utile nei casi in cui l'analizzatore rallenta il funzionamento dei filtri a tal punto da compromettere il funzionamento del sistema in cui è integrato il nostro schema. In questa situazione è necessario rinunciare all'analizzatore e utilizzare la sostituzione delle funzioni di gestione della memoria.

Abbiamo esaminato l'algoritmo per un grafo semplice, privo di diramazioni. Tuttavia, questo approccio può essere applicato anche ad altri casi, ovviamente con maggiore complessità, ma l'idea rimarrà la stessa.

Nell'articolo seguente, esamineremo la questione della valutazione del carico sul ticker e i metodi per affrontare un'eccessiva carico computazionale nel media streamer.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster