Ritorna indietro

Lezione 2 - Processi

Pubblicato il 11/8/21 da SeekBytes – 3390 parole

Ripasso

Definizione di Processo

Un processo è un’istanza di un programma in esecuzione.

Dal punto di vista del kernel, un processo consiste di:

Attributi di un processo

Identificativo di un processo (PID)

getpid

La chiamata di sistema getpid restituisce l’ID di processo del processo chiamante.

#include <unistd.h>
#include <sys/types.h>

pid_t getpid(void);

Il tipo di dati pid_t usato per il valore di ritorno di getpid è un tipo intero allo scopo di memorizzare gli ID dei processi. Con l’eccezione di alcuni processi di sistema come init (process ID 1), non c’è una relazione fissa tra un programma e l’ID del processo processo che viene creato per eseguire quel programma.

Esempio user@localhost[~]$ ps auxf

Nota bene: la chiamata a getpid funziona SEMPRE!!!

Real User ID e Effective User ID

getuid, getgid, geteuid, getegid

Le chiamate di sistema getuid e getgid restituiscono, rispettivamente, l’ID utente reale e l’ID del gruppo reale del processo chiamante. Le chiamate di sistema geteuid e getegid eseguono i compiti corrispondenti per gli ID effettivi.

Nota bene: funzionano sempre, non ritornano alcun tipo di errore.

#include <unistd.h>
#include <sys/types.h>

uid_t getuid(void); // Real user ID
uid_t geteuid(void); // Effective user ID
gid_t getgid(void); // Real group ID
gid_t getegid(void); // Effective group ID
Esempio getuid, getgid, geteuid, getegid

Questo è il contenuto di un file program.c

#include <unistd.h>
#include <sys/types.h>
int main (int argc, char *argv[]) {
	printf("PID: %d, user-ID: real %d, effective %d\n", getpid(), getuid(), geteuid());
	return 0; 
}
user@localhost[~]$ gcc -o program program.c
user@localhost[~]$ ls -l program
-r-xr-xr-x 1 Professor Professor 8712 Jan 16 16:27 program
user@localhost[~]$ ./program
PID: 1234, user-ID: real 1000, effective 1000
user@localhost[~]$ sudo ./program
PID: 1423, user-ID: real 0, effective 0
user@localhost[~]$ sudo chmod u+s program
user@localhost[~]$ ls -l program
-r-sr-xr-x 1 root Professor 8712 Jan 16 16:27 program
user@localhost[~]$ ./program
PID: 4321, user-ID: real 1000, effective 0

Tieni a mente: se lo Sticky bit non è settato, allora i permessi dell’utente sono garantiti all’eseguibile per fare operazioni. Altrimenti se è impostato, allora i permessi del proprietario sono garantiti all’eseguibile.

Environ

Ogni processo ha un array associato di stringhe chiamato lista d’ambiente, o semplicemente ambiente. Ognuna di queste stringhe è una definizione della forma nome = valore. Quando un nuovo processo viene creato, eredita una copia dell’ambiente del suo genitore.

La struttura dell’elenco degli ambienti è la seguente:

All’interno di un programma C, si può accedere all’elenco degli ambienti in due modi:

Esempio prima tecnica
#include <stdio.h>
// Global variable pointing to the enviroment of the process.
extern char **environ;
int main(int argc, char *argv[]) {
	for (char **it = environ; (*it) != NULL; ++it) { 
		printf("--> %s\n", *it);
	}
	return 0;
}
user@localhost[~]$ ./program --> $HOME=/home/Professor
--> $PWD=/tmp
--> $USER=Professor
Esempio seconda tecnica
#include <stdio.h>
int main(int argc, char *argv[], char* env[]) {
	for (char **it = env; (*it) != NULL; ++it) { 
		printf("--> %s\n", *it);
	}
	return 0;
}
user@localhost[~]$ ./program --> $HOME=/home/Professor
--> $PWD=/tmp
--> $USER=Professor

getenv

Dato il nome di variabile name, getenv ritorna un puntatore al valore della stringa accessibile tramite name oppure NULL se non esiste la variabile d’ambiente.

#include <stdlib.h>
// Returns pointer to (value) string, or NULL if no such variable exists
char *getenv(const char *name);	

setenv

Setenv aggiunge name=value all’ambiente, a meno che non esista già una variabile identificata da name e overwrite abbia il valore 0. Se overwrite è diverso da zero, l’ambiente viene sempre cambiato. Ritorna 0 se ha successo, oppure -1 in caso di errore.

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);

unsetenv

unsetenv rimuove la variabile dall’ambiente identificata dal nome.

#include <stdlib.h>
int unsetenv(const char *name);

Directory del processo

Un processo può recuperare la sua directory di lavoro corrente usando getcwd.

getcwd

Quando l’invocazione ha successo, getcwd restituisce un puntatore a cwdbuf come risultato della sua funzione. Se il percorso della directory di lavoro corrente supera i byte di dimensione, allora getcwd restituisce NULL.

#include <unistd.h>
// Returns cwdbuf on success, or NULL on error.
char *getcwd(char *cwdbuf, size_t size);

Il chiamante deve allocare il buffer cwdbuf in modo che abbia una lunghezza minima di byte di dimensione. (Normalmente, dimensioneremmo cwdbuf usando la costante PATH_MAX).

chdir

La chiamata di sistema chdir cambia la directory di lavoro corrente del processo chiamante nel percorso relativo o assoluto specificato in pathname.

#include <unistd.h>
// Returns 0 on success, or -1 on error
int chdir(const char *pathname);

fchdir

La chiamata di sistema fchdir fa la stessa cosa di chdir, eccetto che la directory è specificata tramite un descrittore di file precedentemente ottenuto aprendo la directory con open.

#define _BSD_SOURCE
#include <unistd.h>
	
// Returns 0 on success, or -1 on error.
int fchdir(int fd);
Esempio con la fchdir
char buf[PATH_MAX];
// Open the current working directory
int fd = open(".", O_RDONLY);
getcwd(buf, PATH_MAX);
printf("1) Current dir:\n\t%s\n", buf);
// Move the process into /tmp
chdir("/tmp"); getcwd(buf, PATH_MAX);
printf("2) Current dir:\n\t%s\n", buf);
// Move the process back into the initial directory
fchdir(fd);
getcwd(buf, PATH_MAX);
printf("3) Current dir:\n\t%s\n", buf);
// Close the file descriptor
close(fd);

output:

1) Current dir:
/home/Professor
2) Current dir:
/tmp
3) Current dir:
/home/Professor

File Descriptor Table

Ogni processo ha una file descriptor table associata. Ogni voce rappresenta una risorsa di input/output (es. file, pipe, socket) usata dal processo.

La directory /proc/<PID>/fd contiene un collegamento simbolico per ogni voce della tabella dei descrittori di file di un processo. La cartella /proc/ è uno pseudo-file system che non contiene file reali, ma alcune informazioni di sistema a tempo di esecuzione.

Un processo creato ha sempre tre descrittori di file (stdin, stdout, stderr).

Vedere i file descriptor associati ad un processo
user@localhost[~]$ sleep 30 &
[1] 1344
user@localhost[~]$ ls -l /proc/1344/fd
totale 0
lrwx------ 1 Professor Professor 0 Gen 18 12:35 0 -> /dev/pts/0
lrwx------ 1 Professor Professor 0 Gen 18 12:35 1 -> /dev/pts/0 
lrwx------ 1 Professor Professor 0 Gen 18 12:35 2 -> /dev/pts/0
Visualizzare le voci del descrittore di file di un processo
char buf[PATH_MAX];
// Replace %i with PID, and store the resulting string in buf.
snprintf(buf, PATH_MAX, "/proc/%i/fd/", getpid());
DIR *dir = opendir(buf);

struct dirent *dp;
while ((dp = readdir(dir)) != NULL) {
	if ((strcmp(dp->d_name,".") != 0) && (strcmp(dp->d_name,"..") != 0)) { 
		printf("\tEntry: %s\n", dp->d_name);
	} 
}
closedir(dir);
user@localhost[~]$ ./program
Entry: 0 // link to stdin
Entry: 1 // link to stdout
Entry: 2 // link to stderr
Entry: 3 // link to /proc/<PID>/fd directory

Importante!

Ad una nuova voce nella file descriptor table viene sempre assegnato l’indice più basso disponibile. Guarda l’esempio sotto.
Reindirizzare il flusso di output standard di un processo a un file chiamato myfile
// We close STDOUT which has FD 1. The remaining file descriptors have // index 0 (stdin) and 2 (stderr).
close(STDOUT_FILENO);
// We open a new file, to which will be assigned FD 1 automatically
// because it is the lowest available index in the table.
int fd = open("myfile", O_TRUNC | O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR);
// Printf uses the FD 1, thus, it will print on the file.
printf("ciao\n");
Nessuna stringa sarà visualizzata sul terminale, poiché il flusso stdout è chiuso. Tuttavia, tutte le stringhe stampate da printf saranno riportate in myfile.

dup

La chiamata di sistema dup prende un descrittore di file aperto e restituisce un nuovo descrittore che si riferisce alla stessa descrizione del file aperto. Il nuovo descrittore è garantito essere il più basso descrittore di file inutilizzato.

#include <unistd.h>
// Returns (new) file descriptor on success, or -1 on error.
int dup(int oldfd);
Esempio con dup
// FDT: [0, 1, 2] -> [0, 2]
close(STDOUT_FILENO);
// FDT: [0, 2] -> [0]
close(STDERR_FILENO);
// FDT: [0] -> [0, 1]
int fd = open("myfile", O_TRUNC | O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); // FDT: [0, 1] -> [0, 1, 2]
dup(1);
// FDT: [0: STDIN, 1: myfile, 2: myfile]
printf("Have a good ");
fflush(stdout);
fprintf(stderr, "day!\n");
 user@localhost[~]$ cat myfile Have a good day!

Operazioni con i processi

Terminazione dei processi

_exit

Il processo che chiama _exit() viene sempre terminato con successo.

#include <unistd.h>

void _exit(int status);

Il primo byte dell’argomento status definisce lo stato di terminazione del processo. Per convenzione, il valore zero indica che il processo si è concluso con successo, un valore di stato non nullo indica che il processo si è concluso senza successo.

exit

I programmi generalmente chiamano exit() piuttosto che _exit().

#include <stdlib.h> 

// N.B. provided by C library
void exit(int status);

La libreria C definisce le macro EXIT_SUCCESS (0) e EXIT_FAILURE (1) Le seguenti azioni sono eseguite dal metodo exit():

Un gestore di uscita è una funzione che viene registrata durante la vita di un processo. Viene chiamata automaticamente durante la terminazione del processo tramite exit().

atexit

La atexit() aggiunge il puntatore di funzione fornito func a una lista di funzioni che sono chiamate durante la terminazione del processo.

#include <stdlib.h>

// Returns 0 on success, or nonzero on error.
int atexit(void (*func)(void));

func deve essere definita: non deve prendere argomenti e non deve restituire alcun valore. Se sono registrati più gestori di uscita, allora vengono chiamati nell’ordine inverso di registrazione.

Esempio con atexit
#include <stdlib.h> #include <stdio.h>
#include <unistd.h>
void func1() { printf("\tAtexit function 1 called\n"); }
void func2() { printf("\tAtexit function 2 called\n"); }
int main (int argc, char *argv[]) {
if (atexit(func1) != 0 || atexit(func2) != 0) _exit(EXIT_FAILURE);
exit(EXIT_SUCCESS); }

Ecco l’output del programma:

user@localhost[~]$ ./exit_handlers
Atexit function 2 called
Atexit function 1 called

Un altro modo in cui un processo può terminare è il ritorno da main():

Creazione dei processi

fork

La chiamata di sistema fork() crea un nuovo processo, il figlio, che è un duplicato quasi esatto del processo chiamante, il genitore.

#include <unistd.h>
// Processo padre: ritorna il process ID del figlio se la chiamata ha successo, altrimenti -1 in caso di errore.
// Nel processo figlio creato: ritorna sempre 0.
pid_t fork(void);

Dopo l’esecuzione di una fork(), esistono due processi e, in ogni processo, l’esecuzione continua dal punto in cui la fork() ritorna.

È indeterminato quale dei due processi sia il prossimo ad utilizzare la CPU.

Il processo figlio riceve i duplicati di tutti i descrittori di file del genitore e le relative memorie condivise (vedere le diapositive Filesystem e IPC).

Nel processo padre, ritorna il PID del processo figlio, altrimenti -1. Nel processo appena creato, la fork ritorna 0.

Esempio con la fork
#include <unistd.h>
int main (int argc, char *argv[]) {
	int stack = 111; pid_t pid = fork();
	if (pid == -1) errExit("fork");
	// -->Both parent and child come here !!!<--
	if (pid == 0)
		stack = stack * 4;
		printf("\t%s stack %d\n", (pid==0) ? "(child )" : "(parent)", stack);
	}
	return 0;
}

Output del programma:

user@localhost[~]$ ./example_fork (parent) stack 111
(child ) stack 444
user@localhost[~]$ ./example_fork (child ) stack 444
(parent) stack 111

L’output del terminale mostra che:

getppid

Ogni processo ha un genitore, cioè il processo che lo ha creato.

#include <unistd.h>

// Always successfully returns PID of caller’s parent.
pid_t getppid(void);

L’antenato di tutti i processi è il processo init (PID=1). Se un processo figlio diventa orfano perché il suo genitore termina, allora il figlio viene “adottato” dal processo init. Le successive chiamate a getppid() nel figlio restituiscono 1.

Esempio codice e PID zombie
	#include <unistd.h>
	int main (int argc, char *argv[]) { 
		pid_t pid = fork();
		if (pid == -1) { 
			errExit("fork");
		}
		if (pid == 0) {
			printf("(child ) PID: %d PPID: %d\n", getpid(), getppid());
		}
		else {
    		printf("(parent) PID: %d PPID: %d\n", getpid(), getppid());
		}
		return 0;
	}

L’esecuzione dell’esempio precedente ha tre diversi scenari:

  1. Il figlio viene eseguito dopo il genitore, e il genitore non viene terminato
(genitore) PID: 402 PPID: 350
(figlio) PID: 403 PPID: 402
  1. Il processo figlio viene eseguito prima del processo del genitore
(figlio ) PID: 403 PPID: 402
(genitore) PID: 402 PPID: 350
  1. Il processo figlio viene eseguito dopo la fine del genitore (processo zombie!)
(genitore) PID: 402 PPID: 350
(figlio) PID: 403 PPID: 1

Controllo dei processi figlio

wait

La chiamata di sistema wait attende che uno dei figli del processo chiamante termini. (vedere waitpid per l’argomento di ingresso dello stato).

#include <sys/wait.h>

// Returns PID of terminated child, or -1 on error.
pid_t wait(int *status)

Le seguenti azioni sono eseguite da wait:

Esempio di attesa processo figlio
	for (int i = 1; i <= 3; ++i) {
// Fork and ignore fork failures.
if (fork() == 0) {
printf("Child %d sleeps %d seconds...\n", getpid(), i); // Suspends the calling process for i seconds
sleep(i); _exit(0);
} }
pid_t child;
while ((child = wait(NULL)) != -1)
printf("wait() returned child %d\n", child);
if (errno != ECHILD)
printf("(wait) An unexpected error...\n");

Esempio di output:

user@localhost[~]$ ./example_wait child 75 sleeps 1 seconds
child 76 sleeps 2 seconds
child 77 sleeps 3 seconds
wait() returned child 75
wait() returned child 76
wait() returned child 77

Che cosa succede ad un processo figlio che termina prima che il processo padre abbia l’opportunità di invocare la wait?

Il kernel affronta questa situazione trasformando il processo figlio terminato in un processo zombie. Questo significa che la maggior parte delle risorse detenute dal processo figlio vengono rilasciate al sistema. Le uniche parti del processo terminato ancora mantenute sono:

  1. il suo ID di processo;
  2. il suo stato di terminazione;
  3. le statistiche di utilizzo delle risorse.

Se il processo padre termina senza chiamare wait, allora il processo figlio zombie viene “adottato” dal processo init, che eseguirà una chiamata di sistema wait qualche tempo dopo.

waitpid

La chiamata di sistema waitpid sospende l’esecuzione del processo chiamante finché un figlio specificato dall’argomento pid non ha cambiato stato.

#include <sys/wait.h>

// Returns a PID, 0, or -1 on error.
pid_t waitpid(pid_t pid, int *status, int options);

L’argomento status è lo stesso di wait. Il valore del pid determina quale processo figlio vogliamo aspettare.

L’argomento options della chiamata di sistema waitpid è un OR di zero o alpiù delle seguenti costanti:

Esempio 1 con la waitpid
pid_t pid;
for (int i = 0; i < 3; ++i) {
pid = fork();
if (pid == 0) {
// Code executed by the child process...
_exit(0);
} }
// The parent process only waits for the last created child
waitpid(pid, NULL, 0);
Esempio 2 con la waitpid
pid_t pid = fork(); 
if (pid == 0) {
	// Code executed by the child process
} else {
	// Waiting for a terminated/stopped | resumed child process.
	waitpid(pid, NULL, WUNTRACED | WCONTINUED);
}

Il valore di stato impostato da waitpid, e wait, ci permette di distinguere i seguenti eventi per un processo figlio:

  1. Il processo figlio è terminato chiamando exit (o uscita).
    La macro WIFEXITED restituisce true se il processo figlio è uscito normalmente. La macro WEXITSTATUS restituisce lo stato di uscita del processo figlio.

    Esempio situazione 1
    waitpid(-1, &status, WUNTRACED | WCONTINUED);
    if (WIFEXITED(status)) {
        printf("Child exited, status=%d\n", WEXITSTATUS(status));
    }

  2. Il processo figlio è stato terminato con la consegna di un segnale non gestito.
    La macro WIFSIGNALED restituisce true se il bambino è stato ucciso da un segnale. La macro WTERMSIG restituisce il numero del segnale che ha causato la fine del processo.

    Esempio situazione 2
    waitpid(-1, &status, WUNTRACED | WCONTINUED);
    if (WIFSIGNALED(status)) {
        printf("child killed by signal %d (%s)",
    }
    WTERMSIG(status), strsignal(WTERMSIG(status)));

Il strsignal(int sig) è un metodo di string.h che restituisce una stringa che descrive il segnale sig (vedi IPC parte 1).

  1. Il processo figlio è stato interrotto da un segnale.
    La macro WIFSTOPPED restituisce true se il processo figlio è stato fermato da un segnale. La macro WSTOPSIG(status) restituisce il numero del segnale che ha fermato il processo.

    Esempio situazione 3
    waitpid(-1, &status, WUNTRACED | WCONTINUED); if (WIFSTOPPED(status)) {
    printf("child stopped by signal %d (%s)\n",
        WSTOPSIG(status), strsignal(WSTOPSIG(status)));
    }

  2. Il processo figlio è stato ripreso da un segnale SIGCONT.
    La macro WIFCONTINUED restituisce true se il processo figlio è stato ripreso dalla consegna di SIGCONT.

    Esempio 1
    waitpid(-1, &status, WUNTRACED | WCONTINUED);
    if (WIFCONTINUED(status)) {
        printf("child resumed by a SIGCONT signal\n");
    }
    oppure
    Esempio 2
    waitpid(-1, &status, WCONTINUED);
    printf("child resumed by a SIGCONT signal\n");

Esecuzione di programmi (exec)

Funzioni della libreria exec

#include <unistd.h>
// None of the following returns on success, all return -1 on error.
int execl (const char *path, const char *arg, ... ); // ... variadic functions
int execlp(const char *path, const char *arg, ... );
int execle(const char *path, const char *arg, ... , char *const envp[]);
int execv (const char *path, char *const argv[]);
int execvp(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

Nota: la lista degli argomenti deve essere terminata da un puntatore NULL e, poiché queste sono funzioni variabili, questo puntatore deve essere cast (char *) NULL.

Funzionepathargenvironment envp
execlpath assolutolistaenviron del chiamante
execlpnome del filelistaenviron del chiamante
execlepath assolutolistaarray
execvpath assolutoarrayenviron del chiamante
execvpnome del filearrayenviron del chiamante
execvepath assolutoarrayarray
Programma di esempio con la execv
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
	printf("PID of example.c = %d\n", getpid());
	char *args[] = {"Hello", "C", "Programming", NULL};
	execv("./hello", args);
	printf("Back to example.c");
	return 0;
}
Programma di esempio con il pid
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
	printf("We are in hello.c\n");
	printf("PID of hello.c = %d\n", getpid());
	return 0;
}
user@localhost[~]$ gcc -o example example.c
user@localhost[~]$ gcc -o hello hello.c
user@localhost[~]$ ./example
PID of example.c = 4733
We are in Hello.c
PID of hello.c = 4733

TODO: mancano gli altri esempi su execl

Osservazioni finali sulle funzioni della libreria exec

Quello che dovreste sempre tenere a mente quando usate una funzione exec: