(Distribuito sotto licenza FDL - http://www.gnu.org/copyleft/fdl.txt)

Mi dicono su #perl.it: "scrivici sopra un articolo!", come un espediente per liberarsi di me. Forse parlare di ioctl(2) di Linux in un canale sul Perl non è molto intrigante. Del resto in tale discussione il linguaggio non è molto valorizzato, ioctl(2) è una chiamata di sistema e Perl ne offre solo una interfaccia, ma ho ritenuto che l'uso diretto di ioctl in Perl anziché in linguaggio C potesse dare vantaggio al Perl stesso, permettendogli di approdare alla produzione di utility di sistema di solito appannaggio del linguaggio C.

Immaginate di riscrivere gli script di init del sistema Linux in Perl. Come controllare il Mixer? E come impostare i parametri delle interfacce di Rete? E la tebella di routing? E si possono fare altre cose: impostare parità e velocità di una seriale, scoprire le partizioni di un disco e modificarle, ecc...

Tutto il codice che verrà scritto sarà legato alla tecnologia del kernel, quindi non portatile su tutti i sistemi. Questo è un rammarico! Il Perl è soprattutto un ottimo veicolo per portare con successo il proprio codice tra sistemi diversi senza modifiche o con poche e facilmente controllabili, tuttavia anche codice Perl di controllo a ioctl(2) può essere ben amministrato per poterne fare versioni per vari sistemi Unix(R)/Linux/POSIX, seguendo le classiche regole di separazione delle parti soggette a modifica. ioctl(2) è una syscall standard, solo i suoi argomenti non sono standard, quindi addirittura si potrebbe scrivere codice che il runtime strutturi i dati da usare in modi dipendenti dalla piattaforma.

La mia unica piattaforma è Linux e comunque Linux ha assunto grande importanza e può solo divenire una scelta sempre più importante e strategica, quindi il codice legato a questa piattaforma non è da cosiderarsi una cattedrale nel deserto, uno spreco di tempo. L'ottima documentazione, la vasta comunità e la disponibilità dei sorgenti lo rendono una scelta strategica importante e di cui non ci si pentirà. Proprio l'accesso ai sorgenti ci darà tutte le informazioni di cui abbiamo bisogno per controllare ioctl. Trovando il giusto header di definizione delle strutture dati, l'uso di ioctl diviene banale. Risolte le problematiche di come strutturare i dati con Perl, si è completamente a posto.

Lo scopo è NON usare un modulo binario per Perl, magari scritto in C, ma poter controllare tutti gli aspetti della chiamata a ioctl(2) tramite la funzione che già è presente nel core del linguaggio Perl (perldoc -f ioctl). ioctl(2) di solito riceve tra i suoi argomenti puntatori a strutture dati complesse, con campi a loro volta puntatori ad altre strutture ed è questo l'aspetto difficile da affrontare in linguaggi diversi dal C.

Per chi non ha mai usato ioctl, vediamone il prototipo C e discutiamone giusto il necessario; da man 2 ioctl:

#include <sys/ioctl.h>
int ioctl(int d, int request, ...);

La chiamata a ioctl fatta da linguaggio C può restituire 0 se OK, -1 per errore e in tal caso sarà validata la variabile globale errno, in certi casi potrebbe restituire un valore non negativo come risultato della chiamata specifica. Niente di certo quindi. Probabilmente per questo i progettisti del Perl hanno fatto questa curiosa scelta nel progettare l'interfaccia a tale syscall.

La funzione ioctl del Perl, così come spiegato da perldoc -f ioctl riceverà:

!!! RACCOMANDAZIONE MOLTO IMPORTANTE !!! Nei casi in cui ioctl(2) restituirà informazioni tramite il terzo argomento, non si curerà delle dimensioni dello scalare Perl e aggirerà le protezione del memory-manager del Perl stesso, quindi è importante DIMENSIONARE ADEGUATAMENTE lo scalare passato come terzo argomento prima, perché la syscall non lo ridimensionerà e potrebbe andare a calpestare altre strutture dati del processo Perl in esecuzione. Se lo scalare è gia dimensionato o sovradimensionato non ci saranno problemi.

Iniziamo a giocare: stropicciamo un po' il CDROM. In Linux le definizioni delle costanti dei comandi ioctl (secondo argomento) e annesse strutture dati del terzo argomento le troviamo in:

/usr/include/linux/cdrom.h 
insieme ad un sacco di informazioni su specifiche RedBOOK e buoni consigli. Il primo buon consiglio appare subito nelle prime linee: dice di aprire il device del CDROM con una open non bloccante, per ottenere un file-descriptor valido anche se non è stato inserito alcun CDROM.
drive = open("/dev/cdrom", O_RDONLY | O_NONBLOCK);
Per poter fare questo in Perl non potremo usare la classica open, perché non permette di specificare flag aggiuntivi, ma il Perl è un linguaggio da hacker, non poteva mancare questa possibilità. Difatti nel core del linguaggio troviamo anche sysopen. Subito un esempio funzionante.
sysopen $cdrom, "/dev/cdrom", 2048;
$ioctl3par = "";
ioctl $cdrom, 0x5309, $ioctl3par; # CDROMEJECT == 0x5309
close $cdrom;

Questo sul mio sistema apre il cassettino del device associato al link /dev/cdrom. 2048 è l'equivalente di O_RDONLY | O_NONBLOCK che ho tradotto in valore numerico cercando in altri file include i corrispondenti valori. Se anziché usare sysopen avessi usato:

open $cdrom, "/dev/cdrom";
l'esempio avrebbe funzionato solo se nel dispositivo avessi precedentemente inserito un CD. Solo in tal caso la open avrebbe restituito un FileHandle valido. Quindi abituiamoci a usare sysopen, anche se i prossimi esempi implicheranno comunque il CD inserito e quindi open funzionerà comunque. "/usr/include/linux/cdrom.h" è una buona lettura, quindi vado avanti e provo un po' di tutto. Inserisco un CD Audio regolarmente acquistato e provo a leggerne l'"Universal Product Code":
sysopen $cdrom, "/dev/cdrom", 0 or die "CD non trovato\n";
                              # qui non abbiamo usato 2048 == O_NONBLOCK
                              # anche la normale open avrebbe funzionato
# struct cdrom_mcn
# {
#   __u8 medium_catalog_number[14]; /* 13 ASCII digits, null-terminated */
# };

$ioctl3par = "\x00" x 512; # sovraddimensiono il buffer dove ricevero'
                           # il codice "Universal Product Code"

ioctl $cdrom, 0x5311, $ioctl3par; # CDROM_GET_MCN == 0x5311
# "Universal Product Code"

print "" . (split /\0/, $ioctl3par ,2)[0] . "\n";
close $cdrom;
ottengo 4509937780208, proprio ciò che leggo sul codice a barre del CD "Tenderness" di Al Jarreau.

A questo punto per avere la vita semplice, curioso tra le strategie di software pregiati per vedere che strategie usano, e faccio:

strace -o file.strace cdparanoia -B
e senza la pretesa di implementare le sofisticate fasi di paranoia, correzione jitter, recupero errori ecc... colleziono le informazioni sul CD tamite le stesse chiamate a ioctl(2). Per leggere la TOC del CD uso:
sysopen $cdrom, "/dev/cdrom", 0 or die "CD non trovato\n";
$ioctl3par = "\x00\x00\x00\x00";  # 4 byte causa word-alignment !!
ioctl $cdrom, 0x5305, $ioctl3par; # CDROMREADTOCHDR == 0x5305
print ord substr $ioctl3par, 0, 1;
print "\n";
print ord substr $ioctl3par, 1, 1;
print "\n";
close $cdrom;
Il terzo argomento è questa struct:
struct cdrom_tochdr
{
        __u8    cdth_trk0;      /* start track */
        __u8    cdth_trk1;      /* end track */
};
quindi il primo byte dello scalare $ioctl3par dopo la chiamata sarà impostato al valore di indice della prima traccia (mi sembra sia sempre 1 ...) e il secondo byte al valore di indice dell'ultima. L'esecuzione dà:
1
12
e appunto il CD ha 12 tracce.

cdparanoia ora leggerebbe dove inizia ogni TOCENTRY e a tale scopo usa quello che io ho tradotto in:

# per $indicetraccia da $primatraccia a $ultimatraccia
$ioctl3par = (pack "C", $indicetraccia) ."\x00\x01\x00" . ("\x00" x 8);
ioctl $cdrom, 0x5306, $ioctl3par; # CDROMREADTOCENTRY == 0x5306
$frameiniziotraccia = unpack "V", (substr $ioctl3par, 4, 4);
print "$frameiniziotraccia\n";

# struct cdrom_tocentry
# {
#    __u8    cdte_track;           indice traccia, (1 o 2 o 3 o ...)
#    __u8    cdte_adr        :4;   la lettura lo mette a 1 se
#                                  traccia esiste, a 0 se non esiste.
#    __u8    cdte_ctrl       :4;   la lettura lo mette a 0x00 se traccia
#                                  AUDIO, 0x04 se traccia DATA
#    __u8    cdte_format;          0x01 LBA, 0x02 Min,Sec,Frame
#    union cdrom_addr cdte_addr;   4 byte: LBA o 0,Min,Sec,Frame
#    __u8    cdte_datamode;
# };
# /* attenti al word-alignment !!!! in C 12 byte !!!! */
Questa è la struttura dati C ottenuta dal file cdrom.h e mi ha fatto un po' soffrire. La causa del mio primo insuccesso è stato il word-alignment. Questa struttura dati è da cosiderarsi lunga 12 byte. Validiamo: Dopo la chiamata avremo validati:

Per sapere dove termina l'ultima traccia, dobbiamo leggere dove inizia la leadout track, che ha sempre indice 0xAA. Citazione da cdrom.h:

/* The leadout track is always 0xAA, regardless of # of tracks on disc */
#define CDROM_LEADOUT           0xAA
Non ho al momento disponibili CD Multisessione, né voglia di farne, altrimenti avrei dovuto sapere dove inizia la prossima sessione per sapere dove termina l'ultima traccia audio. cdparanoia effettua questa ioctl(2) per testare se il CD è multisessione.
# struct cdrom_msf0 { __u8 minute; __u8 second; __u8 frame; };
#
# union cdrom_addr { struct cdrom_msf0 msf; int lba; };
#
# struct cdrom_multisession
# {
#   union cdrom_addr addr; /* frame address: start-of-last-session */
#                          /* (not the new "frame 16"!).           */
#                          /* Only valid if the "xa_flag" is true. */
#
#   __u8 xa_flag;          /* 1: "is XA disk" */
#   __u8 addr_format;      /* CDROM_LBA or CDROM_MSF */
# };

# inizio ultima sessione in CDROM MULTISESSIONE
$ioctl3par = "\x00\x00\x00\x00\x00\x01\x00\x00"; # CDROM_LBA
ioctl CDROM, 0x5310, $ioctl3par; # CDROMMULTISESSION == 0x5310

Ed ora estraiamo una traccia CDDA e salviamola in un file .wav !! Per farlo dobbiamo passare a ioctl questa struttura dati:

# /* This struct is used by the CDROMREADAUDIO ioctl */
# struct cdrom_read_audio
# {
#   union cdrom_addr addr; /* frame address */
#   __u8 addr_format;      /* CDROM_LBA or CDROM_MSF */
#   int nframes;           /* number of 2352-byte-frames to read at once */
#   __u8 __user *buf;      /* frame buffer (size: nframes*2352 bytes) */
# };
# attenti al word-alignment !!!!
Validare i primi tre campi è semplice e intuitivo, ma come validare il quarto? Vuole un puntatore a buffer precedentemente allocato e mica gli posso passare un reference del Perl. Immaginate il macello che farebbe ioctl indirizzando a caso la memoria.

Gli amici del chan #perl.it mi sono stati di grande aiuto in questo, indicandomi dove avrei trovato la soluzione: in perldoc -f pack. Una cosa che non avevo letto con attenzione, finalmente ha per me assunto il suo corretto significato e questo a aperto la strada all'uso più libero possibile di ioctl tramite Perl.

Quindi con pack 'P', $scalare; ottengo un valore puntatore valido (32 bit sul mio sistema) che punta ai dati netti dello scalare, direttamente indirizzabili da ioctl. È sufficiente dimensionare tale scalare così:

$frames = "\x00" x (2352 * $numeroframesdaestrarre);
e ricavarne il puntatore con pack 'P', $frames; da concatenare per ottenere il terzo argomento di ioctl. Ed ecco il codice "rippatutto.pl" che estrae una traccia audio in un file .wav:
#!/usr/bin/perl

use strict;

sysopen my $cdrom, $ARGV[0], 0;

$|++;

my $ioctl3par = "";
my $track = pack "C", ($ARGV[1] % 100);
my $audiotrack = undef();

# FASE 1): scopriamo quante tracce ha il nostro CD e di che tipo: Dati o CDDA

$ioctl3par = "\x00" x 512;
ioctl $cdrom, 0x5305, $ioctl3par; # CDROMREADTOCHDR == 0x5305

my $firsttrackpacked = substr $ioctl3par, 0, 1;
my $lasttrackpacked  = substr $ioctl3par, 1, 1;
my $firsttrack = ord $firsttrackpacked;
my $lasttrack  = ord $lasttrackpacked;

print "il CD in $ARGV[0] ha tracce da $firsttrack a $lasttrack\n";

if ((($ARGV[1]) < $firsttrack) || (($ARGV[1]) > $lasttrack))
{ die "traccia $ARGV[1] non trovata in CD $ARGV[0]\n"; }

# dove inizia la traccia richiesta ....
$ioctl3par = $track . "\x00\x01\x00" . ("\x00" x 8);
ioctl $cdrom, 0x5306, $ioctl3par; # CDROMREADTOCENTRY == 0x5306
my $trackbegin = unpack "V", (substr $ioctl3par, 4, 4);

$audiotrack = ord substr $ioctl3par, 2, 1;
if ($audiotrack == 1)
  { print "traccia $ARGV[1] di tipo CDDA\n"; }
elsif ($audiotrack == 41)
  { die "traccia $ARGV[1] di tipo DATA\nEstraggo solo tracce di tipo CDDA\n"; }
else
  { die "cosa potra' essere traccia $ARGV[1] di tipo $audiotrack?\n"; }

# dove termina la traccia richiesta ....
if ($ARGV[1] == $lasttrack) # l'ultima traccia termina alla LEADOUT track (0xAA)
{
  $ioctl3par = "\xaa\x00\x01\x00" . ("\x00" x 8);
  ioctl $cdrom, 0x5306, $ioctl3par; # CDROMREADTOCENTRY == 0x5306  
}
else # la traccia termina dove inizia la prossima
{
  $ioctl3par = pack "C", ($ARGV[1] + 1) . "\x00\x01\x00" . ("\x00" x 8);
  ioctl $cdrom, 0x5306, $ioctl3par; # CDROMREADTOCENTRY == 0x5306
}
my $trackend = unpack "V", (substr $ioctl3par, 4, 4);

print "la traccia $ARGV[1] richiesta inizia da frame $trackbegin e termina a frame " .
      ($trackend - 1) . "\n";

# FINE FASE 1): abbiamo $trackbegin e $trackend della traccia richiesta.
# sappiamo inoltre che la traccia e' di tipo CDDA ($audiotrack == 1).
# per tracce DATA ($audiotrack == 41) l' arg. a ioctl e' diverso come pure
# la lunghezza delle frame
#
# ----------------------------------------------------------------------

# fase 2): estraiamo le frame di 2352 a 64 per chiamata ioctl.

open my $trackfile, ">traccia.wav";
if ($audiotrack == 1)
{
  my $filelength = (($trackend - $trackbegin) * 2352);
  # 44 byte di intestazione traccia.wav
  #                |      RIFF      |   |            LENGHT          |   |      WAVE      |
  my $waveheader = "\x52\x49\x46\x46" . (pack "V", ($filelength + 36)) . "\x57\x41\x56\x45" . 
  #                |     "fmt_"     |   |   sempre 0x10  |   |  0x01 + Stereo |
                   "\x66\x6d\x74\x20" . "\x10\x00\x00\x00" . "\x01\x00\x02\x00" .
  #                |    44100 Hz    |   |    bytes/sec   |   | bytes x sample |
                   "\x44\xac\x00\x00" . "\x10\xb1\x02\x00" . "\x04\x00\x10\x00" .
  #                |      DATA      |   | LENGHT DATA == LENGHT - 36 |
                   "\x64\x61\x74\x61" . (pack "V", $filelength);

  syswrite $trackfile, $waveheader;
}

# /* This struct is used by the CDROMREADAUDIO ioctl */
# struct cdrom_read_audio
# {
#   union cdrom_addr addr; /* frame address */
#   __u8 addr_format;      /* CDROM_LBA or CDROM_MSF */
#   int nframes;           /* number of 2352-byte-frames to read at once */
#   __u8 __user *buf;      /* frame buffer (size: nframes*2352 bytes) */
# };
# attenti al word-alignment !!!!

my $frameindex = $trackbegin;
my $packedidx = pack 'V', $frameindex;
my $frames = "\x00" x (2352 * 128); # sovraddimensionato!
                                    # 128 frame non si possono leggere! ne leggeremo 64!

my $nframes = (($trackend - $frameindex) >= 64) ? 64 : ($trackend - $frameindex);

$ioctl3par = pack('V', $frameindex) .  # leggiamo da questo indice di frame.
             "\x01\x00\x00\x00" .      # indirizziamo in LBA.
             pack('V', $nframes) .     # leggiamo questo numero di frame.
             pack('P', $frames) .      # mettiamo nello scalare puntato da ...
             ("\00" x 16);             # sovraddimensioniamo... :)

$! = 0;
while ($nframes)
{
  ioctl $cdrom, 0x530e, $ioctl3par; # CDROMREADAUDIO == 0x530e
  syswrite $trackfile, $frames, (2352 * $nframes);
  $frameindex += $nframes;
  $nframes = (($trackend - $frameindex) >= 64) ? 64 : ($trackend - $frameindex);
  $ioctl3par = pack('V', $frameindex) .  # leggiamo da questo indice di frame.
               "\x01\x00\x00\x00" .      # indirizziamo in LBA.
               pack('V', $nframes) .     # leggiamo questo numero di frame.
               pack('P', $frames) .      # mettiamo nello scalare puntato da ...
               ("\00" x 16);             # sovraddimensioniamo... :)
  
}

close $trackfile;
close $cdrom;

Il mio divertimento con ioctl sotto Perl continua. Pubblicherò altre semplici utility per altri scopi, ad esempio per controllare le interfacce di rete.

Con questo articolo spero di aver ispirato la creatività di altri a usare Perl per conoscere meglio il sistema Linux e simili e accedere alla potenza della tecnologia del kernel direttamente da Perl con poche linee di codice.

Buon divertimento!

Roberto Cappellini