Compréhension des files de périphériques de caractères (ou de caractères spéciaux)

J'essaie de comprendre les files spéciaux de caractères. De wikipedia , je comprends que ces files "fournissent une interface" pour les appareils qui transmettent des données un caractère à la fois. Je crois comprendre que le système appelle le périphérique caractère au lieu d'appeler directement le pilote du périphérique. Mais comment le file fournit-il cette interface? Est-ce un exécutable qui traduit l'appel système? Quelqu'un peut-il expliquer ce qui se passe?

Ils sont en fait juste cela – interfaces. Encodés par un numéro "majeur" et "mineur", ils fournissent un crochet au kernel.

Ils viennent dans deux saveurs (bien, trois, mais les tuyaux nommés sont hors de la scope de cette explication pour l'instant): Dispositifs de caractère et dispositifs de bloc.

Les périphériques de blocage ont tendance à être des périphériques de stockage, capables de mettre en memory tampon la sortie et de stocker des données pour une récupération ultérieure.

Les périphériques de caractères sont des choses comme des maps audio ou charts, ou des périphériques d'input comme le keyboard et la souris.

Dans chaque cas, lorsque le kernel charge le bon pilote (soit au démarrage, soit via des programmes comme udev ), il parsing les différents bus pour voir si des périphériques gérés par ce pilote sont effectivement présents sur le système. Si c'est le cas, il met en place un appareil qui «écoute» le numéro majeur / mineur approprié.

(Par exemple, le processeur de signal numérique de la première carte audio trouvée par votre système obtient la paire numéro majeur / mineur de 14/3, la seconde obtient 14,35, etc.)

C'est à udev de créer une input dans /dev nommé dsp tant que périphérique de caractères marqué majeur 14 mineur 3.

(Dans les versions significativement plus anciennes ou d'empreinte minimale de Linux, /dev/ peut ne pas être chargé dynamicment, mais simplement contenir tous les files de périphériques possibles statiquement).

Ensuite, quand un programme d'espace user tente d'accéder à un file qui est marqué comme un «file spécial de caractère» avec le numéro majeur / mineur approprié (par exemple, votre lecteur audio essaie d'envoyer de l'audio numérique à /dev/dsp ), le kernel sait que ces données doivent être transmises via le pilote auquel le numéro majeur / mineur est attaché; vraisemblablement dit conducteur sait quoi faire avec lui à son tour.

Chaque file, périphérique ou autre supporte 6 opérations de base dans le VFS:

  1. Ouvrir
  2. Fermer
  3. Lis
  4. Écrire
  5. Chercher
  6. Dire

En outre, les files de périphériques prennent en charge le contrôle d'E / S, ce qui permet d'autres opérations diverses non couvertes par les 6 premiers.

Dans le cas d'un caractère spécial, seek et tell ne sont pas implémentés car ils supportent une interface de streaming . C'est-à-dire, lire ou écrire directement comme cela est fait avec la redirection dans le shell:

 echo 'foo' > /dev/some/char sed ... < /dev/some/char 

"Personnage à la fois" est un terme mal approprié (de même que l'idée que les dispositifs de caractères ne supportent pas forcément la search). En fait, les périphériques "bloc à la fois" (c.-à-d. Ssortingctement orientés vers les loggings, tels qu'un lecteur de bande *) doivent être des périphériques de caractères. Il en est de même pour l'idée qu'un périphérique de caractères doit nécessairement être indécelable – les pilotes de périphériques de caractères définissent une structure file_operations complète qui est libre de définir llseek ou non selon que le périphérique prend en charge l'opération. Les périphériques de caractères que la plupart des gens considèrent comme des exemples sont des périphériques null, urandom, TTY, carte son, souris, etc … qui sont tous indécelables en raison des spécificités de ces périphériques, mais / dev / vcs, / dev / fb0 , et / dev / kmem sont également des périphériques de caractères et ils sont tous recherchés.

Comme je l'ai mentionné, un pilote de périphérique de caractères définit une structure file_operations qui a des pointeurs de fonction pour toutes les opérations que quelqu'un peut vouloir appeler sur un file – chercher, lire, écrire, ioctl, etc. est exécuté avec ce file de périphérique ouvert. Et lire et écrire peut donc faire tout ce qu'il veut avec ses arguments – il peut refuser d'accepter une écriture trop grande ou écrire seulement ce qui convient; il ne peut lire que datatables correspondant à un logging plutôt que le integer d'octets demandé.

Alors, qu'est-ce qu'un périphérique en bloc, alors? Fondamentalement, les périphériques de blocage sont des lecteurs de disque. Aucun autre type de périphérique (à l'exception des lecteurs de disque virtuels , tels que ramdisk et loopback), est un périphérique de bloc. Ils sont embeddeds au système de request d'E / S, à la couche de système de files, au système de memory tampon / cache et au système de memory virtuelle, contrairement aux périphériques de caractères, même lorsque vous accédez à / dev / sda à partir d'un process user . Même les "périphériques bruts" que cette page mentionne comme une exception sont des périphériques de caractères .

* Certains systèmes UNIX ont implémenté ce qui est maintenant appelé "mode bloc fixe" – ce qui permet au groupe de kernelx et aux requêtes d'E / S divisées de correspondre aux limites de bloc configurées de la même manière que pour les unités de disque – en tant que bloc dispositif. Un périphérique de caractères est nécessaire pour le «mode bloc variable», qui préserve les limites de bloc du programme user lorsqu'un seul appel write (2) écrit un bloc et qu'un seul appel read (2) renvoie un bloc. Comme la commutation de mode est maintenant implémentée sous la forme d'un file ioctl plutôt que d'un file de périphérique séparé, un périphérique de caractères est utilisé. Les lecteurs de bande à logging variable sont pour la plupart «non recherchés» car la search implique le comptage d'un nombre d'loggings plutôt que d'un certain nombre d'octets et l'opération de search native est implémentée sous la forme d'un ioctl.

Exemple d'opération file_operations minimales

Une fois que vous voyez un exemple minimal, tout devient évident.

Les idées keys sont les suivantes:

  • file_operations contient les callbacks pour chaque file syscall
  • mknod path c major minor crée un périphérique de caractères qui utilise ces file_operations
  • pour les périphériques de caractères qui allouent dynamicment les numéros de périphériques (la norme pour éviter les conflits), trouvez le numéro avec cat /proc/devices

module du kernel character_device.ko :

 #include <asm/uaccess.h> /* copy_from_user, copy_to_user */ #include <linux/errno.h> /* EFAULT */ #include <linux/fs.h> /* register_chrdev, unregister_chrdev */ #include <linux/jiffies.h> #include <linux/module.h> #include <linux/printk.h> /* printk */ #include <uapi/linux/stat.h> /* S_IRUSR */ #define NAME "lkmc_character_device" MODULE_LICENSE("GPL"); static int major; static ssize_t read(struct file *filp, char __user *buf, size_t len, loff_t *off) { size_t ret; char kbuf[] = {'a', 'b', 'c', 'd'}; ret = 0; if (*off == 0) { if (copy_to_user(buf, kbuf, sizeof(kbuf))) { ret = -EFAULT; } else { ret = sizeof(kbuf); *off = 1; } } return ret; } static const struct file_operations fops = { .owner = THIS_MODULE, .read = read, }; static int myinit(void) { major = register_chrdev(0, NAME, &fops); return 0; } static void myexit(void) { unregister_chrdev(major, NAME); } module_init(myinit) module_exit(myexit) 

Programme de test Userland:

 insmod /character_device.ko dev="lkmc_character_device" major="$(grep "$dev" /proc/devices | cut -d ' ' -f 1)" mknod "/dev/$dev" c "$major" 0 cat /dev/lkmc_character_device # => abcd rm /dev/lkmc_character_device rmmod character_device 

GitHub QEMU + Buildroot en amont avec boilerplate pour l'exécuter:

Exemples plus complexes:

Les périphériques de caractères peuvent être créés par les modules du kernel (ou le kernel lui-même). Lorsqu'un périphérique est créé, le créateur fournit des pointeurs aux fonctions qui implémentent les appels standard comme open, read, etc. Le kernel Linux associe ensuite ces fonctions au périphérique de caractères, par exemple lorsqu'une application en mode user appelle read () sur un file de périphérique de caractères, il en résultera un appel syscall, puis le kernel apathera cet appel vers une fonction de lecture spécifiée lors de la création du pilote. Il existe un tutoriel étape par étape sur la création d'un périphérique de caractères, vous pouvez créer un exemple de projet et le parcourir à l'aide d'un débogueur pour comprendre comment l'object de périphérique est créé et quand les gestionnaires sont appelés.