Understanding the Linux Processes:

A Deep Analysis for Linux Sysadmin (DA4LS)


First Article

 

 



Photo on sxc.hu





Alessio Porcacchia














L’Autore


Alessio Porcacchia at East Point Bussiness Park (Irlanda)




Alessio Porcacchia Nasce a Roma nel 1972, e’ consulente da moltissimi anni dei sistemi Unix/Linux (tutti i derivati del SYS4/5) Lavora Attualmente all’estero Presso una azienda di software molto conosciuta. Ha lavorato su praticamente qualsiasi derivato unix. E’ orgogliosamente un consulente Debian, ed e’ uno dei vecchi membri del Lugroma. E’ un accanito fan di Douglas Adams. Ha pubblicato diversi manuali scaricabili dal suo sito www.porcacchia.altervista.org ed e’ contattabile

Tramite l’indirizzo boxlinux[at]gmail.com

 

 

 

 


Ringraziamenti e dediche

Dedico questo articolo al gruppo di amici Alessandro Avagliano, Antonio Radici e Giovanni Mancuso ; al mio grande amico Andrea Della Porta. Inoltre Dedico questo Lavoro a Marco Gambino
Ringrazio in particolare Francesco Carpine per le correzioni sintattiche e logiche apportate all'articolo.

 

 

 

 






Cominciamo con il dire che, per poter avere un quadro cristallino della situazione e comprendere in modo chiaro e preciso cosa l'amministratore di sistema o l'analista debba fare davanti ad una shell o ad un compilatore, bisogna porsi una domanda fondamentale:

Cosa conosco in modo approfondito del sistema in analisi? Che cosa devo ottimizzare?

Per fare cio' si deve comprendere approfonditamente cosa accade all'interno del nostro sistema Gnu/Linux. Facendo ricerche sul web, spesso ci si trova davanti dei documenti tecnici che risultano o troppo semplificati e superficiali oppure documenti tecnici che vanno troppo nel profondo e si perdono in qualche meandro dove il lettore si chiede: Si ok ma non mi hai spiegato ancora perché ho questo problema. Succede sempre. Cerchero' in questo primo articolo di spiegare come funziona il processo linux. Se i Lettori troveranno interessante tale Articolo, affronteremo, negli Articoli successivi, concetti che riguardano la VM , l'analisi dei Vari Filesystems e i meccanismi del Kernel.


I processi linux:

Cercheremo di analizzare la “la vita, la morte e i miracoli” dei processi linux e come vengono gestiti da Linux. Cominciamo a dire cosa sia un processo linux.

Il modo migliore per definire un processo linux secondo me è questo: "Un Processo e' un qualsiasi programma in esecuzione (running || executing) all'interno del sistema. Il programma, di per sè, è una sequenza di istruzioni (in codice macchina) immagazzinato in un file binario(o eseguibile).

Tale programma a sua volta viene immagazzinato in un device che e' stato costruito specificatamente per questa funzione (hard disk, DVD etc etc) .


All'interno del nostro sistema il Processo e' l'entita' dinamica per eccellenza(pensate al progamma molto semplicemente come ad un oggetto in continuo cambiamento, nella sua continua esecuzione di codice macchina immagazzinato che viene eseguito). A causa dell'esecuzione del programma(quindi della sua copia in memoria e successiva esecuzione) avvengono molte operazioni, delle quali noi abbiamo solo una visibilità parziale, comprese le operazioni sui registri della CPU , i singoli parametri delle varie routines, etc, includendo tutte le attivita' del microprocessore inerenti al programma.

In Linux i processi vengono rappresentati da una Task_Struct, che e' array di puntatori che richiama continuamente se stesso, facendo così in modo che ogni processo venga ripetuto più volte. Per intenderci la possiamo chiamare tecnicamente "circular doubly-linked list", che immagazzina i descrittori dei vari processi.

La struttura e' spiegata: basta usare il man (man sched.h) . L'init_task e' variabile per la generazione di tutti i processi (task e' usato per definire processo) .


#define for_each_process(p) \
        for (p = &init_task ; (p = next_task(p)) != &init_task ; )

Potremmo rappresentare la struttura con uno schema l'init_task in questo modo :





La struttura del processo come una task_struck (nel kernel) e' grande 1024 bytes

Per poter sapere l’estatta grandezza potete compilare il seguente codice e lanciarlo:

#define __KERNEL__

#include <linux/sched.h>

main()

{

printf("sizeof(struct task_struct) - %d\n",

sizeof(struct task_struct));

}

La cosa davvero interessante e' che il numero massimo dei processi per default e' limitato a 512(limite dovuto alla grandezza del vettore task).

Quando un nuovo processo viene creato un nuovo task_struct viene allocato nella memoria ed aggiunto alla task_struct.

Detto questo possiamo definire gli stati di un processo. Quando un processo cambia , puo' trovarsi, in modo rigoroso, solo in uno dei seguenti stati:

  1. Processo in stato Running

Il processo e' attualmente up and running, e' un processo che sta girando sul sistema o e' pronto per poter essere eseguito dal sistema .

  1. Processo in stato Waiting (sleep)

Il processo e’ in attesa di un evento di sistema o attende che venga liberata(o rilasciata) una risorsa .



Processo in stato Uninterrutible Sleep

Il processo è in attesa di un risposta dal sistema (I/O)e non può essere interrotto in nessuna circostanza.

Ora cerchiamo di spiegare le differenze tra interrompibile e non interrompibile:

a) interrompibile : che può essere interrotto da un segnale o un evento (anche a livello utente)

b)non interrompibile: che non può essere interrotto in nessun caso (dipendente dall’hardware o dai devices)

  1. Processo in stato Stopped

E' un processo che e’ stato fermato da un segnale o da un evento (anche a livello utente).

  1. Processo Zombie

E'' un processo che dovrebbe essere nello stato Stopped e invecem per qualche ragionem continua a rimanere nella task_ structure e nel vettore di task. 

I processi zombie sono noti nel mondo Unix. Il processo può divenire "processo Zombie" per due motivi : 1- Il programma compilato, pur funzionando correttamente, non e’ esente da minori errori nel codice (codice non pulito o dirty code) e quindi il processo ad esso relativo(cioè quando il programma viene eseguito) invece raggiungere lo stato di stop, risulta ancora effettivamente presente in memoria(cioè nella struttura ci cui sopra).

Dei processi figli che non hanno ricevuto il segnale di stop, pur morendo il processo principale(cioè il processo padre),

rimangono in uno stato detto "auto-engaged".


Cosa assai importante da dire (che molti testi hanno tralasciato) e che tutti i processi non sono realmente indipendenti l'uno dall’altro, ma sono tutti correlati/legati tra loro. Il Processo di Init iniziale e’ l’unico ad avere indipendenza dai processi successivi, ma se ci riflettiamo possiamo comprendere che anche esso e’ indirettamente correlato agli altri, nel senso che il processo di Init, essendo il processo padre in Linux(da cui tutti gli altri processi derivano), senza i successivi processi figli, non avrebbe senso. Cioè anch'esso è correlato, nel senso suddetto, agli altri, in quanto la sua esistenza(attivazione e presenza in memoria) ha senso logico se e solo se genera altri processi(figli).





LA VFS E IL FILESYSTEM


Il processo, per sua natura, di solito apre e chiude dei files, e tali accessi ai dati si riflettono sul filesystem . Questo quindi influenza le aperture dei puntatori degli i-nodes all’interno del Virtual File System (attenzione che si chiama Virtual File System ma e’ tutt’altro che virtuale!).

La VFS gestisce il filesystem in Kernel Space. Si parla anche in questo caso di Superblocchi o I-Nodes(da Index Node) ma non vanno scambiati tra i Superblocks e gli Inode del Filesystem che conosciamo normalmente (EXT2 EXT3 ReiserFS etc etc), anche se il funzionamento e’ il medesimo.

La VFS gestisce in modo ordinato e uniforme l’accesso al File System da parte degli applicativi che ne richiedono l'uso.

Gerarchicamente il filesystem e’ ovviamente influenzato. L’inode e' i mattone fondamentale di immagazzinamento dei dati di un file.

Ogni blocco di inode definisce la ownership, la timestamps (creazione, modifica, accesso), grandezza, numero di hard links e l' esatta posizione del blocco dei dati , relativi ad ogni specifico file.

Ecco uno schema semplificato di come la VFS interagisce:



I processi inoltre tendono ad interagire con altri “oggetti” dell’ambiente linux. Uno dei principali e' sicuramente la Virtual Memory , anch'essa molto importante e che adesso spiegheremo.


 

LA VIRTUAL MEMORY

 

Molto semplicemente la memoria Virtuale e' "la somma della RAM + la swap" (swap spazio del disco visto come estensione fisica della memoria volatile o RAM). Il ruolo che gioca la Virtual Memory e’ fondamentale all’interno dei sistemi unix/linux . I programmi si riferiscono esclusivamente ad essa per lo spazio che deve essere allocato in memoria e accedono alla memoria fisica solo tramite la MMU. La MMU(Memory Management Unit) e’ un oggetto hardware(un circuito elettronico digitale) che traduce gli indirizzi virtuali in indirizzi fisici (ovvero l’indirizzo reale in memoria), controlla che l’indirizzo fisico di memoria allocato corrisponda effettivamente ad uno spazio libero nella memoria, e gestisce la page fault.

Detto questo, cerchiamo di capire che relazione ha la memoria virtuale con i  processi. Innanzitutto possiamo dire che nel processo stesso la VFS  decide come allocare e gestire la memoria. Ci sono diversi modi per fare ciò: la Segmentazione di memoria (si gestisce con segmenti di grandezza diversa) ;

la Paged Memory, invece, gestisce la memoria in segmenti tutti uguali). La scelta del primo o del secondo modo, o anche la mescolanza di entrambi (ibridazione), puo’ ovviamente avere  effetti diversi sul sistema se pensate che questa procedura deve essere moltiplicato per tutte le operazioni che i processi eseguono in memoria.


Infine abbiamo i timers. Il processore tiene traccia del tempo di creazione di ogni singolo processo.

Ad ogni tick il processore aggiorna l’ammontare del tempo complessivo utilizzato dal processo nel sistema.


Altro importante “oggetto” nel nostro ambiente “vitale” del processo sono gli Identificatori



IDENTIFICATORI


Essendo un sistema unix-like, anche linux basa tutto(compreso i processi) sui permessi. Questo fatto, oltre a riflettersi su file e directory, si riflette anche sui processi. Detto questo possiamo dire che un processo puo’ appartenere fino a 32 gruppi (default). Ovviamente anche il processo, se deve accedere a uno piu’ file e non ha i permessi di lettura o scrittura, questo ovviamente non potrà svolgere quei compiti che gli sono stati affidati.

Esisterà quindi un vettore "Groups" nella nostra ormai famigerata task_struct.

Ma vediamo nel dettaglio che tipo di identificatori di gruppo influenzano la task_struct .



UID E GUID

IDENTIFICANO GRUPPO E UTENTE che ha lanciato tale processo

Vi sono altre tre sottocategorie:

Effettivo:

Succede che alcuni programmi, per poter proseguire i propri tasks, debbano cambiare uid e guid , ad esempio per poter accedere a dei servizi con diverse uid e guid. In questo caso potrebbero sorgere effettivamente dei problemi se tali servizi non hanno gli stessi privilegi di gruppo e di utente. Un guid/uid effettivo e' quello che il processo aveva inizialmente e il kernel lo utilizza per controllare i privilegi effettivi di tale programma.

Di filesystem:

Questo tipo di sotto categoria riguarda tutti i permessi inerenti ad un file che risiede per l’appunto sul filesystem. Questo sia per sicurezza che per praticità. Esempio : un processo deve accedere alla lettura o alla scrittura di un file. Ovviamente in questo caso sono proprio gli identificatori di filesystem a cambiare e non quello effettivo. E utile capire che il mantenimento di questa sotto categoria e’ assai utile nel caso di un Kill Signal.

Se per esempio avessimo un samba filesystem ovviamente il programma in user mode dovrebbe accedere a quel filesystem ai suoi specifici file/s .

Ovviamente il quel caso saranno solamente i uid/guid di filesystem a cambiare.

Saved:

Ovviamente questo uid/guid e’ usato per memorizzare il ‘uid/guid orginale nel momento in cui cambia.




LO SCHEDULING E LO SCHEDULER


Altro meccanismo fondamentale per comprendere come “vive” un processo all’interno del nostro sistema linux e’ lo scheduling.

Il processo viene eseguito in parte in user space e in parte in kernel space. Il processo tende a swappare tra questi "due livelli" grazie alle system call (si veda mio articolo sul problem solving). Ora tutti i processi devono attendere che la CPU ceda a loro attenzione, cioè tempo di elaborazione.

Linux usa la prepre-emptive scheduling. In questo tipo di schedulazione ogni processo ha 200 millisecondi per eseguire le operazioni.

Quando il tempo scade, un nuovo processo prende il suo posto e il processo precendente si mette in coda,  finche’ non termina i suoi compiti(tasks).

Tale tipo di operazione e' chiamato "time-slice" (a fetta di tempo). Il valore della time-slice è controllato dalla "nice" del processo (ovviamente come abbiamo visto il vettore struct e’ anche esso definito nella task_struct).

Lo scheduler si occupa di tale processo interrompendone temporaneamente un altro. Generalmente un sistema monoprocessore è in grado di eseguire un solo programma per volta. Lo scheduler per l’appunto viene utilizzato per far convivere piu’ tasks contemporaneamente.

Uno schema più  preciso e’ il seguente:

Come si vede dallo schema dello scheduler pubblicato da Pellizzari su wikipedia, quando il processo parte dallo stato di Ready passa in modo unidirezionale allo stato di Running. Lo stato di Running invece interagisce in modo bidirezionale sia con lo stato di stop (interazione I/O e infine con lo stato end). Nello stato di interazione con l' I/O, com'è noto si perde molto tempo rispetto alle normali operazioni comuni(svolte dalla CPU).

Le informazioni principali che lo scheduler mantiene nella task_struct e con cui decide le regole generali per le priorita' dei processi sono le seguenti:

 

La Policy

Ci sono due tipi processi in linux: quelli normali e quelli realtime. Ogni processo real time ha una priorità maggiore rispetto ad un qualsiasi processo non real time. Lo scheduler dara’ la priorita’ al processo real time (real time indica sempre un processo la cui correttezza di esecuzione dipende dal tempo di risposta). I processi in real time hanno due tipi di algortimi che vengono utilizzati per la schedulazione: il round robin(RR) e la FIFO(First-In First-Out). Nel primo(RR) i processi  vengono schedulati  uno in coda all'altro in modo "circolare"; nel secondo(FIFO) il primo processo ad entrare in coda è anche il primo processo ad essere servito dalla CPU e quindi il primo ad uscire dalla coda, e il suo ordine non cambia più.

 

Priorita’

E la priorità che lo scheduler dà ai processi. Ed e’ basato anche sul totale del tempo che ci vorra’ per essere eseguito.

Questa priorita’ e basata anche sul renice (man renice) .

 

Priorita’ Real time

Tutti i processi realtime che vengono schedulati anche loro hanno di per se’ una priorita’ tra di  loro, come le priorita’ relative a realtime e processo normale. Lo scheduler anche qui decide tra più processi di tipo real-time quale di questi deve avere priorita’ maggiore rispetto agli altri(che sono pure realtime).

Il tempo (counter)

è l’ammontare totale del tempo che deve essere usato dal processo per elaborare.

Quando il processo la priorita’ decrementa per ogni clock tick, ovvero per ogni ciclo di clock (temporizzazione)

il processo attuale

ovviamente il processo che in quel momento e’ stato processato deve essere obbligatoriamente portato a compimento affinche’ un altro processo possa essere processato.

La selezione dei processi

Lo scheduler controlla i processi in coda e cerca quello che ha la priorita’ ad essere processato. Se ci sono real time processi, applica la rt_policy , il counter per i processi normali e un couter+1000 : questo significa che i processi realtime vengono prima eseguiti con altissima priorita’ e poi vengono applicati le altre varie priorita’ .

. Processi in Swap




Come viene eseguito un binario: Gli ELF

Tutti i programmi di linux sono normalmente eseguiti dall' interprete dei comandi. Ovviamente tale interprete e’ la shell . Un considerazione: per comprendere cosa avviene al programma la cosa migliore e’ che venga lanciato tramite la shell (l’utilizzo di interfacce user-friendly, come KDE o Gnome, non potrà dare la stesse informazioni che si ottengono quando i programmi vengono lanciati da shell ; provate voi stessi a lanciare un programma da interfaccia grafica e da shell e noterete che la shell riportera’ delle informazioni che l’interfaccia grafica per ovvi motivi non puo’ dare in output).

Escludendo CD e PWD (per motivi architetturali del sistema unix sys5) tutti i comandi unix sono dei binari eseguibili tramite shell.

 

Ecco cosa avviene quando lanciamo un comando:

  1. la shell cerca prima di tutto in base alla env PATH (echo $PATH) cerca il binario

  2. se il tool e’ stato trovato viene eseguito, e la shell clona se stessa usando il meccanismo di fork.

  3. Il nuovo processo("figlio" della shell) sostituisce il binario che e’ stato eseguito.

  4. La shell aspetta che il commando sia completato, o aspetta un segnale di uscita(Control-D) o di sospensione (Control-Z, SIG_STOP) ,                          che puo’ essere lanciato dall’utente, o che venga messo ad esempio in background (binario & , SIGCONT).

Un binario eseguibile ha molti formati e può ad esempio essere usato da diversi interpetri (perl python etc etc) o puo essere uno script.

Gli script di shell vengono riconosciuti, e possono essere lanciati ovviamente anche negli shell script.                                                                               L’interprete e’ /bin/shell o più comunemente /bin/bash. Per poter comprendere il tipo di file che si ha davanti semplicemente si puo’ usare il tool file (man file).

Qualsiasi binario eseguibile contene il codice(che deve essere eseguito) e i dati (che vengono caricati nella memoria, assieme al codice), ed eseguito.                    Il piu’ comune tra questi e’ il formato ELF.

 

IL FORMATO ELF

1) Riallocabili , creati da compilatori/assemblatori,  hanno bisogno di essere preventivamente processati dal linker prima di essere eseguiti

2) Eseguibili: sono riallocabili eccetto per i collegamenti nella shared library (che dovrebbero essere risolti all'atto dell'esecuzione)

3) Shared Objects

4) core file o core dump file

 

L’header ELF e’ una preziosa informazione per comprendere il tipo di ELF che ci troviamo di fronte.                                                                                       Per poter leggere un header di un elf basta usare il tool objdump (man objdump).

 

(miglior diagramma rappresentativo dell’header by docs.sun.com)







Ecco la rappresentazione schematica di un ELF


(da tldp.org)

Dal punto di vista dell’header ad esempio il noto helloworld.o descrive con due intestazioni "e_phnum and e_phoff" il primo definisce il numero delle entry

dell'header relative al programma il secondo definisce l'intestazione dell'immagine del nostro binario.

Dopo il caricamento delle informazioni il programma viene eseguito caricando le dovute informazioni  nel caso di hello world la  printf() che stampa in standard output (in questo caso printa in output hello world).

Ma vediamo schematicamente cosa accade ad alto livello del nostro progamma hello world:

section .data (caricamento dati)

Msg db ‘hello world’, 0x0a ( l’output)

len equ $ - msg (lunghezza della stringa hello world)

section .text (sezione testo)

global _start ( dichiarazione obbligatorie per il linker (ld)

_start: (dice al linker dove si trova la entry point)

mov eax, 4 (qui comincia la parte assembler una sys_write)

mov ebx, 1 ( il file descriptor che conosciamo stdout)

mov edx, msg (lungezza del messaggio)

Int 0x80 (la nostra classica chiamata syscall)

mov eax, 1 ( avviene qui una sys_exit() ; numero di chiamata della system call sys_exit)

Int 0x80 (la nostra classica chiamata syscall)


 

A questo punto quando il nostro procresso parte viene settata nella virtual memory la struttura dati del nostro programma. Quando viene eseguito viene poi caricato nella nostra memoria fisica

LE Shared Libraries

Per comprendere come viene eseguito un processo bisogna correlare il nostro ELF al concetto di libreria a collegamento dinamico. La link Library e’ una libreria che non e’ collegata staticamente al nostro ELF quando viene compilata ma che viene caricata solo in fase di esecuzione.                                                       

  La Elf Shared library vengono appunto caricate dinamicamente al lancio dell’esegubile. In questa operazione viene coinvolto anche il dynamic linker .           Quando una shared library e' linkata linux usa alcune dynamic linkers come ad esempio ld.so.1 e l libc.so.Il vantaggio di tali librerie e che il programma utilizzando questo tipo di oggetti, (in sharing) occupa meno spazio e memoria, proprio perche' invece di essere compilati staticamente per ogni programma, ogni programma sfrutta questo tipo di libreria comune a tutti Se non vi fossero questo tipo di oggetti che condividono tali informazioni a medesimi programmi lo spazio e la memoria richiesta aumentere esponenzialmente sul sistema. Questo tipo di librerie comunemente usano il codice come se fossero delle subroutines. Se non ci fosse il dinamic liking il programma dovrebbe avere una copia fisica sul disco, occupando cosi spazio e memoria virtuale. Nel dinamic linking vengono coinvolte le tabelle dell' immagine ELF per ogni routine delle suddette librerie.   Tutte le informazioni correlate al dynamic linker vengono poi linkate nello spazio di indirizzi ad essi correlate .

Detto questo dovremmo aver concluso il nostro articolo su come processi e binari funzionano in Linux .

 

pagina principale del sito