Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
I corsi di Scienze Matematiche Fisiche e Naturali
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Clemente Galdi » 12.Interazione con file di sistema e variabili d'ambiente


Accesso ai file di sistema

I sistemi UNIX-like utilizzano numerosi file di sistema/configurazione per eseguire normali operazioni di routine.
Ad esempio le informazioni contenute nel file /etc/passwd sono utilizzate, oltre che durante la fase di login, anche ogni volta in cui utente esegue I comandi “quota”, “ls -l”, “finger”…
Il file delle password è denominato, nella terminologia POSIX.1, il “database utenti”. Vi si possono trovare fino a dieci campi, tutti presenti nei sistemi derivati da BSD. Lo standard POSIX definisce solo 5 campi. I campi del database utenti sono contenuti in una struttura passwd definita in <pwd.h>. I campi del database utenti sono:

  • User name. POSIX. Contenuta nel campo: char *pw_name.
  • Password cifrata. Non POSIX. Cont. in char *pw_passwd.
  • User id. POSIX. Cont. in uid_t pw_uid.
  • Group id. POSIX. Cont. in gid_t pw_gid.
  • Commento. Non POSIX. Cont. in char *pw_gecos.
  • Home directory. POSIX. Cont. in char *pw_dir.
  • Shell di default. POSIX. Cont. in char *pw_shell.
  • User access class. Non POSIX Cont. in char *pw_class.
  • Prossima data cambio password. Non POSIX. Cont. in time_t pw_change.
  • Data di scadenza account. Non POSIX. Cont. in time_t pw_expire.

Il database utenti

Per avere un’idea concreta, riportiamo alcune righe del file /etc/passwd:

root:x:0:0:root:/root:/bin/sh
daemon:x:1:1::/var/root:/usr/bin/false
nobody:x:-2:-2:Nobody:/var/empty:/usr/bin/false

In tutti I sistemi UNIX esiste sempre un account per l’amministratore di sistema denominato “root”, con uid e gid pari a zero.
I campi del file passwd sono separati da “:”
Storicamente il secondo campo contiene la “password cifrata”. Nei sistemi moderni questo campo contiene un unico carattere mentre la password cifrata è memorizzata in un altro file per ragioni di sicurezza.
Alcuni campi possono risultare vuoti, e.g., il quinto campo (gecos) dell’utente daemon.
Il settimo campo contiene la shell eseguita all’atto del login. In alcuni casi questo campo può indicare oggetti diversi da shell vere e proprie, e.g., /usr/bin/false. Queste pseudo-shell impediscono all’utente di eseguire comandi in modo interattivo. Sono in genere associate a pseudo-utenti, “proprietari” di servizi si sistema.

Il database utenti (segue)

Lo standard POSIX definisce le seguenti funzioni per leggere righe dal file passwd.
struct passwd *getpwuid(uid_t uid);
struct passwd *getpwnam(char *name);

Entrambe le funzioni ritornano un puntatore ad una struct passwd contenente le informazioni associate allo uid od alla username indicate.
La struttura ritornata è, in genere, statica. Quindi, invocazioni successive delle due funzioni sovrascrivono sempre la stessa area di memoria.
Le due system call richiedono la conoscenza della username o dello uid dell’utente per cui si richiedono informazioni. Nel caso in cui sia necessario processare tutte le righe del file passwd è possibile utilizzare le seguenti funzioni:
struct passwd *getpwent(void);
void setpwent();
void endpwent();

  • getpwent() Se non sono già aperti, apre i file necessari per riempire la struttura passwd (e.g., passwd, shadow, etc) e ritorna un puntatore ad una struttura contenente le informazioni relative alla prossima entry nel file passwd.
    • L’ordine in cui vengono ritornati I record può essere arbitrario.
  • setpwent(): reinizializza il puntatore al file passwd al primo record
  • endpwent(): chiude il file delle password.

Esempio

Il programma: Mostra codice, riporta un esempio di utilizzo delle funzioni per l'accesso al database utenti.
L'invocazione di setpwent, in questo contesto, è in realtà in inutile perché serve solo ad aprire I file del database utenti, operazione che verrebbe comunque effettuata da getpwent. In altri contesti in cui il processo potrebbe avere già eseguito accessi al database, è sempre consigliabile inserire una setpwent per eseguire il rewind del file.
Il programma legge l'intero database, eseguendo la funzione getpwent finché questa non ritorna NULL.
In questo caso alcune informazioni contenute nella struttura passwd vengono visualizzate.

Alcune righe dell'output del programma: Mostra codice

Shadow Password

Le password degli utenti vengono codificate attraverso l’algoritmo MD5, una funzione hash one-way.
Non è possibile “decodificare” la password. La verifica consiste nel “codificare” la password indicata dall’utente e verificare se corrisponde alla verifica memorizzata.
Dato il suo ruolo, il file /etc/passwd deve essere leggibile da tutti gli utenti.
D’altro canto memorizzare le password codificate in un file leggibile costituisce un serio problema di sicurezza.

  • È possibile scaricare il file e provare “off-line” ad “indovinare” le password degli utenti.

I sistemi moderni memorizzano le password codificate in un file diverso da passwd, noto con il nome di shadow password.
Le informazioni minime contenute in questo file sono la username e la password cifrata. Molti sistemi, però, memorizzano anche informazioni relative all’aging della password, e.g., intervallo di validità della password, e/o dell’account etc.
Sebbene oramai tutti I sistemi Unix-like prevedano la memorizzazione delle password in un file non leggibile da tutti gli utenti, I.e. uno “shadow password file”, le implementazioni dei diversi sistemi possono differire anche notevolmente tra loro. Ad es.

  • Linux e Solaris definiscono funzioni specifiche per leggere informazioni dallo shadow password ed una struttura spwd per contenerle.

Il database dei gruppi

I sistemi Uni consentono la definizione di gruppi di utenti per semplificare la condivisione di risorse.
Il file /etc/group contiene informazioni sui gruppi definiti sul sistema in esame.
POSIX definisce una struttura, denominata group, definita in, per memorizzare I record del file dei gruppi.
Le informazioni contenute nel file /etc/group sono:

  • Nome del gruppo, POSIX. Definita nel campo char *gr_name.
  • Password codificata, NON POSIX. Definita nel campo char *gr_passwd.
  • Group id, POSIX. Definita nel campo gid_t gr_gid.
  • Elenco username membri del gruppo, POSIX. Definita nel campo char **gr_mem. L’array di stringhe è terminato da un puntatore nullo.

Il database dei gruppi (segue)

Come nel caso delle password, lo standard POSIX definisce le seguenti funzioni per leggere righe dal file group.
struct group *getgrgid(gid_t gid);
struct group *getgrnam(char *name);

Entrambe le funzioni ritornano un puntatore ad una struct group contenente le informazioni associate al gid od alla nome del gruppo indicati.
La struttura ritornata è, in genere, statica. Quindi, invocazioni successive delle due funzioni sovrascrivono sempre la stessa area di memoria.
Le due system call richiedono la conoscenza della nome del gruppo o del gid del gruppo per cui si richiedono informazioni. Nel caso in cui sia necessario processare tutte le righe del file group è possibile utilizzare le seguenti funzioni:
struct group *getgrent(void);
void setgrent(void);
void endgrent(void);

  • getgrent() Se non è già aperto, apre il file /etc/group. Ritorna un puntatore ad una struttura contenente le informazioni relative alla prossima entry nel file group.
  • setgrent(): reinizializza il puntatore al file group al primo record
  • endgrent(): chiude il file dei gruppi.

Esempio

Il programma: Mostra codice, mostra un esempio di utilizzo congiunto delle funzioni per l'accesso ai database gruppi ed utenti.
Dato in input il nome del gruppo, lo scopo del programma è elencare le username e gli associati campi gecos ("il nome utente") del componenti del gruppo.
Il programma, attraverso la funzione getgrname, ottiene la struttura group associata al gruppo passato su riga di comando all'eseguibile.
Per ogni utente presente nella lista contenuta nel campo gr_mem:
viene invocata la funzione getpwnam per riempire la struttura pw con le informazioni associate all'utente.
Vengono visualizzate la username, contenuta nel campo gr->gr_mem[j], ed il campo gecos (contenente in genere "il nome utente") contenuto in pw->pw_gecos.

Alcune righe dell'output del programma: Mostra codice

Gruppi supplementari

Nei sistemi Unix originariamente, ogni utente poteva appartenere in ogni istante ad un unico gruppo.
Gli utenti potevano cambiare gruppo, se ne avevano I permessi, ma la modifica implicava la perdita del gid originario.
I sistemi moderni consentono ad ogni processo di appartenere, oltre al gruppo definito nel file passwd, anche ad altri 16 gruppi supplementari contemporaneamente.
Non è inusuale che un utente appartenga a più gruppi, ad es., iscritto a diversi corsi, membro di più gruppi di sviluppo, etc.
Il vantaggio di questa strategia sta nel non costringere l’utente ad una modifica esplicita del gruppo di appartenenza.
Le procedure di verifica dell’accesso a file verificano se il group id del file corrisponde all’effective group id del process od ad uno dei gruppi supplementari.
Le funzioni disponibili per accedere l’elenco dei gruppi supplementari sono:

  • int getgroups(int gidsetize, gid_t grouplist[]): Riempie I primi gidsetsize elementi dell’array grouplist con I gruppi del processo corrente e ritorna il numero di gruppi supplementari. Se il primo parametro è zero, l’array non viene modificato. In questo caso il valore di ritorno può essere utilizzato per allocare dinamicamente un array per contenere I gruppi supplementari in una seconda invocazione di getgroups.
  • Int setgroup(int ngroups, gid_t groudlist[]). Può essere invocata dl superuser per impostare I gruppi supplementari per un processo. Utilizzata, tipicamente, in fase di login dell’utente.

Esempio

Il programma: Mostra codice, mostra un esempio di utilizzo della funzione getgroups.
Il programma invoca inizialmente getgroups con primo parametro pari a zero in modo da ottenere il numero di gruppi supplementari per il processo corrente.
Questo valore è utlizzato per allocare dinamicamente un array che possa contenere l'elenco di tutti I gid.
La seconda invocazione di getgroups ottiene l'elenco dei gid dei gruppi supplementari per il processo corrente.
I gid così ottenuti vengono convertiti nei nomi dei gruppi corrispondenti attraverso la funzione getgrgid.

Output del programma: Mostra codice.

Altri file dati

I sistemi UNIX definiscono interfacce per accedere ad altri file di configurazione, tra cui:

  • /etc/hosts: Host name database. Contiene un elenco di associazioni, hostname indirizzo di rete. Come vedremo nella lezione 20, quando viene richiesto un indirizzo non presente in questo database, il sistema operativo invia questa richiesta ad un server apposito.
  • /etc/networks: Network database. Database contenenti informazioni sulle “reti” (insiemi di host). Anche in questo caso, una query che non trova riscontro nel database locale, viene inviata ad un server remoto.
  • /etc/protocols: Protocol database. Database che associa informazioni su diversi protocolli di comunicazione.
  • /etc/services: Service datadase. Database che memorizza informazioni sui servizi ed i loro “numeri di porta” standard.

Altri file dati (segue)

Come per I database utenti e gruppi, anche per questi database esistono funzioni per:
L’accesso sequenziale ai record:

  • Get: legge il prossimo record del database
  • Set: se non è ancora aperto, apre il file ed esegue il rewind.
  • End: chiude il database.

L’accesso indicizzato ai record. Tutte le strutture sono definite nell’header netdb.h:

  • host. Struttura hostent. Funzioni: gethostbyname, gethostbyaddr.
  • networks. Struttura netent. Funzioni: getnetbyname, getnetbyaddr.
  • protocols. Struttura protoent. Funzioni: getprotobyname, getprototbynumber.
  • services. Struttura servent. Funzioni: getservbyname, getservbyport.

Accouting utenti

I sistemi Unix mantengono due file per l’accounting degli utenti:

  • Utmp: Elenco utenti correntemente presenti nel sistema;
  • Wtmp: Traccia di login e logout degli utenti.

All’atto del login, il kernel scrive nei file utmp e wtmp un record relativo all’utente conesso.
Quando un utente esegue il logout, viene eliminato dal file utmp il record relativo all’utente e viene aggiunto un record nel file wtmp.
I window manager moderni consentono la connessione di uno stesso utente più volte attraverso diverse “finestre”. L’esecuzione di una nuova shell implica l’aggiunta di un record in utmp e dei corrispondenti record in wtmp.
La struttura dei record può differire in diversi sistemi UNIX-like. In generale, però, vengono mantenute le seguenti informazioni:

struct utmp {
char ut_line[UT_LINESIZE]; // terminale
char ut_name[UT_NAMESIZE]; // nome utente
char ut_host[UT_HOSTSIZE]; // host da cui e' connesso
time_t ut_time; // Ora di login
};

Identificazione del sistema

Lo standard POSIX definisce la funzione uname per ottenere informazioni sull’host corrente.
int uname(struct utsname *nome)
La funzione assegna array di caratteri (terminati dal carattere nullo) ai campi della struttura utsname:

  • sysname: Nome del sistema operativo;
  • nodename: Nome dell’host;
  • release: Release del sistema operativo;
  • version: Versione del sistema operativo;
  • machine: tipo di hardware.

Nei sistemi derivati da BSD, viene anche fornita una funzione gethostname che ritorna solo il nome dell’host. Il valore ritornato è, tipicamente, il nome del nodo nella rete TCP/IP.
Utilizzo della funzione uname: Mostra codice.

Output del programma su sistemi MacOSX e Linux: Mostra codice.

Routine Data ed Ora

I sistemi Unix-like si differenziano da altri sistemi operativi per quel che riguarda la gestione della data e dell’ora corrente.
Data ed ora vengono espressi come numero di secondi dalle ore 00:00:00 del 1 Gennaio 1970 (UTC). Questa rappresentazione è anche nota “calendar time”. Si noti che un calendar time esprime, in un unico valore, contemporaneamente data ed ora di un evento.
La seguente funzione ritorna il calendar time corrispondente alla data e all’ora corrente
time_t time(time_t *calptr)
Il valore di ritorno della funzione è il calendar time corrente che, se il parametro passato alla funzione è non nullo, è anche assegnato alla variabile *calptr.
La funzione time ritorna il calendar time con una risoluzione al secondo. Per ottenere una risoluzione di microsecondi, è possibile con la seguente funzione:
int gettimeofday(struct timeval *tp, void *tzp);
La struct timeval è definita come struct timeval{time_r tv_sec; long tv_usec;}.

Lo standard POSIX definisce “NULL” come unico valore legale per il secondo parametro. In alcuni sistemi è possibile definire attraverso questo parametro il time zone.

La rappresentazione “broken down”

Le variabili calendar time sono indubbiamente utili per la gestione da parte dei calcolatori.
Affinchè siano utilizzabili per l’interazione con gli utenti, devono essere convertite in un formato human readable.
La funzione ctime converte la variabile time_t in una stringa contenente una rappresentazione della data, nel fuso orario locale, nel seguente formato human readable;
Fri Jan 01 18:22:48 2010\n\0
È possibile convertire le variabili time_t in variabili di tipo struct tm: Mostra codice, utilizzando le funzioni localtime e gmttime.
La localtime (risp., gmttime) converte il calendar time (UTC) nella sua rappresentazione "broken down" convertendolo utilizzando il fuso orario locale (risp., UTC).
La conversione inversa, da struct tm (nel fuso orario locale) a calendar time può essere effettuata utilizzando la funzione mktime.
ATTENZIONE: Le funzioni di conversione utilizzano una variabile interna di stato statica e ritornano il puntatore ad essa. In altri termini, queste funzioni allocano la struttura di "ritorno" un'unica volta e la sovrascrivono ad ogni invocazione. Le varianti "_r" delle funzioni di conversione risolvono questo problema.

Routine Data ed Ora

Il programma: Mostra codice, mostra esempi di utilizzo delle routine per data ed ora.

La funzione time ritorna il calendar time corrente. Un calendar time altri non è che un numero.
Le funzioni localtime ed gmtime convertono questo numero nella sua rappresentazione broken down. L'unica differenza tra le due funzioni è che la prima ritorna la rappresentazione del calendar time per il fuso orario locale, mentre la seconda utilizza il fuso orario di Greenwich.
È da rimarcare che il programma visualizza immediatamente il valore di ritorno di asctime.

Si noti che, dato un calendar time, il calcolo del calendar time associato ad uno "spostamento" rispetto a data ed ora corrente può essere effettuato sommando (o sottraendo) lo spostamento.

Nell'esempio, il calendar time relativo alla stessa ora del giorno successivo a quello ritornato da time() si ottiene sommando la costante 24*60*60, I.e., il numero di secondi in un giorno.

L'output del programma: Mostra codice

Routine Data ed Ora (segue)

Esempi di errori indotti dalla staticità delle funzioni di conversione: Mostra codice.
In questo esempio riportiamo un errore frequente nell'utilizzo delle funzioni di conversione.
Quasi tutte le funzioni per la gestione di data ed ora utilizzano variabili interne statiche e ritornano il puntatore ad una variabile interna.
Quindi due invocazioni consecutive della stessa funzione di conversione sovrascrivono la stesa area di memoria.
Per illustrare gli effetti di questa caratteristica, consideriamo l'esempio sopracitato.
Il programma invoca localtime ed asctime e, senza visualizzare o salvare il valore di ritorno di quest'ultima, invoca gmtime e nuovamente asctime.
Le invocazioni di asctime ritornano sempre il putatore alla stessa area di memoria.
La seconda invocazione di asctime sovrascrive, quindi, il valore di ritorno della prima invocazione, che, pertanto, è perso.
Per cui, visualizzando I "due" valori di ritorno di asctime "strlocal" e "strgmt", in realtà si sta visualizzando lo stesso valore.
Più correttamente, le due printf visualizzano "la stessa area di memoria" contenente il "valore di ritorno della seconda invocazione di asctime".

Per ogni funzioni di conversione esiste una variante "reentrant", identificata dal suffisso "_r", che risolve il problema appena descritto.

L'output del programma: Mostra codice.

Process Environment

L’esecuzione di programma C inizia invocando la funzione main, per cui una firma possibile è: int main(int argc, char *argv[]);
La terminazione di un processo può avvenire in uno dei seguenti modi:

  • Terminazione normale: (1) Return da main (2) Invocazione di exit() (3) Invocazione di _exit o _Exit (4) Return dell’ultimo thread (5) Invocazione di pthread_exit
  • Terminazione anormale: (6) Invocazione di abort (7) Ricezione di un segnale (8) Risposta dell’ultimo thread ad una richiesta di cancellazione.

Se il main termina senza invocare return o exit, il kernel esegue la system call exit.
Le funzioni della famiglia exit sono 3: _exit(), _Exit() ed exit(). Le prime due ritornano al kernel immediatamente mentre l’ultima esegue alcune operazioni di “clean up” come lo svuotamento dei buffer per l’I/O bufferizzato.
Tutte le funzioni di exit prendono un parametro intero, detto l’exit status dell’applicazione. Tipicamente è possibile analizzare l’exit status delle applicazioni sia da shell che da altre applicazioni.
La system call atexit() consente di definire fino a 32 funzioni da eseguire dopo l’invocazione della exit e prima che il controllo ritorni al kernel. Queste funzioni, dette exit handlers, non possono ricevere parametri e non ritornano alcun valore.
Gli exit handler consentono al progettista di definire, indipendentemente dal punto di terminazione, le procedure per una chiusura “pulita” dell’applicazione. Ad esempio, eliminazione di file temporanei, signalling ad altri processi etc.

Process Environment (segue)

Quando un programma viene eseguito, la system call exec può passare al programma parametri su riga di comando. In particolare, se il main è dichiarato come segue: int main(int argc, char *argv[]);

  • argc indica il numero totale di stringhe su riga di comando
  • argv[0] contiene il nome dell’eseguibile
  • argv[1] il primo parametro
  • ..
  • argv[argc-1] l’ultimo parametro.

Il seguente programma: Mostra codice riporta un esempio di accesso ai parametri su riga di comando. All'atto della sua esecuzione, il sistema operativo carica nella memoria del processo anche le variabili d'ambiente (visualizzabili da shell con in comando env) nell'array di stringhe extern char **environ. Tutte le stringhe di environ sono:

  • Nella forma nome=valore;
  • Terminate dal carattere nullo;
  • La stringa nome è composta da soli caratteri maiuscoli ed identifica il nome di una variabile d'ambiente;
  • L'ultimo elemento dell'array è un puntatore nullo, utilizzato per identificare la fine dell'array.

Il contenuto della variabile environ: Mostra codice

Atexit: Un esempio

Il seguente programma: Mostra codice riporta un esempio di utilizzo della funzione atexit.
Il programma definisce tre gestori, primo_handler, secondo_handler e terzo_handler, che contengono istruzioni da eseguire "prima della terminazione" del processo.
Il programma d'esempio, installa I tre handler nell'ordine indicato, visualizza un messaggio e termina.
Gli exit handler:

  • Vengono eseguiti quando il processo termina normalmente, eccetto nei casi in cui siano state invocate le system call _exit o _Exit
  • Non vengono eseguiti nel caso terminazione con errore.

Si noti che l'esecuzione degli handler avviene nell'ordine inverso rispetto all'installazione.

Visualizzazione delle variabili d'ambiente: Mostra codice

Il layout della memoria

Storicamente, un programma C è composto dalle seguenti aree:

  • Testo. Contiene le istruzioni macchina del programma.
    • Tipicamente condiviso tra diverse istanze della stessa applicazione, in modo da avere in memoria una sola copia del codice.
    • Protetto dalla scrittura per evitare modifiche accidentali.
  • Inizialized Data segment (o semplicemente data segment). Contiene le variabili definite all’esterno di qualsiasi funzione (globali) ed inizializzate esplicitamente. La loro descrizione ed I relativi valori di inizializzazione sono (ovviamente) memorizzati nel file eseguibile.
  • Uninizialized Data segment. Contiene le variabili definite all’esterno di qualsiasi funzione non esplicitamente inizializzate. Vengono inizializzate dal kernel con “zeri” o puntatori nulli all’atto dell’esecuzione.
  • Stack. Contiene:
    • Le informazioni relative alle chiamate a funzione. In particolare, lo stato della funzione chiamante (valori dei registri della CPU) incluso il l’indirizzo dell’istruzione da eseguire quando la funzione termina.
    • Le variabili locali della funzione in esecuzione. Quando si invoca una funzione, il sistema operativo alloca sullo stack lo spazio per le variabili che la funzione utilizzerà durante la sua esecuzione. All’atto della terminazione della funzione, questo spazio viene deallocato.
  • Heap. Spazio riservato all’allocazione dinamica della memoria.

Shared libraries

Qualsiasi programma (tranne quelli scritti in assembly) utilizzano un insieme di funzioni di libreria.
Storicamente il codice macchina che implementava queste funzioni veniva incluso nel testo (eseguibile) del programma durante la fase di linking.
Questa strategia offriva l’indubbio vantaggio che ogni “programma” era “indipendente” dalle librerie presenti sul calcolatore su cui doveva essere eseguito. Lo stesso eseguibile poteva essere copiato da un computer ad un altro (con la stessa architettura e lo stesso sistema operativo) ed essere eseguito senza alcun problema.
La stessa strategia, però, presenta due effetti collaterali non trascurabili:

  • Il codice della libreria viene memorizzata su disco più volte. In particolare, ogni programma che utilizza una libreria include una copia della libreria stessa.
  • La modifica ad una libreria, e.g., a causa di un bug, richiede la ricompilazione di tutte le applicazioni che utilizzano quella libreria specifica.

Una shared library evita l’inclusione del codice della libreria nell’eseguibile. Il programma mantiene solo un “riferimento” alla libreria. Quando un programma viene eseguito, il sistema operativo verifica se le librerie utilizzate sono o meno in memoria. In caso affermativo, esegue un “collegamento” dinamico dei riferimenti nel programma alle librerie in memoria. In caso contrario, carica in memoria le librerie ed esegue il collegamento.

Allocazione delle memoria

Il linguaggio C standard definisce le seguenti funzioni per l’allocazione della memoria:
void malloc(size_t size): alloca il numero di byte indicato dal parametro size. Lo stato della memoria allocata è indeterminato.
void calloc(size_t nobj, size_t size): alloca spazio per contenere il numero di oggetti indicato da nobj della dimensione del il numero di byte indicato dal parametro size. Lo spazio allocato è inizializzato a zero.
void realloc(void *ptr, size_t newsize): incrementa o diminuisce lo spazio allocato a ptr fino a newsize byte. Se la nuova dimensione è maggiore della precedente, è possibile che sia richiesto lo spostamento dei dati puntati da ptr in una nuova area di memoria. Inoltre, in caso di estensione, lo stato della memoria compresa tra il vecchio ed il nuovo limite è indeterminato.
La funzione void free(void *ptr) rilascia lo spazio puntato dal parametro ptr. Errori classici sono:

  • Scrivere “oltre” i limiti di un’area dinamica. Il linguaggio C non esegue alcun controllo sui limiti delle strutture. Se si è “fortunati” l’indirizzo a cui si tenta di scrivere è protetto (e.g., memoria non allocata) ed il programma termina con segmentation fault. In caso contrario, il programma sovrascrive altre strutture allocate dinamicamente, con il risultato di un comportamento anomalo e difficilmente analizzabile.
  • Mancata deallocazione della memoria. Provoca un aumento costante della memoria allocata dal processo, detto memory leak. In sistemi complessi che rimangono in esecuzione per lunghi periodi di tempo, e.g. un sistema operativo, una piccola memory leak porta al degrado inesorabile delle performance del sistema.
  • Utilizzo di un puntatore nullo o deallocato.
  • Deallocazione multipla dello steso puntatore o deallocazione di puntatori non ritornati dalle funzioni di allocazione.

Allocazione delle memoria: un esempio

Il seguente programma: Mostra codice, mostra un esempio classico di errore legato all'allocazione dinamica della memoria.
Il programma alloca due array di caratteri di 10 byte.
I due array, a e b, vengono inizializzati con 10 caratteri 'a' e 'b', rispettivamente e vengono quindi visualizzati.
Successivamente vengono inseriti 20 elementi nell'array a, utilizzando il doppio della memoria ad esso allocata.
I due array sono contigui in memorie. La seconda reinizializzazione di a sovrascrive, in parte, l'array b, come è evidente dall'output riportato di seguito: Mostra codice.
Array A
aaaaaaaaaa
Array B
bbbbbbbbbb
Array B
aaaabbbbbb

Variabili d’ambiente

Come abbiamo visto, le variabili d’ambiente sono definite da stringhe del tipo nome=valore.
Lo standard ISO definisce funzioni per accedere ai valori delle variabili d’ambiente.
La funzione getenv consente di ottenere il valore di una variabile dato il suo nome:
char *getenv(char *name);
I nomi di alcune variabili d’ambiente sono definite dallo standard POSIX, altri sono system-dependent.
In alcuni casi si rende necessario modificare il valore delle variabili di ambiente esistenti o di creare nuove variabili d’ambiente. È da tener presente, però, che qualsiasi modifica fatta all’environment avrà effetto solo sul processo corrente ed eventuali processi “figli”. Non è possibile modificare, ad esempio, l’ambiente del processo “padre” di quello corrente, che in molti casi è la shell che ha eseguito il processo corrente
Le funzioni per la modifica dell’environment sono:

  • int putenv(char *str). Prende in input una stringa nome=valore e lo aggiunge alla lista delle variabili d’ambiente. Se il nome e’ gia presente nella lista, il suo vecchio valore viene eliminato.
  • int setenv(char *nome, char *valore, int sovrascrivi). Aggiunge alla lista delle variabili d’ambiente la stringa nome=valore. Se nome gia esiste allora (a) se sovrascrivi=0, non viene effettuata alcuna modifica (b) se sovrascrivi≠0, la vecchia definizione viene eliminata.
  • int unsetenv(char *nome). Rimuove dalla lista la coppia nome=valore.

Variabili d’ambiente: un esempio

Il seguente programma: Mostra codice, riporta un esempio di utilizzo delle funzioni getenv e putnev.
Il programma viene eseguito nella directory /home/lso/GETENV che contiene come unica entry la sottodirectory "TEST", oltre "." e "..".
La sottodirectory TEST contiene un solo file regolare, "chdir.txt".
La funzione getenv con parametro "PWD", ritorna il valore della directory corrente, /home/lso/GETENV.
La open del file "chdir.txt" eseguita nella directory corrente, ritorna un errore.
A questo punto, il programma modifica la variabile PWD ponendo il suo valore a:
/home/lo/GETENV/TEST
L'esecuzione della open, questa volta, apre correttamente il file.

L'output del programma: Mostra codice.

Limiti delle risorse

Ad ogni processo sono associate una serie di limiti alle risorse utilizzabili. Il processo può richiedere I valori dei limiti ed, in alcuni casi, modificarli utilizzando le seguenti funzioni:
int getrlimit(int resource, struct rlimit *res);
int setrlimit(int resource, struct rlimit *res);

Entrambe le funzioni:

  • Specificano la risorsa per cui ottenere (getrlimit) o modificare (setrlimit) i limiti
  • Specificano un puntatore ad una struttura struct rlimit {rlim_t rlim_cur; rlim_t rlim_max;} contenente due valori:
    • rlim_cur: limite corrente.
    • rlim_max: limite massimo.
  • Le modifiche sono governate dalle seguenti regole:
    • Ogni processo può aumentare il suo limite corrente fino ad un valore inferiore od uguale al limite massimo;
    • Ogni processo può diminuire il limite massimo fino ad un valore maggiore od uguale al limite corrente. La riduzione è irreversibile per gli utenti normali.
    • Solo il superutente può aumentare il limite massimo associato ad una risorsa.
  • I limiti delle risorse vengono ereditate dai processi “figli”. Quindi, ad esempio, I limiti associati ad una shell vengono ereditati da tutti I processi da essa eseguiti.

Limiti delle risorse

Di seguito riportiamo alcuni esempi di limiti definiti dalla Single Unix Specification. Per specificare un limite infinito, è possibile utilizzare la costante RLIM_INFINITY.
RLIMIT_AS: Dimensione massima in byte della memoria totale disponibile per un processo;
RLIMIT_CORE: Dimensione massima di un file core (immagine della memoria generata in alcuni casi in seguito ad errore). Un limite di zero inibisce la creazione dei file core.
RLIMIT_CPU: Massima quantità di CPU time per il processo in secondi.
RLIMIT_DATA: Dimensione massima in byte del segmento dati, ottenuto sommando dati inizializzati, dati non inizializzati ed heap.
RLIMIT_NOFILE: Numero massimo di file aperti per processo.
RLIMIT_NPROC: Numero massimo di processi per il real user ID. Questo limite è legato agli utenti più che ad un processo. Certamente influenza il processo corrente in quanto inibisce la creazione di nuovi processi.
RLIMIT_STACK: Dimensione massima, in byte, dello stack.

I materiali di supporto della lezione

W.R. Stevens, S.A. Rago - Advanced Programming in the Unix Environment - Capitolo 6, Capitolo 7 (Escluso paragrafo 10)

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion