Multitasking Linux utilizzando la riga di comando. Processi leggeri: esame anatomico dei thread del programma Linux

Insieme all'università.


Naturalmente, avevo 10 minuti, quindi la presentazione - al galoppo in tutta Europa, molto viene semplificato, molto viene trascurato.

Un po' di storia

Una storia relativamente dettagliata della creazione del kernel Linux può essere trovata nel famoso libro di Linus Torvalds "Just for fun". Siamo interessati ai seguenti fatti da esso:

    Il nucleo è stato creato nel 1991 da uno studente dell'Università di Helsinki, Linus Torvalds;

    Come piattaforma, ha usato il sistema operativo Minix, scritto dal suo insegnante Andrew Tanenbaum, in esecuzione su un personal computer con un processore Intel 80386;

    Come esempio per l'imitazione, ha usato il sistema operativo della famiglia Unix e come guida: prima lo standard POSIX e poi solo i codici sorgente dei programmi dal pacchetto GNU (bash, gcc, ecc.).

Questi fatti hanno determinato in gran parte i percorsi di sviluppo del nucleo in futuro, le loro conseguenze sono evidenti nel nucleo moderno.

In particolare, è noto che un tempo i sistemi Unix erano divisi in due fazioni: discendenti di UNIX System V Release 4 (famiglia SVR4) contro discendenti di Berkley Software Distribution v4.2 (BSD4.2). Linux appartiene per la maggior parte alla prima famiglia, ma prende in prestito alcune idee essenziali dalla seconda.

Il nucleo in numeri

  • Circa 30 mila file
  • Circa 8 milioni di righe di codice (esclusi i commenti)
  • Il repository richiede circa 1 GB
  • linux-2.6.33.tar.bz2: 63 Mb
  • patch-2.6.33.bz2: 10 Mb, circa 1,7 milioni di righe modificate
  • Circa 6.000 persone il cui codice è nel core

Informazioni sull'architettura del kernel

Tutti (o quasi) i processori che un produttore di sistemi operativi simile a Unix ha mai visto hanno il supporto hardware per la condivisione dei privilegi. Un codice può fare tutto (compresa la comunicazione diretta con l'apparecchiatura), l'altro non può fare quasi nulla. Tradizionalmente parlano di "terra del kernel" e "terra dell'utente". Le diverse architetture dei kernel del sistema operativo differiscono principalmente nel loro approccio alla risposta alla domanda: quali parti del codice del sistema operativo dovrebbero essere eseguite nella terra del kernel e quali nella terra dell'utente? Il fatto è che per la stragrande maggioranza dei processori, il passaggio tra le due modalità richiede una notevole quantità di tempo. Si distinguono i seguenti approcci:

    Tradizionale: nucleo monolitico. Tutto il codice del kernel è compilato in un unico grande file binario. L'intero kernel viene eseguito in modalità kernel;

    L'opposto, rivoluzionario: il microkernel. In modalità kernel vengono eseguite solo le parti più necessarie, tutto il resto viene fatto in modalità utente;

    Nell'approccio tradizionale, in seguito è apparsa un'opzione: un nucleo modulare. Tutto viene eseguito in modalità kernel, ma il kernel è compilato come un grande file binario e un mucchio di piccoli moduli che possono essere caricati e scaricati secondo necessità;

    E, naturalmente, tutti i tipi di architetture ibride.

Il kernel Linux è iniziato come monolitico (guardando gli Unix di allora). Il moderno kernel Linux è modulare. Rispetto a un microkernel, un kernel monolitico (o modulare) fornisce prestazioni significativamente più elevate, ma impone requisiti significativamente più rigorosi sulla qualità del codice dei vari componenti. Quindi, in un sistema con un microkernel, un driver FS "crash" verrà riavviato senza influire sul funzionamento del sistema; un driver FS bloccato in un kernel monolitico è un panico del kernel e l'arresto del sistema.

Sottosistemi del kernel Linux

C'è un diagramma abbastanza noto che descrive i principali sottosistemi del kernel Linux e come interagiscono. Eccola:

In realtà, al momento è solo visibile che ci sono molte parti e le loro interconnessioni sono molto complesse. Pertanto, considereremo un diagramma semplificato:

Chiamate di sistema

Il livello della chiamata di sistema è la parte del kernel Linux più vicina al programmatore dell'applicazione. Le chiamate di sistema forniscono un'interfaccia utilizzata dalle applicazioni: l'API del kernel. La maggior parte delle chiamate di sistema Linux sono prese dallo standard POSIX, ma ce ne sono alcune che sono specifiche per Sistema Linux sfide.

Vale la pena notare qui alcune differenze nell'approccio alla progettazione delle API del kernel nei sistemi Unix da un lato e in Windows e altri discendenti ideologici di VMS dall'altro. I progettisti Unix preferiscono fornire dieci chiamate di sistema con un parametro invece di una chiamata di sistema con venti parametri. Un classico esempio è la creazione di processi. V Funzione Windows per creare un processo - CreateProcess() - richiede 10 argomenti, di cui 5 sono strutture. Al contrario, i sistemi Unix forniscono due chiamate di sistema (fork() ed exec()), la prima senza parametri e la seconda con tre parametri.

Le chiamate di sistema, a loro volta, chiamano le funzioni dei sottosistemi del kernel di livello inferiore.

Gestione della memoria

Il kernel Linux usa una pagina come unità minima di memoria. La dimensione della pagina può variare in base all'hardware; su x86 è 4K. Per memorizzare le informazioni su una pagina di memoria fisica (indirizzo fisico, proprietà, modalità di utilizzo, ecc.), viene utilizzata una struttura di pagina speciale di 40 byte.

Il kernel utilizza le capacità dei processori moderni per organizzare la memoria virtuale. Manipolando le directory delle pagine di memoria virtuale, ogni processo riceve uno spazio di indirizzi di 4 GB (su architetture a 32 bit). Parte di questo spazio è disponibile al processo solo per la lettura o l'esecuzione: le interfacce del kernel sono mappate lì.

È significativo che un processo in esecuzione nello spazio utente, nella maggior parte dei casi, "non sappia" dove si trovano i suoi dati: nella RAM o nel file di paging. Un processo può chiedere al sistema di allocare memoria nella RAM, ma il sistema non è obbligato a concedere tale richiesta.

Gestione dei processi

Il kernel Linux è stato multitasking dal primo giorno. Ha un supporto multitasking preventivo piuttosto buono ormai.

Nella storia sono stati conosciuti due tipi di multitasking:

Multitasking aziendale.

In questo caso, ogni processo trasferisce il controllo a qualcun altro quando lo ritiene opportuno. Ciò consente di risparmiare tempo per la commutazione delle modalità del processore, ma, ovviamente, non è necessario parlare dell'affidabilità di un tale sistema: un processo congelato non trasferirà il controllo a nessuno. Questa opzione non è utilizzata nei moderni sistemi operativi.

Multitasking preventivo.

Il kernel del sistema operativo alloca un certo quanto di tempo del processore a ciascun processo e trasferisce "forzatamente" il controllo a un altro processo alla scadenza di questo quanto. Ciò crea un sovraccarico per la commutazione delle modalità del processore e il calcolo delle priorità, ma aumenta l'affidabilità e le prestazioni.

I processi di commutazione in Linux possono essere eseguiti al verificarsi di due eventi: un'interruzione hardware o un'interruzione del timer. La frequenza di interruzione del timer viene impostata durante la compilazione del kernel nell'intervallo da 100Hz a 1000Hz. Gli interrupt hardware si verificano quasi più spesso: basta spostare il mouse o premere un pulsante sulla tastiera e i dispositivi interni del computer generano gli interrupt. A partire dalla versione 2.6.23, è diventato possibile creare un kernel che non utilizza la commutazione di processo basata su timer. Ciò consente di ridurre il consumo energetico quando il computer è inattivo.

Il process scheduler utilizza un algoritmo piuttosto complesso basato sul calcolo delle priorità dei processi. Tra i processi spiccano quelli che richiedono molto tempo del processore e quelli che dedicano più tempo all'I/O. Sulla base di queste informazioni, le priorità dei processi vengono ricalcolate regolarmente. Utilizza anche valori piacevoli definiti dall'utente per i singoli processi.

Oltre al multitasking in modalità utente, il kernel Linux utilizza il multitasking in modalità kernel: il kernel stesso è multithread.

I kernel Unix tradizionali avevano il seguente... beh, se non un problema, allora una caratteristica: il kernel stesso non era preventivo. Esempio: il processo / usr / bin / cat vuole aprire un file /media/cdrom/file.txt e usa la chiamata di sistema per questo aprire ()... Il controllo viene trasferito al kernel. Il kernel rileva che il file si trova sul CD e avvia l'inizializzazione dell'unità (rotazione del disco, ecc.). Questo richiede una notevole quantità di tempo. Per tutto questo tempo, il controllo non viene trasferito ai processi dell'utente, poiché lo scheduler non è attivo durante l'esecuzione del codice del kernel. Tutti i processi utente attendono il completamento di questa chiamata open().

Al contrario, il moderno kernel Linux è completamente superato. Lo scheduler viene disabilitato solo per brevi periodi di tempo in cui il kernel non può essere interrotto in alcun modo, ad esempio durante l'inizializzazione di alcuni dispositivi che richiedono l'esecuzione di determinate azioni con ritardi fissi. In qualsiasi altro momento, un thread del kernel può essere anticipato e il controllo può essere trasferito a un altro thread del kernel o processo utente.

Sottosistema di rete

Il sottosistema di rete del kernel del sistema operativo, in teoria, può funzionare quasi tutto nello spazio utente: non sono necessari privilegi per operazioni come la formazione di pacchetti TCP/IP. Tuttavia, nei moderni sistemi operativi, in particolare quelli utilizzati su server ad alto carico, dall'intero stack di rete - l'intera catena dalla formazione del pacchetto al lavoro direttamente con l'adattatore di rete - sono richieste le massime prestazioni. Un sottosistema di rete in spazio utente dovrebbe accedere costantemente al kernel per comunicare con l'hardware di rete e ciò comporterebbe un sovraccarico molto significativo.

Il sottosistema di rete Linux fornisce le seguenti funzionalità:

    Astrazione socket;

    Stack di protocollo di rete (TCP/IP, UDP/IP, IPX/SPX, AppleTalk e molti altri);

    Instradamento;

    Filtro pacchetti (modulo Netfilter);

    Astrazione delle interfacce di rete.

Diversi sistemi Unix utilizzavano due diverse API per fornire accesso alle funzionalità di rete: Transport Layer Interface (TLI) da SVR4 e socket da BSD. L'interfaccia TLI, da un lato, è strettamente legata al sottosistema STREAMS, assente nel kernel Linux, e dall'altro, non è compatibile con l'interfaccia socket. Pertanto, Linux utilizza un'interfaccia socket presa dalla famiglia BSD.

File system

File system virtuale (VFS)

Dal punto di vista dell'applicazione, esiste un solo file system su un sistema operativo simile a Unix. È un albero di directory che cresce dalla "radice". Le applicazioni, nella maggior parte dei casi, non sono interessate al supporto su cui si trovano i dati del file; possono trovarsi su un disco rigido, un disco ottico, un'unità flash o anche su un altro computer e in un altro continente. Questa astrazione e il sottosistema che la implementa sono chiamati file system virtuale (VFS).

Vale la pena notare che VFS nel kernel Linux è implementato tenendo conto delle idee di OOP. Ad esempio, il kernel considera un insieme di inode, ognuno dei quali contiene (tra l'altro):

    Dati da inode su disco (diritti di accesso, dimensione del file, ecc.);

    Puntatore a una struttura che descrive il driver FS a cui appartiene questo inode;

    Puntatore alla struttura delle operazioni inode, che, a sua volta, contiene puntatori a funzioni per creare un inode, modificarne gli attributi, ecc., implementate in uno specifico driver FS.

Le strutture del kernel, che descrivono altre entità FS - superblocco, elemento directory, file, sono disposte in modo simile.

Driver FS

I driver FS, come puoi vedere dal diagramma, sono a un livello molto più alto dei driver di dispositivo. Ciò è dovuto al fatto che i driver FS non comunicano con alcun dispositivo. Autista file system implementa solo le funzioni che fornisce tramite l'interfaccia VFS. In questo caso i dati vengono scritti e letti nella/dalla pagina di memoria; quale di essi e quando verrà registrato sul supporto - decide il livello inferiore. Il fatto che i driver FS in Linux non comunichino con l'hardware ha reso possibile l'implementazione di uno speciale driver FUSE, che delega la funzionalità del driver FS a moduli che vengono eseguiti nello spazio utente.

cache della pagina

Questo sottosistema del kernel opera su pagine di memoria virtuale organizzate in un albero radix. Quando i dati vengono letti dal supporto, i dati vengono letti nella pagina allocata nella cache e la pagina rimane nella cache e il driver FS legge i dati da essa. Il driver FS scrive i dati nelle pagine di memoria che si trovano nella cache. Questo contrassegna queste pagine come sporche. Uno speciale thread del kernel, pdflush, bypassa regolarmente la cache e invia richieste di scrivere pagine sporche. Una pagina sporca scritta sul supporto viene contrassegnata nuovamente come vuota.

Blocco livello I/O

Questo sottosistema del kernel opera con code costituite da biostrutture. Ciascuna di queste strutture descrive un'operazione di I/O (relativamente parlando, una richiesta del modulo "scrivi questi dati nei blocchi ##141-142 del dispositivo /dev/hda1"). Per ogni processo che esegue I/O viene formata una propria coda. Da questo insieme di code viene creata una coda di richieste al driver di ogni dispositivo.

Programmatore I/O

Se si eseguono richieste di I/O del disco dalle applicazioni nell'ordine in cui vengono ricevute, le prestazioni del sistema saranno in media molto lente. Ciò è dovuto al fatto che l'operazione di ricerca del settore desiderato sull'hard disk è molto lenta. Pertanto, lo scheduler elabora le code di richiesta facendo due cose:

    Ordinamento: lo scheduler cerca di mettere in fila le query che indirizzano i settori disco vicini;

    Unione: se, a seguito dell'ordinamento, sono presenti più query che si rivolgono a settori posizionati in sequenza, è necessario combinarle in un'unica query.

Ci sono diversi schedulatori disponibili nel kernel moderno: Anticipatory, Deadline, CFQ, noop. C'è una versione del kernel di Con Kolivas con un altro scheduler - BFQ. Gli scheduler possono essere selezionati quando il kernel è compilato o quando si avvia.

Separatamente, dovremmo soffermarci sullo scheduler noop. Questo pianificatore non ordina o unisce le richieste, ma le inoltra ai driver di dispositivo nell'ordine in cui vengono ricevute. Sui sistemi con dischi rigidi convenzionali, questo pianificatore mostrerà prestazioni molto scarse. Tuttavia, stanno diventando comuni i sistemi che utilizzano supporti flash anziché dischi rigidi. Per tali supporti, il tempo di ricerca del settore è zero, quindi le operazioni di ordinamento e unione non sono necessarie. In tali sistemi, l'utilizzo dello scheduler noop non cambierà le prestazioni e il consumo di risorse diminuirà leggermente.

Gestione delle interruzioni

Quasi tutte le attuali architetture hardware utilizzano il concetto di interruzioni per comunicare con il software. Sembra questo. Un processo è in esecuzione sul processore (non importa se si tratta di un thread del kernel o di un processo utente). Si verifica un'interruzione dal dispositivo. Il processore viene distratto dalle attività correnti e passa il controllo all'indirizzo di memoria associato al dato numero di interruzione in una tabella di interruzione speciale. Il gestore di interrupt si trova a questo indirizzo. Il gestore esegue alcune azioni a seconda di cosa è successo esattamente a questo dispositivo, quindi il controllo viene trasferito allo scheduler del processo, che, a sua volta, decide a chi trasferire ulteriormente il controllo.

C'è una certa sottigliezza qui. Il fatto è che mentre il gestore di interrupt è in esecuzione, il task scheduler non è attivo. Questo non è sorprendente: si presume che il gestore di interrupt lavori direttamente con il dispositivo e il dispositivo potrebbe richiedere l'esecuzione di alcune azioni in un intervallo di tempo ristretto. Pertanto, se il gestore degli interrupt viene eseguito per lungo tempo, tutti gli altri processi e thread nel kernel attenderanno, il che di solito è inaccettabile.

Nel kernel Linux, qualsiasi interrupt hardware trasferisce il controllo alla funzione do_IRQ(). Questa funzione usa una tabella separata di gestori di interrupt registrati nel kernel per determinare dove trasferire il controllo successivamente.

Per garantire un runtime minimo nel contesto di un'interruzione, il kernel Linux utilizza una divisione dei gestori in metà superiore e inferiore. La metà superiore è una funzione registrata dal driver del dispositivo come gestore per un particolare interrupt. Fa solo il lavoro che va assolutamente fatto subito. Quindi registra un'altra funzione (la sua metà inferiore) e restituisce. L'Utilità di pianificazione trasferirà il controllo alla metà superiore registrata il prima possibile. In questo caso, nella maggior parte dei casi, il controllo viene trasferito alla metà inferiore immediatamente dopo il completamento della metà superiore. Ma allo stesso tempo, la metà inferiore funziona come un normale thread del kernel e può essere interrotta in qualsiasi momento, quindi ha il diritto di essere eseguita per tutto il tempo che desideri.

Ad esempio, si consideri il gestore di interrupt dalla scheda di rete, che segnala che è stato ricevuto un pacchetto ethernet. Questo gestore deve fare due cose:

    Prendere un pacchetto dal buffer NIC e segnalare alla NIC che il pacchetto è stato ricevuto dal sistema operativo. Questo va fatto subito dopo aver ricevuto un interrupt, in un millisecondo il buffer conterrà dati completamente diversi;

    Posiziona questo pacchetto in alcune strutture del kernel, scopri a quale protocollo appartiene, passalo alle funzioni di elaborazione appropriate. Questo dovrebbe essere fatto il più rapidamente possibile per massimizzare le prestazioni di rete, ma non necessariamente immediatamente.

Di conseguenza, il primo viene eseguito dalla metà superiore del conduttore e il secondo dall'inferiore.

I driver di periferica

La maggior parte dei driver di dispositivo sono generalmente compilati come moduli del kernel. Il driver del dispositivo riceve richieste da due lati:

    Dal dispositivo - tramite i gestori di interrupt registrati dal driver;

    Da varie parti del kernel - tramite un'API definita da uno specifico sottosistema del kernel e dal driver stesso.

Uno dei momenti più fastidiosi quando si passa da un ambiente basato su Windows all'utilizzo riga di comando- perdita del multitasking facile. Anche su Linux, se stai usando il sistema X Window, puoi usare il mouse per fare semplicemente clic su un nuovo programma e aprirlo. Sulla riga di comando, tuttavia, sei praticamente bloccato con il mono-tasking. In questo articolo, ti mostreremo come passare al multitasking in Linux usando la riga di comando.

Controllo del processo in background e in primo piano

Tuttavia, ci sono ancora modi per eseguire il multitasking in Linux e alcuni sono più completi di altri. Un modo integrato che non richiede alcun supplemento SoftwareÈ solo lo spostamento dei processi in background e in primo piano. Ci siamo. Tuttavia, presenta alcuni svantaggi.

non protetto

in primo luogo Per inviare un processo in background, devi prima sospenderlo. Non è possibile inviare in background un programma già in esecuzione e contemporaneamente mantenerlo.

secondo, è necessario suddividere il flusso di lavoro per avviare un nuovo comando. Devi uscire da ciò che stai facendo attualmente e digitare più comandi nella shell. Funziona, ma è scomodo.

In terzo luogo, dovresti fare attenzione agli output dei processi in background. Qualsiasi output da loro apparirà sulla riga di comando e interferirà con ciò che stai facendo attualmente. Quindi le attività in background devono reindirizzare il loro output a file separato, oppure devono essere completamente disattivati.

Queste carenze pongono enormi problemi con la gestione del processo in background e in primo piano. La soluzione migliore è utilizzare l'utilità della riga di comando "schermo" come mostrato di seguito.

Ma prima, aprirai una nuova sessione SSH

Non dimenticare che stai solo aprendo una nuova sessione SSH.

Può essere scomodo aprire sempre nuove sessioni. Ed è allora che hai bisogno di "schermo"

Utilità schermo ti consente di creare più flussi di lavoro aperti contemporaneamente: l'analogo più vicino di "finestre". Per impostazione predefinita, è disponibile nei normali repository Linux. Installalo su CentOS / RHEL con il seguente comando:

Schermata di installazione di Sudo yum

Apertura di una nuova schermata

Ora avvia una sessione digitando "schermo".

Questo creerà una finestra vuota all'interno della sessione SSH esistente e le darà il numero mostrato nel titolo in questo modo:

Il nostro schermo qui è numerato "0" come mostrato. In questo screenshot stiamo usando un comando fittizio "read" per bloccare il terminale e farlo attendere l'input. Ora diciamo che vogliamo fare qualcos'altro mentre aspettiamo.

Per aprire una nuova schermata e fare qualcos'altro, digitiamo:

Ctrl + un c

"Ctrl + a" è la scorciatoia da tastiera predefinita per controllare gli schermi in un programma schermo. Quello che inserisci dopo determina l'azione. Per esempio:

  • ctrl + a c - C attiva una nuova schermata
  • ctrl + a [numero]- passa a un numero di schermo specifico
  • ctrl + una k - K spegne la schermata corrente
  • ctrl + an - Vai allo schermo n
  • ctrl + a "- visualizza tutte le schermate attive nella sessione

Se premiamo "ctrl + a c", otteniamo una nuova schermata con un nuovo numero.

Puoi usare i tasti freccia per navigare nell'elenco e andare alla schermata che desideri.
Gli schermi sono la cosa più vicina a "finestre", proprio come un sistema a riga di comando Linux. Certo, non è facile come fare clic con il mouse, ma il sottosistema grafico richiede molte risorse. Con gli schermi, puoi ottenere quasi le stesse funzionalità e abilitare il multitasking completo!

Continuando l'argomento del multithreading nel kernel Linux. L'ultima volta che ho parlato degli interrupt, della loro gestione e dei tasklet, e poiché originariamente si presumeva che questo sarebbe stato un articolo, nella mia storia sulla coda di lavoro farò riferimento ai tasklet, supponendo che il lettore li conosca già.
Come l'ultima volta, cercherò di rendere la mia storia il più dettagliata e dettagliata possibile.

Articoli sul ciclo:

  1. Multitasking del kernel Linux: coda di lavoro

coda di lavoro

coda di lavoro sono entità più complesse e ingombranti dei tasklet. Non proverò nemmeno a descrivere tutte le sottigliezze dell'implementazione qui, ma la più importante, spero, analizzerò più o meno in dettaglio.
Le code di lavoro, come le tasklet, servono per la gestione degli interrupt posticipati (sebbene possano essere utilizzate per altri scopi), ma, a differenza delle tasklet, vengono eseguite nel contesto di un processo kernel, quindi non devono essere atomiche e possono usare sleep () funzione, vari strumenti di sincronizzazione, ecc.

Cerchiamo innanzitutto di capire come è generalmente organizzato il processo di elaborazione della coda di lavoro. Nell'immagine è mostrato in maniera molto approssimativa e semplificata, come tutto effettivamente accade, è descritto in dettaglio di seguito.

Diverse entità sono coinvolte in questa oscura vicenda.
in primo luogo, oggetto da lavoro(per brevità, basta lavorare) è una struttura che descrive la funzione (ad esempio un gestore di interruzioni) che vogliamo schedulare Può essere pensata come analoga alla struttura del tasklet. Quando i tasklet di pianificazione sono stati aggiunti alle code nascoste all'utente, ora è necessario utilizzare una coda speciale - coda di lavoro.
I tasklet vengono rastrellati dalla funzione di pianificazione e la coda di lavoro viene elaborata da thread speciali chiamati worker.
Lavoratore E' previsto l'esecuzione asincrona dei lavori dalla coda di lavoro. Sebbene chiamino il lavoro nell'ordine del loro turno, nel caso generale, l'esecuzione rigorosa e sequenziale è fuori questione: dopo tutto, qui hanno luogo la rimozione, il sonno, l'attesa, ecc.

In generale, i lavoratori sono thread del kernel, ovvero sono controllati dallo scheduler principale del kernel Linux. Ma i lavoratori intervengono parzialmente nella programmazione per l'organizzazione aggiuntiva dell'esecuzione parallela del lavoro. Questo sarà discusso più dettagliatamente di seguito.

Per delineare le funzionalità di base del meccanismo della coda di lavoro, suggerisco di esaminare l'API.

Informazioni sulla coda e sulla sua creazione

alloc_workqueue (fmt, flags, max_active, args ...)
I parametri fmt e args sono il formato printf per il nome e gli argomenti ad esso. Il parametro max_activate è responsabile del numero massimo di lavori che da questa coda possono essere eseguiti in parallelo su una CPU.
La coda può essere creata con i seguenti flag:
  • WQ_HIGHPRI
  • WQ_UNBOUND
  • WQ_CPU_INTENSIVE
  • WQ_FREZABLE
  • WQ_MEM_RECLAIM
Presta particolare attenzione alla bandiera WQ_UNBOUND... Con la presenza di questo flag, le code sono divise in tethered e non collegate.
Nelle code vincolate Quando il lavoro viene aggiunto, è vincolato alla CPU corrente, ovvero, in tali code, il lavoro viene eseguito sul core che lo pianifica. A questo proposito, le code associate assomigliano ai tasklet.
In code illimitate il lavoro può essere eseguito su qualsiasi core.

Una caratteristica importante dell'implementazione delle code di lavoro nel kernel Linux è la concorrenza aggiuntiva che hanno le code legate. Di più su di esso è scritto di seguito, ora dirò che è eseguito in modo tale da utilizzare meno memoria possibile e in modo che il processore non sia inattivo. Tutto questo viene implementato partendo dal presupposto che un'opera non utilizzi troppi cicli del processore.
Questo non è il caso delle code non associate. In sostanza, tali code forniscono semplicemente un contesto al lavoro e le avviano il prima possibile.
Pertanto, le code non collegate dovrebbero essere utilizzate quando è previsto un lavoro intensivo del processore, poiché lo scheduler si occuperà quindi dell'esecuzione parallela su più core.

Per analogia con i tasklet, al lavoro può essere assegnata una priorità di esecuzione, normale o alta. La priorità è comune per l'intera coda. Per impostazione predefinita, la coda ha una priorità normale e se imposti il ​​flag WQ_HIGHPRI, quindi, rispettivamente, alto.

Bandiera WQ_CPU_INTENSIVE ha senso solo per le code legate. Questo flag è un rifiuto di partecipare all'organizzazione aggiuntiva dell'esecuzione parallela. Questo flag dovrebbe essere usato quando si prevede che il lavoro consumerà molto tempo della CPU, nel qual caso è meglio trasferire la responsabilità allo scheduler. Questo è descritto più dettagliatamente di seguito.

bandiere WQ_FREZABLE e WQ_MEM_RECLAIM sono specifici e vanno oltre lo scopo dell'argomento, quindi non ci soffermeremo su di essi in dettaglio.

A volte ha senso non creare le proprie code, ma utilizzare quelle generali. I principali sono:

  • system_wq - una coda vincolata per lavori veloci
  • system_long_wq - una coda vincolata per i lavori che dovrebbero richiedere molto tempo per essere eseguiti
  • system_unbound_wq - coda non associata

Sul lavoro e la loro pianificazione

Ora occupiamoci del lavoro. Innanzitutto, diamo un'occhiata alle macro di inizializzazione, dichiarazione e preparazione:
DECLARE (_DELAYED) _WORK (nome, void (* funzione) (struct work_struct * work)); / * in fase di compilazione * / INIT (_DELAYED) _WORK (_work, _func); / * in fase di esecuzione * / PREPARE (_DELAYED) _WORK (_work, _func); / * per cambiare la funzione eseguibile * /
Le opere vengono aggiunte alla coda utilizzando le seguenti funzioni:
bool queue_work (struct workqueue_struct * wq, struct work_struct * work); bool queue_delayed_work (struct workqueue_struct * wq, struct delay_work * dwork, unsigned long delay); / * il lavoro verrà aggiunto alla coda solo dopo il ritardo * /
Vale la pena soffermarsi su questo in modo più dettagliato. Sebbene specifichiamo una coda come parametro, infatti, il lavoro non viene messo nella stessa coda di lavoro, come potrebbe sembrare, ma in un'entità completamente diversa - nella lista-coda della struttura worker_pool. Struttura pool_di_lavoratoriè, infatti, l'entità più importante nell'organizzazione del meccanismo della coda di lavoro, sebbene per l'utente rimanga dietro le quinte. È con loro che lavorano i lavoratori, ed è in loro che sono contenute tutte le informazioni di base.

Ora vediamo quali sono i pool nel sistema.
Innanzitutto, i pool per le code ancorate (nella foto). Per ogni CPU, vengono allocati staticamente due pool di nodi di lavoro: uno per i lavori ad alta priorità, l'altro per i lavori con priorità normale. Cioè, se abbiamo quattro core, ci saranno solo otto pool associati, nonostante il fatto che ci possano essere tutte le code di lavoro che desideri.
Quando creiamo una coda di lavoro, viene allocato un servizio per ogni CPU. pool_workqueue(pwq). Ciascuno di questi pool_workqueue è associato a un pool di nodi di lavoro allocato sulla stessa CPU e che corrisponde in priorità al tipo di coda. Attraverso di essi, la coda di lavoro interagisce con il pool di lavoratori.
I lavoratori eseguono il lavoro dal pool di lavoratori indiscriminatamente, senza distinguere a quale coda di lavoro appartenevano originariamente.

Per le code non associate, i pool di nodi di lavoro vengono allocati dinamicamente. Tutte le code possono essere suddivise in classi di equivalenza in base ai loro parametri e per ciascuna di tali classi viene creato il proprio pool di lavoratori. Vi si accede utilizzando una tabella hash speciale, dove la chiave è un insieme di parametri e il valore, rispettivamente, è il pool di lavoratori.
In effetti, per le code non associate, tutto è un po' più complicato: se pwq e le code per ogni CPU sono state create per le code legate, allora qui vengono create per ogni nodo NUMA, ma questa è un'ottimizzazione aggiuntiva che non considereremo in dettaglio .

Tutti i tipi di piccole cose

Darò anche alcune funzioni dall'API per completezza, ma non ne parlerò in dettaglio:
/ * Force exit * / bool flush_work (struct work_struct * work); bool flush_delayed_work (struct delay_work * dwork); / * Annulla l'esecuzione del lavoro * / bool cancel_work_sync (struct work_struct * work); bool cancel_delayed_work (struct delay_work * dwork); bool cancel_delayed_work_sync (struct delay_work * dwork); / * Elimina la coda * / void destroy_workqueue (struct workqueue_struct * wq);

Come i lavoratori svolgono il loro lavoro

Ora che abbiamo familiarità con l'API, proviamo a capire più nel dettaglio come funziona e come viene gestita.
Ogni pool ha una serie di lavoratori che rastrellano le attività. Inoltre, il numero dei lavoratori cambia dinamicamente, adeguandosi alla situazione attuale.
Come abbiamo già scoperto, i lavoratori sono thread che eseguono il lavoro nel contesto del kernel. Il lavoratore li recupera in ordine, uno per uno, dal pool di lavoratori ad esso associato, e il lavoro, come già sappiamo, può appartenere a diverse code iniziali.

I lavoratori possono trovarsi condizionalmente in tre stati logici: possono essere inattivi, in esecuzione o in gestione.
Il lavoratore può oziare e non fare niente. Questo è, per esempio, quando tutto il lavoro è già stato fatto. Quando l'operaio entra in questo stato, va a dormire e, di conseguenza, non verrà giustiziato finché non si sarà svegliato;
Se la gestione del pool non è richiesta e l'elenco dei lavori pianificati non è vuoto, il lavoratore inizia a eseguirli. Chiameremo convenzionalmente tali lavoratori trascurato.
Se necessario, il lavoratore assume il ruolo gestore piscina. Un pool può avere un solo lavoratore manager o non averne affatto. Il suo compito è mantenere il numero ottimale di lavoratori per pool. Come lo fa? In primo luogo, vengono rimossi i lavoratori che sono rimasti inattivi per lungo tempo. In secondo luogo, vengono creati nuovi lavoratori se sono soddisfatte tre condizioni contemporaneamente:

  • ci sono ancora dei compiti da svolgere (lavori in piscina)
  • nessun lavoratore inattivo
  • nessun lavoratore lavoratore (cioè attivo e non addormentato allo stesso tempo)
Tuttavia, l'ultima condizione ha le sue sfumature. Se le code del pool non sono associate, la contabilità dei lavoratori in esecuzione non viene eseguita, per loro questa condizione è sempre vera. Lo stesso vale nel caso di un lavoratore che esegue un compito da uno legato, ma con il flag WQ_CPU_INTENSIVE, code. Allo stesso tempo, nel caso di code legate, poiché i lavoratori lavorano con lavori da un pool comune (che è uno dei due per ogni core nella figura sopra), risulta che alcuni di essi sono contati come funzionanti e alcuni non sono. Da ciò ne consegue che l'esecuzione di opere da WQ_CPU_INTENSIVE le code potrebbero non iniziare subito, ma esse stesse non interferiscono con l'esecuzione di altri lavori. Ora dovrebbe essere chiaro perché questo flag si chiama così e perché viene utilizzato quando ci aspettiamo che il lavoro richieda molto tempo per essere completato.

La contabilizzazione dei lavoratori viene effettuata direttamente dallo scheduler principale del kernel Linux. Questo meccanismo di controllo fornisce un livello di concorrenza ottimale, impedendo alla coda di lavoro di creare troppi lavoratori, ma anche non facendo attendere troppo a lungo il lavoro inutilmente.

Coloro che sono interessati possono guardare la funzione worker nel kernel, si chiama worker_thread().

Tutte le funzioni e le strutture descritte possono essere trovate più in dettaglio nei file include / linux / workqueue.h, kernel / workqueue.c e kernel / workqueue_internal.h... C'è anche la documentazione per la coda di lavoro in Documentazione / workqueue.txt.

Vale anche la pena notare che il meccanismo della coda di lavoro viene utilizzato nel kernel non solo per la gestione degli interrupt posticipati (sebbene questo sia uno scenario abbastanza comune).

Pertanto, abbiamo esaminato i meccanismi di gestione degli interrupt posticipati nel kernel Linux: tasklet e workqueue, che sono una forma speciale di multitasking. Puoi leggere di interruzioni, tasklet e code di lavoro nel libro "Linux Device Drivers" degli autori di Jonathan Corbet, Greg Kroah-Hartman, Alessandro Rubini, tuttavia, le informazioni a volte sono obsolete.

Linux- sistema operativo multitasking e multiutente per l'istruzione, il business, la programmazione individuale. Linux appartiene alla famiglia simile a UNIX sistemi operativi.

Linux è stato originariamente scritto da Linus Torvalds e da allora è stato migliorato da innumerevoli persone in tutto il mondo. È un clone del sistema operativo Unix, uno dei primi potenti sistemi operativi sviluppati per computer, ma non gratuito. Ma né Unix System Laboratories, i creatori di Unix, né l'Università di Berkeley, gli sviluppatori della Berkeley Software Distribution (BSD), sono stati coinvolti nella sua creazione. Uno dei più fatti interessanti dalla storia di Linux "a" è che persone da tutto il mondo hanno preso parte alla sua creazione allo stesso tempo - dall'Australia alla Finlandia - e continuano a farlo fino ad oggi.

Linux è stato originariamente progettato per funzionare su un processore 386. Uno dei primi progetti di Linus Torvalds era un programma che poteva passare da un processo all'altro, uno dei quali stampava AAAA e l'altro BBBB. Successivamente, questo programma è diventato Linux. Sarebbe più corretto, tuttavia, dire che Linus ha sviluppato il kernel del sistema operativo, ed è della sua stabilità che è responsabile.

Linux supporta la maggior parte del popolare software Unix ", incluso il sistema grafico X Window, che è un numero enorme di programmi, ma vale la pena sottolineare che Linux viene fornito ASSOLUTAMENTE GRATUITO. Il massimo che devi pagare è la confezione e i CD che kit di distribuzione scritto Linux. Il kit di distribuzione è il sistema operativo stesso + un insieme di pacchetti software per Linux. Vale anche la pena ricordare che tutto questo viene fornito con codici sorgente e qualsiasi programma scritto sotto Linux può essere modificato da solo. Questo ti consente anche per portare qualsiasi programma su qualsiasi piattaforma - Intel PC, Macintosh. A proposito, tutto quanto sopra è avvenuto grazie alla Free Software Foundation, il Software gratis che fa parte del Progetto GNU. Ed è per questi scopi che è stata creata la GPL - General Public License, sulla base della quale Linux è libero, come tutto il software per esso, e l'uso commerciale del software per Linux o sue parti è proibito. configurazione del sistema unix linux

A parte quanto sopra, Linux è un sistema operativo molto potente e stabile. Usarlo sul Web paga e non è così facile da hackerare.

Oggi Linux si è sviluppato lungo due strade. La prima, con numeri di versione pari (2.0, 2.2, 2.4), è considerata una versione di Linux più stabile e affidabile. La seconda, le cui versioni sono numerate con numeri dispari (2.1, 2.3), è più ardita e di più veloce sviluppo e quindi (purtroppo) più ricca di bug. Ma questa è già una questione di gusti.

In Linux, non c'è divisione in dischi C, D e il processo di comunicazione con i dispositivi è molto conveniente. Tutti i dispositivi hanno i propri file di sistema, tutti i dischi sono collegati a un file system e sembra tutto monolitico. Una struttura di directory chiara ti consente di trovare qualsiasi informazione all'istante. Per i file di libreria: la propria directory, per i file eseguibili: i propri, per i file con impostazioni proprie, per i file dispositivo i propri e così via.

La modularità del kernel consente di connettere qualsiasi servizio del sistema operativo senza riavviare il computer. Inoltre, puoi rifare il kernel del sistema operativo stesso, poiché i sorgenti del kernel sono disponibili anche in qualsiasi kit di distribuzione.

Il sistema operativo Linux molto abilmente, per così dire, usa l'idea del multitasking, ad es. tutti i processi nel sistema vengono eseguiti contemporaneamente (confronta con Windows: la copia di file su un dischetto e il tentativo di ascoltare la musica in questo momento non sono sempre compatibili).

Ma non tutto è così semplice. Linux è un po' più complesso di Windows e non è così facile per tutti passarci dopo aver usato Windows. A prima vista, può anche sembrare che sia molto scomodo e difficile da personalizzare. Ma questo non è il caso. Il punto forte di Linux "a è che può essere personalizzato per te stesso, configurato in modo da provare una grande soddisfazione dall'utilizzo di questo sistema operativo. Un numero enorme di impostazioni ti consente di modificare l'aspetto esterno (e interno) del sistema operativo e non un singolo sistema Linux sarà simile al tuo. In Linux puoi scegliere di utilizzare una shell grafica, ce ne sono diversi suite per ufficio, programmi server, firewall... Solo un sacco di programmi diversi per tutti i gusti.

Nel 1998, Linux è stato il sistema operativo per server in più rapida crescita, aumentando la sua distribuzione del 212% nello stesso anno. Ci sono oltre 20.000.000 di utenti Linux oggi. Sono disponibili molte applicazioni Linux sia per uso domestico che per workstation UNIX e server Internet completamente funzionali.

Linux non è più solo un sistema operativo. Linux sta iniziando ad assomigliare sempre di più a un culto. Andare a fondo della verità nel caso di una setta diventa sempre più difficile. Partiamo dai fatti. Quindi, Linux è:

  • * clone di Unix gratuito (o meglio, liberamente ridistribuibile);
  • * sistema operativo con vero multitasking;
  • * OS, che ciascuno dei suoi "utenti" può modificare, poiché è possibile trovare codici sorgente per quasi tutte le sue parti;
  • * che è personalizzabile esattamente come vuoi e non come preferisce il produttore.

I nuovi arrivati ​​a Linux sono principalmente attratti dal fatto che è "cool" e alla moda. C'è un mito che questo sistema operativo non sia davvero adatto per l'utente finale. Per costruire un server affidabile e resistente agli hacker, questo è più che buona decisione, ma non per un utente normale che ha bisogno di comodità, praticità e non vuole assolutamente capire e sentire il sistema con cui sta attualmente lavorando. Questo non è del tutto vero. Un sistema GUI Linux personalizzato è facile da usare e intuitivo come un sistema operativo Microsoft. Questo è solo per configurare Linux, avrai bisogno di molto impegno e conoscenza.

Come risultato di queste peculiarità della sua creazione e sviluppo, Linux ha acquisito "tratti caratteriali" molto specifici. Da un lato, è un tipico sistema UNIX, multiutente e multitasking. D'altra parte, è un tipico sistema di hacker, studenti e, in generale, tutte le persone a cui piace imparare continuamente e capire tutto nei minimi dettagli. Nella flessibilità di personalizzazione e utilizzo, Linux probabilmente non ha eguali. Puoi usarlo al livello in cui funziona win95 - cioè, avere un desktop grafico con tutte le sue caratteristiche sotto Windows: icone, barra delle applicazioni, menu contestuale, ecc. Inoltre, puoi installare un desktop che non differirà nell'aspetto e funzione da "Windows". (In generale, ci sono molte opzioni per i gestori di finestre sotto Linux, dal super-spartano icewm al super-fantasioso Enlightment + Gnome). D'altra parte, Linux offre un'accessibilità senza precedenti all'hardware a qualsiasi livello di disponibilità. È vero, per questo non sarà sufficiente essere in grado di battere il tasto destro del mouse, dovrai imparare il SI e l'architettura del computer. Ma una persona che una volta ha annusato questo odore di pensiero, questa è l'ispirazione di un programmatore, quando tieni la macchina "per le orecchie" e puoi fare con essa letteralmente tutto ciò di cui è capace - una persona simile non sarà mai in grado di ritorno alle morbide zampe di "vinose".

Se, utilizzando un sistema operativo commerciale, l'utente è costretto ad attendere il rilascio della versione successiva per ottenere un sistema senza glitch e bug della versione precedente, allora la modularità di Linux consente di scaricare un nuovo kernel, che viene rilasciato almeno una volta ogni due mesi, o anche più spesso (versione stabile)...

Risposte alla domanda "Cos'è Linux?" ne trovi tanti. Molte persone pensano che Linux sia solo il kernel. Ma il kernel da solo è inutile per l'utente. Sebbene il kernel sia senza dubbio la spina dorsale del sistema operativo Linux, l'utente deve lavorare sempre con le applicazioni. Questi programmi sono importanti quanto il kernel. Pertanto, Linux è una raccolta del kernel e dei programmi applicativi di base che di solito sono installati su ogni computer con questo sistema operativo. L'unificazione del kernel e dei programmi applicativi in ​​un unico insieme si manifesta nel nome del sistema: GNU/Linux. GNU è un progetto per creare una suite di software, simile a quella che solitamente accompagna un sistema di tipo Unix.

I sostenitori di Linux sono spesso accusati di elencare gli svantaggi di Windows quando si parla dei vantaggi di Linux. Ma questo è spesso inevitabile, poiché tutto viene appreso per confronto e la maggior parte degli utenti di computer ora ha familiarità solo con Windows. Quindi cosa offre Linux?

Originale: Processi leggeri: dissezione di thread Linux
Autori: Vishal Kanaujia, Chetan Giridhar
Data di pubblicazione: 1 agosto 2011
Traduzione: A. Panin
Data di pubblicazione della traduzione: 22 ottobre 2012

Per gli sviluppatori Linux e gli studenti di informatica, questo articolo discute le basi del threading e come vengono implementati utilizzando processi leggeri in Linux e include esempi di codice sorgente per una migliore comprensione.

I thread sono l'elemento base di un ambiente software multitasking. Un thread di programma può essere descritto come l'ambiente di esecuzione di un processo; pertanto, ogni processo ha almeno un thread. Il multithreading implica la presenza di più ambienti di esecuzione dei processi paralleli (su sistemi multiprocessore) e solitamente sincronizzati.

I thread del programma hanno i propri ID di thread e possono essere eseguiti indipendentemente l'uno dall'altro. Condividono tra loro uno spazio di indirizzi di processo e utilizzano questa funzionalità come vantaggio, che consente di non utilizzare i canali IPC (sistemi di comunicazione tra processi - memoria condivisa, pipe e altri sistemi) per lo scambio di dati. I thread del programma di un processo possono interagire, ad esempio i thread indipendenti possono ottenere/modificare il valore di una variabile globale. Questo modello di interazione esclude costi extra risorse per le chiamate IPC a livello di kernel. Poiché i thread operano in un singolo spazio di indirizzi, i cambi di contesto per un thread sono veloci e non richiedono molte risorse.

Ogni thread del programma può essere elaborato individualmente dall'utilità di pianificazione, quindi le applicazioni multithread sono adatte per l'esecuzione parallela su sistemi multiprocessore. Inoltre, la creazione e la distruzione dei thread è veloce. A differenza di una chiamata fork(), un thread non crea una copia dello spazio degli indirizzi del genitore; invece, i thread condividono lo spazio degli indirizzi con altre risorse, inclusi descrittori di file e gestori di segnale.

Un'applicazione multithread utilizza le risorse in modo ottimale ed efficiente. In tale applicazione, i thread del programma sono impegnati in varie attività, tenendo conto dell'uso ottimale del sistema. Un flusso può leggere un file dal disco, un altro scrivere dati su un socket. Entrambi i flussi funzioneranno in tandem, essendo indipendenti l'uno dall'altro. Questo modello ottimizza l'utilizzo del sistema, migliorando così le prestazioni.

Diverse caratteristiche curiose

La caratteristica più famosa dell'utilizzo dei thread è la loro sincronizzazione, soprattutto se è presente una risorsa condivisa contrassegnata come segmento critico. È un segmento di codice che accede a una risorsa condivisa e non dovrebbe essere disponibile per più di un thread in un dato momento. Poiché ogni thread può essere eseguito in modo indipendente, l'accesso alla risorsa condivisa non è controllato, il che richiede l'uso di primitive di sincronizzazione, tra cui mutua esclusione, semafori, blocchi di lettura/scrittura e altro.

Queste primitive consentono ai programmatori di controllare l'accesso a una risorsa condivisa. Oltre a quanto sopra, i thread, come i processi, sono inclini a passare a stati di blocco o di attesa se il modello di sincronizzazione non è progettato correttamente. Anche il debug e l'analisi delle applicazioni multithread possono essere piuttosto complicati.

Come vengono implementati i thread in Linux?

Linux consente di sviluppare e utilizzare applicazioni multithread. A livello di utente, l'implementazione del threading Linux segue lo standard aperto POSIX (Portable Operating System Interface for uniX), designato IEEE 1003. La libreria di livello utente (glibc.so su Ubuntu) fornisce un'implementazione dell'API POSIX per i thread.

In Linux, i thread del programma esistono in due spazi separati: spazio utente e spazio kernel. Nello spazio utente, i thread vengono creati utilizzando l'API conforme a POSIX della libreria pthread. Questi thread nello spazio utente sono indissolubilmente legati ai thread nello spazio del kernel. In Linux, i thread nello spazio del kernel sono indicati come "processi leggeri". Un processo leggero è un'unità dell'ambiente di runtime principale. A differenza delle varie versioni di UNIX, inclusi sistemi come HP-UX e SunOS, Linux non ha un sistema di threading separato. Un processo o thread in Linux è considerato un "task" e utilizza le stesse strutture interne (un insieme di struct task_structs).

Per un certo numero di thread di processo creati nello spazio utente, ci sono un certo numero di processi leggeri ad essi associati nel kernel. Un esempio illustra questa affermazione: #include #includere #includere Int main() (pthread_t tid = pthread_self (); int sid = syscall (SYS_gettid); printf ("LWP id is% dn", sid); printf ("POSIX thread id is % dn", tid); return 0; )

Utilizzando l'utilità ps, puoi ottenere informazioni sui processi, nonché sui processi / thread leggeri di questi processi: [e-mail protetta]: ~ / Desktop $ ps -fL UID PID PPID LWP C NLWP STIME TTY TIME CMD kanaujia 17281 5191 17281 0 1 Jun11 punti / 2 00:00:02 bash kanaujia 22838 17281 22838 0 1 08:47 punti / 2 00:00: 00 ps -fL kanaujia 17647 14111 17647 0 2 00:06 punti / 0 00:00:00 vi clone.s

Cosa sono i processi leggeri?

Un processo leggero è un processo che mantiene un thread nello spazio utente. Ogni thread dello spazio utente è indissolubilmente legato a un processo leggero. La procedura per la creazione di un processo leggero è diversa dalla procedura per la creazione di un processo normale; Il processo utente "P" può avere un numero di processi leggeri associati con lo stesso ID di gruppo. Il raggruppamento consente al kernel di condividere le risorse (le risorse includono lo spazio degli indirizzi, le pagine della memoria fisica (VM), i gestori di segnale e i descrittori di file). Consente inoltre al kernel di evitare cambi di contesto quando si tratta di questi processi. La condivisione esauriente delle risorse è la ragione per nominare questi processi leggeri.

In che modo Linux crea processi leggeri?

V Creazione Linux i processi leggeri sono gestiti dalla chiamata di sistema clone() non standard. È simile alla chiamata fork(), ma con più funzionalità. In generale, una chiamata fork() viene implementata utilizzando una chiamata clone() con parametri aggiuntivi che indicano le risorse da condividere tra i processi. La chiamata clone() crea un processo, con il processo figlio che condivide elementi di runtime dal genitore, inclusi memoria, descrittori di file e gestori di segnale. La libreria pthread utilizza anche la chiamata clone() per implementare i thread. Fare riferimento al file del codice sorgente ./nptl/sysdeps/pthread/createthread.c nella directory dei sorgenti della versione 2.11.2 di glibc.

Creare il tuo processo leggero

Dimostriamo un esempio di utilizzo della chiamata clone(). Dai un'occhiata al codice sorgente dal file demo.c di seguito:

#includere #includere #includere #includere #includere #includere #includere // 64kB stack #define STACK 1024 * 64 // Il thread figlio eseguirà questa funzione int threadFunction (void * argomento) (printf ("thread figlio che immette \ n"); close ((int *) argomento); printf ("figlio thread in uscita \ n "); return 0;) int main() (void * stack; pid_t pid; int fd; fd = open (" / dev / null ", O_RDWR); if (fd< 0) { perror("/dev/null"); exit(1); } // Резервирование памяти для стека stack = malloc(STACK); if (stack == 0) { perror("malloc: could not allocate stack"); exit(1); } printf("Creating child thread\n"); // Вызов clone() для создания дочернего потока pid = clone(&threadFunction, (char*) stack + STACK, SIGCHLD | CLONE_FS | CLONE_FILES |\ CLONE_SIGHAND | CLONE_VM, (void*)fd); if (pid == -1) { perror("clone"); exit(2); } // Ожидание завершения дочернего потока pid = waitpid(pid, 0, 0); if (pid == -1) { perror("waitpid"); exit(3); } // Попытка записи в файл закончится неудачей, так как поток // закрыл файл if (write(fd, "c", 1) < 0) { printf("Parent:\t child closed our file descriptor\n"); } // Освободить память, используемую для стека free(stack); return 0; }

Il programma demo.c consente di creare thread essenzialmente allo stesso modo della libreria pthread. Tuttavia, l'uso diretto della chiamata clone() è sconsigliato, poiché l'applicazione in fase di sviluppo potrebbe non riuscire se utilizzata in modo improprio. La sintassi per la funzione clone() su Linux è mostrata di seguito: #include int clone (int (* fn) (void *), void * child_stack, flag int, void * arg);

Il primo argomento è la funzione stream; viene chiamato quando viene avviato il thread. Dopo che la chiamata clone() ha esito positivo, la funzione fn inizia l'esecuzione contemporaneamente al processo di chiamata.

L'argomento successivo è un puntatore a una posizione di memoria per lo stack del bambino. Un passaggio prima di chiamare fork() e clone(), il programmatore deve riservare memoria e passare un puntatore per utilizzarlo come stack del processo figlio, poiché i processi padre e figlio condividono pagine di memoria tra loro - includono lo stack. Un processo figlio può chiamare una funzione diversa dal processo padre, quindi è necessario uno stack separato. Nel nostro programma, allochiamo questo pezzo di memoria nell'heap usando la funzione malloc(). La dimensione dello stack è stata impostata su 64 KB. Poiché lo stack si riduce sull'architettura x86, è necessario simulare un comportamento simile utilizzando la memoria allocata dalla fine della sezione. Per questo motivo, passiamo il seguente indirizzo alla funzione clone(): (char *) stack + STACK

Il prossimo argomento delle bandiere è particolarmente importante. Consente di specificare quali risorse devono essere condivise con il processo creato. Abbiamo scelto un po' di mascherina SIGCHLD | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_VM descritto sotto:

  • SIGCHLD: il flusso invia un segnale SIGCHLD al processo padre al completamento. L'impostazione di questo parametro consente al processo padre di utilizzare la funzione wait() per attendere il completamento di tutti i thread.
  • CLONE_FS: condivide le informazioni sul filesystem tra il processo padre e il thread. Le informazioni includono la radice del filesystem, la directory di lavoro e il valore umask.
  • CLONE_FILES: condivide la tabella dei descrittori di file tra il processo padre e il thread. Le modifiche alla tabella si riflettono nel processo padre e in tutti i thread.
  • CLOSE_SIGHAND: Condividi la tabella del gestore del segnale tra padre e figlio. Di nuovo, se il processo padre o uno dei thread cambia il gestore del segnale, la modifica si rifletterà nelle tabelle degli altri processi.
  • CLONE_VM: il processo padre e i thread funzionano nello stesso spazio di memoria. Tutte le scritture in memoria o le mappature effettuate da uno di essi sono disponibili per altri processi.

L'ultimo parametro è l'argomento passato alla funzione (threadFunction), nel nostro caso è il descrittore di file.

Fare riferimento all'esempio demo.c di utilizzo di processi leggeri che abbiamo pubblicato in precedenza.

Il thread sta chiudendo il file (/dev/null) aperto dal processo padre. Poiché il processo padre e il thread condividono la tabella dei descrittori di file, la chiusura del file influisce anche sul processo padre, con conseguente errore di una successiva chiamata a write(). Il processo padre è in attesa che il thread termini (quando viene ricevuto il segnale SIGCHLD). Successivamente, rilascia la memoria riservata e restituisce il controllo.

Compila ed esegui il programma come al solito; l'output dovrebbe essere simile a quello seguente: $ gcc demo.c $. / a.out Creazione di un thread figlio del thread figlio in entrata del thread figlio in uscita Parent: child ha chiuso il nostro descrittore di file $

Linux fornisce supporto per un'infrastruttura di threading efficiente, semplice e scalabile. Ciò stimola l'interesse dei programmatori a sperimentare e sviluppare librerie per lavorare con i thread utilizzando clone() come funzione principale.

Pubblicazioni correlate

  • Come collegare due unità di sistema a un monitor Come collegare due unità di sistema a un monitor

    Un personal computer nella vita e nel lavoro di tutti i giorni può risolvere una vasta gamma di compiti e di solito è sufficiente un'unità di sistema. Ma a volte...

  • Importa messaggi da file EML Importa messaggi da file EML

    A volte l'utente ha bisogno di cambiare, integrare, trasferire l'elenco dei contatti alla persona giusta, trasferirlo su un dispositivo aggiuntivo ...