Es scheint viele Leute da draußen zu geben, die eine benutzerdefinierte Anwendung auf einer Linux-basierten Plattform ausführen möchten, welche auf einem Solid-State-Speichergerät läuft. Von Zeit zu Zeit erhalten wir Fragen von Kunden, die ihre Linux-Plattformen schreibgeschützt machen möchten, um die Langlebigkeit ihrer Flash-Geräte zu maximieren. Ich habe gedacht, ich nutze die Chance und schreibe einen Blog-Beitrag, der eine Möglichkeit zeigt, wie dies zu tun ist. 

Es gibt einige verschiedene Ansätze, um Linux-Systeme schreibgeschützt zu machen. Leider ist dies nicht so einfach wie die Verwendung eines konventionellen Dateisystems, das mit der Schreibschutz-Option gekoppelt ist. Viele Programme gehen davon aus, dass zumindest einige Teile des Systems beschreibbar sind. In einigen Fällen funktionieren diese Programme nicht richtig, wenn sich zeigt, dass dies nicht der Fall ist.

Ich werde hier skizzieren, was meiner Meinung nach der beste Ansatz für die meisten Anwendungen ist. Es ist ähnlich wie bei der aktuellen Generation von Live-CD-Ausgaben.

Live-CDs haben typischerweise einen schreibgeschützten Zugang zu einem Stammdateisystem, das oft in eine einzige Datei komprimiert ist, um später mittels eines Loopback-Gerätes eingebunden zu werden. Knoppix hat neue Wege beschritten mit der Verwendung des Cloop-Dateisystems für diesen Zweck. Die jüngsten Live-Ausgaben gehen noch einen Schritt weiter, indem sie ein Union-Dateisystem verwenden, um das Stammdateisystem beschreibbar zu machen. Dieser Ansatz ist für unsere Intention ebenfalls sehr hilfreich.

Union-Dateisysteme

Im Allgemeinen kombiniert ein Union-Dateisystem mehrere Dateisysteme in einem einzigen virtuellen Dateisystem. Es gibt zwei beliebte Union-Dateisysteme, die mir bekannt sind: unionfs und aufs. Beide haben das gleiche Basismodell. Das Folgende ist eine drastische Vereinfachung:

  • Dateisysteme sind vertikal angeordnet.
  • Lesezugänge werden werden auf jedem Dateisystem abwechselnd von oben nach unten durchgeführt. Das erste Dateisystem, das die zu lesende Datei enthält, wird für den Lesevorgang verwendet.
  • Schreibzugänge werden ähnlich ausgeführt, jedoch werden die Dateien, auf die geschrieben wird, im obersten beschreibbaren Dateisystem gespeichert. Das bedeutet in der Regel, dass es eine einzige beschreibbare Ebene in der Union gibt. Wenn Dateien, die in einer schreibgeschützten Ebene existieren, beschrieben werden, werden sie zunächst in die nächsthöhere Ebene kopiert.

Natürlich gibt es viele Feinheiten und Sonderfälle, die ich hier nicht präsentiere. Wichtig ist, dass wir ein schreibgeschütztes Dateisystem verwenden können (das ein Image eines komprimierten Dateisystems oder ein Flash-Gerät sein kann, das ein konventionelleres Dateisystem wie ext3 enthält) und ein beschreibbares System darauf aufbauen. Alles, was wir benötigen ist die Vereinigung eines beschreibbaren Dateisystems mit der Leseebene.

Die beschreibbare Ebene

Welche Art eines beschreibbaren Dateisystems Sie verwenden hängt davon ab, was Sie erreichen möchten. Wenn Sie keine Persistenz zwischen den Boots benötigen, ist es ziemlich leicht, tmpfs zu verwenden. Schreibvorgänge in das System werden in RAM gespeichert, während das System hochgefahren ist, verschwinden jedoch, wenn das System heruntergefahren oder neu gestartet wird. 

Wenn Sie Persistenz über die gesamte Systemstruktur möchten, müssen Sie eine dauerhaft beschreibbare Ebene verwenden. Dies ist wahrscheinlich ein konventionelles Dateisystem auf einem anderen Medium (vielleicht eine zweite Festplatte). Vermutlich ist dies am nützlichsten für Live-Systeme oder Thin-Clients, bei denen die Verwendung einer schreibgeschützten Basis nicht so sehr für die Langlebigkeit als vielmehr zur Minimierung der lokalen Speicheranforderungen erfolgt.

In vielen Fällen, in denen Sie Persistenz benötigen, brauchen Sie diese nur für spezifische Dateien. Zum Beispiel muss die Datenbank auf der Festplatte verbleiben, wenn Sie einen Kiosk haben, der Nutzer-Input in einer lokalen Datenbank speichert, aber Sie möchten wahrscheinlich nicht, dass temporäre Daten oder anderen transiente Laufzeitdaten fortbestehen. Der beste Ansatz zum Umgang mit diesem üblichen Anwendungsfall ist es, eine tmpfs-Lese-Schreibebene zu haben und dann einige beschreibbare Medien an einem beliebigen Aktivierungspunkt wie zum Beispiel /var/local/data  einzufügen.

Implementierung

Die Implementierung eines schreibgeschützten Systems erfordert das Einklinken in den Boot-Vorgang. Wie dies zu tun ist, variiert von Ausgabe zu Ausgabe und kann innerhalb einer Ausgabe wahrscheinlich auf viele Arten erfolgen. In diesem Artikel zeige ich einen Ansatz, der mit Ubuntu 8.04 arbeitet.

Standardmäßig nutzt Ubuntu 8.04 ein initramfs. Dies ist der beste Ort, um unsere Modifikation durchzuführen, da wir so sicherstellen können, dass das Union-Dateisystem früh im Boot-Vorgang eingebunden wird.

initramfs-tools

Ubuntu hat ein erweiterbares System für den Bau von initramfs, “initramfs-Tools”. Wir können es nutzen, um einige Skripte an die initramfs anzubinden. Es gibt einige verschiedene Wege, initramfs-Tools zu erweitern: “hooks” und “scripts”. 

Hooks werden beim Aufbau der initramfs ausgeführt und sind nützlich, um Kernel-Module oder oder ausführbare Dateien zum initramfs-Image hinzufügen. Hooks, die mit den Packages ausgegeben werden, sind meist unter /usr/share/initramfs-tools/hooks installiert und nutzen die Funktionen, welche unter /usr/share/initramfs-tools/hook-functions definiert sind. Lokale Hooks sollten unter /etc/initramfs-tools/hooks platziert werden.

Skripte werden innerhalb der initramfs-Umgebung zur Boot-Zeit ausgeführt. Diese können verwendet werden, um den frühen Boot-Vorgang zu modifizieren. Wie bei den Hooks werden Skripte, die mit den Packages ausgegeben werden, meist unter /usr/share/initramfs-tools/scripts installiert. Lokale Skripte sollten unter /etc/initramfs-tools/scripts sein.

Die initramfs-Generation wird von den Konfigurationsdateien gesteuert die unter /etc/initramfs-tools zu finden sind. /etc/initramfs-tools/initramfs.conf ist die primäre Konfigurationsdatei, aber Dateien können auch unter /etc/initramfs-tools/conf.d abgelegt werden. Die primäre Boot-Methode kann unter initramfs.conf konfiguriert werden, indem der Wert der Variable “BOOT” geändert wird. Standardmäßig ist dieser “local”, eine Boot-Methode, die das Stammdateisystem auf ein lokales Medium wie eine Festplatte einsetzt.

Für jede Boot-Methode gibt es ein Skript mit dem Namen, der die Funktion dieser Boot-Methode steuert. Es gibt zum Beispiel ein Skript mit dem Namen “local”, das definiert, wie ein lokaler Boot ausgeführt wird. Viele solcher Skripte liefern auch Hooks für andere Shell-Skripte, die an bestimmten Punkten während des Boot-Vorgangs ausgeführt werden. Beispielsweise werden alle Skripte, die unter /usr/share/initramfs-tools/local-premount platziert werden, vom “local”-Skript unmittelbar vor dem Einhängen des Stammdateisystems ausgeführt. Das Init-Skript selbst (welches als Prozess #1 funktioniert bis zum Punkt, an dem der echte Init-Daemon gestartet wird, nach dem Einhängen des Stammdateisystems) liefert ähnliche Hooks. Sehen Sie die Inhalte unter /usr/share/initramfs-tools/scripts an, um eine Idee davon zu erhalten, welche anderen Hooks verfügbar sind. 

Zum Schluss müssen sowohl Hooks als auch Skripte so geschrieben werden, dass sie, wenn sie mit einem einzigen Argument „prereqs“ ausgeführt werden, eine durch Leerzeichen getrennte Liste mit den Namen anderer Skripte oder Hooks ausgeben, die vor der Ausführung dieses bestimmten Skripts oder Hooks ausgeführt werden sollten. Das liefert ein einfache System aus Abhängigkeiten zwischen Hooks und Skripten. Ich finde, dass ich diese Funktion sehr selten benutze, aber sie ist verfügbar, sollte Ihre Anwendung sie benötigen. 

Hooks und Skripte

Wir implementieren unser schreibgeschütztes System, indem wir einen Hook und ein Skript einbringen. Unser Skript wird eigentlich ein Init-Bottom-Skript sein und laufen, nachdem das echte Root-Gerät bereits verbunden ist. Unser Ziel wird es sein, ein bereits eingebundenes Stammdateisystem zu nehmen und es umzumischen als Basis für eine aufs-Union mit einer tmpfs-beschreibbaren Ebene. Das ermöglicht uns, weiterhin die standardmäßigen Ubuntu-Konfigurationsmechanismen zu verwenden, um das Gerät zu spezifizieren, welches das echte Stammdateisystem enthält.

Wir benötigen einen Hook, um den initramfs-Tools zu sagen, dass wir einige Kernel-Module (aufs und tmpfs, von denen beide in Ubuntu 8.04 enthalten sind) benötigen sowie eine ausführbare (chmod). Wir sehen in Kürze, warum wir chmod brauchen. Unser Hook ist ziemlich simpel (wie die meisten). Wir nennen dies hooks/ro_root:

#!/bin/sh

PREREQ=''

prereqs() {
  echo "$PREREQ"
}

case $1 in
prereqs)
  prereqs
  exit 0
  ;;
esac

. /usr/share/initramfs-tools/hook-functions
manual_add_modules aufs
manual_add_modules tmpfs
copy_exec /bin/chmod /bin

Das Skript leistet die wirkliche Arbeit, indem es dafür sorgt, dass alle Dateisysteme an den richtigen Orten eingebunden sind. An diesem Punkt im Boot-Vorgang wurde das echte Root-Gerät auf $rootmnt installiert und und /sbin/init steht an diesem Einhängepunkt kurz vor der Ausführung. An diesem Punkt werden wir versuchen, das Root-Gerät auf einen anderen Einhängepunkt zu verschieben und unser Union-Mount an dieser Stelle aufzubauen.

Wir machen dies folgendermaßen:

  1. Verschieben Sie $rootmnt nach /${rootmnt}.ro (das ist die schreibgeschützte Ebene).
  2. Hängen Sie unsere beschreibbare Ebene als tmpfs in /${rootmnt}.rw ein.
  3. Fügen Sie die Union auf ${rootmnt} ein.

Zusätzlich möchten wir einen von der Union unabhängigen Zugang zu den schreibgeschützten und Lese-/Schreibebenen haben. Um Zugänge zu diesen Trägern zu haben, müssen wir sie in einen neuen Einbaupunkt unter ${rootmnt} einbinden. Wir machen dies mit  “mount –bind”.

Die Union ist weiterhin in der Lage, auf den originalen schreibgeschützten und Lese-/Schreib-Zugang zuzugreifen, selbst nachdem die Stammfunktion gedreht und init gestartet wurde. Das führt dazu, dass diese Einhängepunkte aus dem neuen Stammdateisystem herausfallen. Ich nehme an, dass aufs diese Verzeichnisse zur Einhängezeit öffnet und die Dateisysteme weiterhin so lange zugänglich sind, wie die Prozesse offene Dateizugriffe haben. Der Kernel scheint im Umgang mit dieser Art von interessanten Situationen ziemlich schlau zu sein.

Um zum Wesentlichen zurückzukommen: Hier ist das init-bottom-Skript, das wir verwenden werden (scripts/init-bottom/ro_root):

#!/bin/sh

PREREQ=''

prereqs() {
  echo "$PREREQ"
}

case $1 in
prereqs)
  prereqs
  exit 0
  ;;
esac

ro_mount_point="${rootmnt%/}.ro"
rw_mount_point="${rootmnt%/}.rw"

# Create mount points for the read-only and read/write layers:
mkdir "${ro_mount_point}" "${rw_mount_point}"

# Move the already-mounted root filesystem to the ro mount point:
mount --move "${rootmnt}" "${ro_mount_point}"

# Mount the read/write filesystem:
mount -t tmpfs root.rw "${rw_mount_point}"

# Mount the union:
mount -t aufs -o "dirs=${rw_mount_point}=rw:${ro_mount_point}=ro" root.union "${rootmnt}"

# Correct the permissions of /:
chmod 755 "${rootmnt}"

# Make sure the individual ro and rw mounts are accessible from within the root
# once the union is assumed as /.  This makes it possible to access the
# component filesystems individually.
mkdir "${rootmnt}/ro" "${rootmnt}/rw"
mount --bind "${ro_mount_point}" "${rootmnt}/ro"
mount --bind "${rw_mount_point}" "${rootmnt}/rw"

Neuaufbau der initramfs

Der Hook und das init-bottom-Skript, das wir oben geschrieben haben, können an den folgenden Orten entsprechend eingerichtet werden:

  • /etc/initramfs-tools/hooks/ro_root.
  • /etc/initramfs-tools/scripts/init-bottom/ro_root.

Sie sollten beide das Ausführerlaubnis-Bit gesetzt haben. 

Nachdem Sie die Dateien an den richtigen Ort kopiert haben, generieren Sie Ihre initramfs neu mit Update-initramfs:

update-initramfs -u

Der -u-Schalter weist update-initramfs an, die initramfs für den neuesten Kernel auf dem System zu aktualisieren. Ich nehme an, dass dies der Kernel ist, den Sie einsetzen. Für die meisten Embedded- oder anderen Einzweck-Maschinen ist typischerweise nur ein Kernel installiert.

Booting

Das System sollte den Anschein machen, als würde es booten ohne die von uns vorgenommenen Änderungen. Dennoch können Sie, nachdem das Booten beendet ist, Folgendes bestätigen:

  • /ro enthält das schreibgeschützte Basis-Dateisystem.
  • /rw enthält die Lese-/Schreibebene und hat meist einige neue Dateien, die unmittelbar nach dem Boot folgen (/var/run, etc.).
  • Wenn Sie eine Datei erstellen und dann rebooten, wird die Datei weg sein.

System wie dieses einige Einschränkungen:

  • Die Inhalte von /etc/mtab sind wahrscheinlich nicht korrekt, daher fehlen der Ausgabe des Einhängen-Befehls vermutlich einige Informationen. Es gibt Schritte, die wir vornehmen können, um /etc/mtab zu korrigieren, aber ich werde hier nicht im Detail darauf eingehen.
  • Kein Laufzeitstatus ist gespeichert. Vergessen Sie das nicht und speichern Sie eine Datei in der Erwartung, dass Sie nach einem Reboot noch vorhanden ist!
  • Subtile semantische Unterschiede zwischen aufs, tmpfs und herkömmlichen Dateisystemen können Probleme mit einigen Anwendungen verursachen. Die meisten Anwendungen werden es nicht bemerken, aber die, die fortgeschrittenere Dateisystem-Funktionen nutzen oder sich auf Details der Dateisystem-Implementierungen verlassen, könnten auf Fehler stoßen oder, noch schlimmer, subtil scheitern. Ich glaube, die meisten dieser Arten von Problemen gehören mittlerweile der Vergangenheit an, aber wenn Sie auf der rätselhaften Suche nach Fehlern sind, denken Sie daran.

Diese Art der Systemanpassung zeigt wirklich die Leistung und Flexibilität der Konfigurationsinfrastruktur der initramfs-Tools. Dieser Architektur-Stil ist in Debian und Ubuntu üblich und macht diese Ausgaben zur idealen Wahl für Embedded- und angewandte Informatikprojekte.

Ich hoffe, das war hilfreich. Wie immer, freue ich mich auf Kommentare und Fragen.

Verbesserungen

[Abschnitt hinzugefügt am 23.02.2009, aktualisiert am 28.04.2009]

Das folgende aktualisierte Skript beinhalten einige Verbesserungen, die bei gewissen Problemen geholfen haben, auf die die Kommentierenden gestoßen sind:

  • Booten Sie mit dem normal eingestellten Lese-/Schreib-Stammdateisystem, wenn der Nutzer einen Einzelbenutzermodus (auch Wiederherstellungsmodus) anfordert.
  • Verhindern Sie die Ausführung von /etc/init.d/checkroot.sh beim Booten in das schreibgeschützte System.
  • Verwenden Sie “mount –move instead of mount –bind” beim Verschieben der Einhängepunkte in die neue Root.
#!/bin/sh

PREREQ=''

prereqs() {
  echo "$PREREQ"
}

case $1 in
prereqs)
  prereqs
  exit 0
  ;;
esac

# Boot normally when the user selects single user mode.
if grep single /proc/cmdline >/dev/null; then
  exit 0
fi

ro_mount_point="${rootmnt%/}.ro"
rw_mount_point="${rootmnt%/}.rw"

# Create mount points for the read-only and read/write layers:
mkdir "${ro_mount_point}" "${rw_mount_point}"

# Move the already-mounted root filesystem to the ro mount point:
mount --move "${rootmnt}" "${ro_mount_point}"

# Mount the read/write filesystem:
mount -t tmpfs root.rw "${rw_mount_point}"

# Mount the union:
mount -t aufs -o "dirs=${rw_mount_point}=rw:${ro_mount_point}=ro" root.union "${rootmnt}"

# Correct the permissions of /:
chmod 755 "${rootmnt}"

# Make sure the individual ro and rw mounts are accessible from within the root
# once the union is assumed as /.  This makes it possible to access the
# component filesystems individually.
mkdir "${rootmnt}/ro" "${rootmnt}/rw"
mount --move "${ro_mount_point}" "${rootmnt}/ro"
mount --move "${rw_mount_point}" "${rootmnt}/rw"

# Make sure checkroot.sh doesn't run.  It might fail or erroneously remount /.
rm -f "${rootmnt}/etc/rcS.d"/S[0-9][0-9]checkroot.sh