Introduzione a sysfs

di Alessandro Rubini

Riprodotto con il permesso di Linux Magazine, Edizioni Master.

Questo numero presenta il meccanismo con cui un modulo può esportare parametri configurabili attraverso sysfs, evitando di addentrarsi nei dettagli più complicati per non appesantire eccesivamente la trattazione.

Una delle novità del kernel 2.6 è il filesystem sysfs, progettato come interfaccia verso lo spazio utente delle strutture kobject, progettate per semplificare comuni operazioni di gestione degli oggetti e renderle immuni da corse critiche.

Nonostante un utilizzo canonico del filesystem richieda una struttura rigorosa dei file creati e una standardizzazione nella presentazione del loro contenuto, è possibile prendere confidenza con le strutture dati associate a sysfs anche senza rispettare tutte le regole. Il codice descritto è stato utilizzato con Linux-2.6.6 per esportare attraverso sysfs un parametro intero di un modulo, il cui valore puo` variare solo in un intervallo predefinito; gli estremi dell'intervallo sono anch'essi esportati attraverso sysfs, ma in sola lettura.

sysctl

Non è la prima volta che il kernel offre un meccanismo centralizzato per semplificare l'esportazione di parametri verso lo spazio utente. Il meccanismo sysctl, acessibile attraverso una chiamata di sistema apposita oppure tramite l'albero di directory /proc/sys/, era già disponibile in Linux-2.0, e svolgeva il proprio lavoro in maniera accettabile. Una descrizione di sysctl si trova, per esempio, su http://www.linux.it/kerneldocs/sysctl.

Quello che sysctl non offre è una strutturazione ordinata ed scalabile dei driver; l'immunità da corse critiche; una gestione "ad oggetti" delle entità registrate. Con strutturazione scalabile si intende un'organizzazione delle informazioni che non collassi al crescere delle unita` di informazione trattate.

L'interfaccia sysctl è ancora disponibile immutata nel kernel 2.6; e` ancora possibile usarla e sperimentarne i limiti; in particolare si puo` facilmente generare una corsa critica da cui scaturisce un errore del kernel. I comandi che seguono fanno uso di un modulo smallsysctl, che è disponibile, con gli altri sorgenti di questo articolo, in src.tar.gz (URL completo: http://www.linux.it/kerneldocs/sysfs/src.tar.gz).

   burla$ insmod smallsysctl.ko
   burla% echo 1 2 3 4 > /proc/sys/dev/smallsysctl/ints
   burla% cat /proc/sys/dev/smallsysctl/ints
   1       2       3       4
   burla$ (sleep 10; cat) < /proc/sys/dev/smallsysctl/ints
   Segmentation fault
   burla$ cat /proc/sys/dev/smallsysctl/ints
   Segmentation fault

L'errore (un Oops del kernel che continuerà a ripresentarsi fino al riavvio della macchina) deriva dall'aver rimosso il modulo smallsysctl, da un altra sessione, durante l'esecuzione di sleep 10, mentre il file ints era aperto.

Anche se almeno uno dei due utenti deve essere l'amministratore di sistema, risulta chiaro come occorra pensare a un meccanismo piu` affidabile.

struct kobject

La struttura kobject, definita in <linux/kobject.h>, può essere considerata una classe prototipale dalla quale si possono derivare altre classi che ne ereditano i metodi (usando la terminologia della programmazione orientata agli oggetti). Ogni istanza della classe è dotata di un nome, di un contatore dei riferimenti attivi, di una posizione tra gli altri oggetti del sistema in una struttura ad albero, di alcuni metodi prefefiniti e di un tipo (struct kobj_type). Il "tipo" serve a dichiarare il distruttore dell'oggetto, una serie di attributi e i metodi per leggere e scrivere tali attributi attravero sysfs

La struttura kobject viene comunemente usata come elemento di una struttura più articolata. Per esempio, il file di esmepio int.c, riprodotto in questo articolo, dichiara nelle prime righe una la struttura kint, una classe derivata da kobject, contenente tre numeri interi.

La riga successiva nel sorgente usa la macro container_of per recuperare il puntatore a kint a partire dal puntatore alla struttura kobject ivi contenuta. La macro, definita in <linux/kernel.h>, funziona correttamente anche quando la struttura interna non è il primo campo della struttura che la contiene (come rappresentato in figura 1).

Le funzioni int_init e int_exit usano i metodi predefiniti per l'inizializzazione dell'oggetto (init, set_name). Secondo le convenzioni del codice di Linux, un nuovo riferimento all'oggetto viene creato dal metodo get e viene rimosso dal metodo put; poiché il contatore dei riferimenti all'oggetto viene posto a 1 da kobject_init, nel codice appare solo kobject_put, nella funziona di uscita.

Il distruttore dell'oggetto, in questo caso la funzione kint_release, viene invocato solo quando l'oggetto non è più referenziato da altro codice; se l'oggetto è associato ad un file e questo file è aperto il contatore dei riferimenti non può essere zero, perche` ad ogni apertura di un file associato all'oggetto il kernel invoca il suo metodo get.

Gestire l'interazione tra i riferimenti a kobject e i riferimenti ad un modulo in corso di rimozione non è immediato; per una discussione del problema e del codice rimando agli articoli di Jonathan Corbet (si veda il riquadro 2). Nel modulo di esempio usato in questa sede, la procedura di uscita del modulo semplicemente aspetta che l'oggetto venga distrutto prima di ritornare, poiche` al suo ritorno il chiamante distruggera` il modulo. È una soluzione subottimale ma sufficiente per impedire l'errore visto in precedenza con sysctl.



Figura 1
Uso di container_of

La figura è anche disponibile in PostScript


Riquadro 1 - Il modulo int.c
   /* Copyright (c) 2004 Alessandro Rubini. GNU GPL 2 or later */
   #include <linux/config.h>
   #include <linux/module.h>
   #include <linux/init.h>
   #include <linux/kobject.h>
   #include <linux/sysfs.h>
   
   MODULE_LICENSE("GPL");
   
   /* the toplevel kint structure */
   struct kint {
	   struct kobject kobj;
	   int numbers[3]; /* min, val, max */
   };
   
   /* macros to extract the container structure from the generic one */
   #define to_kint(obj)      container_of(obj, struct kint, kobj)
   #define to_int_attr(attr) container_of(attr, struct kint_attribute, attr)
   
   /* Functions to read and write one signed integer in a range */
   ssize_t kint_one_show(int *what, char *buf)
   {
	   return sprintf(buf, "%i\n", *what);
   }
   ssize_t kint_one_store(int *what, const char *buf, size_t count)
   {
	   int result;
	   char s[4];
   
	   if (sscanf(buf, "%i %2s", &result, s) != 1)
		   return -EINVAL;
	   if (result < what[-1] || result > what[1])
		   return -EINVAL;
	   *what = result;
	   return strlen(buf);
   }
   
   /* This customized attribute allows to read min/max and read/write val */
   struct kint_attribute {
	   struct attribute attr;
	   int index;
	   ssize_t (*show)(int *, char *);
	   ssize_t (*store)(int *, const char *, size_t count);
   };
   
   struct kint_attribute kint_attr_min = {
	   .attr    = {.name = "min", .mode = S_IRUGO },
	   .index   = 0,
	   .show    = kint_one_show
   };
   struct kint_attribute kint_attr_val = {
	   .attr    = {.name = "val", .mode = S_IRUGO | S_IWUGO },
	   .index   = 1,
	   .show    = kint_one_show,
	   .store   = kint_one_store
   };
   struct kint_attribute kint_attr_max = {
	   .attr    = {.name = "max", .mode = S_IRUGO },
	   .index   = 2,
	   .show    = kint_one_show
   };
   
   struct attribute * kint_default_attrs[] = {
	   &kint_attr_min.attr,
	   &kint_attr_val.attr,
	   &kint_attr_max.attr,
	   NULL
   };
   
   
   /* top level show/store functions, called by sysfs */
   ssize_t kint_show(struct kobject * kobj, struct attribute * attr, char * buf)
   {
	   struct kint *ki = to_kint(kobj);
	   struct kint_attribute *ka = to_int_attr(attr);
	   ssize_t ret = 0;
   
	   if (ka->show)
		   ret = ka->show(&ki->numbers[ka->index], buf);
	   return ret;
   }
   ssize_t kint_store(struct kobject * kobj, struct attribute * attr, 
		  const char * buf, size_t count)
   {
	   struct kint *ki = to_kint(kobj);
	   struct kint_attribute *ka = to_int_attr(attr);
	   ssize_t ret = 0;
   
	   if (ka->store)
		   ret = ka->store(&ki->numbers[ka->index], buf, count);
	   return ret;
   }
   
   struct sysfs_ops kint_sysfs_ops = {
	   .show  = kint_show,
	   .store = kint_store,
   };
   
   /* our object, dynamically allocated */
   struct kint *kint;
   
   /* our release method */
   void kint_release(struct kobject *kobj)
   {
	   struct kint *ki = to_kint(kobj);
	   kfree(ki);
	   kint = NULL; /* no doubt it's this one */
   }
   
   /* the ktype is referenced by kobject */
   struct kobj_type ktype_int = {
	   .release       = kint_release,
	   .sysfs_ops     = &kint_sysfs_ops,
	   .default_attrs = kint_default_attrs,
   };
   
   /* module initialization and cleanup */
   int int_init(void)
   {
	   int i, ret;
   
	   kint = kmalloc(sizeof(*kint), GFP_KERNEL);
	   if (!kint) return -ENOMEM;
	   memset(kint, 0, sizeof(*kint));
	   kobject_init(&kint->kobj);
	   kint->numbers[0] = 0;
	   kint->numbers[1] = 1;
	   kint->numbers[2] = 5;
	   kint->kobj.ktype = &ktype_int;
   
	   ret = kobject_set_name(&kint->kobj, "sample-int-range");
	   if (!ret) ret = kobject_add(&kint->kobj);
	   if (!ret) {
		   for (i=0; i<3; i++)
			   sysfs_create_file(&kint->kobj,
					     kint_default_attrs[i]);
	   }
   
	   if (ret)
		   kfree(kint);
	   return ret;
   }
   
   void int_exit(void)
   {
	   int i;
   
	   for (i=0; i<3; i++)
		   sysfs_remove_file(&kint->kobj,
				     kint_default_attrs[i]);
	   kobject_del(&kint->kobj);
	   kobject_put(&kint->kobj);
	   /* wait untile the kobj is freed */
	   
	   while (kint) {
		   schedule_timeout(HZ/2);
	   }
	   return;
   }
   
   module_init(int_init);
   module_exit(int_exit);


struct attribute

Ogni tipo di oggetti ha un insieme predefinito di attributi, ai quali ogni istanza di oggetto può aggiungerne altri. Il modulo di esempio, che gestendo una sola istanza di oggetto, usa solo gli attributi predefiniti; il tipo ktype_int, definito in questo modulo, dichiara tre attributi, corrispondenti ai tre numeri interi che fanno parte della struttura kint.

Le strutture struct attribute vengono gestite come struct kobject: sono pensate per essere parte di una struttura più ampia e per questo definiscono solo un nome e una maschera di permessi di accesso, due parametri usati per instaziare l'attributo in sysfs. Nel programma di esempio la truttura più ampia che contiene struct attribute e` stata chiamata kint_attribute; essa definisce un campo index per identificare l'attributo nel vettore che fa parte di struct kint e i due metodi show e store.

I due metodi vengono usati per comunicare con lo spazio utente: show mostra il valore dell'attributo in un file, mentre store serve a memorizzare un nuovo valore dell'attributo quando l'utente scrive il file associato.

Il collegamento tra i metodi associati ai singoli attributi di un oggetto e il virtual file system sono le sysfs_ops dichiarate nella struttura kobj_type. L'utilizzo dei nomi show e store sia per le operazioni generiche sull'oggetto sia per quelle specifiche su ciascun attributo è la prassi per i programmatori del kernel, quindi l'esempio int.c segue la stessa convenzione.

sysfs

Appoggiandosi su questa infrastruttura di oggetti e attributi, sysfs esporta verso lo spazio utente l'albero degli oggetti e dei loro attributi sotto forma di file. Il filesystem implementa la sua struttura file_operations (introdotta da Daniele Bellucci il mese scorso nel riquadro degli approfondimenti) come ponte tra le chiamate di sistema effettuate dai processi e i metodi show e store implementati da ciascun oggetto.

Se sysfs non è ancora montato nell'albero dei file della macchina, si può montare, dopo aver creato la directory /sys, con il comando:

   mount -t sysfs none /sys

Si noti che a partire dalla versione 2.6.6 il codice relativo a sysfs può essere disabilitato al momento della configurazione del kernel, mentre nelle versioni precedenti era sempre presente nell'immagine compilata del sistema. Ha senso eliminare sysfs solo quando si lavora in ambienti limitati come i dispositivi embedded; chi avesse disabilitato sysfs nel proprio PC e` invitato a ricompilare il kernel.

La registrazione di un oggetto nell'albero dei file viene effettuata dalla funzione kobject_add, che int.c chiama nella procedura di inizializzazione; la rimozione è ottenuta tramite kobject_del.

Ogni oggetto viene rappresentato in sysfs come una directory; avrete probabilmente notato, infatti, come molte directory dell'albero non contengono file (per esempio, sulla mia macchina /sys/devices/system contiene dieci subdirectory e nessun file). La posizione nell'albero della directory relativa ad ogni oggetto dipende da come l'oggetto e` stato inserito nell'infrastruttura di sistema.

Poiche` nel modulo di esempio non abbiamo inserito l'oggetto nell'albero degli oggetti di sistema, la directory sample-int-range, (nome di kint->kobj), appare direttamente sotto la radice di /sys.

I file regolari che appaiono all'interno di sysfs rappresentano gli attributi, ma il sistema non esporta implicitamente tutti gli attributi degli oggetti che vengono registrati; per questo motivo int_init chiama la procedura sysfscreatefile una volta per ogni attributo. Quando si vuole rimuovere un attributo dal filessytem, la procedura da chiamare e` sysfs_remove_file. Per simmetria, il modulo int invoca sysfs_remove_file nella sua procedura di pulizia, anche se gli attributi vengono automaticamente rimossi quando kobject_del rimuove l'oggetto dal filesystem.

Gli attributi di int.c

I tre attributi esportati da int.c sono tre numeri interi, associati a tre file ASCII nella directory sample-int-range. I tre file rappresenntano valore minimo, valore corrente e valorme massimo di un parametro intero modificabile dall'utente. I file associati ai minimo e al massimo (chiamati min e max) sono in sola-lettura mentre il file relativo al valore corrente, (chiamato val) e` scrivibile. I permessi di accesso sono stati indicati tramite il campo mode di struct attribute.

La lettura di un attributo (metodo show) viene effettuata semplicemente con sprintf, per convertire in ASCII il valore binario del numero intero richiesto.

Il buffer passato al metodo show e` sempre di dimensione PAGE_SIZE, perche` sysfs, come tmpfs alloca le pagine dei file direttamente nella cosiddetta page cache. Una pagina e` solitamente 4kB o piu`, ma il valore dipende dalla piattaforma hardware su cui sta girando il sistema. Per sicurezza si consiglia di usare snprintf per riempire il buffer, ma nel nostro esempio sprintf e` accettabile perche` la rappresentazione ASCII di una variabile intera e` sicuramente inferiore ad una pagina.

La scrittura (metodo store) si appoggia su sscanf e confronta il valore passato con il minimo e il massimo; a tal fine l'implementazione di store sa che i due estremi dell'intervallo accettabile di valori sono consecutivi al valore corrente in memoria, all'interno in un vettore di interi.

Se un metodo ritorna il valore negativo -EINVAL, l'errore viene riportato al processo nello spazio utente. Questo succede, qualora il valore passato non ricade nell'intervallo accettabile oppure non e` la rappresentazione testuale di un numero intero (sono pero` consentiti caratteri di spazio prima e dopo il numero).

   burla$ grep . /sys/sample-int-range/*
   /sys/sample-int-range/max:5
   /sys/sample-int-range/min:0
   /sys/sample-int-range/val:1
   burla$ /bin/echo 2 > /sys/sample-int-range/val
   burla$ /bin/echo 20 > /sys/sample-int-range/val
   /bin/echo: write error: Invalid argument
   burla$ /bin/echo 3tigri > /sys/sample-int-range/val
   /bin/echo: write error: Invalid argument
   burla$ cat /sys/sample-int-range/val
   2


Riquadro 2 - Approfondimenti
Una discussione più approfondita di struct kobject, sysfs, e del modello di driver (driver model) si trova nei file della documentazione del kernel:

Documentation/kobject.txt descrive strutture kobject e la struttura in cui si inseriscono (struct kset e altro);

Documentation/filesystems/sysfs.txt descrive l'interfaccia di sysfs, gli attributi e i tipi di attributi predefiniti;

Documentation/driver-model descrive in vari file il driver model

La documentazione di Jonathan Corbet, http://lwn.net/Kernel/, è come sempre molto interessante e ben scritta.


Conclusioni

Questa introduzione a sysfs rimane molto alla superficie dell'infrastruttura a oggetti che permea la versione 2.6 del kernel, un'infrastruttura che permette tra l'altro rimpiazzare devfs nello spazio utente (come gia` riportato da Daniele Bellucci il mese scorso), come pure di gestire l'inserimento e la rimozione a caldo delle periferiche. Una trattazione approfondita dovrebbe toccare molte altre strutture dati ed estendersi su molte piu` pagine, diventando sicuramente tediosa. Invito chi e` interessato ad approfondire ad usare i riferimenti riportati nel riquadro 2; riferimenti che sono per l'appunto molto piu` ampi di questo articolo.