Vediamo come possiamo creare una shell minimale, che :
- attende che l’utente digiti un comando
- avvia un processo per eseguire il comando
- attende che il processo sia terminato
Creazione processo - fork() - wait() - execv()
Per creare un processo possiamo utilizzare :
pid_t fork();- duplica il processo corrente
- restituisce il pid () del figlio al chiamante (genitore)
- restituisce 0 nel nuovo processo figlio
pid_t pid = fork();
if (pid > 0) {
// Questo codice viene eseguito NEL GENITORE
// pid contiene il PID del figlio appena creato
} else if (pid == 0) {
// Questo codice viene eseguito NEL FIGLIO
// pid è 0
}Per attendere che un processo termini possiamo utilizzare :
pid_t wait(int *wstatus);- attende che i processi figli cambino stato
- blocca il processo genitore finché un figlio non termina
- restituisce il pid del figlio terminato
- scrive lo stato in
wstaus
void main(void){
int pid, child_status;
if (fork() == 0){
do_something_in_child();
} else {
wait(&child_status); // wait for child
}
}Per eseguire un binario (path) nel processo corrente, possiamo usare :
int execv(const char *path, char *constargv[]);- carica un binario e rimuove tutte le altre mappature in memoria
constargvcontiene gli args del programma, dove l’ultimo è sempreNULL(es.{"/bin/ls", "-a", NULL})- esistono diverse varianti di
exec{v}{p}
Quindi ora se mettiamo insieme i pezzi, possiamo creare la nostra shell minimale :
wait(1){
char cmd[256], *args[256];
int status;
pid_t pid;
read_command(cmd, args);
pid = fork();
if (pid == 0){
execv(cmd, args);
exit(1);
} else {
wait(&status);
}
}Gestione dei segnali - signal - alarm - kill
Ci domandiamo ora come funziona CTRL+C e come sia possibile che termini il programma. La risposta sono i segnali come abbiamo già visto in Processi.
Quando un processo deve essere terminato, viene inviato un segnale al processo.
Per registrare un gestore di segnali per un segnale specifico (signum) possiamo utilizzare :
sighandler_t signal(int signum, sighandler_t handler);Per esempio :
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// La funzione gestore DEVE ricevere un int (il numero del segnale)
void mio_gestore(int segnale_ricevuto) {
printf("Ho ricevuto il segnale %d\n", segnale_ricevuto);
}
int main() {
// Registro il gestore per SIGINT (CTRL+C)
signal(SIGINT, mio_gestore);
// Oppure posso ignorare un segnale
signal(SIGQUIT, SIG_IGN); // Ignora CTRL+\
while(1) {
printf("In esecuzione...\n");
sleep(1);
}
}Per inviare un SIGALARM in numero di secondi specificato abbiamo :
unsigned int alarm(unsigned int seconds);Per esempio :
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void gestore_timer(int sig) {
printf("\n🔔 DRIN! Sono passati 3 secondi!\n");
alarm(3); // Riavvio l'allarme per farlo andare all'infinito
}
int main() {
signal(SIGALRM, gestore_timer);
// Imposto un allarme tra 3 secondi
alarm(3);
printf("Timer avviato, suonerà ogni 3 secondi\n");
while(1) {
printf("Lavoro principale...\n");
sleep(1);
}
}Mentre per consegnare un segnale (sig) ad un processo (pid) abbiamo :
int kill(pid_t pid, int sig);Comunicazione tra processi con pipe
Il concetto di pipe è già noto, serve per avere una sorta di flusso di comunicazione tra due processi. Abbiamo visto l’esempio :
cat file1 | grep "amici"Possiamo anche avere dei “pipe con i nomi”, e si creano con il comando mkfifo :
mkfifo named.pipeun utilizzo che si può fare è quello di voler eseguire un comando su un terminale e mostrare l’output su un’altro :
Terminale 1 :
ls > named.pipeTerminale 2 :
cat < named.pipe (questa roba si può fare anche con tail -f <file>)
Per aprire un file specificato (pathname) abbiamo :
int open(const char *pathname, int flags);Per chiuderlo :
int close(int fd);Per creare una pipe con due file (fd) alle sue estremità (write - read) abbiamo :
int pipe(int pipefd[2]);dove :
pipefd[0]: estremità di readpipefd[1]: estremità di write
Per creare una copia del file descriptor (oldfd) utilizzando un file descriptor con il numero più basso disponibile, abbiamo :
int dup(int oldfd);che :
- prende un fd già esistente
oldfd - crea un nuovo fd che punta alla stessa risorsa
- restituisce il nuovo fd (o -1 per errore)
Abbiamo anche :
int dup2(int oldfd, int newfd);che duplica oldfd su un fd specifico newfd.
Vengono utilizzati per reindirizzare l’output, per esempio per un programma che logga su un file :
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
// Apro file di log
int log_fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
// Salvo stdout
int salva_stdout = dup(1);
// Reindirizzo stdout al file
dup2(log_fd, 1);
// Tutti i printf finiscono nel log!
printf("Programma avviato\n");
printf("Log Log Log Log\n");
fflush(1); // libera il buffer
// Ripristino stdout
dup2(salva_stdout, 1);
close(salva_stdout);
printf("Programma terminato (questo a schermo)\n");
close(log_fd);
return 0;
}Vediamo un’altro esempio dove cloniamo il comportamento di ps -aux | grep obsidian :
#include <stdio.h>
#include <unistd.h>
int main() {
int fd[2];
pipe(fd); // fd[0] <-------> fd[1]
if (fork() == 0) {
// figlio
close(fd[0]); // chiusura lettura per sicurezza
dup2(fd[1], 1);
execlp("ps", "ps", "aux", NULL);
} else {
// padre
close(fd[1]); // chiusura scrittura per sicurezza
dup2(fd[0], 0);
execlp("grep", "grep", "obsidian", NULL);
}
//dead code here
return 0;
}Notiamo che dopo
execlpè “dead code”, siccome il comando inexeclpsostituisce il processo che lo chiama e quindi non torna indietro (se senza errori).
Vediamo ora un’altro esempio :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define STDIN 0
#define STDOUT 1
#define PIPE_RD 0
#define PIPE_WR 1
int main(int argc, char** argv)
{
pid_t cat_pid, sort_pid;
int fd[2];
pipe(fd);
cat_pid = fork();
if (cat_pid == 0) {
close(fd[PIPE_RD]);
close(STDOUT);
dup(fd[PIPE_WR]);
execl("/bin/cat", "cat", "names.txt", NULL);
}
sort_pid = fork();
if (sort_pid == 0) {
close(fd[PIPE_WR]);
close(STDIN);
dup(fd[PIPE_RD]);
execl("/usr/bin/sort", "sort", NULL);
}
close(fd[PIPE_RD]);
close(fd[PIPE_WR]);
return 0;
}Notiamo che dup(fd[PIPE_WR]) copierà un nuovo fd che punterà al lato di scrittura della pipe, ma il più piccolo, dopo la chiusura di stdout (1) è proprio 1 (stdout) (identica cosa per stdin per sort)
Ci chiediamo se possiamo evitare le chiusure dei lati di lettura e scrittura della pipe? :
- Evitare blocchi : chiudere uno dei lati inutilizzati della pipe per impedire al processo di bloccarsi
- Ricezione EOF : se
sortnon chiude il suo lato di scrittura, la pipe ha uno scrittore attivo (se stesso) e quindisortaspetterà finché qualcuno non chiude il lato di scrittura per inviare un EOF - Evitare letture accidentali : cat chiude il lato lettura per evitare di leggere cose inaspettate dalla pipe
- Reindirizzamento di
stdoutincat: dopo la duplicazione chiudiamo ilfd[PIPE_WR](originale) in modo da avere solo quello creato con ildupche punta verso il lato di scrittura (forma corretta non presente nel codice perché ci pensa il kernel dopoexeclp) - Reindirizzamento di
stdoutinsort: identica cosa ma per lettura - Descriptor nel processo padre : dopo la fork, il processo padre dovrebbe chiudere entrambe le estremità della pipe
