Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

venerdì 14 maggio 2021

Better Call Go
come scrivere Server e Client TCP in Go e C - pt.1

Jimmy McGill (Saul Goodman): Quanti avvocati servono per cambiare una lampadina? Tre. Uno sale sulla scala, uno lo fa cadere e uno fa causa all'azienda della scala. 

Better Call Saul è un altro grande esempio di Cinema per la TV (è uno spin-off di Breaking Bad... ma questo lo sapete tutti, No? Non lo sapete? e vabbè, continuiamo così, facciamoci del male…), come la serie di cui è un prequel. Il nostro Saul, che qui si chiama ancora Jimmy McGill, è un personaggio ineffabile, un avvocato sui generis. È un vulcano di idee, citazioni, iniziative. È un personaggio "todoterreno", un po' come lo è il Go, che è un linguaggio super eclettico, con molteplici usi in ambito backend, network, microservizi... e con uno speciale occhio di riguardo per la programmazione concorrente. Nonostante la mia nota predilezione e passione per il C non ho nessun problema a decantare le doti del Go, anche perché è, comunque, della famiglia: sia per la forma (è C-like) che per la storia (due dei tre creatori sono Ken Thompson e Rob Pike... e ho detto tutto!). E poi per chi viene dal C è veramente facile impararlo, e non ti senti in colpa se, magari, ti trovi a usarlo per scrivere cose che prima facevi esclusivamente in C (a parte la programmazione di sistema). Insomma: il Go non sostituisce il C, ma lo affianca.

(...e chi si deve preoccupare, semmai, è il C++, visto che in Google hanno creato Go proprio per liberarsi un po' delle sue criticità... ma questa è un altra storia, che avevo già accennato. Magari la riprenderò nella seconda parte dell'articolo...)

...le dure decisioni della vita: C o Go?...

Ho già parlato del Go in passato (qui e qui) mostrando alcuni usi interessanti. Oggi ho deciso di offrirvi un confronto tra le versioni C e Go di due classici: un Server TCP e un Client TCP (quest'ultimo nella seconda parte dell'articolo). Ho scritto decine di programmi come questo in C e C++ per lavoro e per studio, e ne ho scritto, in varie versioni (TCP, UDP, con OpenSSL), anche su queste pagine. Cominciamo, allora, con un semplice esempio in C, praticamente identico a quello che avevo descritto in un vecchio articolo: vai col codice!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>

#define MYBUFSIZE 1024
#define BACKLOG 10 // per listen()

int main(int argc, char *argv[])
{
// test argomenti
if (argc != 2) {
// errore di chiamata
printf("%s: numero argomenti errato\n", argv[0]);
printf("uso: %s port [e.g.: %s 9999]\n", argv[0], argv[0]);
return EXIT_FAILURE;
}

// creo il socket in modo internet/TCP
int srv_sock;
if ((srv_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {
// errore socket()
printf("%s: non posso creare il socket (%s)\n", argv[0], strerror(errno));
return EXIT_FAILURE;
}

// preparo la struttura sockaddr_in per questo server
struct sockaddr_in server;
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET; // set famiglia di indirizzi
server.sin_addr.s_addr = INADDR_ANY; // set indirizzo del server
server.sin_port = htons(atoi(argv[1])); // set port del server

// associo l'indirizzo del server al socket
if (bind(srv_sock, (struct sockaddr *)&server, sizeof(server)) == -1) {
// errore bind()
printf("%s: errore bind (%s)", argv[0], strerror(errno));
close(srv_sock);
return EXIT_FAILURE;
}

// start ascolto con una coda di max BACKLOG connessioni
if (listen(srv_sock, BACKLOG) == -1) {
// errore listen()
printf("%s: errore listen (%s)\n", argv[0], strerror(errno));
close(srv_sock);
return EXIT_FAILURE;
}

// accetto connessioni da un client entrante
printf("%s: attesa connessioni entranti...\n", argv[0]);
socklen_t socksize = sizeof(struct sockaddr_in);
struct sockaddr_in client; // struttura sockaddr_in per il client remoto
int cli_sock;
if ((cli_sock = accept(srv_sock, (struct sockaddr *)&client, &socksize)) == -1) {
// errore accept()
printf("%s: errore accept (%s)\n", argv[0], strerror(errno));
close(srv_sock);
return EXIT_FAILURE;
}

// chiudo il socket non più in uso
close(srv_sock);

// loop di ricezione messaggi dal client
char cli_msg[MYBUFSIZE];
int recv_size;
while ((recv_size = recv(cli_sock, cli_msg, MYBUFSIZE, 0)) > 0 ) {
// send messaggio di ritorno al client
printf("%s: ricevuto messaggio dal sock %d: %s\n", argv[0], cli_sock, cli_msg);
char srv_msg[MYBUFSIZE];
sprintf(srv_msg, "mi hai scritto: %s", cli_msg);
if (send(cli_sock, srv_msg, strlen(srv_msg), 0) == -1) {
// errore send()
printf("%s: errore send (%s)\n", argv[0], strerror(errno));
close(cli_sock);
return EXIT_FAILURE;
}

// clear del buffer
memset(cli_msg, 0, MYBUFSIZE);
}

// loop terminato: test motivo
if (recv_size == -1) {
// errore recv()
printf("%s: errore recv (%s)\n", argv[0], strerror(errno));
close(cli_sock);
return EXIT_FAILURE;
}
else if (recv_size == 0) {
// Ok: il client si è disconnesso
printf("%s: client disconnesso\n", argv[0]);
}

// esco con Ok
close(cli_sock);
return EXIT_SUCCESS;
}

Ok, come vedete è ampiamente commentato e quindi è auto-esplicativo, per cui non mi dilungherò sulle singole istruzioni e/o gruppi di istruzioni (leggete i commenti! sono li per quello!), ma aggiungerò solo qualche dettaglio strutturale.

Il flusso è quello classico ed elementare di un Server TCP:

  1. creo il socket in modo internet/TCP
  2. preparo la struttura sockaddr_in per questo Server
  3. associa l'indirizzo del server al socket
  4. start ascolto con una coda di max BACKLOG connessioni
  5. accetta connessioni da un Client entrante
  6. loop di ricezione messaggi dal Client
    - riceve un messaggio
    - send messaggio di ritorno al client

ovviamente esistono varianti di questa struttura, ma questa è quella classica. Come avrete notato nel loop di lettura c’è il re-invio al Client del messaggio ricevuto, e questo ci aiuta, durante l'esecuzione, a osservare il corretto funzionamento della coppia Client/Server che metteremo in prova.

Evidentemente questo esempio, pur essendo abbastanza completo (è quasi un codice di produzione), è relativamente semplice, sono poche righe e fa il suo dovere. Ma si può rendere ancora più semplice e compatto? La risposta è si, chiedendo aiuto al nostro nuovo amico Go (o Golang, se preferite). Ecco un esempio di Server TCP in Go, guardate e stupite!

package main

import (
"bufio"
"fmt"
"net"
"os"
)

func main() {
// test argomenti
if len(os.Args) != 2 {
// errore di chiamata
fmt.Printf("%s: numero argomenti errato\n", os.Args[0])
fmt.Printf("uso: %s port [e.g.: %s 9999]\n", os.Args[0], os.Args[0])
return
}

// start ascolto sul port richiesto
port := ":" + os.Args[1] // set port (i.e.: ":port")
lner, err := net.Listen("tcp", port) // set listener con network di tipo TCP
if err != nil {
// errore di ascolto
fmt.Println(err)
return
}

defer lner.Close() // prenoto la chiusura del listener

// accetta connessioni da un client entrante
fmt.Printf("%s: attesa connessioni entranti...\n", os.Args[0])
conn, err := lner.Accept()
if err != nil {
// errore di accept
fmt.Println(err)
return
}

// loop di ricezione messaggi dal client
connrdr := bufio.NewReader(conn) // reader sulla connessione
for {
// attende la ricezione di un messaggio
client_msg, err := connrdr.ReadString('\n') // leggo con il conn reader
if err != nil {
// errore di ricezione
fmt.Println(err)
return
}

// mostra il messaggio ricevuto e compone la risposta
fmt.Printf("%s: ricevuto messaggio: %s", os.Args[0], string(client_msg))
server_msg := fmt.Sprintf("mi hai scritto %s", string(client_msg))

// send messaggio di ritorno al client
_, err = conn.Write([]byte(server_msg)) // scrivo sulla connessione
if err != nil {
// errore di send
fmt.Println(err)
return
}
}
}

È veramente semplicissimo e compattissimo! Ho, volutamente (come sempre) esagerato coi commenti per descrivere ogni singola attività, e ho usato (sempre volutamente) le stesse frasi nelle descrizioni dei passi del flusso che ho usato nella versione C, così è più facile fare una comparazione. Risulta evidente che, grazie ai package inclusi nel linguaggio e grazie alla natura stessa del linguaggio, questo Server TCP in Go ha una struttura con meno passi ed ogni passo è più semplice da scrivere (e da leggere!) dell'equivalente in C. Vediamo la struttura:

  1. start ascolto sul port richiesto
  2. accetta connessioni da un Client entrante
  3. loop di ricezione messaggi dal Client
    - riceve un messaggio
    - send messaggio di ritorno al client

Sono veramente quattro righe, e fa esattamente lo stesso lavoro del TCP Server in C con cui abbiamo introdotto l'argomento. Fantastico.

Nel prossimo articolo vedremo, come promesso, il Client TCP in Go e C. Una volta compilati potrete verificare che si comportano esattamente nella stessa maniera, provare per credere!

Ciao, e al prossimo post!

mercoledì 7 aprile 2021

Breaking Monitor
come scrivere un Process Monitor in C

Saul Goodman: Come l'avete portato qui? Noi siamo come i tre moschettieri! "Tutti per uno e uno per tutti" Non ci serve un quarto moschettiere.
Walter White: Saul, Mike conosce il suo lavoro e i distributori. È uno a posto.
Saul Goodman: Uno a posto? Ha promesso di rompermi le gambe! E non mi dite che non parlava sul serio, perché quando fa lo sguardo da pesce morto parla sul serio!
Walter White: Ha minacciato anche me e Jesse e chissà quante altre persone minaccia ogni giorno. È il suo mestiere. Andiamo, è fatto così.

Questa volta invece di citare un film citeremo una serie TV di alta qualità, tanto alta da farne un "film lunghissimo". Breaking Bad è Cinema, e anche di quello buono. E cosa vuol dire Breaking Bad? Vuol dire "allontanarsi dalla retta via", che è proprio quello che fanno, a volte, i processi di un Sistema Operativo: smettono di funzionare e creano problemi. E quale potrebbe essere la soluzione? Ma usare un Process Monitor, Elementare, mio caro Watson!

...senza un monitor non andiamo da nessuna parte...

Allora, chiariamo subito di cosa stiamo parlando: vi sarà capitato sicuramente (a chi non è capitato? sono cose che succedono tutti i giorni!) di scrivere una applicazione che, a sua volta, lancia altre applicazioni, quindi siamo nel caso classico (già visto su questi schermi) di un processo che fa  fork + exec (su Linux e sistemi POSIX), magari più volte. Normalmente il processo padre lavora in collegamento con i processi figli, e se qualche processo muore (il padre o i figli) potrebbero crearsi dei (notevoli) problemi di funzionamento. Normalmente la cosa si risolve a livello OS, per esempio usando una applicazione ad-hoc (una buona e molto usata è Monit) oppure ci si aiuta con le utility del sistema operativo, usando systemd o scrivendo degli shell script.

Ma questa è la soluzione che vi proporrebbe un sistemista, mentre noi siamo programmatori! (...beh, per esigenze di lavoro, molti sono entrambe le cose, incluso il sottoscritto, ma questa è un altra storia...). E il discorso è semplice: se abbiamo un mix di applicazioni slegate, che devono partire e lavorare insieme, allora si usa l'approccio sistemistico, ma se il progetto prevede già una applicazione master che avvia, con fork(), dei sotto-processi, la maniera più intelligente di monitorare è di farlo già a livello codice, senza scomodare il Sistema Operativo e/o applicazioni esterne.

E come si può scrivere un semplice ed efficiente Process Monitor in C? Lo vedremo tra poco! Allora, come sempre vi mostrerò del codice il più possibile semplice e chiaro, ma già preparato per essere usato in produzione (previa l'aggiunta di una gestione più completa degli errori). Non sarà un semplice demo ultra-semplificato, sarà una roba ben fatta (scusate la modestia) sviluppata a mo' di libreria e che può essere facilmente integrata in qualsiasi progetto.

Il nostro Process Monitor, quindi, è una piccola libreria formata da due soli file, un header e una implementazione. L'ho chiamata con l'originalissimo nome di ProcMonit. Ma facciamo parlare il codice, partiamo dall'header  ProcMonit.h:

#ifndef PROCMONIT_H_
#define PROCMONIT_H_

#include <stdbool.h>
#include <pthread.h>
#include <sys/types.h>

// struttura dati per ProcMonit
typedef struct {
// dati del processo figlio da monitorare
const char* file; // nome del file da eseguire
char* const* argv; // lista di argomenti del programma da eseguire
pid_t pid; // pid del processo figlio avviato

// dati del thread del monitor
pthread_t t_procmonit; // descrittore del thread
bool stop_t_procmonit; // flag per lo stop del thread
} ProcMonit;

// prototipi globali
void pm_init(ProcMonit *pm, const char* pm_file, char* const pm_argv[]);
void pm_start(ProcMonit *pm);
void pm_stop(ProcMonit *pm);

#endif // PROCMONIT_H_

La semplicità è disarmante e già lascia presagire come sarà l'implementazione: abbiamo una struttura dati che contiene tutti i (pochi) dati necessari al funzionamento (ben descritti nei commenti, come sempre) e solo tre funzioni (con nomi abbastanza espliciti): pm_init(), pm_start() e pm_stop(). Il meccanismo di Monitor che vogliamo implementare è il seguente:

  • avere la possibilità di avviare n processi figli (usando n  volte pm_init() e pm_start()).
  • sorvegliare lo stato dei processi figli: se uno termina (per un errore interno, per un segnale esterno, ecc) bisogna riavviarlo automaticamente.
  • terminare automaticamente i processi figli nel caso di terminazione del processo padre.

Ovviamente si possono prevedere varianti di questo funzionamento, ma vi assicuro che quello che si usa di solito (con Monit, ad esempio) segue lo schema citato. E come si implementa questo? Vediamolo, vai con ProcMonit.c!

#define _GNU_SOURCE
#include "ProcMonit.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/prctl.h>

// prototipi locali
static void *pm_thread(void *arg);

// pm_init - inizializza la struttura del monitor
void pm_init(ProcMonit *pm, const char *pm_file, char* const pm_argv[])
{
// set/reset dati del monitor
pm->file = pm_file; // nome del file da eseguire
pm->argv = pm_argv; // lista di argomenti del programma da eseguire
pm->pid = -1; // pid del processo figlio avviato
pm->t_procmonit = 0; // descrittore del thread
pm->stop_t_procmonit = false; // flag per lo stop del thread
}

// pm_start - avvia il monitor di un processo figlio
void pm_start(ProcMonit *pm)
{
// test se il thread non è già allocato
if (pm->t_procmonit == 0) {
// start del thread di procmonit
pm->stop_t_procmonit = false; // reset flag per stop thread
int error;
if ((error = pthread_create(&pm->t_procmonit, NULL, &pm_thread, pm)) != 0) {
// errore: lo mostro e azzero il descrittore del thread
printf("%s: non posso creare il thread (%s)\n", __func__, strerror(error));
pm->t_procmonit = 0;
}
}
}

// pm_stop - ferma il monitor di un processo figlio
void pm_stop(ProcMonit *pm)
{
// test se il thread è allocato
if (pm->t_procmonit) {
// uccide il processo figlio
kill(pm->pid, SIGKILL);

// stop e join del thread di procmonit
pm->stop_t_procmonit = true; // set flag per lo stop del thread
pthread_join(pm->t_procmonit, NULL);
pm->t_procmonit = 0; // reset del descrittore del thread
}
}

// pm_thread - funzione per il thread di procmonit
static void *pm_thread(void *arg)
{
// estraggo l'argomento con un cast
ProcMonit *pm = (ProcMonit *)arg;

// redirigo stdout e stderr su un file di log (questa parte è opzionale)
char log_fname[64];
snprintf(log_fname, sizeof(log_fname), "%s.log", pm->file);
int fd = open(log_fname, O_WRONLY | O_CREAT | O_TRUNC, DEFFILEMODE);
//dup2(fd, STDOUT_FILENO); // invia stdout al file fd (opzionale)
//dup2(fd, STDERR_FILENO); // invia stderr al file fd (opzionale)
close(fd); // posso chiudere fd visto che è già stato duplicato

// loop del thread
while (!pm->stop_t_procmonit) {
// fork del processo
pid_t parent_pid = getpid();
pm->pid = fork();

// test dei pid dei processi
char errmsg_buf[256]; // buffer per la strerror_r(): 256 è il size raccomandato
if (pm->pid == 0) {
// sono il figlio: con prctl() prenoto lo stop (via signal) se termina il padre
if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
// errore nella prctl()
fprintf(stderr, "%s: sono il figlio (%d): errore prtctl(): %s\n",
pm->argv[1], getpid(),
strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
exit(EXIT_FAILURE);
}

// nel caso che il processo padre esca prima di chiamare la prctl()
if (getppid() != parent_pid) {
// errore nella prctl()
fprintf(stderr, "%s: sono il figlio (%d): errore prtctl()\n",
pm->argv[1], getpid());
exit(EXIT_FAILURE);
}

// continua l'esecuzione del figlio eseguendo un nuovo processo
fprintf(stderr, "%s: sono il figlio (%d): eseguo il nuovo processo: %s\n",
pm->argv[1], getpid(), pm->file);
execv(pm->file, pm->argv);
exit(EXIT_FAILURE); // exec non ritorna mai
}
else if (pm->pid > 0) {
// sono il padre: attendo l'uscita del figlio
fprintf(stderr, "%s: sono il padre (%d): "
"attendo la terminazione del figlio %s (%d)\n",
pm->argv[1], getpid(), pm->file, pm->pid);
int status;
if (waitpid(pm->pid, &status, 0) != pm->pid) {
// errore nella waitpid()
fprintf(stderr, "%s: sono il padre (%d): "
"errore waitpid() figlio %s (%d): %s\n",
pm->argv[1], getpid(), pm->file, pm->pid,
strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
}
else {
// processo terminato: mostro lo status
if (WIFEXITED(status))
fprintf(stderr, "%s: sono il padre (%d): "
"figlio %s (%d) uscito ((status=%d))\n",
pm->argv[1], getpid(), pm->file, pm->pid,
WEXITSTATUS(status));
else if (WIFSIGNALED(status))
fprintf(stderr, "%s: sono il padre (%d): "
"figlio %s (%d) ucciso dal segnale %d\n",
pm->argv[1], getpid(), pm->file, pm->pid,
WTERMSIG(status));
else if (WIFSTOPPED(status))
fprintf(stderr, "%s: sono il padre (%d): "
"figlio %s (%d) fermato dal segnale %d\n",
pm->argv[1], getpid(), pm->file, pm->pid,
WSTOPSIG(status));
else
fprintf(stderr, "%s: sono il padre (%d): "
"figlio %s (%d) terminato ((status=%d))\n",
pm->argv[1], getpid(), pm->file, pm->pid, status);
}
}
else {
// errore nella fork(): mostro errore e continuo
fprintf(stderr, "%s: processo %s: errore fork(): %s\n",
pm->argv[1], pm->file,
strerror_r(errno, errmsg_buf, sizeof(errmsg_buf)));
}
}

// il thread esce
pthread_exit(NULL);
}

Che ne dite? Come si nota il funzionamento si basa su un thread (in questo articolo c'è un bel mix esplosivo, processi e thread allo stesso tempo!) che si occupa di monitorare lo stato del processo figlio. Riepiloghiamo il funzionamento centrandoci sulle tre funzioni globali:

  • pm_init(): inizializza la struttura dati del Monitor. Tra i dati possiamo evidenziare il filename che verrà eseguito dal processo figlio (con i relativi argomenti di esecuzione) e le variabili di supporto per gestire il thread di Monitor e il processo figlio.
  • pm_start(): avvia il thread che si occupa di eseguire il processo figlio e di sorvegliarne la attività, per gestire gli eventuali restart. Qualcuno potrebbe (giustamente) commentare: "Ma la pm_init() e la pm_start() non potrebbero essere una unica funzione?" No, e la risposta si chiama pm_stop().
  • pm_stop(): ferma il processo figlio e il thread di controllo. Dopo lo stop si può, se necessario, eseguire un nuovo start... "Ah, allora è per questo che abbiamo init e start in due fasi! La init si chiama una volta sola, mentre la start si può ri-chiamare dopo ogni stop!" Bravo, risposta esatta.

E adesso è il turno del cuore del Process Monitor, la funzione pm_thread() che è quella che viene eseguita dal thread di controllo (e non è esportata globalmente nel header, visto che viene usata solo internamente alla libreria). Il funzionamento è abbastanza semplice e ricalca, in una certa maniera, la struttura del file processes.c di alcuni recenti articoli: esegue fork() e divide il flusso in padre (che sorveglia) e figlio (che esegue con execv() un nuovo processo). E come avviene il monitoring? Il flusso padre resta in attesa dell'uscita del figlio all'interno di un loop pseudo-infinito: in questa maniera, se il figlio muore viene rieseguita la fork() e si ripete il ciclo indefinitamente, fino a quando non viene richiesto lo stop definitivo attraverso la funzione pm_stop(). Non male, no?

Nella funzione pm_thread() ci sono almeno tre trucchetti interessanti, che possiamo descrivere brevemente (per i dettagli leggere bene i commenti nel codice):

  1. Redirezione di stdout e stderr: visto che abbiamo più processi in esecuzione, con (vari ed eventuali) messaggi di errore, è una buona idea redirigere le scritture (su un file di log, come nell'esempio, oppure su /dev/null se vogliamo silenziare il tutto). Questa parte è opzionale (ma utile).
  2. Intercettazione dei segnali attraverso la funzione prctl(): serve per automatizzare la terminazione del processo figlio nel caso di uscita del processo padre (questa parte è veramente interessante, segnatevela).
  3. nel flusso padre, trattamento completo dello status di uscita del processo figlio: ho usato le apposite (e non molto conosciute) macro della wait(): WIFEXITED(), WEXITSTATUS(), WIFSIGNALED(), WTERMSIG(), WIFSTOPPED() e WSTOPSIG(): in questa maniera il controllo di eventuali errori è veramente ben fatto.

Cosa ci manca per terminare? Ma un semplicissimo esempio d'uso! Quindi vi propongo un caso veramente elementare, con un programma padre parent.c e un programma figlio child.c. Il processo padre in questo caso monitorizza solo un figlio, ma può monitorizzarne n  a piacere semplicemente moltiplicando le chiamate di init, start e stop: ogni nuova pm_init() inizializza un nuovo processo da eseguire su cui possiamo eseguire le relative pm_start() e pm_stop(). Semplice e praticissimo. Vediamo il codice!

#include "ProcMonit.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// nome del processo figlio da avviare
#define CHILD_PROC "child"

// funzione main del processo padre
int main(int argc, char *argv[])
{
// init struttura procmonit
char* argv_pm[] = { (char*)CHILD_PROC, argv[0], (char*)NULL };
ProcMonit procmonit;
pm_init(&procmonit, CHILD_PROC, argv_pm);

// avvio il processo figlio usando procmonit
printf("%s: prenoto lo start del processo %s\n", argv[0], argv_pm[0]);
pm_start(&procmonit);

// main loop di test
for (int i = 0; i < 10; i++) {
printf("%s: loop interno di test: %d\n", argv[0], i);
sleep(3);
}

// fermo il processo figlio usando procmonit
printf("%s: prenoto lo stop del processo %s\n", argv[0], argv_pm[0]);
pm_stop(&procmonit);

// esco con Ok
return EXIT_SUCCESS;
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// funzione main del processo figlio
int main(int argc, char *argv[])
{
// test loop
int i = 0;
for (;;) {
// test indice per chiusura forzata
if (i++ > 100) {
// chiusura forzata
printf("%s: il processo esce\n", argv[0]);
return EXIT_FAILURE;
}

// thread sleep (100 ms)
nanosleep((const struct timespec[]){{0, 100000000L}}, NULL);
}

// esco con Ok
return EXIT_SUCCESS;
}

Potete fare copia-incolla della libreria e degli esempi d'uso e compilare: con i tempi che ho impostato (e le scritte di log) vedrete il processo figlio che termina e si riavvia varie volte. E se provate, allo stesso tempo, a inviare un segnale di terminazione o uccisione al figlio potete mettere alla prova il riavvio automatico dopo un evento esterno. E se inviate un sigkill al padre vedrete che termina anche il figlio. Spero che vi piaccia (e se non vi piace me ne farò una ragione...). Ah, dimenticavo: i sorgenti di questo articolo li potete trovare anche nel mio repository GitHub.

Ciao, e al prossimo post!