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:
char *pw_name.
char *pw_passwd.
uid_t pw_uid.
gid_t pw_gid.
char *pw_gecos.
char *pw_dir.
char *pw_shell.
char *pw_class.
time_t pw_change.
time_t pw_expire.
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.
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();
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
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.
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.
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:
char *gr_name.
char *gr_passwd.
gid_t gr_gid.
char **gr_mem.
L’array di stringhe è terminato da un puntatore nullo.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);
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
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.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.
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.Come per I database utenti e gruppi, anche per questi database esistono funzioni per:
L’accesso sequenziale ai record:
L’accesso indicizzato ai record. Tutte le strutture sono definite nell’header netdb.h:
I sistemi Unix mantengono due file per l’accounting 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
};
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:
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.
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.
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.
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
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.
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:
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.
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[]);
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:
Il contenuto della variabile environ: Mostra codice
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:
Si noti che l'esecuzione degli handler avviene nell'ordine inverso rispetto all'installazione.
Visualizzazione delle variabili d'ambiente: Mostra codice
Storicamente, un programma C è composto dalle seguenti aree:
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:
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.
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:
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
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.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.
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:
struct rlimit {rlim_t rlim_cur; rlim_t rlim_max;}
contenente due valori:
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.
1. Introduzione ai sistemi Unix
2. Principi di programmazione Shell
3. Esercitazioni su shell scripting - parte prima
5. Esercitazioni su shell scripting - parte seconda
6. Espressioni Regolari ed Introduzione ad AWK
7. Esercitazioni su espressioni regolari ed awk scripting
9. Esercitazioni su awk scripting - parte seconda
10. Programmazione in linguaggio C: Input/Output di basso livello
11. Esercitazioni su I/O di basso livello
12. Interazione con file di sistema e variabili d'ambiente
13. Esercitazioni sulla gestione dei file di sistema e le variabili...
14. System call per la gestione di file e directory
15. Esercitazioni su gestione file e directory
16. La programmazione multi-processo
17. Esercitazioni su programmazione multi-processo
18. I Segnali
20. I Socket
W.R. Stevens, S.A. Rago - Advanced Programming in the Unix Environment - Capitolo 6, Capitolo 7 (Escluso paragrafo 10)