Condivisione della tecnologia

Server reattore ad alta concorrenza [medio]

2024-07-12

한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina

4. Controllo del processo e sincronizzazione del processo

1. Segnale

1.1 Concetti base sui segnali

Il segnale (segnale) è un'interruzione del software. È un metodo per trasmettere messaggi tra processi. Viene utilizzato per notificare al processo che si è verificato un evento, ma non può passare alcun dato al processo.

Ci sono molti motivi per cui i segnali vengono generati nella Shell, è possibile utilizzarlikillEkillallComando per inviare il segnale:

kill -信号的类型 进程编号
killall -信号的类型 进程名
  • 1
  • 2

1.2 Tipologie di segnali

nome del segnalevalore del segnaleAzione di elaborazione predefinitaMotivo della segnalazione
ISCRIVITI1UNIl terminale si blocca o il processo di controllo termina
SIGINT2UNInterruzione da tastiera Ctrl+c
SIGQUIT3CViene premuto il tasto Esc della tastiera
Sigillo4CIstruzione illegale
Sigtrap5Cistruzioni sui punti di interruzione
Sigabrt6CSegnale di interruzione emesso da abort(3)
SIGBUS7Cerrore dell'autobus
SIGFPE8Ceccezione in virgola mobile
SIGILLO9UNkill -9 uccide il processo, questo segnale non può essere catturato e ignorato
SIGUSR110UNSegnale definito dall'utente 1
SIGSEGV11CRiferimento di memoria non valido (array fuori dai limiti, operazione con puntatore nullo)
SIGUSR212UNSegnale definito dall'utente 2
TUBO PER SIGILLO13UNScrivere i dati in una pipe senza un processo di lettura
Sigillo14UNSegnale sveglia, segnale inviato dalla funzione alert()
SIGTERM15UNSegnale di terminazione, il segnale inviato per impostazione predefinita
Sig.ra16UNerrore di pila
SIGILLATO17BEmesso al termine del processo figlio
SIGCONT18DRiprendere un processo interrotto
SIGSTOP19DInterrompere il processo
SigtTP20DIl terminale preme il tasto stop
Sigillo21DIl processo in background richiede di leggere il terminale
SIGTTOU22DIl processo in background richiede di scrivere sul terminale
Sigillo23BRilevamento delle condizioni di emergenza (prese)
SIGXCPU24CLimite di tempo della CPU superato
SigXFSZ25CLimite dimensione file superato
SIGVTALRM26UNsegnale dell'orologio virtuale
Sigprof27UNAnalizzare i segnali di clock
VERRICELLO SIGILLANTE28BLa dimensione della finestra cambia
Sondaggio Sigma29BPolling (Sistema V)
Sigmawr30UNmancanza di energia elettrica
SIGSISTEMI31CChiamata di sistema illegale

L'azione predefinita di A è terminare il processo.

L'azione predefinita di B è ignorare questo segnale.

L'azione predefinita di C è terminare il processo ed eseguire un dump dell'immagine del kernel.

L'azione predefinita di D è arrestare il processo e il programma che entra nello stato interrotto può continuare l'esecuzione.

1.3 Elaborazione del segnale

I processi possono gestire i segnali in tre modi:

  1. L'operazione predefinita del sistema viene utilizzata per gestire questo segnale. L'operazione predefinita della maggior parte dei segnali consiste nel terminare il processo.
  2. Imposta la funzione di elaborazione dell'interruzione Dopo aver ricevuto il segnale, la funzione lo gestirà.
  3. Ignora un segnale e non fare nulla con il segnale come se non fosse mai successo.

signal()Le funzioni possono impostare il modo in cui il programma gestisce i segnali.

Dichiarazione di funzione:

#include <signal.h>

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
  • 1
  • 2
  • 3
  • 4

Descrizione dei parametri:

  • sig:Specificare il segnale da catturare.
  • func : Puntatore alla funzione di elaborazione del segnale. La funzione di elaborazione deve ricevere un parametro intero, che è il numero del segnale catturato.
  1. SIG_DFL La macro :SIG_DFL rappresenta il metodo di elaborazione del segnale predefinito.utilizzoSIG_DFLCOMEsignalIl secondo parametro della funzione indica che per il segnale viene utilizzato il metodo di elaborazione predefinito del sistema.
  2. SIG_IGN La macro :SIG_IGN significa ignorare il segnale.utilizzoSIG_IGNCOMEsignal Il secondo parametro della funzione indica che quando il processo riceve il segnale, lo ignorerà e non eseguirà alcuna elaborazione. Ciò può impedire che i processi vengano terminati o interrotti inaspettatamente in determinate circostanze.
  3. SIG_ERR:SIG_ERR Le macro vengono utilizzate per indicare gli errori.non è cosìsignalIl secondo parametro della funzione viene invece utilizzato comesignal Il valore restituito dalla funzione indica che la chiamata non è riuscita.SesignalSe la chiamata alla funzione fallisce, verrà restituitaSIG_ERR .Questo viene in genere utilizzato per rilevare ed elaboraresignalErrore nella chiamata di funzione.

immagine-20240709113614147

immagine-20240709113230874

immagine-20240709113240944

1.4 A cosa servono i segnali?

Il programma di servizio viene eseguito in background. Se vuoi fermarlo, ucciderlo non è una buona idea, perché quando il processo viene interrotto, muore improvvisamente e non viene organizzato alcun lavoro successivo.

Se si invia un segnale al programma di servizio, dopo aver ricevuto il segnale, il programma di servizio chiama una funzione e scrive il codice successivo nella funzione, e il programma può uscire in modo pianificato.

L'invio di un segnale pari a 0 al programma di servizio può rilevare se il programma è attivo.

immagine-20240709135336848

1.5 Invio di segnali

Il sistema operativo Linux fornisce kill Ekillall Il comando invia un segnale al programma Nel programma è possibile utilizzarekill() Le funzioni di libreria inviano segnali ad altri processi.

Dichiarazione di funzione:

int kill(pid_t pid, int sig);
  • 1

kill() La funzione accetta i parametrisig Il segnale specificato viene passato al parametropid processo specificato.

parametro pid Ci sono diverse situazioni:

  1. pid > 0 Passa il segnale al processo comepid processi.
  2. pid = 0 Passa il segnale a tutti i processi nello stesso gruppo di processi del processo corrente Viene spesso utilizzato dal processo genitore per inviare segnali al processo figlio. Si noti che questo comportamento dipende dall'implementazione del sistema.
  3. pid < -1 Passa il segnale all'ID del gruppo di processi di|pid| di tutti i processi.
  4. pid = -1 Passa il segnale a tutti i processi che dispongono dell'autorizzazione per inviare il segnale, ma non al processo che ha inviato il segnale.

2. Conclusione del processo

Esistono 8 modi per terminare un processo, 5 dei quali sono terminazioni normali, ovvero:

  1. esistere main() Per le funzionireturn ritorno;
  2. Chiamato in qualsiasi funzione exit() funzione;
  3. Chiamato in qualsiasi funzione _exit() O_Exit() funzione;
  4. L'ultimo thread inizia dalla sua routine di avvio (funzione principale del thread) con return ritorno;
  5. Chiamato nell'ultimo thread pthread_exit() ritorno;

Esistono tre modi per terminare in modo anomalo:

  1. trasferimento abort() interruzione della funzione;
  2. Viene ricevuto un segnale;
  3. L'ultimo thread risponde alla richiesta di annullamento.

2.1 Stato di conclusione del processo

esistere main() Nella funzione,return In caso contrario, il valore restituito è lo stato di terminazionereturn dichiarazione o chiamataexit(), allora lo stato di terminazione del processo è 0.

Nella shell, visualizza lo stato di terminazione del processo:

echo $?
  • 1

3 funzioni per terminare il processo normalmente (exit() E_Exit() è specificato dalla ISO C,_exit() è specificato da POSIX):

void exit(int status);
void _exit(int status);
void _Exit(int status);
  • 1
  • 2
  • 3

status Lo stato di conclusione del processo.

immagine-20240709143530327

immagine-20240709143615950

2.2 Problema di rilascio delle risorse

  • return Indica che al termine della funzione verrà chiamato il distruttore dell'oggetto locale.main() in funzionereturn Viene chiamato anche il distruttore dell'oggetto globale.
  • exit() Indica di terminare il processo, non verrà chiamato il distruttore dell'oggetto locale, verrà chiamato solo il distruttore dell'oggetto globale.
  • _exit() E_Exit() Esci direttamente e non verrà eseguita alcuna operazione di pulizia.

2.3 Funzione di conclusione del processo

Il processo è disponibile atexit() La registrazione delle funzioni termina le funzioni (fino a 32), queste funzioni sarannoexit() Chiamato automaticamente.

int atexit(void (*function)(void));
  • 1

exit() L'ordine in cui vengono richiamate le funzioni di terminazione è invertito dal momento della registrazione.

immagine-20240709143824286

immagine-20240709143830549

3. Richiamare il programma eseguibile

3.1 funzione sistema()

system()La funzione fornisce un metodo semplice per eseguire il programma, passando il programma e i parametri che devono essere eseguiti come una stringa.system()Funziona e basta.

Dichiarazione di funzione:

int system(const char * string);
  • 1

system()Il valore restituito dalla funzione è più problematico.

  1. Se il programma eseguito non esiste,system()La funzione restituisce un valore diverso da zero;
  2. Se l'esecuzione del programma ha esito positivo e lo stato di esecuzione del programma eseguito è 0,system()La funzione restituisce 0;
  3. Se l'esecuzione del programma ha esito positivo e lo stato di terminazione del programma eseguito non è 0,system()La funzione restituisce un valore diverso da zero.

3.2 famiglia di funzioni esecutive

execLe famiglie di funzioni forniscono un altro modo per chiamare programmi (binari o script di shell) all'interno di un processo.

execLa famiglia di funzioni è dichiarata come segue:

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

Avviso

  1. Se l'esecuzione del programma fallisce, viene restituito direttamente -1 e viene memorizzato il motivo dell'erroreerrnomezzo.
  2. Il numero di processo del nuovo processo è lo stesso del processo originale, ma il nuovo processo sostituisce il segmento di codice, il segmento di dati e lo stack del processo originale.
  3. Se l'esecuzione ha esito positivo, la funzione non verrà restituita. Se richiamata con successo nel programma principaleexecSuccessivamente, il programma chiamato sostituirà il programma chiamante, ovveroexecNessun codice dopo la funzione verrà eseguito.
  4. Nello sviluppo attuale, il più comunemente usato èexecl()Eexecv(), altri sono usati raramente.

4. Creare un processo

4.1 Processi Linux 0, 1 e 2

Tutti i processi nell'intero sistema Linux sono disposti in una struttura ad albero.

  • **Processo n. 0 (processo di sistema)** è l'antenato di tutti i processi. Ha creato i processi n. 1 e n. 2.
  • **Processo n. 1 (systemd)** è responsabile dell'esecuzione dell'inizializzazione del kernel e della configurazione del sistema.
  • **Processo n. 2 (kthreadd)** è responsabile della pianificazione e della gestione di tutti i thread del kernel.

utilizzopstreeÈ possibile visualizzare l'albero del processo con il comando:

pstree -p 进程编号
  • 1

4.2 Identificazione del processo

Ogni processo ha un ID processo univoco rappresentato da un numero intero non negativo. Sebbene univoci, gli ID processo possono essere riutilizzati. Quando un processo termina, il suo ID processo diventa un candidato per il riutilizzo. Linux utilizza un algoritmo di riutilizzo ritardato in modo che l'ID di un processo appena creato sia diverso dall'ID utilizzato dal processo recentemente terminato. Ciò impedisce che i nuovi processi vengano confusi con un processo terminato che utilizza lo stesso ID.

Funzione per ottenere l'ID del processo:

pid_t getpid(void);    // 获取当前进程的ID。
pid_t getppid(void);   // 获取父进程的ID。
  • 1
  • 2

4.3 funzione fork()

Un processo esistente può chiamarefork()La funzione crea un nuovo processo.

Dichiarazione di funzione:

pid_t fork(void);
  • 1

Dipende dafork()Il nuovo processo creato è chiamato processo figlio.

fork() La funzione viene chiamata una volta ma restituisce due volte. La differenza tra i due valori restituiti è che il valore restituito del processo figlio è 0, mentre il valore restituito del processo padre è l'ID del processo figlio appena creato.

Il processo figlio e il processo padre continuano ad essere eseguitifork()Il codice successivo, Il processo figlio è una copia del processo genitore. Il processo figlio ha una copia dello spazio dati, dell'heap e dello stack del processo genitore (nota: il processo figlio possiede una copia, non condivisa con il processo genitore).

fork()Successivamente, l'ordine di esecuzione dei processi genitore e figlio non è definito.

immagine-20240709221535371

immagine-20240709221546617

4.4 Due usi di fork()

  1. Il processo genitore vuole copiare se stesso, quindi il processo genitore e il processo figlio eseguono ciascuno codice diverso.Questo utilizzo è molto comune nei programmi di servizi di rete. Il processo principale attende la richiesta di connessione del client. Quando arriva la richiesta, il processo principale chiamafork(), lascia che il processo figlio gestisca queste richieste, mentre il processo genitore continua ad attendere la successiva richiesta di connessione.
  2. Il processo vuole eseguire un altro programma.Questo utilizzo è molto comune nelle shell, da cui inizia il processo figliofork()Chiamato subito dopo il rientroexec

4.5 File condivisi

fork()Una caratteristica è che i descrittori di file aperti nel processo genitore verranno copiati nel processo figlio e il processo genitore e il processo figlio condividono lo stesso offset di file.

Se un processo genitore e un processo figlio scrivono su un file puntato dallo stesso descrittore senza alcuna forma di sincronizzazione, il loro output potrebbe essere mescolato tra loro.

immagine-20240709222929369

immagine-20240709222803641

A questo punto puoi vedere che ci sono solo 100.000 righe di dati.

immagine-20240709222853769

immagine-20240709223236254

In questo momento, dovrebbero esserci 200.000 righe di dati. Una riga in meno potrebbe essere dovuta al fatto che l'operazione di scrittura del file non è atomica. In assenza di un meccanismo di sincronizzazione, due processi potrebbero tentare di scrivere parti diverse del file contemporaneamente. causando il fallimento della scrittura.

4.6 Funzione vfork()

vfork()Le chiamate di funzione e i valori restituiti sono gli stessi difork()Uguale, ma la loro semantica è diversa.

vfork()La funzione viene utilizzata per creare un nuovo processo il cui scopo èexecUn nuovo programma che non copia lo spazio degli indirizzi del processo genitore perché il processo figlio chiama immediatamenteexec , quindi lo spazio degli indirizzi del processo genitore non verrà utilizzato. Se il processo figlio utilizza lo spazio degli indirizzi del processo padre, potrebbero verificarsi risultati sconosciuti.

vfork()Efork()Un'altra differenza è:vfork()Assicurati che il processo figlio venga eseguito per primo e chiamalo nel processo figlioexecOexitQuindi il processo genitore riprende l'operazione.

5. Processo zombi

Nel sistema operativo, un processo zombie si riferisce a un processo figlio che è terminato ma il suo processo genitore non ha ancora letto il suo stato di uscita. Sebbene il processo zombie non sia più in esecuzione, occupa ancora una voce nella tabella dei processi in modo che il kernel possa salvare le informazioni sullo stato di uscita del processo (come ID del processo, stato di uscita, ecc.) finché il processo genitore non legge queste informazioni.

5.1 Cause dei processi zombie

Se il processo genitore termina prima del processo figlio, il processo figlio sarà ospitato dal processo 1 (questo è anche un modo per consentire l'esecuzione del processo in background).

Se il processo figlio termina prima del processo genitore e il processo genitore non elabora le informazioni di uscita del processo figlio, allora il processo figlio diventerà un processo zombie.

5.2 Il danno dei processi zombie

Il kernel conserva una struttura dati per ogni processo figlio, incluso il numero del processo, lo stato di terminazione, il tempo della CPU utilizzato, ecc. Se il processo genitore elabora le informazioni di uscita del processo figlio, il kernel rilascerà questa struttura dati. Se il processo genitore non elabora le informazioni di uscita del processo figlio, il kernel non rilascerà questa struttura dati e il numero di processo del processo figlio sarà sempre occupato. I numeri di processo disponibili nel sistema sono limitati Se viene generato un numero elevato di processi zombie, il sistema non sarà in grado di generare nuovi processi perché non sono disponibili numeri di processo.

5.3 Metodi per evitare processi zombie

  1. Gestire il segnale SIGCHLD : Quando il processo figlio esce, il kernel invierà il segnale SIGCHLD al processo genitore.Se il processo padre utilizzasignal(SIGCHLD, SIG_IGN)Notifica al kernel che non è interessato all'uscita del processo figlio e la sua struttura dati verrà rilasciata immediatamente dopo l'uscita del processo figlio.
  2. utilizzowait()/waitpid()funzione: Il processo genitore attende la fine del processo figlio chiamando queste funzioni e ottiene il suo stato di uscita, liberando così le risorse occupate dal processo figlio.
pid_t wait(int *stat_loc); 
pid_t waitpid(pid_t pid, int *stat_loc, int options); 
pid_t wait3(int *status, int options, struct rusage *rusage); 
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
  • 1
  • 2
  • 3
  • 4

Il valore restituito è il numero del processo figlio.

stat_loc Le informazioni sulla terminazione del processo figlio sono:

a) Se terminata normalmente, la macro WIFEXITED(stat_loc) Restituisce vero, macroWEXITSTATUS(stat_loc) È possibile ottenere lo stato di risoluzione;

b) Se termina in modo anomalo, la macro WTERMSIG(stat_loc) Ottiene il segnale per terminare il processo.

immagine-20240709230911352

immagine-20240709231034423

immagine-20240709231050581

immagine-20240709231124375

immagine-20240709231140813

Se il processo principale è occupato, puoi acquisire SIGCHLD Segnale, richiamato nella funzione di elaborazione del segnalewait()/waitpid()

immagine-20240709231439475

immagine-20240709231422927

6.Processi e segnali multipli

[Invia segnali tra processi](##1.5 Invia segnali)

In un programma di servizio multiprocesso, se il sottoprocesso riceve un segnale di uscita, uscirà da solo.

Se il processo genitore riceve un segnale di uscita, dovrebbe inviare segnali di uscita a tutti i processi figli e poi uscire da solo.

immagine-20240711222919564

immagine-20240711222900141

immagine-20240711223111481

7. Memoria condivisa

I multi-thread condividono lo spazio degli indirizzi di un processo,Se più thread devono accedere alla stessa memoria, utilizza semplicemente le variabili globali.

In più processi, lo spazio degli indirizzi di ciascun processo è indipendente e non condiviso.Se più processi devono accedere alla stessa memoria, non è possibile utilizzare le variabili globali, è possibile utilizzare solo la memoria condivisa.

La memoria condivisa consente a più processi (non è richiesta alcuna relazione di sangue tra processi) di accedere allo stesso spazio di memoria. È il modo più efficace per condividere e trasferire dati tra più processi. I processi possono connettere la memoria condivisa al proprio spazio di indirizzi. Se un processo modifica i dati nella memoria condivisa, cambieranno anche i dati letti da altri processi.

La memoria condivisa non fornisce un meccanismo di blocco, vale a dire, quando un processo legge/scrive la memoria condivisa, non impedisce ad altri processi di leggerla/scriverla.Se vuoi bloccare la lettura/scrittura della memoria condivisa, puoi usare un semaforo . Linux fornisce una serie di funzioni per il funzionamento della memoria condivisa.

7.1 funzione shhmget

Questa funzione viene utilizzata per creare/acquisire memoria condivisa.

 int shmget(key_t key, size_t size, int shmflg);
  • 1
  • chiave Il valore della chiave della memoria condivisa è un numero intero (typedef unsigned int key_t), generalmente in esadecimale, ad esempio 0x5005, le chiavi di diverse memorie condivise non possono essere le stesse.
  • misurare Dimensione della memoria condivisa, in byte.
  • Sbattimento Le autorizzazioni di accesso per la memoria condivisa sono le stesse delle autorizzazioni per i file, ad esempio0666|IPC_CREAT Indica che se la memoria condivisa non esiste, crearla.
  • valore di ritorno: Restituisce l'id della memoria condivisa (un numero intero maggiore di 0) in caso di successo, -1 in caso di fallimento (il sistema ha memoria insufficiente e nessuna autorizzazione).

immagine-20240711224223200

immagine-20240711224212293

utilizzo ipcs -m È possibile visualizzare la memoria condivisa del sistema, tra cui: chiave, ID memoria condivisa (shmid), proprietario, autorizzazioni (permanenti) e dimensione (byte).

utilizzo ipcrm -m 共享内存id La memoria condivisa può essere cancellata manualmente come segue:

immagine-20240711225202860

Nota: i contenitori non possono essere utilizzati per i tipi di dati nella memoria condivisa, è possibile utilizzare solo i tipi di dati di base.

7.2 funzione shmat

Questa funzione viene utilizzata per connettere la memoria condivisa allo spazio degli indirizzi del processo corrente.

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 1
  • smorfia Dipende dashmget() L'identificatore di memoria condivisa restituito dalla funzione.
  • Sbagliato Specificare la posizione dell'indirizzo in cui la memoria condivisa è connessa al processo corrente. Di solito inserire 0 per consentire al sistema di selezionare l'indirizzo della memoria condivisa.
  • Sbattimento Bit flag, solitamente riempito con 0.

Restituisce l'indirizzo iniziale della memoria condivisa quando la chiamata ha esito positivo e restituisce quando fallisce. (void *)-1

7.3 funzione shmdt

Questa funzione viene utilizzata per staccare la memoria condivisa dal processo corrente, che è equivalente a shmat() L'operazione inversa di una funzione.

int shmdt(const void *shmaddr);
  • 1
  • Sbagliato shmat() L'indirizzo restituito dalla funzione.

Restituisce 0 se la chiamata ha esito positivo e -1 se fallisce.

7.4 funzione shmctl

Questa funzione viene utilizzata per gestire la memoria condivisa. L'operazione più comunemente utilizzata è eliminare la memoria condivisa.

int shmctl(int shmid, int command, struct shmid_ds *buf);
  • 1
  • smorfia shmget() L'ID della memoria condivisa restituito dalla funzione.
  • comando Istruzioni per il funzionamento della memoria condivisa Se si desidera eliminare la memoria condivisa, compilareIPC_RMID
  • buffafa L'indirizzo della struttura dati che gestisce la memoria condivisa. Se si desidera eliminare la memoria condivisa, inserire 0.

Restituisce 0 se la chiamata ha esito positivo e -1 se fallisce.

Nota, usa root La memoria condivisa creata non può essere eliminata dagli utenti ordinari indipendentemente dalle autorizzazioni create.

immagine-20240711230653886

immagine-20240711230522921

7.5 Coda circolare

7.6 Coda circolare basata sulla memoria condivisa

Restituisce 0 se la chiamata ha esito positivo e -1 se fallisce.

7.4 funzione shmctl

Questa funzione viene utilizzata per gestire la memoria condivisa. L'operazione più comunemente utilizzata è eliminare la memoria condivisa.

int shmctl(int shmid, int command, struct shmid_ds *buf);
  • 1
  • smorfia shmget() L'ID della memoria condivisa restituito dalla funzione.
  • comando Istruzioni per il funzionamento della memoria condivisa Se si desidera eliminare la memoria condivisa, compilareIPC_RMID
  • buffafa L'indirizzo della struttura dati che gestisce la memoria condivisa. Se si desidera eliminare la memoria condivisa, inserire 0.

Restituisce 0 se la chiamata ha esito positivo e -1 se fallisce.

Nota, usa root La memoria condivisa creata non può essere eliminata dagli utenti ordinari indipendentemente dalle autorizzazioni create.

[Le immagini del collegamento esterno sono in fase di trasferimento...(img-v6qW3XRA-1720711279572)]

[Le immagini del collegamento esterno sono in fase di trasferimento...(img-CG0tGAne-1720711279572)]Il trasferimento dell'immagine del collegamento esterno non è riuscito. Il sito di origine potrebbe avere un meccanismo anti-leeching. Si consiglia di salvare l'immagine e caricarla direttamente.
Il trasferimento dell'immagine del collegamento esterno non è riuscito. Il sito di origine potrebbe avere un meccanismo anti-leeching. Si consiglia di salvare l'immagine e caricarla direttamente.
Il trasferimento dell'immagine del collegamento esterno non è riuscito. Il sito di origine potrebbe avere un meccanismo anti-leeching. Si consiglia di salvare l'immagine e caricarla direttamente.

7.5 Coda circolare

7.6 Coda circolare basata sulla memoria condivisa