Er lijken veel mensen op zoek te zijn om een op maat gemaakte applicatie te draaien op een Linux-gebaseerd platform op een solid-state opslagapparaat. Van tijd tot tijd ontvangen we vragen van klanten die hun linux platforms read-only willen maken om de levensduur van hun flash drives te maximaliseren. Ik vond dit een mooie kans om er een blogpost aan te wijden die beschrijft hoe je dit doet.

Er zijn een aantal verschillende manieren om een Linux systeem read-only te maken.Helaas is het normaalgesproken niet zo eenvoudig als gewoon een conventioneel bestandssysteem te gebruiken met de read-only optie. Veel programma’s verwachten dat tenminste op een aantal delen van het systeem kan worden geschreven. In sommige gevallen is de werking van deze programma’s niet correct als dit niet mogelijk is.

Ik zal de aanpak beschrijven waarvan ik denk dat deze het beste is voor de meeste toepassingen. Het is vergelijkbaar met de huidige generatie van live CD-distributies.

Live CD’s hebben over het algemeen read-only toegang tot een a root bestandssysteem. Deze is vaak gecomprimeerd in een enkel bestand om later gebruikt te worden met behulp van een loopback-apparaat. Knoppix heeft iets nieuws bedacht door het cloop bestandssysteem te gebruiken voor dit doel. Recentere live distributies gaan een stap verder door gebruik te maken van een union-bestandssysteem om het root-bestandssysteem beschrijfbaar te maken. Deze aanpak is ook best handig voor onze doelstelling.

Union-bestandssystemen

In het algemeen combineert een union-bestandssysteem meerdere bestandssystemen in een enkel virtueel bestandssysteem. Er zijn 2 populaire union-bestandssystemen waar ik bekend mee ben: unionfs en aufs. Beide hebben hetzelfde basismodel. Het volgende is een zeer versimpelde weergave:

  • Bestandssystemen zijn verticaal opgestapeld.
  • Read-toegang wordt geprobeerd op elk bestandssysteem in de volgorde van boven naar beneden. Het eerste bestandssysteem dat het te lezen bestand bevat wordt gebruikt voor de read-opdracht.
  • Schrijftoegang wordt op eenzelfde wijze uitgevoerd, maar bestanden die worden geschreven worden opgeslagen in het hoogst beschrijfbare bestandssysteem.

Meestal betekent dit dat er een enkele beschrijfbare laag is in de union. Als bestanden in de read-only laag ook geschreven zijn, worden deze eerst gekopieerd naar de volgende hoogst beschrijfbare laag. Natuurlijk zijn er nog andere verdiepingen en bijzaken die ik hier niet bespreek. Het belangrijkste is dat we een read-only bestandssysteem kunnen gebruiken (Mag een gecomprimeerde bestandssysteem afbeelding of flash-apparaat met een meer conventioneel bestandssysteem zijn, zoals ext3) en een schrijfbaar systeem hier bovenop bouwen. Het enige dat we nodig hebben is een beschrijfbaar bestandssysteem naar union met de read-only laag.

De beschrijfbare laag

Het soort beschrijfbare bestandssysteem je gebruikt, hangt af van wat je wilt bereiken. Als je geen persistentie nodig hebt tussen boots is het gemakkelijk om tmpfs te gebruiken. Schrijfopdrachten naar het systeem worden bewaard in RAM als het systeem aanstaat, maar zullen verdwijnen zodra het systeem wordt afgesloten of opnieuw opgestart.

 Wanneer je persistentie over de gehele directory structuur van het systeem wilt hebben, heb je een voortdurend beschrijfbare laag nodig. Dit is waarschijnlijk een conventioneel bestandssysteem op een ander media-apparaat (mogelijk een tweede disk). Dit is in de meeste gevallen handig voor live systemen of thin clients waar het gebruik van een read-only basis niet zoveel doet voor de levensduur, omdat het om de lokale opslagvereisten te minimaliseren is.

In veel gevallen, als je persistentie wilt, is het alleen voor specifieke bestanden nodig. Bijvoorbeeld; als je een kiosk hebt die gebruikersinput opslaat in een lokale database, moet de database bestaan op de disk, maar wil je waarschijnlijk niet dat er tijdelijke bestanden of andere dingen voor korte termijn runtime data opstaan. De beste aanpak voor dit veelvoorkomende geval is om een tmpfs read/write laag te hebben en deze koppelen aan een willekeurig koppelpunt, zoals like /var/local/data (bijvoorbeeld).

Implementatie

Implementatie van een read-only systeem vereist aansluiting in het boot proces. Hoe dit moet, hangt af van distributie tot distributie en kan mogelijk op meerdere manieren worden gedaan per distributie. In dit artikel demonstreer ik een aanpak die werkt met Ubuntu 8.04.

Ubuntu 8.04 gebruikt standaard een initramfs. Dit is de beste locatie om onze modificatie te maken, omdat we er voor kunnen zorgen dat het union bestandssysteem vroeg op het boot proces is aangesloten.

initramfs-tools

Ubuntu heeft een uitgebreid systeem voor het bouwen van de initramfs, genoemd“initramfs-tools”. We kunnen dit gebruiken om wat scripts in de initramfs te plaatsen.Er zijn een aantal verschillende manieren waarop de initramfs-tools uitgebreid kunnen worden: “hooks” en “scripts.”     

Hooks werken wanneer de initramfs wordt opgebouwd en zijn handig voor het toevoegen van kernel modules of executables naar de initramfs image. Hooks die worden verspreid met packages zijn normaalgesproken geïnstalleerd in /usr/share/initramfs-tools/hooks, en gebruiken de functies die zijn gemaakt in usr/share/initramfs-tools/hook-functions. Lokale hooks moet je plaatsen in /etc/initramfs-tools/hooks.

Scripts werken binnen de initramfs omgeving tijdens het booten. Deze kunnen gebruikt worden om het vroege boot proces aan te passen. Net als met hooks zijn scripts die worden verspreid met packages normaalgesproken geïnstalleerd in /usr/share/initramfs-tools/scripts. Lokale scripts horen naar het volgende te gaan: /etc/initramfs-tools/scripts.

initramfs generatie wordt beheerd bij de configuratiebestanden en bevinden zich in/etc/initramfs-tools. /etc/initramfs-tools/initramfs.conf is het primaire configuratiebestand, maar bestanden kunnen ook in /etc/initramfs-tools/conf.d geplaatst worden. De primaire boot methode kan worden geconfigureerd in initramfs.conf door het veranderen van de waarde van de variabele “BOOT.” Standaard is deze “local,” een boot methode die het root bestandssysteem op een lokale media plaatst, zoals een harddisk.

Voor elke boot-methode is er een script met die naam en bepaalt hoe deze boot-methode werkt. Er is bijvoorbeeld een script met de naam “local” dat definieert hoe een lokale boot wordt uitgevoerd. Veel van dergelijke scripts bieden ook hooks voor andere shell-scripts die op bepaalde punten tijdens het opstartproces moeten worden uitgevoerd.Bijvoorbeeld, alle scripts die zijn geplaatst in /usr/share/initramfs-tools/local-premount zullen worden uitgevoerd door het “local” script net voorafgaand aan het mounten van het rootbestandssysteem. Het init-script zelf (dat fungeert als proces # 1 tot het punt waar de echte init daemon wordt gestart, na mounting van het rootbestandssysteem) biedt soortgelijke hooks. Zie de inhoud van /usr/share/initramfs-tools/scripts om een idee te krijgen welke andere hooks beschikbaar zijn.

Ten slotte moeten zowel hooks als scripts zo worden geschreven dat, als ze worden uitgevoerd met één argument ‘prereqs’, ze een door spaties gescheiden lijst afdrukken met de namen van andere scripts of hooks die moeten worden uitgevoerd voordat dit specifieke script of hook wordt uitgevoerd. Dit biedt een eenvoudig systeem met dependencies tussen hooks en scripts. Ik merk dat ik zelden gebruik maak van deze functie, maar het is het wel beschikbaar indien jouw toepassing dit vereist.

Hooks and Scripts

We zullen ons read-only systeem implementeren door één hook en één script te introduceren. Ons script zal eigenlijk een init-bottom script zijn, uitgevoerd nadat het echte root-apparaat al is gemonteerd. Ons doel zal zijn om de reeds gemonteerde root bestandssysteem te nemen en het te gebruiken als de basis voor een aufs-unie met een tmpfs beschrijfbare laag. Hierdoor kunnen we het standaard Ubuntu configuratiemechanisme blijven gebruiken voor het specificeren van het apparaat dat het echte root-bestandssysteem bevat.

We hebben een hook nodig om de initramfs-tools te vertellen dat we een paar kernelmodules nodig hebben (aufs en tmpfs, die beide zijn opgenomen in Ubuntu 8.04) en een uitvoerbaar bestand (chmod). We zullen binnenkort zien waarom we chmod nodig hebben. Onze hook is vrij eenvoudig (zoals de meeste zijn). We noemen dit hooksro_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

Het script doet het echte werk om ervoor te zorgen dat de bestandssystemen allemaal op de juiste plaatsen zijn gemount. Op dit moment van het opstartproces is het echte root-apparaat op $rootmnt gemount en /sbin/init op dat mount-punt staat klaar om te worden uitgevoerd. Hier willen we de root-apparaat mount naar een ander mount-punt verplaatsen en onze union-mount op die plaats bouwen.

Hier is hoe we dit doen:

  1. Verplaats $rootmnt naar /${rootmnt}.ro (dit is de read-only laag).
  2. Mount onze beschrijfbare laag als tmpfs op /${rootmnt}.rw.
  3. Mount de union op ${rootmnt}.

Daarnaast willen we mogelijk toegang hebben tot de read-only en read/write-lagen onafhankelijk van de union. Om toegang tot deze mounts te behouden, moeten we ze binden aan een nieuw mount point onder ${rootmnt}. We doen dit met “mount –bind”.

De union heeft nog steeds toegang tot de oorspronkelijke read-only en read/write mounts, zelfs nadat de root is geroteerd en init is gestart, waardoor die mount-punten buiten het nieuwe root-bestandssysteem vallen. Ik ga ervan uit dat aufs deze mappen tijdens het mount-moment opent en dat de bestandssystemen toegankelijk blijven zolang processen open bestandsmogelijkheden hebben. De kernel lijkt behoorlijk slim te zijn in de omgang met dit soort interessante situaties.

Om erop terug te komen, is hier het init-bottom script dat we gebruiken (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"

Herbouwen van de initramfs

De hook en init-bottom script dat we hierboven geschreven hebben kan geïnstalleerd worden in de volgende locaties: 

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

Ze moeten beide de execute-permissie ingesteld hebben 

Na het kopiëren van de bestanden op deze locaties, regenereer je jouw initramfs met Update-initramfs:

update-initramfs -u

De –u switch vertelt update-initramfs om de initramfs voor de meest recente kernel op het systeem bij te werken. Ik neem aan dat dit de kernel is die je gebruikt. Voor de meeste embedded of andere single-purpose machines is meestal slechts één kernel geïnstalleerd.

Booting

Het systeem zou moeten opstarten zoals het is zonder de wijzigingen die we hebben aangebracht. Echter, zodra het opstarten is voltooid, kunt je bevestigen dat:

 

  • /ro bevat het read-only base bestandssysteem.
  • /rw bevat de read/write laag en heeft meestal een aantal nieuwe bestanden in de volgende boot (/var/run, etc.).
  • Als je een bestand creëert en dan reboot, zal het bestand weg zijn.

Natuurlijk heeft een systeem als dit een paar kanttekeningen:

  • De inhoud van /etc/mtab is waarschijnlijk niet correct, dus de output van het mount commando mist waarschijnlijk wat informatie. Er zijn stappen die we kunnen nemen om /etc/mtab te corrigeren, maar ik zal hier niet uitgebreid op ingaan.
  • Er wordt geen runtime-status bewaard. Vergeet dit niet en sla een bestand op, in de verwachting dat het er na een reboot zal zijn!
  • Subtiele semantische verschillen tussen aufs, tmpfs en traditionele bestandssystemen kunnen bij sommige toepassingen problemen veroorzaken. De meeste toepassingen zullen dit niet merken, maar toepassingen die meer geavanceerde functies van het bestandssysteem gebruiken of afhankelijk zijn van details van de implementatie van het bestandssysteem, kunnen fouten tegenkomen of, erger nog, subtiel falen. Ik geloof dat de meeste van dit soort problemen nu tot het verleden behoren, maar als je merkt dat je mysterieuze fouten tegenkomt, houd daar dan rekening mee.

Dit soort systeemaanpassing demonstreert echt de kracht en flexibiliteit van de configuratie-infrastructuur van initramfs-tools. Deze architecturale stijl is gebruikelijk in Debian en Ubuntu, waardoor deze distributies ideale keuzes zijn voor embedded en toegepaste computerprojecten.

Ik hoop dat dit behulpzaam is geweest. Zoals altijd, kijk ik uit naar reacties envragen.

Verbeteringen

[Sectie toegevoegd 2009-02-23, geüpdatet 2009-04-28.]

Het volgende bewerkte script bevat enkele verbeteringen die hebben geholpen bij enkele problemen waar gebruikers tegenaan liepen:

  • Opstarten met normaal gemounte write/read root-bestandssysteem wanneer de gebruiker een enkele gebruikersmodus aanvraagt (ook wel herstelmodus genoemd).
  • Voorkom dat /etc/init.d/checkroot.sh wordt uitgevoerd tijdens het opstarten van het read-only systeem.
  • Gebruik mount –move in plaats van mount –bind bij het verplaatsen van de ro en rw mount-punten in de nieuwe 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