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.

martedì 19 dicembre 2023

Tutta colpa della fork
come usare la fork(2) in multithreading in C

albergatore: Lui mi sembra una persona in gamba, intelligente, per bene... dice sempre buongiorno e buonasera... sempre buongiorno e buonasera... però... boh.
Romeo: Come boh?
albergatore: No per carità, mica per dirne male, per l'amor di Dio... uno che dice sempre buongiorno e buonasera... però... boh.

Il dialogo surreale qui sopra è tratto dal bel Tutta colpa del Paradiso del compianto Francesco Nuti. Un dialogo che rappresenta l'incertezza nel giudicare persone e cose, e che ci aiuta a introdurre il tema del giorno: la fork(2) che è una system call veramente classica, preziosa e indispensabile dei sistemi POSIX (e di cui abbiamo parlato qui e non solo) si può sempre usare "come se niente fosse" o bisogna usarla con cautela? Uhmm... vediamolo!

...la fork? Si, funziona bene, però... boh...

Allora, tanto per tagliare subito la testa al toro possiamo dire che la fork(2) in un "programma normale" è affidabile al 100%, e ci mancherebbe solo: è una system call che esiste da sempre su UNIX (e, dopo, anche su Linux e in tutta la famiglia POSIX) ed è alla base della scrittura delle applicazioni multiprocess.

Ma cosa si intende per "programma normale"  ? Ecco, direi che in questo caso si intende una applicazione singlethread: la fork(2) risale a una delle primissime versioni di UNIX: la V1 (detta anche UNIX First Edition, 1971) ed è stata poi standardizzata nel primo standard POSIX 1003.1-1988. Internamente la fork(2) ha avuto varie evoluzioni: su Linux, ad esempio, è implementata internamente tramite una chiamata alla system call clone(2), ma alla fin fine rimane sempre e comunque la cara, vecchia e affidabile fork(2). Il multithreading è apparso su UNIX ben più tardi, ed è stato standardizzato, poi, con POSIX 1003.1c-1995, e i (presunti) problemi della fork(2), come vedremo più avanti, sono cominciati lì...

E come si usa la fork(2)? Come detto sopra ne abbiamo già parlato, comunque vi propongo, di seguito, uno degli esempi più classici di fork + exec + wait.  Vai col codice!

// testfork.c - test della fork(2)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>

// funzione main()
int main(int argc, char* argv[])
{
// eseguo la fork()
pid_t pid = fork();
if (pid == 0) {
// sono il figlio: eseguo il comando "ls ./testfork"
char *pathname = "/usr/bin/ls";
char *newargv[] = { "ls", "./testfork", NULL };
execv(pathname, newargv);
exit(EXIT_FAILURE); // exec non ritorna mai
}
else if (pid > 0) {
// sono il padre: attendo l'uscita del figlio
int status;
waitpid(pid, &status, 0);
exit(EXIT_SUCCESS);
}
else {
// errore fork()
printf("error: %s\n", strerror(errno));
}
}

E non credo che ci sia nulla da aggiungere su questo codice chiaro (spero) e ben commentato (o meglio: rileggetevi gli articoli citati sopra, grazie).

Ma veniamo al nocciolo della questione: come siamo messi col multithreading mischiato col multiprocessing? Ecco, prima di impazzire cercando articoli e riferimenti sull'argomento (spinoso, devo dire) e/o scrivere test complicati, è meglio dare un occhiatina al manuale, che è sempre la fonte primaria e più completa di informazioni. La pagina ufficiale del The Linux man-pages projectSystem Calls Manual - fork(2) dice:

After a fork() in a multithreaded program, the child can
safely call only async-signal-safe functions (see
signal-safety(7)) until such time as it calls execve(2).

e, per completare il quadro, vediamo anche cosa dice il POSIX Programmer's Manual - FORK(3P):

A process shall be created with a single thread. If a multi-
threaded process calls fork(), the new process shall contain
a replica of the calling thread and its entire address space,
possibly including the states of mutexes and other resources.
Consequently, to avoid errors, the child process may only
execute async-signal-safe operations until such time as one
of the exec functions is called.

Ok, sembra che qualche problemino c'è...

In pratica cosa succede? Succede che se un programma multithread chiama la fork(2), si crea, come previsto, un processo figlio che è una copia esatta del processo padre (un clone: non per nulla su Linux, come visto sopra, la fork(2) usa internamente la system call clone(2)), ma il nuovo processo è singlethread, ed è una copia del thread in cui è stata invocata la fork(2). Infatti, sempre nel manuale di Linux troviamo:

A process shall be created with a single thread. If a multi-
threaded process calls fork(), the new process shall contain
a replica of the calling thread and its entire address space,
possibly including the states of mutexes and other resources.
Consequently, to avoid errors, the child process may only
execute async-signal-safe operations until such time as one
of the exec functions is called.

E quindi, alla fine della fiera, il problema principale risiede nelle race-conditions dovute a eventuali mutex (o altri tipi di lock) che risiedono, contemporaneamente, nei processi padre e figlio, il che crea la possibilità che si verifichino strani blocchi. E, se vogliamo completare il discorso, bisogna aggiungere che anche la gestione dei segnali diretti al nostro processo padre diventa problematica, visto che dopo avere eseguito fork(2) abbiamo in circolazione anche un figlio-clone.

Ma come si risolve tutto questo? Beh, negli estratti dei manuali appena presentati si raccomanda di usare nel processo figlio, prima di una eventuale exec(3), solo funzioni della famiglia async-signal-safe; queste sembrano tante ma... non fatevi ingannare, in realtà sono pochissime! Pensate che, tra le tante cose assenti, c'è tutto stdio, inclusa la printf(3)! E non si può neanche manipolare la memoria con malloc(3) e free(3)! E non si può neanche registrare un problema con syslog(3) visto che usa dei mutex (un grazie al collega Xavier P. per avermelo fatto notare). È un bel problema... In parallelo alla raccomandazione precedente si può, poi, usare pthread_atfork(3), ma con molta cautela, perché è possibile dimenticarsi qualche dettaglio visto che non è una soluzione molto semplice da realizzare.

E quali sono i sintomi tipici di "qualcosa è andato male nella fork + exec" ? Direi che l'evento più probabile è che la exec(3) non si esegua perché il child process si è bloccato a causa di un lock ereditato dal padre: in questo caso, usando semplicemente il comando ps, si noterà che abbiamo due processi con lo stesso nome, padre e figlio, con il figlio che non è stato (ahimè) sostituito da un altro programma tramite la exec(3).

Credo che a questo punto sia il caso di mostrare una piccola guida riassuntiva di come procedere quando non si può fare a meno di usare la fork(2) in un programma multithread (applicherò, semplicemente, le avvertenze dei manuali). E quindi: la fork(2) si usa senza paura anche in multitreading (io l'ho fatto molte volte) ma seguendo il seguente schema numerato in ordine di importanza:

  1. Se possibile usate sempre la sequenza fork + exec + wait, ed eseguite exec(2) immediatamente dopo la fork(2) senza mettere praticamente nulla in mezzo (esattamente come nell'esempio mostrato più sopra): la exec(2) cancella tutti gli (eventuali) lock in comune tra padre e figlio e il problema è risolto alla radice. Notare che POSIX mette addirittura a disposizione una funzione, la posix_spawn(3), che esegue fork + exec in un passaggio solo, ed è quindi intrinsecamente sicura, ma non è semplicissima da usare (bene).
  2. Se proprio non potete eseguire immediatamente exec(2), prima di eseguirla dovete avere l'accortezza di usare solo funzioni di tipo async-signal-safe, e ricordate: non potete usare neanche la "innocua" printf(3), per cui dovrete arrangiarvi con la write(2).
  3. Se proprio non dovete eseguire exec(2), riducete al minimo il codice del processo figlio (usando solo funzioni di tipo async-signal-safe) e uscite quanto prima usando _exit(2) (e non exit(3)!). Non chiamate altre funzioni del programma, a meno che non siate sicuri al 100% che siano assolutamente innocue a livello di lock del multithreading e che usino solo funzioni async-signal-safe.
  4. Se proprio siete in una situazione "speciale" (non compresa nei tre punti precedenti) usate, con molta attenzione, pthread_atfork(3), che è l'ultima risorsa disponibile.

E per oggi può bastare. Spero di aver contribuito a sfatare alcuni (falsi) miti sulla problematicità della fork(2), una system call storica e indispensabile, e che funziona benissimo... ma bisogna saperla usare. Immagino che molti di voi saranno già in pieno assetto pre-festivo, e invasi dallo Spirito Natalizio (beh, io si). Quindi vi lascerò in pace per un po':  Buon Natale e Buon Anno a tutti!

Ciao, e al prossimo post!

lunedì 13 novembre 2023

Scusate la memcmp
considerazioni sull'uso della memcmp(3) in C

Tonino: Vincè, io mi uccido: meglio un giorno da leone che cento giorni da pecora! O no Vincè? Meglio un giorno da leone!
Vincenzo: Tonì, che ne saccio io d''a pècura o d''o leone? Fa' cinquanta juórne da orsacchiotto.

Nonostante l'intenzione (giuro) di scrivere un altro articolo non propriamente di programmazione (come ho fatto negli gli ultimi due, qui e qui) sono stato improvvisamente travolto da un problema reale (di codice, eh!) e ho deciso di soprassedere (e rimandiamo, rimandiamo...) e di tornare al nostro amato C.

Il problema reale citato riguardava l'uso (e il mal uso) della funzione della libc memcmp(3), che è utilissima, ben fatta e, a volte, indispensabile, ma che può anche provocare dei notevoli mal di testa. E quindi ci vuole un articolo: inizialmente avevo pensato a uno della serie "No, Grazie!" (ne ho scritti già un po' e vi invito a leggerli o rileggerli, qui, qui, quiqui, qui e qui)), ma sarebbe stato ingeneroso verso la memcmp(3), che, come detto sopra, è una buona funzione, basta usarla bene. E allora, invece di un "No, grazie!" sarà uno "Scusate", ispirato nuovamente al grande Massimo Troisi che con il suo Scusate il Ritardo si scusava per il tempo passato dal suo ultimo film (ma il gioco di parole significava anche altro).

...ma quante volte ti ho detto di non usare la memcmp?...

E allora, andiamo al dunque: la memcmp(3) esegue, come dice il nome, una comparazione di memoria, ossia ci dice se due blocchi di memoria sono uguali (o diversi). Vediamone una semplice implementazione tanto per chiarire di cosa stiamo parlando. Vai col codice!

#include <string.h>

int memcmp(const void *vl, const void *vr, size_t n)
{
const unsigned char *l=vl, *r=vr;
for (; n && *l == *r; n--, l++, r++);
return n ? *l-*r : 0;
}

questa è l'implementazione della (notevole e raccomandabile) musl libc, ed è, come spesso accade in questa libreria, ridotta all'osso privilegiando semplicità e funzionalità. La memcmp(3) della glibc, pur eseguendo lo stesso compito, è implementata in una maniera abbastanza più complicata (per correggere alcune vulnerabilità della funzione) e quindi ve la risparmio: per rendere l'idea va benissimo la versione della musl libc.

Come si nota, stiamo parlando di una funzione abbastanza semplice (nell'esempio sono 3 linee!) che compara "byte-a-byte" (anzi, "char-a-char") due zone di memoria, ritornando zero se sono uguali, e un valore diverso da zero (positivo o negativo) se sono diverse: il codice canta. Apparentemente non ci possono essere grossi problemi, e infatti, normalmente, la funzione è utilissima e funzionale, ma... in alcuni casi i problemi ci sono! Eccome! Il trucco è ben descritto nello standard del C11 (cap. 6.2.6.1/6):

When a value is stored in an object of structure or union type, including in a member
object, the bytes of the object representation that correspond to any padding bytes
take unspecified values.

Ecco, il problema principale della memcmp(3) (a parte le vulnerabilità corrette dalla glibc) è questo, e si chiama "Structure Padding". È noto (o almeno dovrebbe esserlo) che una C struct viene, per default, allineata dal compilatore in maniera che il size della struttura sia un multiplo di 4. È evidente, quindi, che una struct, come la vediamo noi, può essere abbastanza diversa da come la rappresenta il compilatore. E quindi? Quindi, bisogna usare la memcmp(3) con molta cautela quando si comparano i contenuti di strutture, i bug sono in agguato e possono provocare comportamenti molto inaspettati. Vediamo un semplicissimo esempio:

#include <stdio.h>
#include <string.h>
#include <stdint.h>

// una struttura di esempio
typedef struct {
uint8_t achar;
uint32_t anint;
} Test;

// testmemcmp - funzione main()
int main(int argc, char *argv[])
{
// definisco e inizializzo test1 e test2
Test test1;
test1.achar = 11;
test1.anint = 2222222222;

Test test2;
test2.achar = 11;
test2.anint = 2222222222;

// comparo test1 e test2 con memcmp(3)
if (memcmp(&test1, &test2, sizeof(Test)) == 0)
printf("%s: test1 e test2 sono uguali\n", argv[0]);
else
printf("%s: test1 e test2 sono diverse\n", argv[0]);

return 0;
}

Ecco, questo esempio, una volta compilato ed eseguito produce questo risultato:

aldo@Linux $ gcc testmemcmp.c -o testmemcmp
aldo@Linux $ ./testmemcmp
./testmemcmp: test1 e test2 sono diverse
aldo@Linux $

Ma come? Le due strutture test1 e test2 hanno un contenuto diverso? Ma se le ho inizializzate nella stessa maniera! Ma che cavolo dice la memcmp(3)?... Ebbene si! Sono diverse, perché io ho inizializzato solo i campi che visibili (achar e anint) ma non ho inizializzato i campi invisibili, e cioè i byte di padding aggiunti dal compilatore! Infatti, se aggiungete una printf(3) che vi stampi i sizeof  di test1 e test2 scoprirete che il size è 8, anche se, a prima vista, dovrebbe essere 5 (un char  da 1 byte + un int da 4 byte): i 3 byte in più sono proprio i byte di padding aggiunti dal compilatore per allineare il membro achar, che ora occupa 4 byte. Maledetto compilatore, mi hai fregato un'altra volta!

Il riassunto di tutto questo potrebbe essere: "La memcmp(3) è una ottima funzione per comparare memoria, ma non usatela per comparare strutture!". Ma questo è un po' semplicistico, eddài, sicuramente si può trovare una soluzione!. Ok, e allora vediamo alcune possibili soluzioni:

1) fare un reset della memoria della struct prima di usarla

Avete mai sentito parlare della memset(3)? Immagino di si. Può tornare utile per far funzionare l'esempio mostrato sopra. Vediamo come:

#include <stdio.h>
#include <string.h>
#include <stdint.h>

// una struttura di esempio
typedef struct {
uint8_t achar;
uint32_t anint;
} Test;

// testmemcmp - funzione main()
int main(int argc, char *argv[])
{
// definisco e inizializzo test1 e test2
Test test1; // oppure, senza memset(3): Test test1 = {0};
memset(&test1, 0, sizeof(Test));
test1.achar = 11;
test1.anint = 2222222222;

Test test2;
memset(&test2, 0, sizeof(Test));
test2.achar = 11;
test2.anint = 2222222222;

// comparo test1 e test2 con memcmp(3)
if (memcmp(&test1, &test2, sizeof(Test)) == 0)
printf("%s: test1 e test2 sono uguali\n", argv[0]);
else
printf("%s: test1 e test2 sono diverse\n", argv[0]);

return 0;
}

Ecco, questo esempio dà il seguente risultato:

aldo@Linux $ gcc testmemcmp.c -o testmemcmp
aldo@Linux $ ./testmemcmp
./testmemcmp: test1 e test2 sono uguali
aldo@Linux $

il trucco usato è abbastanza evidente: con la memset(3) azzeriamo anche i byte di padding, quindi la successiva chiamata a memcmp(3) funziona bene, visto che ora i campi nascosti sono uguali nelle due strutture. Il prezzo da pagare è, però, relativamente alto, perché bisogna ricordarsi di usare la memset(3) ogni volta che si definisce (o si alloca) una nuova struct. da comparare.

Notare che, come indicato nel commento del codice qui sopra, si può azzerare la struct  anche senza usare memset(3) inizializzandola nella definizione (questo però non vale se definiamo un pointer da usare successivamente con malloc(3)). E non fatevi ingannare da falsi risultati provocati dalla fortuna: può succedere che, casualmente, i byte di padding abbiano lo stesso valore anche senza eseguire la memset(3), sia dopo una definizione sia dopo una allocazione (e malloc(3) non inizializza la memoria!), quindi oggi il codice funziona, e magari domani no... occhio a questi dettagli!

2) istruire il compilatore su come usare il padding

Questo si può fare modificando minimamente il codice, dicendo al compilatore di non fare il padding per quella particolare struct. Si fa così (riporto solo il dettaglio del codice modificato rispetto al primo esempio di testmemcmp.c):

// una struttura di esempio "packed"
typedef struct __attribute__((__packed__)) {
uint8_t achar;
uint32_t anint;
} Test;

questo funziona, ma ha alcune controindicazioni: la prima è che il padding serve a gestire più efficientemente la memoria e quindi ometterlo potrebbe peggiorare le prestazioni. E poi bisogna ricordasi di farlo sempre (stesso problema del punto 1). L'ultima controindicazione è la scarsa portabilità di questo trucco, che dipende dal compilatore (__packed__ è un attributo del solo GCC) e dalla CPU (potrebbe non funzionare su CPU diverse da x86 o amd64). Tra l'altro in rete si trovano esempi di casi particolari di malfunzionamento e, dulcis in fundo, bisogna tener presente che GCC, anticamente, trattava questo caso come codice pericoloso segnalando dei Warning preoccupanti (adesso non più, probabilmente hanno trovato la maniera di renderlo meno pericoloso). In ogni caso vi ricordo che scrivere codice non portabile non è mai una buona idea.

3) comparare le struct senza usare memcmp(3) (spoiler: questa è la mia opzione preferita).

Come anticipato dallo spoiler, questa è la mia opzione preferita, anche se non è a costo zero, visto che bisogna scrivere un po' di codice in più (ma neanche tanto). Vi avverto: è una soluzione un po' lapalissiana ed è molto semplice: bisogna scrivere una versione custom della memcmp(3) per ogni tipo di struttura da comparare, e usarla sempre al posto della versione generica. Senza ulteriori giri di parole, facciamo cantare un'altra volta il codice!

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

// una struttura di esempio
typedef struct {
uint8_t achar;
uint32_t anint;
} Test;

// prototipi locali
bool myMemcmp(const Test *s1, const Test *s2);

// testmemcmp - funzione main()
int main(int argc, char *argv[])
{
// definisco e inizializzo test1 e test2
Test test1;
test1.achar = 11;
test1.anint = 2222222222;

Test test2;
test2.achar = 11;
test2.anint = 2222222222;

// comparo test1 e test2 con myMemcmp()
if (!myMemcmp(&test1, &test2))
printf("%s: test1 e test2 sono uguali\n", argv[0]);
else
printf("%s: test1 e test2 sono diverse\n", argv[0]);

return 0;
}

// myMemcmp - versione specializzata di memcmp(3) per il tipo Test
bool myMemcmp(const Test *s1, const Test *s2)
{
if (s1 && s2 && (s1->achar == s2->achar) && (s1->anint == s2->anint))
return false; // sono uguali: la memcmp(3) ritornerebbe 0
else
return true; // sono diverse: la memcmp(3) ritornerebbe !0
}

Che ne dite? La versione specializzata della memcmp(3) confronta campo-a-campo invece che byte-a-byte (e grazie al... che funziona: per questo è lapalissiana) è il risultato è sempre sicuro. È un po' più laborioso, perché per ogni tipo di struct bisogna scrivere la versione specializzata, però è a prova di errore, e ne vale la pena. In più c'è il vantaggio di poter ristornare direttamente un bool, il che è molto comodo. Io normalmente seguo questa via.

Direi che per oggi può bastare. Ho introdotto questo nuovo tipo di articolo "Scusate", che è meno drastico del "No Grazie!", visto che la memcmp(3) ha alcuni difetti a livello di uso, ma, nel complesso, è una funzione buona e utile, basta usarla come e quando si deve. Non so esattamente di cosa parlerò nel prossimo articolo, ma sarà di sicuro interessante, ve lo prometto! E, come sempre, non trattenete il respiro nell'attesa!

Ciao, e al prossimo post!

mercoledì 11 ottobre 2023

Ricomincio da DEB
come creare un Debian Package - pt.2

Robertino: Ma mammina dice che io ho i complessi nella testa.
Gaetano: E foss' 'o Ddio! Quali complessi? Tu tieni l'orchestra intera 'ncapa, Robbe'.

E allora, dove eravamo rimasti? Ah, si! Abbiamo ricominciato da tre (anzi, da DEB), con un articolo che invece di descrivere "come sviluppare" (nel nostro amato C) una applicazione, descriveva "come distribuire" la stessa applicazione (nel nostro amato Linux). E noi non faremo come il Robertino qui sopra, che aveva un po' di confusione in testa su quale via intraprendere, ma faremo come gli consigliava l'impagabile Gaetano, e prenderemo una via molto creativa, scriveremo uno shell script per creare automaticamente un Debian Package!

...ma possibile che non hai mai scritto un installer?...

La prima parte dell'articolo l'avevamo conclusa su questa lista di cose da fare (con il grado di difficoltà incluso!):

  • costruire l'albero di installazione (facile!)
  • copiarci dentro eseguibili e servizi (facilissimo!)
  • scrivere il file control (si può fare...)
  • scrivere gli eventuali script di pre/post installazione (spoiler: in questo semplice esempio ci serve solo lo script postinst)

E avevo anticipato che nella seconda parte (e cioè qui) avremmo cercato di fare il tutto con lo spirito del programmatore, e cioè avremmo presentato uno shell script che esegue i punti precedenti in modo automatico, e che diventerà la base (con piccoli adattamenti) per altri script di creazione per qualsiasi applicazione che svilupperemo... un compito troppo ambizioso? In realtà è molto semplice!.

Ok, andiamo per passi: vediamo di descrivere i punti della lista e poi, alla fine, automatizzeremo il tutto. Cominciamo con i primi 2 punti (condensati in 1 solo)!

1) costruire l'albero di installazione + copiarci dentro eseguibili e servizi

Nel nostro caso l'albero di installazione avrà questa forma:

├── DEBIAN
│ ├── control
│ └── postinst
├── etc
│ └── systemd
│ └── system
|── cansetup.service
│ └── canrecv.service
└── usr
└── bin
|── cansetup.sh
└── canrecv

per cui, una volta creato (da qualche parte nella nostra directory di lavoro) un albero della forma descritta, sarà un gioco da ragazzi copiarci dentro i file presentati nella prima parte dell'articolo (a parte i file control e postinst che non abbiamo ancora visto). Notare che questo primo punto lo faremo, automaticamente, attraverso il nostro shell script di creazione del package, quello che vedremo tra poco.

2) scrivere il file control

Il file control  ha, come visto nel precedente articolo, una struttura minima abbastanza semplice. Una struttura che si può complicare, a piacere, aggiungendo altre linee di dettagli che, in alcuni casi, possono anche essere utili, ma direi che per la nostra applicazione canrecv  (che è un semplice CAN Server) è sufficiente un file control come questo:

Package: canrecv
Version: 1.0-amd64
Architecture: amd64
Depends: libc6 (>= 2.34)
Maintainer: A.Abate <artcprogramming@gmail.com>
Description: canrecv DEB package
installa la applicazione canrecv e i relativi servizi systemd

dove si possono cambiare, se necessario, la versione Version (N.B.: deve cominciare sempre con un numero) e l'architettura Architecture (nel''esempio ho messo quella su cui lavoro io). Le dipendenze Depends sono quelle ottenute con il procedimento descritto nello scorso articolo, il responsabile Mantainer  sarà l'autore dell'installer stesso e la descrizione corta Description sarà quella adatta all'installer in oggetto, seguita nella linea successiva dalla descrizione lunga, che può essere anche multi-linea, con linee che cominciano sempre con uno spazio. Facile, no?

3) scrivere gli eventuali script di pre/post installazione.

Per il nostro package è necessario solo uno script di post-installazione, uno script che installa e attiva i nuovi servizi. E vediamolo!

#!/bin/bash

# set uscita immediata su errore
set -e

# abilita e avvia il servizio cansetup
systemctl enable cansetup.service
systemctl start cansetup.service
systemctl daemon-reload

# abilita e avvia il servizio canrecv
systemctl enable canrecv.service
systemctl start canrecv.service
systemctl daemon-reload

Questo script è necessario, in questo caso, perché il nostro package contiene dei servizi systemd, che devono essere opportunamente installati nel sistema. Il procedimento consiste nell'abilitazione del servizio, seguita dal suo avvio e dal riavvio del daemon di systemd (tutto questo ripetuto per i due servizi inclusi nel package). Ovviamente per altri tipi di applicazione uno script di postinst  potrebbe non essere necessario o fare ben altre cose. Comunque i tipi di script disponibili li trovate elencati nella prima parte dell'articolo.

A questo punto abbiamo anche i file che ci mancavano da aggiungere al nostro albero creato qui sopra, quindi cosa ci manca? Ma ci manca il nostro (oramai mitico) shell script per automatizzare il tutto! Vediamolo!

#!/bin/bash

# check degli argomenti
if [ $# -eq 0 ]; then
echo "numero argomenti errato"
echo "uso: $0 nome_release (e.g.: $0 1.0-amd64)"
exit 1
fi

# set della directory di lavoro
mydir=canrecv-$1

# compilo l'ultima versione di canrecv.c
cd ..
gcc canrecv.c -o install/base/canrecv
cd install

# creo l'albero di directory destinazione
mkdir $mydir
mkdir $mydir/DEBIAN
mkdir $mydir/usr
mkdir $mydir/usr/local
mkdir $mydir/usr/local/bin
mkdir $mydir/etc
mkdir $mydir/etc/systemd
mkdir $mydir/etc/systemd/system

# copio i file nella directory destinazione
cp base/control $mydir/DEBIAN
cp base/postinst $mydir/DEBIAN
cp base/cansetup.sh $mydir/usr/local/bin
cp base/canrecv $mydir/usr/local/bin
cp base/cansetup.service $mydir/etc/systemd/system
cp base/canrecv.service $mydir/etc/systemd/system

# costruisco il DEB package
dpkg-deb --build --root-owner-group $mydir/

Avrete notato che è uno script molto compatto che esegue solo le operazioni indispensabili (opportunamente commentate):

  • check degli argomenti
  • set della directory di lavoro
  • compilo l'ultima versione di canrecv.c (nel package deve entrare sempre l'ultima versione, no?)
  • creo l'albero di directory destinazione (usando delle semplici istruzioni mkdir)
  • copio i file nella directory destinazione (usando delle semplici istruzioni cp)
  • costruisco il DEB package (usando il classico comando Linux dpkg-deb)

Semplice, no? E come si può usare lo script?  Io suggerisco suggerisco di creare un ambiente di creazione di questo tipo:

├── base
│   ├── canrecv
│   ├── canrecv.service
│   ├── cansetup.service
│   ├── cansetup.sh
│   ├── control
│   └── postinst
└── buildpkg.sh

con una directory "base"  dove risiede la base di installazione (che è, anche, la destinazione della istruzione di compilazione presente nello script, non so se avete notato). Entrando in questo ambiente di creazione ed eseguendo il comando:

aldo@Linux $ ./buildpkg.sh 1.0-amd64
dpkg-deb: generazione del pacchetto "canrecv" in "canrecv-1.0-amd64.deb".
aldo@Linux $

otterremo che il nostro albero di creazione si espanderà così:

├── base
│   ├── canrecv
│   ├── canrecv.service
│   ├── cansetup.service
│   ├── cansetup.sh
│   ├── control
│   └── postinst
├── buildpkg.sh
├── canrecv-1.0-amd64
│   ├── DEBIAN
│   │   ├── control
│   │   └── postinst
│   ├── etc
│   │   └── systemd
│   │   └── system
│   │   ├── canrecv.service
│   │   └── cansetup.service
│   └── usr
│   └── local
│   └── bin
│   ├── canrecv
│   └── cansetup.sh
└── canrecv-1.0-amd64.deb

ovvero, troveremo il nostro Debian Package in forma espansa (la directory canrecv-1.0-amd64) e compressa (il file canrecv-1.0-amd64.deb), e quest'ultima ci permetterà di distribuire il nostro installer a chi lo vorrà!

E qui direi che l'articolo si può dire concluso. Abbiamo ricominciato da tre (anzi, da DEB), e per una volta abbiamo parlato di distribuzione invece che di programmazione. Quasi quasi nel prossimo articolo, per cavalcare l'onda, potrei parlare si una argomento toccato di striscio nella prima parte, e cioè la compilazione abbinata alla distribuzione (Autotool). Beh, vedremo, e in ogni caso sarà una sorpresa! (spero lieta...).

Ciao, e al prossimo post!

lunedì 25 settembre 2023

Ricomincio da DEB
come creare un Debian Package - pt.1

Gaetano: Chello ch'è stato è stato, basta! Ricomincio da tre!
Lello: Da zero.
Gaetano: Eh?
Lello: Da zero! Ricominci da zero!
GaetanoNossignore, ricomincio da... cioè, tre cose me so' riuscite ind'a vita, pecchè aggià perdere pure cheste?! Aggià ricominciare da zero? Da tre!

Dopo il tormentone del CAN bus (un articolo in ben 4 parti!) ho deciso di ricominciare da zero, anzi... Ricomincio da tre, come il grande Massimo Troisi nella sua opera prima capolavoro. Ricominciare in che senso? Nel senso che, almeno per questa volta, invece di parlare della mia (nostra) amata programmazione in C parleremo di un argomento collegato e un po' trascurato, che corrisponde alla domanda: "Ma dopo aver sviluppato una applicazione Linux come la distribuisco?". Ecco, ci sono vari metodi, ma oggi ho voglia di parlare del più elementare e facile da usare, e cioè il "pacchetto di distribuzione" e, in particolare, mi soffermerò sul tipo più usato, il Debian Package (DEB Package o Pacchetto Debian per gli amici) che si usa nelle distribuzioni Linux più diffuse (Ubuntu, Mint, Debian, etc.).

...ma perché devo ricominciare da zero? Io ricomincio da DEB!...

Come detto sopra, ci sono vari metodi per distribuire una applicazione Linux e, prima di parlare del Debian Package, bisogna fare alcune precisazioni: indubbiamente, il metodo più rigoroso di distribuzione è basato su autotool: si distribuisce tutto l'ambiente di sviluppo (sorgenti, makefile e file di configurazione), e quindi si compila e installa usando il classico comando "./configure && make install". Questo metodo è rigoroso perché si auto-adatta (grazie ad autotool) alla macchina destinazione su cui può essere in uso una qualsiasi versione di Linux, tanto l'applicazione viene compilata e linkata localmente con le risorse a disposizione.

Ma usare autotool è un metodo molto specialistico (credo sia evidente... è roba da programmatori ah ah ah) mentre gli utenti "normali"  vogliono installare e usare in quattro e quattr’otto, (e se gli utenti hanno un passato Windows non ne parliamo neanche...). E quindi anche nel mondo Linux si usano degli installer che si chiamano "pacchetti di distribuzione", e che sono facilissimi da usare ma sono un po' meno flessibili di autotool: non si adattano automaticamente alla macchina (contengono l' applicazione precompilata) e quindi in alcuni casi potrebbero non essere installabili al primo colpo: ad esempio è possibile che la applicazione da installare usi una libreria di una versione differente di quella già presente nella macchina, e questo genera la segnalazione di un errore. In generale, però, è sempre abbastanza facile risolvere eventuali problemi, se l'installer è ben fatto.

Ah, dimenticavo: ovviamente un Debian Package ha un doppio uso: installare e disinstallare (e anche questa seconda attività è importante, no?). E allora, come si crea un pacchetto di questo tipo? Vediamo prima di tutto come è fatto: il Debian Package è un file di tipo archivio compresso che, internamente, ha la seguente struttura tipica:

├── DEBIAN
│ ├── control
│ ├── preinst
│ ├── postinst
│ ├── prerm
│ └── postrm
├── etc
│ └── systemd
│ └── system
│ └── myservice.service
└── usr
└── bin
└── myapp

ossia: ci sono tre directory principali (DEBIAN, etc e usr) che hanno il seguente uso:

  • DEBIAN: contiene i file di controllo e di pre/post installazione/disinstallazione:
    • control: è il file di controllo master, quello che guida l'installazione, infatti è l'unico file obbligatorio.
    • preinst: è uno shell script preparatorio eseguito automaticamente prima dell'installazione vera e propria. È opzionale.
    • postinst: è uno shell script di finalizzazione eseguito automaticamente dopo l'installazione. È opzionale.
    • prerm: è uno shell script preparatorio eseguito automaticamente prima della disinstallazione vera e propria. È opzionale.
    • postrm: è uno shell script di finalizzazione eseguito automaticamente dopo la disinstallazione. È opzionale.
  • lib: contiene l'albero di directory che replica l'albero dei servizi systemd della macchina destino: in questo lib/systemd/system si copieranno i servizi (file con estensione .service) necessari alla nostra applicazione (questa directory lib è opzionale, serve solo se il Package installa anche dei servizi systemd).
  • usr: contiene l'albero di directory che replica l'albero delle applicazioni della macchina destino: in questo usr/bin si copieranno gli eseguibili che compongono la nostra applicazione (nell'esempio sopra è una sola applicazione che si chiama "myapp").

E come è fatto un file control? È un file di testo che contiene alcune linee che descrivono il Package, e le linee che, come minimo, devono essere presenti sono:

  • Package: il nome dell'applicazione da installare.
  • Version: la versione dell'applicazione da installare.
  • Maintainer – il nome e l'indirizzo email del responsabile del Package.
  • Description – una descrizione corta dell'applicazione. Di solito questa è l'ultima linea del control: sotto questa linea si può aggiungere una descrizione più lunga che deve, però, cominciare con uno spazio.

si possono aggiungere molte altre linee con funzioni particolari, in particolare due linee che non dovrebbero mai mancare sono queste (N.B.: in realtà la lista dei campi required/recommended/optional varia un po' tra un manuale e l'altro... diciamo che usando i quattro campi qui sopra più i due qui sotto non si dovrebbero avere problemi):

  • Architecture – la architettura dove può correre la applicazione (i.e.: all, oppure amd64 oppure i386, oppure sparc... e molte altre. all si usa per le installazioni compatibili con qualsiasi macchina).
  • Depends: descrive le librerie (con le rispettive versioni) indispensabili all'installazione.

La linea Depends è un poco particolare: ci sono vari metodi per cercare quali sono le librerie indispensabili alla nostra applicazione, e uno dei metodi più interessanti usa un meccanismo dello stesso ambiente di generazione del Package, però con alcune stranezze (la prima volta che l'ho usato mi è costato un po' farlo funzionare). Ma uno dei miei compiti è svelare i trucchi, no? E vediamoli! Bisogna creare un mini-albero secondario che somiglia a quello principale mostrato sopra, un mini-albero come questo:

├── debian
│ └── control
└── usr
└── bin
└── myapp

sempre supponendo che la applicazione si chiami "myapp". Notare che debian è in minuscolo e che il file control  deve contenere solo questa linea:

source: myapp

dopodiché,  posizionandosi nella root-directory del mini-albero , si può eseguire il comando dpkg-shlibdeps ottenendo un risultato di questo tipo:

aldo@Linux $ dpkg-shlibdeps -O usr/local/bin/myapp
dpkg-shlibdeps: Avviso: binaries to analyze should already be installed in their package's directory
shlibs:Depends=libc6 (>= 2.34)
aldo@Linux $

Il risultato è, quindi, la lista delle librerie indispensabili (nell'esempio sopra c'è solo: libc6 dalla ver.2.34 in su) che potremo aggiungere in DEBIAN/control nella linea Depends. Dopodiché il mini-albero che abbiamo usato possiamo cancellarlo (ha già esaurito il suo compito) e possiamo tornare a usare l'albero principale.

Dopo questa introduzione teorica è l'ora di passare a un caso pratico, no? Vedendo un caso reale molti dubbi vengono cancellati automaticamente (beh, almeno a me succede spesso). Visto che ce l'abbiamo fresco in mente e abbiamo già il codice disponibile negli ultimi articoli pubblicati, cercheremo di creare un Debian Package che installa nel sistema canrecv, che è un CAN Server (e scusate se ritiro fuori un argomento appena visto!).

Ah, fino a qui ho dato per scontato che tutti sappiano cos'è un servizio systemd (e magari ci tornerò in futuro con un articolo) comunque in questo caso è sufficiente sapere che è la maniera più "moderna" di assegnare compiti a Linux, compiti del tipo: "al boot avvia questo", "se si interrompe riavvialo", ecc. Prima si usavano altri metodi (non so se a tutti è familiare il vecchio e sempre valido "init" derivato da "SysVinit"), ma da qualche anno a questa parte si usa quasi sempre systemd per cui lo useremo anche noi.

E torniamo al punto: l'obiettivo è, quindi, che il nostro Debian Package installi queste cose nel sistema:

  • un eseguibile cansetup.sh
  • un eseguibile canrecv
  • un servizio systemd  cansetup.service che produce l'esecuzione del CAN setup al boot  del sistema
  • un servizio systemd  canrecv.service che produce l'avvio del Server al boot  del sistema

Il codice di canrecv (canrecv.c) l'ho già pubblicato e lo possiamo trovare qui: è un Server molto minimale (riceve un messaggio ed esce) però per questo semplice esempio va più che bene (compito a casa: modificare canrecv.c per ottenere un CAN Server che riceva messaggi in loop). Anche lo shell script  cansetup.sh lo abbiamo visto in quest'altro articolo, ed è indispensabile al funzionamento del Server, visto che prepara il dispositivo virtual CAN del sistema. Ci mancano solo i due servizi: questo è cansetup.service:

[Unit]
Description=setup del vcan0 device
After=network.target

[Service]
ExecStart=/usr/local/bin/cansetup.sh

[Install]
WantedBy=default.target
mentre questo è canrecv.service:
[Unit]
Description=avvia il processo canrecv
After=cansetup.service

[Service]
ExecStart=/usr/local/bin/canrecv

[Install]
WantedBy=default.target

Come potete vedere sono abbastanza semplici (oserei dire: auto-esplicativi) e, comunque, spiegare come funzionano non è l'argomento di questo articolo (ripeto la semi-promessa: magari ci tornerò in futuro). L'importante è lo scopo di questi servizi, e quello credo che sia abbastanza chiaro. Quindi non ci resta che:

  • costruire l'albero di installazione (facile!)
  • copiarci dentro eseguibili e servizi (facilissimo!)
  • scrivere il file control (si può fare...)
  • scrivere gli eventuali script di pre/post installazione (spoiler: in questo semplice esempio ci serve solo lo script postinst)

E qui potrei concludere l'articolo aggiungendo le parti appena elencate... ma qui viene fuori la mia (nostra) anima di programmatore: perché fare a mano le operazioni descritte quando si possono automatizzare? Non posso concludere un articolo senza aggiungere una minima parte di programmazione, no?  E allora, nella seconda parte che arriverà prossimamente (giuro), vi presenterò un utile shell script che ho scritto per creare e ricreare (a piacere) il nostro Debian Package: è facilmente adattabile ad ogni uso futuro ed è una buona base per qualsiasi tipo di applicazione che abbisogna di un installer. Quindi per il momento vi saluto e, come sempre, vi consiglio di non trattenere il respiro nell'attesa!

Ciao, e al prossimo post!