Laborator 12 - Linux Device Model. Plug and Play

Obiectivele laboratorului

  • Dobândirea de cunoștințe legate de Linux Device Model și principalele componente ale acestuia: dispozitive, drivere, clase și magistrale.
  • Cunoștințe legate de modul de funcționare al mecanismelor Hotplug / Plug and Play.

Cuvinte cheie

  • Magistrala
  • Device
  • Driver
  • Kobject
  • Hotplug
  • Plug and Play
  • Udev

Materiale ajutătoare

Noțiuni teoretice

Plug and Play este o tehnologie care oferă suport pentru adăugarea și eliminarea automată a dispozitivelor în sistem. În acest mod se reduc conflictele legate de resursele folosite de acestea prin configurare automată la pornirea sistemului. Pentru a îndeplini aceste scopuri, sunt necesare următoarele caracteristici:

  • Detectarea automată a adaugării și eliminării dispozitivelor din sistem (dispozitivul și magistrala acestuia trebuie sa anunțe driver-ul corespunzător că a apărut o modificare în configurație).
  • Managementul resurselor (adrese, linii irq, canale DMA, zone de memorie), ce include alocarea resurselor către dispozitive și rezolvarea conflictelor ce pot apărea.
  • Dispozitivele trebuie să permită configurarea software (resursele dispozitivului – porturi, întreruperi, resurse DMA – trebuie să permită atribuirea din partea driver-ului).
  • Driver-ele necesare pentru dispozitive noi trebuie să fie încarcate automat de sistemul de operare atunci când este nevoie.
  • Atunci când dispozitivul și magistrala acestuia permit, sistemul ar trebui să poată adăuga sau elimina dispozitivul din sistem în timp ce acesta rulează, fără a fi necesară repornirea sistemului (hotplug).

Pentru ca un sistem să suporte plug and play trebuie ca BIOS-ul, sistemul de operare și dispozitivul să suporte această tehnologie. Dispozitivul trebuie să dețină un ID pe care să îl ofere driver-ului pentru identificare, iar sistemul de operare trebuie să poată să identifice aceste modificări în configurație pe măsură ce ele apar.

Dispozitive care suportă plug and play sunt: dispozitivele PCI (plăci de rețea), USB (tastatură, mouse, imprimantă), etc.

Linux Device Model

Înainte de versiunea 2.6, kernel-ul nu dispunea de un model unificat prin care să se obțină informații despre acesta. Din acest motiv s-a realizat un model pentru dispozitivele din Linux, Linux Device Model.

Scopul principal al acestui model este de a menține structuri de date interne care să reflecte starea și structura sistemului. Astfel de infomații includ ce dispozitive există în sistem, în ce stare se află din punct de vedere al managementului consumului (power management), la ce magistrală sunt atașate, ce drivere au asociate, alături de structura magistralelor, dispozitivelor, driverelor din sistem.

Pentru a menține aceste informații, kernel-ul folosește următoarele entități:

  • dispozitiv - un dispozitiv fizic care este atașat unei magistrale
  • driver – o entitate software care poate fi asociată unui dispozitiv și execută operații cu acesta
  • magistrală (bus) – un dispozitiv la care se pot atașa alte dispozitive
  • clasă – un tip de dispozitive care au o comportare similară; există o clasă pentru discuri, partiții, porturi seriale, etc.
  • subsistem – o vedere asupra structurii sistemului; subsistemele din kernel includ dispozitive (devices - o vedere ierarhică asupra tuturor dispozitivelor din sistem), magistrale (bus - o vedere a dispozitivelor în funcție de cum sunt atașate la magistrale), clase, etc.

sysfs

Kernel-ul oferă o reprezentare a modelului său în userspace prin intermediul sistemului virtual de fișiere sysfs. Acesta este de obicei montat în directorul /sys și conține următoarele subdirectoare:

  • block - toate dispozitivele de tip bloc disponibile în sistem (discuri, partiții)
  • bus - tipuri de magistrale la care se conectează dispozitivele fizice (pci, ide, usb)
  • class - clase de drivere care sunt disponibile în sistem (net, sound, usb)
  • devices - structura ierarhica a dispozitivelor conectate în sistem
  • firmware - informații obținute de la firmware-ul sistemului (ACPI)
  • fs - informații legate de sistemele de fișiere montate
  • kernel - informații legate de starea kernel-ului (utilizatorii logați, hotplug)
  • module - lista modulelor încarcate la momentul curent
  • power - informații legate de subsistemul de power management

După cum se poate observa, există o corespondență între structurile de date din kernel în cadrul modelului descris și subdirectoarele din sistemul virtual de fișiere sysfs. Deși această asemănare poate duce la confundarea celor două concepte, ele sunt diferite. Modelul pentru dispozitive în kernel poate funcționa și fără sistemul de fișiere sysfs, dar reciproca nu este adevărată.

Informația din sysfs se găsește în fișiere ce conțin câte un atribut. Câteva atribute standard (reprezentate de fișiere sau directoare cu același nume) sunt următoarele:

  • dev - identificatorul major și minor al dispozitivului; acesta poate fi folosit pentru a crea automat intrările în directorul /dev
  • device - o legătură simbolică spre directorul ce conține dispozitive; acesta poate fi folosit pentru a descoperi dispozitivele hardware care oferă un anumit serviciu (spre exemplu dispozitivul PCI al plăcii de rețea eth0)
  • driver - o legătură simbolică spre directorul ce conține drivere (care se află în /sys/bus/*/drivers)

Sunt disponibile și alte atribute, în funcție de magistrala și driverul folosit.

Structuri de bază în dispozitivele Linux

Linux Device Model oferă o serie de structuri care să asigure interacțiunea între un dispozitiv hardware și un device driver. Întreg modelul are la bază structuri struct kobject. Cu ajutorul acestei structuri sunt construite ierarhii și sunt implementate următoarele structuri: struct bus_type, struct device și struct device_driver.

Structura kobject

O structură kobject nu îndeplinește o funcție de una singură. O astfel de structură este de obicei integrată într-o structură mai mare. O structură kobject incorporează de fapt un set de facilități care vor fi oferite unui obiect de nivel de abstracție mai înalt în ierarhia Linux Device Model.

Spre exemplu, structura cdev are următoarea definiție:

struct cdev {
        struct kobject kobj;
        struct module *owner;
        const struct file_operations *ops;
        struct list_head list;
        dev_t dev;
        unsigned int count;
};

Se observă că această structură include un câmp de tipul struct kobject.

O structură struct kobject este definită astfel:

struct kobject {
        const char              *name;
        struct list_head        entry;
        struct kobject          *parent;
        struct kset             *kset;
        struct kobj_type        *ktype;
        struct sysfs_dirent     *sd;
        struct kref             kref;
        unsigned int state_initialized:1;
        unsigned int state_in_sysfs:1;
        unsigned int state_add_uevent_sent:1;
        unsigned int state_remove_uevent_sent:1;
        unsigned int uevent_suppress:1;
};

După cum se observă, structurile kobject sunt într-o ierarhie: un obiect are un părinte și deține un membru kset, care cuprinde obiecte de pe același nivel.

Lucrul cu structura presupune inițializarea ei, cu ajutorul funcției kobject_init. Tot în procesul de inițializare este necesară stabilirea numelui structurii kobject, care va apărea în sysfs, cu ajutorul funcției kobject_set_name.

Orice operație asupra unui kobject se realizează prin incrementarea contorului intern al acestuia cu kobject_get, sau decrementarea în cazul în care nu se mai folosește cu kobject_put. Astfel, un obiect de tip kobject nu va fi eliberat decât în momentul în care contorul intern al acestuia ajunge la valoarea 0. Este nevoie de o metodă de notificare a acestui lucru, pentru ca, apoi, să fie eliberate resursele asociate structurii de dispozitiv care includea struct kobject (spre exemplu cdev). Metoda se numește release și este asociată obiectului prin intermediul câmpului ktype (de tip struct kobj_type).

Structura struct kobject este structura de bază din Linux Device Model. Structurile din nivelurile mai înalte ale modelului sunt struct bus_type, struct device și struct device_driver.

Magistrale

O magistrală este un canal de comunicație între procesor și un dispozitiv de intrare/ieșire. Pentru asigurarea genericității modelului, toate dispozitivele de intrare/ieșire sunt conectate la procesor prin intermediul unei astfel de magistrale (chiar dacă aceasta poate fi una virtuală fără un corespondent fizic hardware).

La adăugarea unei magistrale în sistem, aceasta va apărea în sistemul de fișiere sysfs, în /sys/bus. Ca și la structurile kobjects, magistralele pot fi organizate în ierarhii și vor fi reprezentate astfel și în sysfs.

În Linux Device Model, o magistrală este reprezentată de structura struct bus_type:

struct bus_type {
        const char              *name;
        const char              *dev_name;
        struct device           *dev_root;
        struct bus_attribute    *bus_attrs;
        struct device_attribute *dev_attrs;
        struct driver_attribute *drv_attrs;
        struct subsys_private *p;
 
        int             (*match)(struct device * dev, struct device_driver * drv);
        int             (*uevent)(struct device *dev, struct kobj_uevent_env *env);
        int             (*probe)(struct device * dev);
        int             (*remove)(struct device * dev);
        //...
};

Se observă că unei magistrale îi este asociat un nume, liste de atribute default, o serie de funcții specifice și datele private ale driverului. Funcția uevent (fostă hotplug), este folosită în cazul dispozitivelor hotplug.

Operațiile cu magistrala sunt operații de înregistrare, implementarea operațiilor descrise în structura struct bus_type și operații de parcurgere și inspecție a dispozitivelor conectate la magistrală.

Înregistrarea unei magistrale se realizează folosind bus_register, iar deînregistrarea folosind bus_unregister:

Afișați exemplu implementare

Ascundeți exemplu implementare

#include <linux/device.h>
/* mybus.c */
 
//bus type
struct bus_type my_bus_type = {
	.name	= "mybus",
	.match	= my_match,
	.uevent	= my_uevent,
};
 
static int __init my_bus_init (void)
{
	int err;
 
	//...
	err = bus_register (&my_bus_type);
	if (err)
		return err;
	//...
}
 
static void __exit my_bus_exit (void)
{
	//...
	bus_unregister (&my_bus_type);
	//...
}


Funcțiile care vor fi inițializate uzual în cadrul unei structuri struct bus_type sunt match și uevent:

#include <linux/device.h>
#include <linux/string.h>
/* mybus.c */
 
// match devices to drivers; just do a simple name test 
static int my_match (struct device *dev, struct device_driver *driver)
{
	return !strncmp (dev_name(dev), driver->name, strlen (driver->name));
}
 
// respond to hotplug user events; add environment variable DEV_NAME
 static int my_uevent (struct device *dev, struct kobj_uevent_env *env)
{
	add_uevent_var(env, "DEV_NAME=%s", dev_name(dev));
	return 0;
}

Funcția match este utilizată în momentul în care se adaugă la magistrală un nou dispozitiv sau un nou driver. Rolul ei este de a realiza comparația între identificatorul dispozitivului și cel al driverului. Funcția uevent este apelată înainte de generarea unui eveniment de tip hotplug în user-space și are drept rol adăugarea de variabile de mediu.

Alte operații posibile în cazul unei magistrale sunt parcurgerea driverelor sau dispozitivelor atașate acesteia. Deși nu le putem accesa în mod direct (listele cu driverele și dispozitive fiind stocate în datele private ale driverului, câmpul struct subsys_private *p), acestea pot fi parcurse cu ajutorul macrodefinițiilor bus_for_each_dev și bus_for_each_drv.

Interfața Linux Device Model permite crearea de atribute pentru obiectele asociate. Aceste atribute vor avea drept corespondent un fișier în subdirectorul magistralei din sysfs. Atributele asociate unei magistrale sunt descrise de structura bus_attribute:

struct bus_attribute {
         struct attribute        attr;
         ssize_t (*show)(struct bus_type *, char * buf);
         ssize_t (*store)(struct bus_type *, const char * buf, size_t count);
};

De obicei, un atribut se definește cu ajutorul macrodefiniției BUS_ATTR. Pentru adăugarea/ștergerea unui atribut din cadrul structurii de magistrală se folosesc funcțiile bus_create_file și bus_remove_file.

Un exemplu de definire a unui atribut pentru magistrala my_bus este prezentat în continuare:

Afișați exemplu de definire a unui atribut

Ascundeți exemplu de definire a unui atribut

/* mybus.c */
 
#define MY_BUS_DESCR     "SO2 rules forever"
 
// export a simple bus attribute
static ssize_t my_show_bus_descr (struct bus_type *bus, char *buf)
{
        return snprintf (buf, PAGE_SIZE, "%s\n", MY_BUS_DESCR);
}
 
/*
 * define attribute - attribute name is descr;
 * full name is bus_attr_descr;
 * sysfs entry should be /sys/bus/mybus/descr
 */
BUS_ATTR (descr, 0444, my_show_bus_descr , NULL);
 
// specify attribute - in module init function
static int __init my_bus_init (void)
{
        int err;
        //...
        err = bus_create_file (&my_bus_type, &bus_attr_descr);
        if(err) {
                /* handle error */
        }
        //...
}
 
static void __exit my_bus_exit (void)
{
        //...
        bus_remove_file (&my_bus_type, &bus_attr_descr);
        //...
}


Magistrala este reprezentată atât de un obiect bus_type, cât și de un obiect device, așa cum vom vedea în continuare (magistrala este și ea un dispozitiv).

Dispozitive

Orice dispozitiv din sistem are asociată o structură struct device. Dispozitivele sunt descoperite prin diferite metode de kernel (hotplug, de către device drivere, la inițializarea sistemului) și sunt înregistrate în sistem. Toate dispozitivele prezente în kernel au câte o intrare în /sys/devices.

La cel mai jos nivel, un dispozitiv în Linux Device Model este reprezentat de o structură struct device:

struct device {
        //...
        struct device           *parent;
        struct device_private   *p;
        struct kobject          kobj;
 
        const char              *init_name; /* initial name of the device */
        //...
        struct bus_type         *bus;       /* type of bus device is on */
        struct device_driver    *driver;    /* which driver has allocated this
                                             device */
        //...
        void    (*release)(struct device * dev);
};

Printre câmpurile structurii se numără dispozitivul părinte care este de obicei un controller, obiectul kobject asociat, magistrala pe care se găsește, driverul care se ocupă de dispozitiv și o funcție apelată în momentul în care contorul dispozitivului ajunge la 0.

Ca de obicei, avem funcții de înregistrare/deînregistrare device_register și device_unregister.

Pentru lucrul cu atributele avem structura struct device_attribute, macrodefiniția DEVICE_ATTR pentru definire și funcțiile device_create_file și device_remove_file pentru adăugarea atributului la dispozitiv.

Un lucru important de notat este faptul că, de obicei, nu se lucrează direct cu o structură de tip struct device, ci cu o structură care o conține pe aceasta, de forma:

// my device type
struct my_device {
	char *name;
	struct my_driver *driver;
	struct device dev;
};

De obicei, un modul va exporta funcții de înregistrare/deînregistrare ale unui astfel de dispozitiv, după modelul prezentat mai jos:

Afișați cod

Ascundeți cod

/* mybus.c */
 
/* BUS DEVICE (parent) */
 
// parent device release
static void my_bus_device_release (struct device *dev)
{
}
 
// parent device
static struct device my_bus_device = {
	.init_name   = "mybus0",
	.release  = my_bus_device_release
};
 
/* DEVICE */
 
/*
 * as we are not using the reference count, we use a no-op
 * release function
 */
static void my_dev_release (struct device *dev)
{
}
 
int my_register_device (struct my_device *mydev)
{
	mydev->dev.bus = &my_bus_type;
	mydev->dev.parent = &my_bus_device;
	mydev->dev.release = my_dev_release;
	dev_set_name(&mydev->dev, mydev->name);
 
	return device_register (&mydev->dev);
}
 
void my_unregister_device (struct my_device * mydev)
{
	device_unregister (&mydev->dev);
}
 
/* export register/unregister device functions */
EXPORT_SYMBOL (my_register_device);
EXPORT_SYMBOL (my_unregister_device);


După cum se poate observa, funcțiile my_register_device și my_unregister_device de adăugare, respectiv scoatere a unui device de pe o magistrală sunt definite în fișierul în care este definită magistrala. Nu se inițializează obiecte de tip dispozitiv; acestea se vor ințializa în momentul în care vor fi descoperite în sistem (prin hotplug sau înregistrare directă din driver) și se va apela funcția my_register_device pentru adăugarea la magistrală.

Pentru utilizare (în implementarea driver-ului), trebuie declarată o structură de tipul celei exportate, inițializată și înregistrată cu metoda exportata de magistrală:

Afișați exemplu înregistrare device

Ascundeți exemplu înregistrare device

/* mydriver.c */
 
static struct my_device mydev;
char devname[NAME_SIZE];
//...
 
//register
int err;
 
sprintf(devname, "mydev0");
mydev.name = devname;
mydev.driver = &mydriver;
dev_set_drvdata(&mydev.dev, &mydev);
err = my_register_device(&mydev);
if (err < 0) {
	/*handle error */
}
 
//..
 
//unregister
my_unregister_device(&mydev);


Drivere

Linux Device Model este folosit pentru a permite asocierea foarte ușoară între dispozitivele sistemului și drivere. Driverele pot exporta informații independente de dispozitivul fizic din spate.

În sysfs informațiile despre drivere nu au asociate un singur subdirector; ele se pot găsi în structura de directoare, în diferite locuri: în /sys/module se găsește modulul încărcat, în devices se poate găsi driverul asociat fiecărui device, în classes driverele care aparțin unei clase, în /sys/bus driverele asociate fiecărei magistrale.

Un device driver este identificat prin structura struct device_driver:

struct device_driver {
         const char              *name;
         struct bus_type         *bus;
 
         struct driver_private   *p;
 
         struct module           *owner;
         const char              *mod_name;     /* used for built-in modules */
 
         int     (*probe)        (struct device * dev);
         int     (*remove)       (struct device * dev);
         void    (*shutdown)     (struct device * dev);
         int     (*suspend)      (struct device * dev, pm_message_t state);
         int     (*resume)       (struct device * dev);
};

Printre câmpurile structurii regăsim numele driver-ului (apare în sysfs), magistrala cu care driverul lucrează și funcții apelate în diverse momente din funcționarea unui dispozitiv.

Ca și până acum, avem funcțiile de înregistrare/deînregistrare driver_register și driver_unregister.

Pentru lucrul cu atributele avem structura struct driver_attribute, macrodefiniția DRIVER_ATTR pentru definire și funcțiile driver_create_file și driver_remove_file pentru adăugarea atributului la dispozitiv.

Ca și în cazul dispozitivelor, structura struct device_driver este, de obicei, incorporată în cadrul unei alte structuri specifice unei anumite magistrale (PCI, USB, etc.):

Afișați cod

Ascundeți cod

/* mybus.c */
 
// my driver type
 struct my_driver {
	struct module *module;
	struct device_driver driver;
};
 
#define to_my_driver(drv) container_of(drv, struct my_driver, driver);
 
int my_register_driver (struct my_driver *driver)
{
	int err;
 
	driver->driver.bus = &my_bus_type;
	err= driver_register (&driver->driver);
	if (err)
		return err;
	return 0;
}
 
void my_unregister_driver (struct my_driver *driver)
{
	driver_unregister (&driver->driver);
}
 
/* export register/unregister driver functions */
EXPORT_SYMBOL (my_register_driver);
EXPORT_SYMBOL (my_unregister_driver);


Se observă că se exportă operațiile de înregistrare/deînregistrare de driver pentru folosire în cadrul altor module.

Ca și în cazul dispozitivelor, și operațiile cu drivere sunt definite la inițializarea magistralei, și sunt exportate pentru a putea fi folosite de drivere. În momentul în care se implementează un driver ce lucrează cu dispozitive atașate la această magistrală, acesta va apela funcțiile my_register_driver și my_unregister_driver pentru a se asocia cu aceasta.

Pentru utilizare (în implementarea driver-ului), trebuie declarată o structură de tipul celei exportate, inițializată și înregistrată cu metoda exportata de magistrală:

/* mydriver.c */
 
static struct my_driver mydriver = {
	.module = THIS_MODULE,
	.driver = {
		.name = "mydriver",
	},
};
//...
 
//register
int err;
err = my_register_driver(&mydriver);
if (err < 0) {
	/*handle error */
}
//..
 
//unregister
my_unregister_driver(&mydriver);

Clase

O clasă este o vedere de nivel înalt asupra Linux Device Model, care abstractizează detaliile de implementare. Spre exemplu, există drivere pentru discurile SCSI și drivere pentru discurile ATA, dar toate aparțin clasei discuri. Clasele asigură o grupare a dispozitivelor bazată pe funcționalitate, nu pe modul în care sunt conectate sau cum funcționează. Clasele au corespondent în /sys/classes.

Există doua structuri principale care descriu clasele: struct class și struct device. Structura class descrie o clasa generică, în timp ce structura struct device descrie o clasă asociată unui dispozitiv. Există funcții pentru inițializare/deinițializare și adăugare de atribute pentru fiecare dintre acestea, descrise în include/linux/device.h.

Avantajul folosirii claselor este că programul udev din userspace, despre care vom discuta în continuare, permite crearea automată a dispozitivelor în directorul /dev pe baza informațiilor clasei.

Din acest motiv, vom prezenta în continuare un set restrâns de funcții care lucrează cu clasele pentru a simplifica folosirea mecanismului de plug and play.

O clasă generică este descrisă de structura struct class:

struct class {
         const char              *name;
         struct module           *owner;
         struct kobject          *dev_kobj;
 
         struct subsys_private   *p;
 
         struct class_attribute          *class_attrs;
         struct class_device_attribute   *class_dev_attrs;
         struct device_attribute         *dev_attrs;
 
         int     (*dev_uevent)(struct device *dev, struct kobj_uevent_env *env);
         void    (*class_release)(struct class *class);
         void    (*dev_release)(struct device *dev);
         //...
};

Pentru inițializare/deinițializare există funcțiile class_register și class_unregister:

Afișați cod

Ascundeți cod

static struct class my_class = {
        .name = "myclass",
};
 
static int __init my_init(void)
{
        int err;
        //...
        err = class_register(&my_class);
        if (err < 0) {
                /* handle error */
        }
        //...
}
 
static void __exit my_cleanup (void) 
{
        //...
        class_unregister(&my_class);
        //...
}


O clasă asociată unui dispozitiv este descrisă de structura device. Pentru inițializare/deinițializare există funcțiile device_create și device_destroy. Funcția device_create inițializează stuctura device, îi asociază structura generică class și dispozitivul primit ca parametru; în plus, va crea un atribut al clasei, dev, care conține minorul și majorul dispozitivului (minor:major). Astfel, utilitarul udev din usermode poate citi datele necesare din acest fișier atribut pentru a crea un nod în directorul /dev apelând makenod.

Un exemplu de inițializare:

struct device* my_classdev;
struct cdev cdev;
struct device dev;
 
//init class for device cdev.dev
my_classdev = device_create(&my_class, NULL, cdev.dev, &dev, "myclass0");
 
//destroy class for device cdev.dev
device_destroy(&my_class, cdev.dev);

În momentul în care un nou dispozitiv va fi descoperit, i se va asocia o clasă și un nod în directorul /dev. Pentru exemplul de mai sus, se va genera un nod /dev/myclass0.

Hotplug

Hotplug descrie mecanismul de adăugare sau eliminare a unui dispozitiv din sistem în timp ce acesta rulează, fără a fi necesară repornirea sistemului.

Un eveniment hotplug este o notificare din partea kernel-ului către user-space atunci când se modifică ceva în configurația sistemului. Aceste evenimente sunt generate la crearea sau eliminarea unui obiect kobject din kernel. Întrucât aceste obiecte stau la baza Linux Device Model, fiind incluse în toate structurile ( struct bus_type, struct device, struct device_driver, struct class, etc.), se va genera un eveniment hotplug la crearea sau eliminarea oricăreia dintre aceste structuri (uevent). În momentul în care un dispozitiv este descoperit în sistem, se generează un eveniment. În funcție de punctul în care se află în Linux Device Model, se vor apela funcțiile corespunzătoare apariției unui eveniment (de obicei, este cazul funcției uevent a magistralei sau clasei). Driverul are posibilitatea, prin intermediul acestor funcții, să seteze variabile de sistem pentru user-space. Evenimentul generat ajunge în user-space apoi. Aici există utilitarul udev, care capturează aceste evenimente. În directorul /etc/udev/ există fișierele de configurare pentru acest utilitar. Se pot specifica diferite reguli pentru a captura numai anumite evenimente și a executa anumite acțiuni, în funcție de variabilele de sistem setate din kernel sau în funcțiile uevent.

O consecință importantă este că în acest mod se poate realiza mecanismul plug and play; cu ajutorul lui udev si a claselor, descrise anterior, se pot crea automat intrările în directorul /dev pentru dispozitive, iar cu ajutorul udev se pot încărca automat driverele necesare pentru un dispozitiv. În acest fel, întreg procesul este automatizat.

Regulile pentru udev sunt localizate în /etc/udev/rules.d. Orice fișier de aici care se termină cu .conf va fi parsat la apariția unui eveniment. Pentru mai multe detalii despre cum se scriu regulile în aceste fișiere consultați Writing udev rules. Pentru testare, există utilitarele udevmonitor, udevinfo și udevtest.

Pentru un scurt exemplu, sa consideram situația în care dorim sa încarcăm automat un driver pentru un dispozitiv în momentul apariției unui eveniment. Putem crea un nou fișier /etc/udev/rules.d/myrules.rules, în care vom avea următoarea linie:

SUBSYSTEM=="pnp", ATTRS{id}=="PNP0400", RUN+="/sbin/insmod /root/mydriver.ko"

Astfel, se vor alege dintre evenimentele generate doar cele care aparțin subsistemului pnp (sunt conectate la magistrala PNP) și au un atribut id cu valoarea PNP0400. În momentul în care se va găsi această regulă, se va executa comanda ce inserează driverul corespunzător în kernel.

Plug and Play

După cum s-a specificat mai sus, în Linux Device Model toate dispozitivele sunt conectate printr-o magistrală, chiar dacă are corespondent fizic hardware sau este virtuală.

În kernel, există deja implementate majoritatea magistralelor prin definirea unei structuri bus_type și a funcțiilor de înregistrare/deînregistrare a driverelor și dispozitivelor asociate. Pentru implementarea unui driver trebuie determinată magistrala la care se atașează dispozitivele suportate și trebuie folosite funcțiile și structurile acesteia. Principalele magistrale sunt PCI, USB, PNP, IDE, SCSI, platform, ACPI, etc.

Magistrala PNP

Mecanismul plug and play oferă un mijloc de detectarea și setare a resurselor pentru drivere legacy sau care nu pot fi configurate în alt mod. Toate driverele plug and play, protocoalele, serviciile au la bază nivelul Plug and Play. Acesta este responsabil cu schimbul de informație între drivere și protocoale. Următoarele protocoale sunt disponibile:

  • PNPBIOS - folosite pentru sisteme cum ar fi porturile seriale și paralele
  • ISAPNP - oferă suport pentru magistrala ISA
  • ACPI - care oferă, printre altele, informații despre dispozitivele la nivel de sistem

În kernel există o magistrala pnp_bus, care este folosită pentru conectare de multe drivere. Implementarea și modul de lucru cu această magistrală respectă modelul Linux Device Model și este foarte asemănatoare cu ce s-a prezentat până acum.

Principalele funcții și structuri exportate de această magistrală, și care pot fi folosite de către drivere sunt:

După cum am observat în secțiunile anterioare, magistrala are o funcție match cu ajutorul căreia asociaza dispozitive cu driverele corespunzătoare. Spre exemplu, în cazul descoperirii unui dispozitiv, se va căuta driver-ul care îndeplinește condiția dată de această funcție relativă la dispozitiv. De obicei, această condiție reprezintă o comparație de id-uri ale driverului și dispozitivului. Un mecanism răspândit este folosirea unei tabele statice în fiecare driver, ce conține informații despre dispozitivele suportate de driver și vor fi folosite de magistrală la comparație. Spre exemplu, pentru un driver de port paralel vom avea tabela parport_pc_pnp_tbl:

static const struct pnp_device_id parport_pc_pnp_tbl[] = {
         /* Standard LPT Printer Port */
         {.id = "PNP0400", .driver_data = 0},
         /* ECP Printer Port */
         {.id = "PNP0401", .driver_data = 0},
};
 
MODULE_DEVICE_TABLE(pnp,parport_pc_pnp_tbl);

Se declară și inițializează o structură pnp_driver, cum ar fi parport_pc_pnp_driver:

static int parport_pc_pnp_probe (struct pnp_dev * dev, const struct pnp_id *card_id, 
                                                      const struct pnp_id *dev_id);
static void parport_pc_pnp_remove(struct pnp_dev * dev);
 
static struct pnp_driver parport_pc_pnp_driver = {
         .name           = "parport_pc",
         .id_table       = parport_pc_pnp_tbl,
         .probe          = parport_pc_pnp_probe,
         .remove         = parport_pc_pnp_remove,
};

După cum se poate observa, structura are ca parametri un pointer către tabela declarată mai sus două funcții care se apelează la detectarea unui dispozitiv, respectiv la eliminarea lui din sistem. La fel ca toate structurile prezentate, driver-ul trebuie înregistrat în sistem:

static int __init parport_pc_init(void)
{
        err = pnp_register_driver (&parport_pc_pnp_driver);
        if (err < 0) {
                /* handle error */
        }
}
 
static void __exit parport_pc_exit(void)
{
        pnp_unregister_driver (&parport_pc_pnp_driver);
}

Operații PNP

Până acum am discutat despre modelul Linux Device Model și API-ul folosit. Pentru a implementa un driver plug and play, trebuie respectat modelul Linux Device Model.

De cele mai multe ori, adăugarea unei magistrale în kernel nu este necesară (bus), întrucât deja sunt implementate majoritatea magistralelor (PCI, USB, etc.). Astfel, mai întâi trebuie identificată magistrala la care se atașează dispozitivul. În exemplele de mai jos, vom considera că această magistrală este magistrala PNP. Astfel, se vor folosi structurile și funcțiile prezentate mai sus.

Adăugare driver

Pe lângă operațiile uzuale, un driver trebuie să respecte modelul Linux Device Model. Astfel, se va înregistra în sistem folosind funcțiile puse la dispoziție de magistrală în acest scop. De obicei, magistrala pune la dispoziție o structură particulară de driver, ce conține o structură device_driver, pe care driverul trebuie să o inițializeze și să o înregistreze cu o funcție *_register_driver. Spre exemplu, pentru magistrala PNP, driverul trebuie să declare și să inițializeze o structură de tipul pnp_driver, pe care să o înregistreze cu pnp_register_driver:

static struct pnp_driver my_pnp_driver = {
         .name           = "mydriver",
         .id_table       = my_pnp_tbl,
         .probe          = my_pnp_probe,
         .remove         = my_pnp_remove,
};
 
static int __init my_init(void)
{
        err = pnp_register_driver (&my_pnp_driver);
}

Spre deosebire de driverele legacy, driverele plug and play nu înregistrează dispozitivele la inițializare, în funcția my_init (register_device). După cum s-a descris mai sus, fiecare magistrală are o funcție match care se apelează la detectarea unui dispozitv pentru a determina driverul asociat acestuia. Prin urmare, trebuie să existe o modalitate ca fiecare driver să exporte informații despre ce dispozitive suportă, pentru a putea trece de această comparație și pentru a fi apelate funcțiile sale. În exemplele prezentate în laborator, se face o simplă comparație între numele dispozitivului și numele driver-ului. Cele mai multe drivere folosesc o tabelă cu informații despre dispozitiv, pentru care au un pointer în structura driver-ului. Spre exemplu, un driver asociat cu o magistrală PNP, declară o tabelă de tipul pnp_device_id, și inițializează câmpul id_table din structura pnp_driver cu un pointer către aceasta:

static const struct pnp_device_id my_pnp_tbl[] = {
         /* Standard LPT Printer Port */
         {.id = "PNP0400", .driver_data = 0},
         /* ECP Printer Port */
         {.id = "PNP0401", .driver_data = 0},
         { }
};
 
MODULE_DEVICE_TABLE(pnp,my_pnp_tbl);
 
static struct pnp_driver my_pnp_driver = {
         //...
         .id_table       = my_pnp_tbl,
         //...
};

În exemplul de mai sus driver-ul suportă operații pentru portul paralel. Aceaste informații sunt folosite de magistrală, în funcția match_device. La adăugarea unui driver, se va asocia driverul magistralei și se vor crea intrări în sysfs bazate pe numele driver-ului. Apoi se va apela funcția match a magistralei pentru toate dispozitivele asociate, pentru a asocia driver-ul cu orice dispozitiv conectat pe care îl suportă.

Eliminare driver

Pentru a elimina un driver din kernel, pe lângă operațiile necesare unui driver legacy, trebuie deînregistrată structura device_driver. În cazul unui driver pentru un dispozitiv asociat magistralei PNP, trebuie deînregistrată structura pnp_driver cu ajutorul funcției pnp_unregister_driver:

static struct pnp_driver my_pnp_driver;
 
static void __exit my_exit(void)
{
        pnp_unregister_driver (&my_pnp_driver);
}

Spre deosebire de driverele legacy, driverele plug and play nu deînregistrează dispozitivele la deînregistrarea driver-ului, în funcția my_exit (unregister_device). La eliminarea unui driver, se vor elimina toate referințele către el pentru toate dispozitivele pe care le suportă și se vor șterge intrările din sysfs.

Adăugare dispozitiv

Dupa cum am văzut mai sus, driverele plug and play nu înregistrează dispozitivele la inițializare. Această operație se va realiza în funcția probe, care se va apela la detectarea unui dispozitiv. În cazul unui driver pentru un dispozitiv atașat la magistrala PNP, adăugarea se va realiza în funcția probe din structura pnp_driver:

static int my_pnp_probe (struct pnp_dev * dev, const struct pnp_id *card_id, 
                                                      const struct pnp_id *dev_id) {
        int err, iobase, nr_ports, irq;	
 
        //get irq & ports
        if (pnp_irq_valid(dev, 0))    
                irq = pnp_irq(dev, 0);
        if (pnp_port_valid(dev, 0)) {
                iobase = pnp_port_start(dev, 0);
        } else
                return -ENODEV;
        nr_ports = pnp_port_len(dev, 0);
 
        /* register device dev */
}
 
static struct pnp_driver my_pnp_driver = {
         //...
         .probe          = my_pnp_probe,
         //...
};

La detectarea unui dispozitiv în kernel (în procesul de boot sau la adăugarea dispozitivului prin hotplug), se transmite o întrerupere în sistem care ajunge la magistrală. Dispozitivul este înregistrat cu ajutorul funcției device_register și este atașat magistralei (și se va genera un apel în userspace, care poate fi detectat de udev). Apoi se va parcurge lista de drivere a magistralei și se va apela funcția match pentru fiecare dintre ele. Funcția match încearcă să asocieze un driver cu un dispozitiv. După ce a fost determinat driverul asociat dispozitivului, se va apela funcția probe a driver-ului. Dacă funcția se termină cu succes, dispozitivul este adăugat în lista de dispozitive a driver-ului și se creează intrările corespunzătoare în sysfs bazate pe numele dispozitivului.

Eliminare dispozitiv

Dupa cum am văzut mai sus, driverele plug and play nu deînregistrează dispozitivele la deînregistrarea driver-ului. Această operație se va realiza în funcția remove, care se va apela la detectarea eliminării unui dispozitiv din kernel. În cazul unui driver pentru un dispozitiv atașat la magistrala PNP, adăugarea se va realiza în funcția remove din structura pnp_driver:

static void my_pnp_remove(struct pnp_dev * dev) {
         /* unregister device dev */
}
 
static struct pnp_driver my_pnp_driver = {
         //...
         .remove         = my_pnp_remove,
};

După cum se poate observa, la detectarea eliminării unui dispozitiv din sistem, se va apela funcția remove a driver-ului, se va genera un apel în user-space, ce poate fi detectat de udev și se vor elimina intrările din sysfs.

Resurse utile

so2/laboratoare/lab12.txt · Last modified: 2017/05/14 15:00 by octavian.purdila
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0