Un processo (o task) è l’istanza dell’esecuzione di un determinato programma: quando si lancia un file eseguibile viene creato un processo in memoria che esegue le istruzioni indicate dal programma. In genere l’esecuzione di un programma genera un processo, ma programmi più complessi (per esempio quelli che gestiscono comunicazioni client-server) possono generare più processi.
Quando viene lanciato in esecuzione un programma, il kernel crea un processo in memoria assegnandogli un numero intero che lo identifica univocamente all’interno della macchina: il PID (Process IDentifier). In genere il PID è sequenziale, ovvero ad ogni processo viene assegnato il PID del processo precedente incrementato di 1 ed al primo processo lanciato dal sistema (init) viene assegnato il PID uguale ad 1. Infatti, quando viene avviato il sistema, il kernel crea il processo init lanciando in esecuzione il comando /sbin/init (man page init(8))22 e questo è l’unico processo creato dal kernel. Qualunque altro processo viene sempre creato a partire da una richiesta effettuata da un processo già esistente. In questo modo si viene a creare una gerarchia di processi (process tree o albero dei processi) alla cui radice c’è init. Un processo A che crea un processo B è detto processo genitore (o processo padre) del processo B ed il processo B è detto processo figlio del processo A. Poiché ogni processo è identificato dal PID, è una convenzione diffusa riferirsi al PID del processo genitore con il termine PPID (Parent PID).
La struttura dati relativa ad un processo è riportata di seguito (sorgente C)
struct task_struct { /* these are hardcoded - don't touch */ volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ long counter; long priority; unsigned long signal; unsigned long blocked; /* bitmap of masked signals */ unsigned long flags; /* per process flags, defined below */ int errno; long debugreg[8]; /* Hardware debugging registers */ struct exec_domain *exec_domain; /* various fields */ struct linux_binfmt *binfmt; struct task_struct *next_task, *prev_task; struct task_struct *next_run, *prev_run; unsigned long saved_kernel_stack; unsigned long kernel_stack_page; int exit_code, exit_signal; /* ??? */ unsigned long personality; int dumpable:1; int did_exec:1; int pid; int pgrp; int tty_old_pgrp; int session; /* boolean value for session group leader */ int leader; int groups[NGROUPS]; /* * pointers to (original) parent process, youngest child, younger sibling, * older sibling, respectively. (p->father can be replaced with * p->p_pptr->pid) */ struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct wait_queue *wait_chldexit; unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; unsigned long timeout, policy, rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; long utime, stime, cutime, cstime, start_time; /* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; unsigned long swap_address; unsigned long old_maj_flt; /* old value of maj_flt */ unsigned long dec_flt; /* page fault count of the last time */ unsigned long swap_cnt; /* number of pages to swap on next pass */ /* limits */ struct rlimit rlim[RLIM_NLIMITS]; unsigned short used_math; char comm[16]; /* file system info */ int link_count; struct tty_struct *tty; /* NULL if no tty */ /* ipc stuff */ struct sem_undo *semundo; struct sem_queue *semsleeping; /* ldt for this task - used by Wine. If NULL, default_ldt is used */ struct desc_struct *ldt; /* tss for this task */ struct thread_struct tss; /* filesystem information */ struct fs_struct *fs; /* open file information */ struct files_struct *files; /* memory management info */ struct mm_struct *mm; /* signal handlers */ struct signal_struct *sig; #ifdef __SMP__ int processor; int last_processor; int lock_depth; /* Lock depth. We can context switch in and out of holding a syscall kernel lock... */ #endif };
Generalmente il PGID di un processo coincide con il suo PID, tranne nel caso in cui il processo venga lanciato in esecuzione come parte di un comando più complesso, che ne lancia altri in cascata. Come sarà accennatto nella sez. 7.6 ed illustrato nella sez. 3.4.13, è possibile lanciare in esecuzione una serie di comandi correlati tra loro: si può far eseguire, ad esempio, un comando A, indicando al sistema di fornire il risultato dello stesso ad un altro comando B. Si consideri la seguente riga di comando
$ ls -l | less
La riga di comando precedente è in realtà formata da due comandi distinti ls -l
(comando A) e less (comando B). In tal caso il sistema non fa altro che eseguire il comando
ls con l’opzione -l, che produce in uscita il contenuto della working directory e quindi lo
passa al comando less che permette uno scorrimento più agevole dello stesso. Quindi, il
comando ls genererà un processo con il proprio PID e con PGID = PID, mentre less ne
genererà un altro con il proprio PID ma con lo stesso PGID del processo generato da ls,
poiché entrambi facenti parti dello stesso process group, cioè i due comandi fanno parte della
stessa riga di comando. Il processo generato da less attenderà, per mezzo di una
pipe23,
il risultato del processo generato da ls e quindi produrrà il suo output.
In genere il real UID coincide con l’effective UID (ed analogamente il real GID coincide con l’effective GID), ma se un file ha impostato il bit suid (v. cap. 4), le proprietà di tipo real si riferiscono all’utente che ha lanciato l’esecuzione del file, mentre quelle di tipo effective si riferiscono all’utente proprietario del file stesso.
I sistemi Unix-like che si attengono alle specifiche POSIX, utilizzano anche le proprietà saved UID e saved GID che sono una copia dei valori delle relative proprietà di tipo effective e servono per tener traccia di tali valori qualora le proprietà di tipo effective varino.
Le proprietà di tipo effective caratterizzano il processo, ovvero determinano i privilegi che esso ha sul sistema.
Le proprietà di tipo filesystem sono utilizzate per l’accesso ai file, ovvero determinano i permessi del processo sul filesystem: il processo apre i file come se fosse l’utente identificato dall’effective UID (appartenente al gruppo effective GID). In particolare, quando un file viene creato, il proprietario è l’utente identificato dall’effective UID. Esse sono state introdotte da GNU/Linux per rendere l’accesso al NFS24 più sicuro. In genere tali valori coincidono con quelle di tipo effective poiché una qualunque variazione di esse si riflette su quest’ultime, ma c’è un caso in cui le proprietà di tipo filesystem sono diverse dalle relative di tipo effective: quando il server NFS deve impostare i privilegi di accesso al NFS (in questo modo il server NFS concede agli utenti gli opportuni privilegi soltanto per l’accesso al NFS, ma non per la comunicazione con il server NFS stesso).
Il kernel di GNU/Linux permette l’accesso ai dati contenuti nella struttura dei processi per mezzo di un filesystem virtuale montato nella directory /proc. Tali dati sono utilizzati dalla maggior parte dei programmi per la gestione dei processi.
Per ogni processo il kernel riserva uno specifico spazio di memoria: in questo modo due processi che girano “contemporaneamente” sul sistema non si accorgono l’uno dell’altro e il malfunzionamento dell’uno non causa problemi all’altro. Inoltre, un processo non può accedere alla memoria assegnata ad un altro processo se non attraverso l’utilizzo di particolari meccanismi gestiti e controllati dal kernel.