Serveur d'impression

Le pilote d'isolement (partie II) – OSR – Serveur d’impression

Le 9 avril 2020 - 23 minutes de lecture

Dans la première partie de cette série (The Isolation Driver (Part I)), nous avons fourni une introduction de haut niveau à notre modèle de construction d'un pilote d'isolement. Depuis lors, nous avons reçu des commentaires d'un certain nombre de personnes qui ont trouvé que ce modèle général était applicable aux problèmes qu'ils tentaient de résoudre.

En examinant le modèle d'origine que nous avons fourni dans la partie I, nous avons décidé de séparer logiquement la fonctionnalité du pilote d'isolement en deux parties distinctes: le isolement conducteur et accomplissement chauffeur.

Architecture du pilote d'isolement

Architecture du pilote d'isolement

En séparant la fonctionnalité en deux parties logiques, nous permettons la construction d'un cadre de pilote d'isolement commun et sa combinaison avec un pilote d'exécution distinct. Si votre projet ne nécessite pas ce type de séparation, vous pouvez combiner leur fonction logique ensemble – nous venons de décider de le diviser car cela nous permet de créer un filtre d'isolement commun (de base) et de permettre à quelqu'un d'autre de construire un pilote de réalisation, sans exiger qu'ils comprennent les nuances du processus d'isolement.

Bien sûr, notre intérêt est d'examiner les problèmes impliqués dans la création d'un pilote d'isolement, pas dans le pilote de réalisation (en d'autres termes, nous ne nous soucions pas vraiment de l'origine des données – c'est le travail du pilote de réalisation. Nous avons juste besoin de gérer la présentation de ces données aux applications, ainsi que la myriade de cas spéciaux et de situations qui pourraient survenir.

En fait, il existe un nombre important de ces problèmes, notamment:

  • E / S asynchrones
  • Remplacer, supprimer et tronquer des flux
  • Verrous de plage d'octets
  • Systèmes de fichiers réseau
  • Problèmes mixtes 32/64 bits
  • Problèmes de PNP / démontage
  • Interactions filtre-filtre
  • Transactions

Nous les couvrirons ici, le reste de la couverture des problèmes restants devant être traités dans la partie III (prise en charge multi-versions, fichiers compressés, appels de création entrants, points d'analyse, etc., etc.).

Dans cet article et les suivants, nous couvrirons ces sujets pour motiver davantage notre exemple de pilote d'isolement.

E / S asynchrones

Bien que la plupart des opérations d'E / S soient synchrones, pour certaines applications ainsi que pour les appelants au niveau du système d'exploitation, il existe des cas où l'opération d'E / S peut être effectuée de manière asynchrone. Il s'agit d'un problème important pour le pilote d'isolement car il s'agit d'un hybride – pas un «vrai» pilote de système de fichiers, mais pas non plus un pilote de filtre traditionnel, car il contrôle son propre cache.

Ainsi, nous devons déterminer comment les E / S asynchrones doivent être implémentées. Mais commençons par clarifier certaines des règles de base des E / S qui sont vraies pour tous les pilotes sous Windows:

  • Tout IRP peut être implémenté de manière asynchrone par un pilote. Cela est vrai même si le IRP_SYNCHRONOUS_API bit est défini à l'emplacement de la pile d'E / S, ou FO_SYNCHRONOUS_IO bit est défini dans l'objet fichier. Correspond à cela est la règle pour tous les autres pilotes du système: si vous appelez un autre pilote, vous devez être prêt à gérer l'achèvement asynchrone par le pilote que vous appelez, quelles que soient les options que vous définissez dans la demande. Certaines opérations peuvent être «encapsulées» et attendre (telles que IoForwardIrpSynchronously). Soyez prudent, cependant, car certaines opérations (notifications de changement de répertoire et certaines opérations FSCTL, par exemple) ne peuvent pas être traitées de manière synchrone.
  • Toute opération d'E / S peut être implémentée de manière synchrone par un pilote de système de fichiers. Même si l'appelant n'a pas demandé un comportement synchrone, il peut être implémenté de cette manière. Un pilote de filtre peut généralement le faire également.
  • Un pilote peut déterminer si l'opération d'E / S est synchrone à partir de l'IRP en utilisant IoIsOperationSynchronous. Un pilote de filtre peut déterminer si l'opération d'E / S est synchrone à partir de la structure FLT_CALLBACK_DATA, en appelant FltIsOperationSynchronously.

En ce qui concerne le filtre d'isolement, nous avons décidé que le bon choix était d'implémenter nos opérations d'E / S de manière synchrone (au moins en règle générale) et de permettre la accomplissement pilote pour implémenter les E / S asynchrones. Cela fonctionne pour nous car le pilote d'exécution devra dans tous les cas gérer la livraison asynchrone, comme si la livraison des données est satisfaite par un service en mode utilisateur (en utilisant un appel inversé, modèle, par exemple).

Remplacer, supprimer et tronquer

Les subtilités que le filtre d'isolement doit gérer sont les différentes façons dont un élément peut être supprimé. Il s'agit notamment des opérations de création «destructives», notamment:

FILE_SUPERSEDE – cela a pour effet de supprimer tous les flux. Expérimentalement, nous avons constaté qu'un remplacement échoue si des flux du fichier sont ouverts, et c'est quelque chose que nous devons garder à l'esprit lors de la construction du filtre d'isolement – en fonction des fonctionnalités spécifiques dont nous avons besoin, nous pourrait pouvoir reporter cette décision au système de fichiers sous-jacent. En particulier, dans les systèmes antérieurs à Vista, nous n'avons pas par fichier contextes, et donc nous devons soit nous limiter aux systèmes Vista (et plus récents) ou nous devrons construire notre propre schéma de suivi de contexte par fichier. En outre, dans tous les cas, nous devons également garder à l'esprit les cas de fichiers mappés (et en particulier si notre service de fournisseur et / ou notre pilote d'exécution utilisent des flux pour tout ou partie de leurs fonctionnalités, ce qui peut compliquer les choses). Pour notre exemple de code, nous allons «garder cela simple» mais pour votre propre projet d'isolement, vous devrez peut-être résoudre ce problème.

FILE_OVERWRITE – cela a pour effet de tronquer les données dans le flux, ainsi que de supprimer tous les autres flux lorsque le flux de données principal (par défaut) est écrasé. Contrairement au cas de remplacement, d'après ce que nous avons observé, les flux sont supprimés lorsqu'ils sont fermés. Encore une fois, pour notre exemple, nous autoriserons simplement cela à être géré par le système de fichiers sous-jacent, mais vous souhaiterez peut-être envisager de gérer cela dans votre propre projet de pilote d'isolement.

De plus, nous avons deux façons de supprimer un fichier:

FILE_DELETE_ON_CLOSE – cette option de création a des propriétés intéressantes. Tout d'abord, il est suivi sur une instance par ouverture (pour un mini-filtre, vous pouvez le considérer comme un état associé au «contexte de gestion de flux»). Ainsi, il est possible qu'un fichier soit ouvert plusieurs fois, l'un d'eux étant FILE_DELETE_ON_CLOSE. Lorsque le descripteur spécifique est fermé, cela est converti en une demande de «supprimer le fichier» et les tentatives d'ouverture du fichier échoueront avec STATUS_DELETE_PENDING.

IRP_MJ_SET_INFORMATION (Disposition) – c'est la deuxième façon dont un fichier peut être supprimé. Dans ce cas, la suppression est un intention et peut être «annulé» jusqu'à la fermeture du fichier lui-même. De plus, il existe des problèmes intéressants dans le nettoyage et le traitement rapproché en ce qui concerne les fichiers qui sont en attente de suppression, car ils peuvent être mappés en mémoire – et dans ce cas, la suppression de fichier ne peut pas être traitée jusqu'à ce que le IRP_MJ_CLOSE (et cela pourrait en fait se produire sur un autre FILE_OBJECT que la dernière poignée fermée).

IRP_MJ_SET_INFORMATION (Renommer) – ceci est un cas subtil, mais un cas spécial de renommer un fichier est l'option "remplacer s'il existe" dans le renommer. Pour les filtres d'isolement, il peut être nécessaire de suivre ces événements et de les signaler au composant d'exécution.

IRP_MJ_SET_INFORMATION (lien matériel) – c'est le même cas que pour renommer, juste une opération différente (et beaucoup plus rare). Tous les systèmes de fichiers ne prennent pas en charge les liens durs.

Notez que nous avons évoqué ici la «suppression du fichier». Si autre chose que le flux de données par défaut est ouvert de cette manière, seul le flux lui-même sera supprimé, pas le fichier entier. De plus, il existe certaines subtilités impliquant des flux que nous n'explorons pas complètement ici.

Un point important à comprendre ici: il n'est pas possible, au sein d'un pilote de filtre, de savoir si le fichier a été supprimé. Étant donné que la suppression est une intention, un filtre inférieur (par exemple, un «filtre de suppression») peut inverser la suppression. Ce comportement est invisible pour tout ce qui est logiquement «au-dessus» de ce filtre (y compris notre filtre d'isolement).

Verrous de plage d'octets

Les verrous de plage d'octets présentent un problème intéressant pour le filtre d'isolement: parce que la vue des données de l'application est différente de la vue des données du pilote d'exécution (et du service du fournisseur), elles doivent être gérées par le pilote d'isolement. En d'autres termes, ne supposez pas que vous pouvez compter sur le pilote de système de fichiers sous-jacent pour les mettre en œuvre «correctement».

Si votre filtre d'isolement n'a besoin que d'isoler local systèmes de fichiers, ce n'est pas une tâche difficile à réaliser – vous pouvez simplement utiliser les fonctions FsRtl ou Flt adaptées à la tâche (voir FltInitializeFileLock ou FsRtlInitializeFileLock pour plus d'informations.) L'utilisation de ces fonctions nécessite:

  • Gestion des appels verrouillés et déverrouillés (IRP et Fast I / O)
  • Application de verrous de plage d'octets pour les opérations de lecture et d'écriture d'E / S sans pagination. C'est une erreur d'appliquer les verrous de plage d'octets pour les E / S de pagination (ce qui signifie que pour les fichiers mappés en mémoire, les verrous de plage d'octets sont consultatifs, mais il n'y a pas de mécanisme pour distinguer les modifications utilisateur d'un fichier mappé par rapport aux réécritures de la cache)

Nous discuterons séparément des problèmes d'isolement des systèmes de fichiers réseau.

En considérant les verrous de plage d'octets, gardez à l'esprit que nous avons deux «vues» distinctes du fichier et il n'y a aucune raison de considérer que le verrouillage de plage d'octets de l'un devrait interagir avec le verrouillage de plage d'octets de l'autre.

Qu'est-ce que cela signifie pour le pilote d'isolement? Si une application verrouille une région de la vue isolée, elle doit le faire contre d'autres applications accédant à cette vue isolée. Le pilote d'exécution et le service fournisseur n'ont pas du tout besoin de connaître ces verrous de plage d'octets et, en effet, c'est beaucoup plus simple si nous nous appuyons sur la gestion des verrous de plage d'octets du système de fichiers sous-jacent dans de tels cas.

Ainsi, les verrous de plage d'octets sont gérés par le pilote d'isolement en ce qui concerne les applications qui accèdent à la vue isolée du fichier (par rapport à la vue native du fichier). Cela garantit que le service du fournisseur ne se heurtera pas aux verrous de plage d'octets contre la vue isolée.

Systèmes de fichiers réseau

Un filtre d'isolement peut, en théorie, fonctionner avec n'importe quel système de fichiers, local ou réseau. Pour les systèmes de fichiers réseau, cependant, un certain nombre de problèmes devront être pris en compte pour notre filtre d'isolement. Par exemple, il y a une question fondamentale de "comment pouvons-nous interverrouiller entre les applications s'exécutant sur différents systèmes clients?" Il existe essentiellement trois façons de proposer de résoudre ce problème:

  • Refuser d'autoriser plusieurs accès client à la vue d'isolement du fichier. Il s'agit certainement de l'implémentation la plus simple, mais elle conduira à un comportement d'application différent de l'accès natif au fichier. Par exemple, Microsoft Word «détecte» lorsqu'un fichier est déjà utilisé en utilisant un accès partagé à ce fichier (et en faisant une copie du fichier en cas de détection d'un conflit). Ceci est perdu si vous refusez d'autoriser plusieurs accès client .
  • Autorisez plusieurs accès client à la vue d'isolement du fichier, en vous appuyant sur le système de fichiers sous-jacent pour contrôler ce comportement. Cela représente un défi distinct, car nous n'avons qu'un seul fichier et nous essayons maintenant de contrôler le comportement de partage pour deux vues différentes du fichier (les vues natives et isolées du fichier). À moins que deux fichiers (ou deux flux du même fichier) soient utilisés pour arbitrer cet accès, la combinaison de ces accès sur les deux fichiers ne fournira probablement pas de solution satisfaisante.
  • Autorisez plusieurs accès client à la vue d'isolement du fichier, en vous appuyant sur le service du fournisseur (ou peut-être le pilote d'exécution) pour arbitrer correctement ce comportement. En substance, cela devient une implémentation d'un schéma de gestion de verrouillage distribué (simplifié) d'une certaine sorte.

Au-delà du partage de fichiers, nous pouvons alors décider comment gérer les verrous de plage d'octets: en utilisant des plages fractionnées (donc avec un seul fichier sur la télécommande, vous utiliseriez 0-4EB pour les verrous de vue d'isolement et 4-8EB pour les verrous de vue natifs, par exemple, ) ou en étendant la gestion des verrous distribués pour implémenter réellement le verrouillage de plage pour la vue d'isolement (puis en s'appuyant simplement sur le système de fichiers natif pour gérer les verrous de plage d'octets sur la vue native du fichier).

Les Oplocks sont une autre complication ici, car les oplocks et les verrous de plage d'octets sont souvent incompatibles les uns avec les autres – les verrous de plage d'octets doivent être contrôlés sur le serveur, les oplocks permettent aux clients de mettre en cache les données, ce qui leur évite de soumettre des opérations d'E / S. au serveur. Bien que ce soit le redirecteur SMB qui utilise des oplocks pour sa politique de mise en cache, il ne rend pas les changements d'état d'oplock visibles aux filtres au-dessus du redirecteur. Cela nous obligera soit à désactiver les oplocks (en supprimant les verrous de plage d'octets, généralement), à créer un filtre de protocole réseau SMB (quelque chose que nous avons théorisé mais jamais fait), ou à finir par briser la cohérence du cache entre plusieurs clients.

Pour notre filtre d'isolation prototype, nous nous en tiendrons à un modèle sans accès partagé, ce qui évite nos préoccupations ici. Ces préoccupations sont «réelles» et peuvent être un problème pour votre propre projet de pilote d'isolement, auquel cas vous devrez répondre à ces préoccupations au-delà de ce que nous avons fait dans notre échantillon.

Problèmes mixtes 32/64 bits

Parce que nous devons coexister avec des applications utilisateur 32 bits dans un monde 64 bits, il est important de garder à l'esprit que notre filtre d'isolement doit gérer correctement ces cas. Le problème le plus important ici est le problème des structures contenant des valeurs HANDLE. Pour une application 32 bits, ce seront des valeurs 32 bits, tandis que pour une application 64 bits, ce sera une valeur 64 bits.

REMARQUE: Pendant que nous discutons de ce problème, nous devons noter qu'il existe un bogue d'API connu: les deux IoGetRequestorProcessId et FltGetRequestor ProcessId renvoient des valeurs ULONG, mais les ID de processus sont des descripteurs. Il s'agit d'un problème mineur (car jusqu'à présent, aucun système n'a eu suffisamment de processus pour obtenir une valeur dépassant 32 bits), mais il montre à quel point il est facile de mal gérer la prise en charge 32 bits / 64 bits dans Windows.

Les plus gênants sont l'inclusion de poignées dans le bloc de paramètres d'E / S (IO_PARAMETER_BLOCK ou FLT_IO_PARAMETER_BLOCK):

                //
    // IRP_MJ_SET_INFORMATION
    //
    struct 
        ULONG Longueur;
        FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
        PFILE_OBJECT ParentOfTarget;
        syndicat 
            struct 
                BOOLEAN ReplaceIfExists;
                BOOLEAN AdvanceOnly;
            ;
            ULONG ClusterCount;
            HANDLE DeleteHandle;
        ;
        PVOID InfoBuffer; // Pas dans la liste des paramètres IO_STACK_LOCATION
     SetFileInformation;

La présence de cette valeur HANDLE au sein de la structure nous crée du chagrin, car la taille réelle de cette valeur de données dépendra du fait qu'il s'agisse ou non d'un processus 32 bits sur un système d'exploitation 64 bits. Si tel est le cas, la taille de la poignée du processus est ne pas identique à la taille de la poignée du pilote, et il est de la responsabilité du conducteur d’adapter ce commutateur dans les tailles.

En effet, il existe un certain nombre d'opérations dans lesquelles une poignée est intégrée, notamment:

  • IRP_MJ_FILE_SYSTEM_CONTROL – cela comprend un certain nombre d'opérations FSCTL, y compris FSCTL_MARK_HANDLE, et FSCTL_MOVE _FILE.
  • IRP_MJ_SET_INFORMATION – en plus du bloc de paramètres que nous avons mentionné précédemment, cela comprend également FILE_RENAME_INFORMATION, FILE_MOVE_CLUSTER_INFORMATION, et FILE_LINK_INFORMATION.

Dans de tels cas, le filtre d'isolement (et éventuellement le pilote de réalisation) devra implémenter une logique appropriée pour tenir compte de cette différence de taille de poignée. Notre exemple de filtre d'isolement démontrera ce point lorsque nous y parviendrons dans une copie ultérieure de cet article.

Problèmes de PNP / démontage

Si votre filtre d'isolement traite des supports amovibles ou des périphériques amovibles, vous devrez gérer les problèmes de montage et de démontage ainsi que le plug-and-play.

Dans les deux cas, l'aspect le plus compliqué n'est pas seulement la gestion des événements eux-mêmes, mais la sérialisation par rapport à ces changements d'état tout au long de autre chemins de code. Après tout, la fonctionnalité de base lors d'un événement de suppression de périphérique ou de suppression de média est «relativement simple». Nous devons simplement supprimer nos structures de données. Cependant, si ces mêmes structures de données sont utilisées dans un autre chemin de code, nous ne pouvons pas les supprimer.

En général, la manière la plus simple de se protéger contre cela consiste à utiliser quelque chose IO_REMOVE_LOCK. Cependant, ceux-ci sont documentés comme ne fonctionnant que s'ils se trouvent dans une extension de périphérique. Donc, pour protéger toute autre structure que nous pourrions avoir, nous devrons essentiellement construire la nôtre, probablement en utilisant soit ERESOURCE serrures ou peut-être FltInitializePushLock (en général, nous avons tendance à éviter d'utiliser des verrous push car ils rendent le débogage des blocages qui les impliquent beaucoup plus difficile). Ainsi, tout chemin de code qui utilise l'une de vos structures de données qui est détruit sur les chemins de démontage ou de retrait de périphérique devra être protégé. Vous pouvez accomplir cela en acquérant le verrou (IoAcquireRemoveLock ou ExAcquireResourceSharedLite) et libérer le verrou (IoReleaseRemoveLock ou ExReleaseResourceLite) lorsque vous quittez le chemin protégé. Ensuite, lorsque vous devez détruire la structure de données, vous devez la verrouiller de la manière appropriée (voir IoReleaseRemoveLockAndWait Ou utiliser ExAcquireResourceExclusiveLite).

Filtrer pour filtrer les interactions

Un domaine de complication important pour tout filtre concerne les interactions filtre à filtre. Il est irréaliste de supposer que nous pouvons énumérer toutes les sources de telles interactions ou conseiller sur la façon de les éviter toutes. Cependant, nous pouvons faire un certain nombre de choses pour les minimiser:

  • Ne présumez jamais que vous pouvez ignorer une fonctionnalité rarement utilisée. Nous avons vu que les filtres ne géraient pas toutes sortes de conditions spécialisées (ouverture par ID de fichier, points d'analyse, liens durs, etc.). Il est important de réfléchir à ces cas car ils devront être traités à un moment donné.
  • Assurez-vous toujours que votre filtre peut coexister avec lui-même. Par exemple, si vous avez besoin de détecter des appels entrants, assurez-vous d'utiliser une technique de détection qui serait compatible avec elle-même (l'ajout d'un préfixe ou d'un suffixe au nom de fichier est un excellent exemple d'une telle technique qui n'empile pas Les ECP, en revanche, s’empilent, à condition que chaque filtre utilise sa propre entrée ECP).
  • Ne contournez pas les filtres en dessous de vous. C'est parfois tentant mais peut déclencher des problèmes de compatibilité.
  • Assurez-vous d'aller à Plugfest. C'est le meilleur moyen de tester un certain nombre d'autres filtres, de rencontrer d'autres développeurs, sans parler de trouver et de résoudre les problèmes dans un environnement «réel».

Gardez à l'esprit, peu importe la façon dont vous construisez votre filtre, les problèmes d'interaction sont une réalité de la vie. Les interférences fonctionnelles (par exemple, les filtres logiques d'analyse des données par rapport aux filtres de compression / chiffrement) ne peuvent pas être éliminées et les filtres actifs modifient le comportement de la pile du système de fichiers, ce qui complique l'environnement.

Transactions

Peu de choses peuvent être plus compliquées à faire correctement que les transactions (en particulier dans un filtre complexe, tel qu'un filtre d'isolement des données). La chose la plus simple à faire dans un filtre d'isolement est de refuser d'autoriser les opérations transactionnelles sur des fichiers isolés – il s'agit probablement d'un «premier arrêt» pour une implémentation de première génération, mais cela peut également entraîner des défaillances d'applications spécifiques.

D'après notre expérience à ce jour, les transactions ne sont utilisées que par les programmes d'installation (y compris Windows Update) et nos programmes de test. Les transactions ne sont pas encore utilisées dans les applications courantes; si elles le seront ou non reste à voir, mais il est logique de s'attendre à les voir dans des types d'applications spécifiques à l'avenir.

Par conséquent, il est important de les considérer au moins dans un filtre d'isolement complet (nous n'allons pas résoudre ce problème dans notre exemple de filtre d'isolement, mais c'est un problème qui peut vous obliger à le résoudre dans une implémentation commerciale).

La question est alors: comment prenez-vous en charge les transactions dans un filtre d'isolement? En fait, le modèle que nous utilisons dans un filtre d'isolement (vues fractionnées) est vraiment inspiré du modèle dans lequel les transactions sont implémentées dans NTFS – en utilisant des vues distinctes des données («isolation des données») via la structure Section Object Pointers.

Cependant, les transactions comportent bien plus que la simple prise en charge des vues fractionnées. Si votre système de fichiers sous-jacent prend en charge les transactions, vous pouvez reporter un grand nombre d'opérations au système de fichiers sous-jacent (notamment celles qui traitent de la «forme» de l'espace de noms, par exemple). Sans une telle prise en charge, il est peu probable que vous puissiez facilement créer une fonction de renommage transactionnel (par exemple) dans un filtre d'isolement sans construire votre propre gestionnaire de ressources persistantes. Remarque: discuter de la création d'un gestionnaire de ressources de tout type dépasse le cadre de cette discussion.

Ainsi, si vous pouvez vous limiter à l'isolement des données, vous pouvez alors simplement traiter la transaction comme étant une «vue» distincte des données (bien qu'avec un modèle pour la façon dont vous gérez la restauration et la validation, peut-être en utilisant la prise en charge CLFS dans Windows Server 2003 et plus récent).

Résumé

J'aimerais pouvoir dire que nous avons terminé, mais nous laisserions de côté certains problèmes très importants à comprendre en ce qui concerne la mise en œuvre d'un pilote d'isolement. Nous terminerons la discussion de ces derniers dans la partie III.

Résumé

Nom d'article

Le conducteur d'isolement (partie II)

La description

Les pilotes d'isolement sont un moyen efficace de séparer les vues physiques (le contenu physique d'un fichier) des vues logiques (le contenu logique d'un fichier). La deuxième partie de cet article d'une série passe de l'architecture fondamentale d'un pilote d'isolement à certaines des questions liées à sa création.

Auteur

OSR

Commentaires

Laisser un commentaire

Votre commentaire sera révisé par les administrateurs si besoin.