Home>Berichten>Techniek uitgelegd>How To: Bouw een alleen-lezen Linux-System

How To: Bouw een alleen-lezen Linux-System

By ·Categorieën: Techniek uitgelegd·Published On: juli 14th, 2022·10,8 min read·

Van tijd tot tijd ontvangen we vragen van klanten die hun Linuxplatforms alleen-lezen willen maken om de levensduur van hun flashstations en solid-state opslagapparatuur te maximaliseren. We vonden dit een mooie kans om een blog te schrijven waarin precies wordt uitgelegd hoe je een alleen-lezen Linux-systeem bouwt.

Er zijn een aantal verschillende manieren om een Linux systeem alleen-lezen te maken. Helaas is het doorgaans iets ingewikkelder dan gewoon een conventioneel bestandssysteem met de alleen-lezen optie gebruiken. Voor veel programma’s moet het systeem ten minste gedeeltelijk beschreven kunnen worden. In sommige gevallen werken deze programma’s niet correct als dit niet het geval is.

Ik zal de aanpak beschrijven die volgens mij de beste is voor de meeste toepassingen. Deze is vergelijkbaar met de aanpak van één van de belangrijkste live cd-distributies.

Live cd’s hadden over het algemeen alleen-lezen toegang tot een root-bestandssysteem. Dit was vaak gecomprimeerd in één bestand dat later met een loopback-apparaat kon worden gekoppeld. Knoppix bedacht iets nieuws door hiervoor het cloopbestandssysteem te gebruiken. Andere live distributies gaan een stap verder door een union-bestandssysteem te gebruiken om het root-bestandssysteem beschrijfbaar te maken. Deze aanpak is ook handig voor onze doelstelling.

Union-bestandssystemen

In het algemeen combineert een union-bestandssysteem meerdere bestandssystemen in één virtueel bestandssysteem. Doorgaans verwijzen wij naar twee populaire union-bestandssystemen: unionfs en aufs. Beide hebben hetzelfde basismodel. Het volgende is een zeer vereenvoudigde weergave:

  • Bestandssystemen zijn verticaal opgestapeld.
  • Leestoegang wordt op elk bestandssysteem van boven naar beneden geprobeerd. Het eerste bestandssysteem dat het te lezen bestand bevat wordt gebruikt voor de leesopdracht. 
  • Schrijftoegang wordt op eenzelfde wijze uitgevoerd, maar bestanden die worden geschreven, worden opgeslagen in het hoogste beschrijfbare bestandssysteem. Meestal betekent dit dat er de union één beschrijfbare laag bevat. Als bestanden in de alleen-lezen laag ook geschreven zijn, worden deze eerst gekopieerd naar de volgende hoogste beschrijfbare laag. 

Natuurlijk zijn er nog andere details en bijzaken die ik hier niet bespreek. Het belangrijkste is dat we een alleen-lezen bestandssysteem kunnen gebruiken (mag een gecomprimeerde bestandssysteem-image of flashapparaat zijn met een meer conventioneel bestandssysteem, zoals ext3) en hierop een beschrijfbaar systeem bouwen. Het enige dat we nodig hebben is een beschrijfbaar bestandssysteem dat we kunnen verenigen met de alleen-lezen laag.

De beschrijfbare laag

Welk 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 verdwijnen zodra het systeem wordt afgesloten of opnieuw opgestart.

Voor persistentie over de gehele directorystructuur van het systeem heb je een voortdurend beschrijfbare laag nodig. Dit is doorgaans een conventioneel bestandssysteem op een ander media-apparaat (mogelijk een tweede disk). Dit is waarschijnlijk heel handig voor live systemen of thin clients waar een alleen-lezen basis niet zozeer wordt gebruikt om de levensduur te verlengen, maar eerder om de lokale opslagvereisten te minimaliseren.

Persistentie heb je in de meeste gevallen alleen nodig voor specifieke bestanden. Als je bijvoorbeeld een kiosk hebt die gebruikersinput opslaat in een lokale database, moet de database permanent beschikbaar zijn op de disk. Tijdelijke bestanden of andere tijdelijke runtime gegevens wil je echter niet permanent opslaan. Je pakt deze veelvoorkomende situatie het best aan door een tmpfs lees-/schrijflaag te koppelen en dan op een willekeurig koppelpunt, zoals /var/local/data (bijvoorbeeld) een aantal schrijfbare media te koppelen.

Implementatie

De implementatie van een alleen-lezen systeem vereist aansluiting in het opstartproces. Hoe je dit doet, hangt af van de betreffende distributie en kan mogelijk op meerdere manieren worden gedaan. 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 wijziging aan te brengen omdat we ervoor kunnen zorgen dat het union-bestandssysteem vroeg in het opstartproces wordt gekoppeld.

initramfs-tools

Ubuntu heeft een uitgebreid systeem voor het bouwen van de initramfs, de zogeheten “initramfs-tools”. Hiermee kunnen we een aantal scripts in de initramfs plaatsen. Er zijn verschillende manieren waarop de initramfs-tools uitgebreid kunnen worden: “hooks” en “scripts.”     

Hooks worden uitgevoerd wanneer de initramfs wordt gebouwd. Ze zijn handig om kernelmodules of uitvoerbare bestanden aan de initramfs-image toe te voegen. Hooks die met pakketten worden gedistribueerd, worden normaal gesproken geïnstalleerd in /usr/share/initramfs-tools/hooks. Ze maken gebruik van de functies die in usr/share/initramfs-tools/hook-functions zijn gedefinieerd. Lokale hooks moet je in /etc/initramfs-tools/hooks neerzetten.

Scripts worden tijdens het opstarten in de initramfs-omgeving uitgevoerd. Hiermee kun je het vroege opstartproces aanpassen. Net als hooks worden scripts die via pakketten worden gedistribueerd normaal gesproken geïnstalleerd in /usr/share/initramfs-tools/scripts. Lokale scripts horen in /etc/initramfs-tools/scripts te worden neergezet.

Het genereren van initramfs wordt uitgevoerd door de configuratiebestanden 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 opstartmethode kan in initramfs.conf worden geconfigureerd door de waarde van de variabele “BOOT” te veranderen. Deze is standaard “local”; dit is een opstartmethode waarbij het root-bestandssysteem aan een lokaal medium wordt gekoppeld, zoals een harddisk.

Voor elke opstartmethode is er een gelijknamig script dat bepaalt hoe deze opstartmethode 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. Zo worden alle scripts in /usr/share/initramfs-tools/local-premount door het “local” script uitgevoerd net voordat het root-bestandssysteem wordt gekoppeld. Het init-script zelf (dat fungeert als proces # 1 tot het punt waar de echte init daemon wordt gestart, na koppeling van het root-bestandssysteem) biedt soortgelijke hooks. Zie de inhoud van /usr/share/initramfs-tools/scripts om een idee te krijgen van welke andere hooks beschikbaar zijn.

Ten slotte moeten zowel hooks als scripts zo worden geschreven dat ze, als ze worden uitgevoerd met één ‘prereqs’-argument, een door spaties gescheiden lijst weergeven 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 afhankelijkheden tussen hooks en scripts. Persoonlijk maak ik zelden gebruik van deze functie, maar hij is er voor het geval jouw toepassing deze vereist.

Hooks en scripts

We implementeren ons alleen-lezen systeem door één hook en één script te introduceren. Ons script wordt eigenlijk een init-bottom script; het wordt uitgevoerd nadat het echte root-apparaat is gekoppeld. We willen uiteindelijk het reeds gekoppelde root-bestandssysteem gaan gebruiken als de basis voor een aufs-union met een beschrijfbare tmpfslaag. We kunnen het standaard Ubuntu-configuratiemechanisme dan blijven gebruiken om het apparaat op te geven 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). Je leest zo waarom we chmod nodig hebben. Onze hook is vrij eenvoudig (zoals de meeste zijn). We noemen deze 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 en zorgt ervoor dat de bestandssystemen allemaal op de juiste plaatsen worden gekoppeld. Op dit moment van het opstartproces is het echte root-apparaat op $rootmnt gekoppeld en staat /sbin/init op dat koppelpunt op het punt om te worden uitgevoerd. Hier willen we de koppeling van het root-apparaat naar een ander koppelpunt verplaatsen en op die plaats onze union-koppeling bouwen.

Dit gaat als volgt:

  1. Verplaats $rootmnt naar /${rootmnt}.ro (dit is de alleen-lezen laag).
  2. Koppel de beschrijfbare laag als tmpfs op /${rootmnt}.rw.
  3. Koppel de union op ${rootmnt}. 

Daarnaast moeten we de alleen-lezen en lees-/schrijflagen misschien onafhankelijk van de union kunnen benaderen. Om deze koppelingen te behouden, moeten we ze binden aan een nieuw koppelpunt onder ${rootmnt}. We doen dit met “mount –bind”.

De union heeft nog steeds toegang tot de oorspronkelijke alleen-lezen- en lees-/schrijfkoppelingen, zelfs nadat de root is geroteerd en init is gestart; daardoor vallen die koppelpunten buiten het nieuwe root-bestandssysteem. Ik ga ervan uit dat aufs deze mappen tijdens het koppelmoment opent en dat de bestandssystemen toegankelijk blijven zolang processen open bestandsingangen hebben. De kernel lijkt behoorlijk slim te zijn in de omgang met dit soort interessante situaties.

Maar dat terzijde. Hier is 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”

# Maak koppelpunten voor de alleen-lezen- en lees-/schrijflagen:

mkdir “${ro_mount_point}” “${rw_mount_point}”

#

Verplaats het gekoppelde root-bestandssysteem naar het ro-koppelpunt:

mount –move “${rootmnt}” “${ro_mount_point}”

# Koppel het lees-/schrijfbestandssysteem:

mount -t tmpfs root.rw “${rw_mount_point}”

# Koppel de union:

mount -t aufs -o “dirs=${rw_mount_point}=rw:${ro_mount_point}=ro” root.union “${rootmnt}”

#Corrigeer de machtigingen van /:

chmod 755 “${rootmnt}”

#Controleer of de afzonderlijke ro- en rw-koppelingen toegankelijk zijn

# vanuit de root zodra de union is verondersteld als /. Zo kunnen de

# samengestelde bestandssystemen afzonderlijk worden benaderd.

mkdir “${rootmnt}/ro” “${rootmnt}/rw”

mount –bind “${ro_mount_point}” “${rootmnt}/ro”

mount –bind “${rw_mount_point}” “${rootmnt}/rw”

De initramfs opnieuw bouwen

Het hook en init-bottom-script dat we hierboven geschreven hebben, kunnen we op de volgende locaties installeren, respectievelijk: 

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

Voor beide moet de uitvoeringsmachtiging zijn ingesteld.

Wanneer je de bestanden naar deze locaties hebt gekopieerd, genereer je de initramfs opnieuw met update-initramfs:

update-initramfs -u

De schakeloptie –u instrueert 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 maar één kernel geïnstalleerd.

Opstarten

Je zou denken dat het systeem zonder de aangebrachte wijzigingen zou moeten opstarten. Maar zodra het opstarten is voltooid, zie je het volgende:

  • /ro bevat het alleen-lezen basisbestandssysteem.
  • /rw bevat de lees-schrijflaag en bevat direct na het opstarten doorgaans een aantal nieuwe bestanden (/var/run, etc.).
  • Als je een bestand maakt en dan opnieuw opstart, is het bestand verdwenen.

Natuurlijk heeft een systeem als dit een paar kanttekeningen:

  •  De inhoud van /etc/mtab is waarschijnlijk niet correct, dus de output van de mount-opdracht 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 genereren of mislukken. Ik denk echter dat de meeste van dit soort problemen nu tot het verleden behoren, maar als je vreemde fouten tegenkomt, houd dit dan in je achterhoofd.

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 je hier iets aan hebt gehad. Als je nog vragen hebt over hoe je een alleen-lezen Linux-systeem bouwt, neem dan vandaag nog contact op met ons team.

Verbeteringen

[Sectie toegevoegd op 23 februari 2009, bijgewerkt op 14 juli 2022.]

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

  • Opstarten met normaal gekoppeld lees-/schrijf root-bestandssysteem wanneer de gebruiker de modus met één gebruiker aanvraagt (ook wel herstelmodus genoemd).
  • Voorkomen dat /etc/init.d/checkroot.sh wordt uitgevoerd tijdens het opstarten van het alleen-lezen systeem.
  • Het gebruik van mount –move in plaats van mount –bind bij het verplaatsen van de ro- en rw-koppelpunten naar de nieuwe root.

#!/bin/sh

PREREQ=”

prereqs() {

  echo “$PREREQ”

}

case $1 in

prereqs)

  prereqs

  exit 0

  ;;

esac

# Start normaal op wanneer de gebruiker de 1-gebruikersmodus selecteert.

if grep single /proc/cmdline >/dev/null; then

  exit 0

fi

ro_mount_point=”${rootmnt%/}.ro”

rw_mount_point=”${rootmnt%/}.rw”

# Maak koppelpunten voor de alleen-lezen en lees-/schrijflagen:

mkdir “${ro_mount_point}” “${rw_mount_point}”

# Verplaats het gekoppelde root-bestandssysteem naar het ro-koppelpunt:

mount –move “${rootmnt}” “${ro_mount_point}”

# Koppel het lees-/schrijfbestandssysteem:

mount -t tmpfs root.rw “${rw_mount_point}”

# Koppel de union:

mount -t aufs -o “dirs=${rw_mount_point}=rw:${ro_mount_point}=ro” root.union “${rootmnt}”

# Corrigeer de machtigingen van /:

chmod 755 “${rootmnt}”

# Controleer of de afzonderlijke ro- en rw-koppelingen toegankelijk zijn

# vanuit de root zodra de union is verondersteld als /. Zo kunnen de

# samengestelde bestandssystemen afzonderlijk worden benaderd.

mkdir “${rootmnt}/ro” “${rootmnt}/rw”

mount –move “${ro_mount_point}” “${rootmnt}/ro”

mount –move “${rw_mount_point}” “${rootmnt}/rw”

# Controleer of checkroot.sh niet wordt uitgevoerd. Deze kan mislukken of abusievelijk opnieuw worden gekoppeld /.

rm -f “${rootmnt}/etc/rcS.d”/S[0-9][0-9]checkroot.sh

Ontvang de laatste Tech Updates

Abonneer je op onze nieuwsbrief en ontvang updates van OnLogic. Hoor als eerste OnLogic nieuws en inzichten van onze experts. Meld je aan op de inschrijfpagina.

Inschrijven

Delen