20

Ich habe das Kapitel in Beautiful Code auf dem Linux-Kernel gelesen und der Autor diskutiert, wie Linux-Kernel die Vererbung in der C-Sprache (neben anderen Themen) implementiert. Kurz gesagt, eine 'Basis'-Struktur wird definiert und um von ihr zu erben, platziert die Struktur' Unterklasse 'eine Kopie der Basis am Ende der Unterklassen-Strukturdefinition. Der Autor verbringt dann ein paar Seiten mit der Erläuterung eines cleveren und komplizierten Makros, um herauszufinden, wie viele Bytes zurück zu übertragen sind, um vom Basisteil des Objekts in den Unterklasse-Teil des Objekts zu konvertieren.Linux Kernel: Warum setzen 'Subklassen' Strukturen am Ende Basisklasseninformationen?

Meine Frage: Innerhalb der Unterklasse Struktur, warum nicht erklären die Basis-Struktur als erste Sache in der Struktur, anstelle der letzten Sache?

Der Hauptvorteil der ersten Base Struct Stuff ist, wenn Sie von der Basis auf die Unterklasse umwandeln, müssen Sie den Zeiger überhaupt nicht bewegen - im Wesentlichen bedeutet die Cast nur dem Compiler, den Code verwenden zu lassen die 'zusätzlichen' Felder, die die Unterklassenstruktur nach dem von der Basis definierten Inhalt platziert hat.

Nur meine Frage ein wenig lassen Sie mich einige Code werfen, um zu klären, aus:

struct device { // this is the 'base class' struct 
    int a; 
    int b; 
    //etc 
} 
struct usb_device { // this is the 'subclass' struct 
    int usb_a; 
    int usb_b; 
    struct device dev; // This is what confuses me - 
         // why put this here, rather than before usb_a? 
} 

Wenn man innerhalb eines usb_device Objekt einen Zeiger auf das „dev“ Feld haben geschieht dann, um es zu werfen Zurück zu diesem usb_device-Objekt muss man 8 von diesem Zeiger subtrahieren. Aber wenn "dev" das erste Ding in einem usb_device-Casting wäre, müsste der Zeiger den Zeiger überhaupt nicht bewegen.

Jede Hilfe zu diesem würde sehr geschätzt werden. Selbst Ratschläge, wo man eine Antwort finden könnte, wären willkommen - ich bin mir nicht sicher, wie Google den architektonischen Grund für eine solche Entscheidung nennen soll. Die nächstgelegene ich hier auf Stackoverflow finden könnte, ist: why to use these weird nesting structure

Und nur klar zu sein - Ich verstehe, dass viele hellen Menschen auf dem Linux-Kernel für eine lange Zeit gearbeitet hat, so klar es gibt einen guten Grund, es zu tun Auf diese Weise kann ich einfach nicht herausfinden, was es ist.

+3

Das sieht mehr wie Komposition als Vererbung für mich. Gibt es im Linux-Kernel wirklich Code, der "struct device" herunterlädt? – nwellnhof

+0

Im Linux-Kernel gibt es kein Problem, es vor, nach oder in der Mitte zu setzen. Dieser spezifische Auszug ist ein Beispiel für Stil. Der reale Benutzer ruft in jedem Fall container_of_makro auf, um eine Unterklasse von einer Basisklasse zu erhalten. Also, struct usb_device * ud = container_of (p, struct device, dev); ', wobei p ein Zeiger auf das Objekt struct device ist. – 0andriy

+0

Rate: Nun, ein Cast auf einen Wert (kein Pointer) würde etwas ähnliches wie das Slicen machen, was es dir erlauben würde, es als 'Gerät' zu benutzen. Dies wäre nicht der Fall, wenn es zuerst platziert würde. –

Antwort

12

Das Amiga OS verwendet diesen "common header" -Trick an vielen Stellen und es sah damals nach einer guten Idee aus: Subclassing durch einfaches Umwandeln des Zeigertyps. Aber es gibt Nachteile.

Pro:

  • Sie erweitern können Datenstrukturen bestehende
  • Sie können den gleichen Zeiger in allen Orten, an denen der Basistyp erwartet wird, keine arithmetische Zeiger benötigt, spart wertvolle Zyklen
  • Es fühlt sich an Natur

Con:

  • Verschiedene Compiler neigen dazu, Datenstrukturen unterschiedlich auszurichten. Wenn die Basisstruktur mit char a; endet, können Sie danach 0, 1 oder 3 Pad-Bytes haben, bevor das nächste Feld der Unterklasse startet. Dies führte zu recht unangenehmen Bugs, besonders wenn man rückwärtskompatibel sein musste (aus irgendeinem Grund musste man eine gewisse Padding haben, weil eine alte Compiler-Version einen Bug hatte und jetzt gibt es viel Code, der das Buggy-Padding erwartet) .
  • Sie merken nicht schnell, wenn Sie die falsche Struktur herumreichen. Mit dem Code in Ihrer Frage werden Felder sehr schnell verworfen, wenn die Zeigerarithmetik falsch ist. Das ist eine gute Sache, da es die Chancen erhöht, dass ein Fehler früher entdeckt wird.
  • Es führt zu einer Einstellung "mein Compiler wird es für mich reparieren" (was es manchmal nicht tut) und alle Casts führen zu einer "Ich weiß besser als der Compiler" Haltung. Letzteres würde dazu führen, dass Sie automatisch Umwandlungen einfügen, bevor Sie die Fehlermeldung verstehen, was zu allen Arten von seltsamen Problemen führen würde.

Der Linux-Kernel setzt die gemeinsame Struktur woanders hin; Es kann sein, muss aber nicht am Ende sein.

Pro:

  • Bugs früh
  • zeigen Sie müssen für jede Struktur eine Pointer-Arithmetik tun, so dass Sie es gewohnt sind
  • Sie nicht
wirft brauchen

Con:

  • nicht offensichtlich
  • -Code ist komplexer
+0

Das erklärt die "common header" -Technik, aber die Frage geht um "common tail". – MSalters

9

Ich bin neu in der Linux-Kernel-Code, also nehmen Sie meine Geschwafel hier mit einem Körnchen Salz. Soweit ich sagen kann, gibt es keine Anforderung, wo die "Unterklasse" struct. Genau das bieten die Makros: Sie können unabhängig vom Layout in die Struktur "Unterklasse" umwandeln. Dies bietet Robustheit für Ihren Code (das Layout einer Struktur kann geändert werden, ohne dass Sie Ihren Code ändern müssen.) Vielleicht gibt es eine Konvention, die Struktur "Basisklasse" am Ende zu platzieren, aber ich bin mir dessen nicht bewusst. Ich habe viel Code in Treibern gesehen, wo verschiedene "Basisklassen" -Strukturen verwendet werden, um in die gleiche "Unterklassen" -Struktur (von verschiedenen Feldern in der "Unterklasse" natürlich) zurückzuspringen.

5

Ich nicht habe neue Erfahrungen aus dem Linux-Kernel, aber aus anderen Kernen. Ich würde sagen, dass das überhaupt keine Rolle spielt.

Sie sollten nicht von einem zum anderen werfen.Erlauben Castings wie das sollte nur getan werden In sehr speziellen Situationen reduziert es in den meisten Fällen die Robustheit und Flexibilität des Codes und wird berücksichtigt ziemlich schlampig. Der tiefste "architektonische Grund", nach dem Sie suchen, könnte also sein, "weil es die Reihenfolge ist, in der jemand sie geschrieben hat". Oder, alternativ, das ist, was die Benchmarks zeigten, wäre das Beste für die Leistung einiger wichtiger Code-Pfade in diesem Code. Oder, alternativ, die Person, die es geschrieben hat, denkt, dass es hübsch aussieht (ich baue immer Upside-Down-Pyramiden in meinen Variablendeklarationen und Strukturen, wenn ich keine anderen Einschränkungen habe).Oder jemand hat es vor 20 Jahren so geschrieben und seitdem haben es alle anderen kopiert.

Es könnte ein tieferes Design dahinter sein, aber ich bezweifle es. Es gibt einfach keinen Grund, diese Dinge überhaupt zu entwerfen. Wenn Sie von einer autoritativen Quelle erfahren wollen, warum es auf diese Weise gemacht wurde, senden Sie einfach einen Patch an Linux, der es ändert und sehen, wer Sie anschreien wird.

+0

Das ist urkomisch: "Schicken Sie einen Patch [...] und sehen Sie, wer Sie anschreien" :) Ich denke, es ist ein bisschen brillant. Während ich das selbst nicht versuchen werde, schätze ich es, dass ich es nur vorgeschlagen habe, weil ich nie in einer Million Jahren darüber nachgedacht hätte. +1 nur dafür :) – MikeTheTall

1

Es ist für Mehrfachvererbung ist. struct dev ist nicht die einzige Schnittstelle, die Sie auf eine Struktur im Linux-Kernel anwenden können, und wenn Sie mehrere haben, würde es einfach nicht funktionieren, die Unterklasse in eine Basisklasse umzuwandeln.Zum Beispiel:

struct device { 
    int a; 
    int b; 
    // etc... 
}; 

struct asdf { 
    int asdf_a; 
}; 

struct usb_device { 
    int usb_a; 
    int usb_b; 
    struct device dev; 
    struct asdf asdf; 
};