Écrire un module kernel Linux depuis zéro — enregistrement d'un character device, interface file_operations, puis driver I2C intégré dans le sous-système Linux.
Un character device crée une entrée dans /dev/ accessible depuis userspace comme un fichier ordinaire. Le kernel route les appels open/read/write vers nos fonctions via la struct file_operations.
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> MODULE_LICENSE("GPL"); MODULE_AUTHOR("ElieSas"); MODULE_DESCRIPTION("Character device minimal"); /* Variables du module — static = privées au fichier .c */ static int major_number; static char message[256] = {0}; static size_t message_len; /* ── Implémentation des callbacks file_operations ── */ static int dev_open(struct inode *i, struct file *f) { printk(KERN_INFO "hello_dev: opened\n"); return 0; } static int dev_release(struct inode *i, struct file *f) { printk(KERN_INFO "hello_dev: closed\n"); return 0; } static ssize_t dev_read(struct file *f, char __user *buf, size_t len, loff_t *off) { /* copy_to_user : kernel → userspace (ne jamais utiliser memcpy !) */ if (copy_to_user(buf, message, message_len)) return -EFAULT; return message_len; } static ssize_t dev_write(struct file *f, const char __user *buf, size_t len, loff_t *off) { /* copy_from_user : userspace → kernel */ if (copy_from_user(message, buf, len)) return -EFAULT; message_len = len; return len; } /* ── Table des callbacks — lien entre VFS et nos fonctions ── */ static struct file_operations fops = { .owner = THIS_MODULE, .open = dev_open, .release = dev_release, .read = dev_read, .write = dev_write, }; /* ── Cycle de vie du module ── */ static int __init hello_init(void) { major_number = register_chrdev(0, "hello_dev", &fops); printk(KERN_INFO "hello_dev: major=%d\n", major_number); return 0; } static void __exit hello_exit(void) { unregister_chrdev(major_number, "hello_dev"); } module_init(hello_init); module_exit(hello_exit);
| Commande | Action |
|---|---|
| make | Compiler le module → hello_dev.ko |
| sudo insmod hello_dev.ko | Charger le module dans le kernel |
| lsmod | grep hello | Vérifier que le module est chargé |
| cat /proc/devices | Trouver le numéro major attribué |
| sudo mknod /dev/hello c 240 0 | Créer le fichier device (major=240, minor=0) |
| dmesg | tail -10 | Lire les printk() du module |
| echo "test" > /dev/hello | Écrire depuis userspace → dev_write() |
| cat /dev/hello | Lire depuis userspace → dev_read() |
| sudo rmmod hello_dev | Décharger le module |
Le driver I2C s'intègre dans le sous-système I2C du kernel via i2c_driver et les callbacks probe/remove. Le kernel appelle probe automatiquement quand un périphérique correspondant est détecté sur le bus.
#include <linux/i2c.h> #include <linux/module.h> #include <linux/sysfs.h> #include <linux/device.h> #include <linux/slab.h> /* Structure privée — une instance par capteur détecté */ struct tmp_data { struct i2c_client *client; // handle vers le périphérique I2C int temp_raw; }; /* Attribut sysfs — expose /sys/.../temperature en lecture */ static ssize_t temperature_show(struct device *dev, struct device_attribute *attr, char *buf) { struct tmp_data *data = dev_get_drvdata(dev); /* Sur vrai hardware : i2c_smbus_read_word_data(data->client, 0x00) */ data->temp_raw += 1; // simulation return sprintf(buf, "%d\n", data->temp_raw); } static DEVICE_ATTR_RO(temperature); // macro kernel — génère l'attribut /* probe() — appelé par le kernel quand le périphérique est détecté */ static int tmp_probe(struct i2c_client *client) { struct tmp_data *data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; data->client = client; dev_set_drvdata(&client->dev, data); // stocker le contexte device_create_file(&client->dev, &dev_attr_temperature); dev_info(&client->dev, "tmp_i2c: probe ok @ 0x%02x\n", client->addr); return 0; } /* remove() — appelé lors du rmmod ou déconnexion */ static void tmp_remove(struct i2c_client *client) { device_remove_file(&client->dev, &dev_attr_temperature); dev_info(&client->dev, "tmp_i2c: removed\n"); } /* Table de correspondance — déclenche le probe pour adresse 0x48 */ static const struct i2c_device_id tmp_id[] = { { "tmp102", 0 }, {} }; MODULE_DEVICE_TABLE(i2c, tmp_id); static struct i2c_driver tmp_driver = { .driver = { .name = "tmp102", .owner = THIS_MODULE }, .probe = tmp_probe, .remove = tmp_remove, .id_table = tmp_id, }; module_i2c_driver(tmp_driver); // macro qui gère init/exit automatiquement MODULE_LICENSE("GPL"); MODULE_AUTHOR("ElieSas");
Toujours préférer devm_kzalloc à kmalloc — la mémoire est libérée automatiquement par le kernel lors du remove. Évite les fuites mémoire en cas d'erreur dans probe.
En espace kernel : pas de libc, pas de float, pas de printf (→ printk). Pour les accès userspace : toujours copy_to_user() / copy_from_user(). Un crash kernel = kernel panic — pas de try/catch.
file_operations est le lien entre le VFS et notre driver. Chaque champ est un pointeur de fonction. Les champs non remplis restent NULL → opération non supportée.register_chrdev → le kernel attribue un major dynamiquement.probe quand un périphérique matche la table d'ID. remove est appelé au déchargement ou déconnexion. Toujours nettoyer dans remove ce qu'on a créé dans probe./sys/.../temperature) est plus propre qu'un character device — intégration avec udev, Python, et les outils Linux standard.