Een diepgaand onderzoek naar SDCardFS, Google's vervanger voor FUSE, en hoe de implementatie ervan de I/O-overhead zal verminderen.
Enkele maanden geleden heeft Google iets toegevoegd genaamd “SDCardFS” naar de officiële AOSP-takken voor de Linux-kernel. Destijds werd de verhuizing alleen opgemerkt door sommige kernelontwikkelaars, maar vloog verder onder de radar van de meeste gebruikers. Geen verrassing gezien het feit dat de meeste gebruikers, inclusief ikzelf, niet echt weten wat er zich afspeelt onder de motorkap van het Android-besturingssysteem en de kernel ervan.
Echter, de meest recente aflevering van de Android-ontwikkelaars backstage podcast hernieuwde interesse in dit onderwerp. De podcast, gehost door Chet Haase (een senior software-ingenieur bij Google), onderzocht recente en aanstaande wijzigingen in de kernel. Op de show was een Linux-kernelontwikkelaar werkzaam bij het Android-team: Rom Lemarchand. Het duo besprak vooral welke wijzigingen er waren aangebracht om A/B-updates mogelijk te maken, maar in de laatste vijf minuten van de aflevering sprak de heer Lemarchand over “the next big thing” waar zijn team aan werkte:
SDCardFS.Ik moet toegeven dat ik na het luisteren naar deze podcast over het bestaan van SDCardFS hoorde. Natuurlijk was ik niet de enige die interesse had in dit onderwerp, want a recente Reddit-thread heeft getoond. Ik was echter niet tevreden met de basisuitleg die in de podcast werd aangeboden, en in een poging om een deel van de problemen weg te nemen Omdat er desinformatie werd verspreid, heb ik zelf wat onderzoek gedaan en met een paar experts gesproken met relevante kennis over de kwestie materie.
Grote dank aan softwareontwikkelaar Michal Kowalczyk voor het bijdragen van zijn kennis aan dit artikel en voor het nemen van de tijd om mijn vragen te beantwoorden.
‘Extern’ is echt intern
Er zullen ongetwijfeld enkele misvattingen zijn die we moeten ophelderen, anders zal de rest van het artikel erg verwarrend zijn. Het is nuttig om de geschiedenis van SD-kaarten en Android-telefoons te bespreken.
In de begindagen van Android-telefoons was bijna elk apparaat afhankelijk van het gebruik van hun microSD-kaarten voor opslag. Dit was te wijten aan het feit dat telefoons destijds werden geleverd met minuscule interne opslagcapaciteiten. SD-kaarten die worden gebruikt voor het opslaan van applicaties bieden echter vaak geen geweldige gebruikerservaring, althans niet vergeleken met de snelheid waarmee het interne flashgeheugen gegevens kan lezen/schrijven. Daarom werd het toenemende gebruik van SD-kaarten voor externe gegevensopslag een zorg voor de gebruikerservaring voor Google.
Vanwege de vroege verspreiding van SD-kaarten als externe opslagapparaten, waren de naamgevingsconventies van Android gebaseerd op het feit dat elk apparaat een daadwerkelijke, fysieke microSD-kaartsleuf had. Maar zelfs op apparaten die geen SD-kaartsleuf hadden, werd het /sdcard-label nog steeds gebruikt om naar de daadwerkelijke interne opslagchip te verwijzen. Nog verwarrender is het feit dat apparaten die zowel een fysieke SD-kaart als een opslagchip met hoge capaciteit voor opslag gebruikten, hun partities vaak een naam gaven op basis van de SD-kaart. In deze apparaten zou het /sdcard-mountpunt bijvoorbeeld verwijzen naar de daadwerkelijke interne opslagchip, terwijl zoiets als /storage/sdcard1 zou verwijzen naar de fysieke externe kaart.
Dus hoewel de microSD-kaart praktisch als een externe opslag wordt beschouwd, resulteerde de naamgevingsconventie erin dat “SDCard” lang bleef hangen na het daadwerkelijke gebruik van een fysieke kaart. Deze verwarring met opslag bezorgde applicatieontwikkelaars ook enige hoofdpijn vanwege het feit dat applicatiegegevens en de bijbehorende media gescheiden waren tussen de twee partities.
De lage opslagruimte van de eerste interne opslagchips zorgde ervoor dat gebruikers er op frustrerende wijze achter kwamen dat ze geen applicaties meer konden installeren (omdat de /data-partitie vol was). Ondertussen werden hun microSD-kaarten met grotere capaciteit gedegradeerd tot het bevatten van alleen media (zoals foto's, muziek en films). Gebruikers die vroeger onze forums bezochten, herinneren zich misschien deze namen: Link2SD en Apps2SD. Dit waren (root)oplossingen waarmee gebruikers hun applicaties en de bijbehorende gegevens allemaal op de fysieke SD-kaart konden installeren. Maar dit waren verre van perfecte oplossingen, dus Google moest ingrijpen.
Het is bekend dat Google al heel vroeg de stekker uit SD-kaarten trok. De Nexus One blijft het enige Nexus-apparaat met een microSD-kaartsleuf (en dat zal voor altijd zo blijven aangezien het merk Nexus feitelijk dood is). Met de Nexus S was er nu nog maar één uniforme partitie voor het opslaan van alle applicatiegegevens en media: de /data-partitie. Wat ooit bekend stond als het /sdcard-mountpunt, verwees nu eenvoudigweg naar een virtueel bestandssysteem (geïmplementeerd onder de SAMENSMELTEN protocol zoals hieronder besproken) in de gegevenspartitie - /data/media/0.
Om de compatibiliteit te behouden en verwarring te verminderen, gebruikte Google nog steeds deze nu virtuele ‘sdcard’-partitie om media op te slaan. Maar nu deze virtuele “sdcard”-partitie zich daadwerkelijk in /data bevond, zou alles wat daarin was opgeslagen, meetellen voor de opslagruimte van de interne opslagchip. Het was dus aan OEM's om te overwegen hoeveel ruimte ze moesten toewijzen aan applicaties (/data) versus media (/data/media).
Google hoopte dat fabrikanten hun voorbeeld zouden volgen en SD-kaarten zouden afschaffen. Gelukkig konden telefoonfabrikanten deze componenten in de loop van de tijd met hogere capaciteiten leveren en tegelijkertijd kosteneffectief blijven, zodat de behoefte aan SD-kaarten schaars begon te worden. Maar de naamgevingsconventies zijn blijven bestaan om de hoeveelheid moeite die ontwikkelaars en OEM's zouden moeten doen om zich aan te passen, te verminderen. Wanneer we het momenteel hebben over ‘externe opslag’, bedoelen we dat een van de twee dingen: de daadwerkelijke verwijderbare microSD-kaart of de virtuele “SDCard”-partitie in /data/media. Deze laatste zijn praktisch gezien is eigenlijk interne opslag, maar de naamgevingsconventie van Google onderscheidt dit vanwege het feit dat deze gegevens toegankelijk zijn voor de gebruiker (bijvoorbeeld wanneer deze op de computer is aangesloten).
Wanneer we het momenteel hebben over ‘externe opslag’, bedoelen we dat een van de twee dingen: de daadwerkelijke verwijderbare microSD-kaart of de virtuele “SDCard”-partitie in /data/media.
De geschiedenis van de virtuele bestandssystemen van Android
Nu “sdcard” wordt behandeld als een virtueel bestandssysteem, betekende dit dat het kon worden geformatteerd als elk bestandssysteem dat Google wilde. Vanaf de Nexus S en Android 2.3 heeft Google ervoor gekozen om “sdcard” te formatteren als VFAT (virtuele FAT). Deze stap was destijds logisch, omdat het monteren van VFAT vrijwel elke computer toegang zou geven tot de gegevens die op je telefoon zijn opgeslagen. Er waren echter twee grote problemen bij deze eerste implementatie.
Bij de eerste gaat het primair om de eindgebruiker (u). Om uw apparaat op uw computer aan te sluiten, gebruikt u de USB-massaopslagmodus om gegevens over te dragen. Hiervoor moest het Android-apparaat echter de virtuele partitie ontkoppelen voordat de computer toegang kon krijgen tot de gegevens. Als een gebruiker zijn apparaat wil gebruiken terwijl het is aangesloten, worden veel dingen weergegeven als niet beschikbaar.
De introductie van het Media Transfer Protocol (MTP) heeft dit eerste probleem opgelost. Wanneer de computer is aangesloten, beschouwt uw computer uw apparaat als een “media-opslagapparaat”. Er wordt een lijst met bestanden van uw telefoon opgevraagd en MTP retourneert een lijst met bestanden die de computer van het apparaat kan downloaden. Wanneer wordt verzocht om verwijdering van een bestand, verzendt MTP een opdracht om het aangevraagde bestand uit de opslag te verwijderen. In tegenstelling tot de USB-massaopslagmodus, waarbij feitelijk de “sd-kaart” wordt geactiveerd, kan de gebruiker met MTP zijn apparaat blijven gebruiken terwijl het is aangesloten. Bovendien is het bestandssysteem op de Android-telefoon niet langer van belang voor de computer om de bestanden op het apparaat te herkennen.
Ten tweede was er het feit dat VFAT niet het robuuste toestemmingsbeheer bood dat Google nodig had. In het begin behandelden veel applicatieontwikkelaars de ‘sdcard’ als een dumpplaats voor de gegevens van hun applicatie, zonder een uniform idee van waar ze hun bestanden moesten opslaan. Veel applicaties zouden eenvoudigweg een map met de app-naam maken en de bestanden daar opslaan.
Bijna elke applicatie die er destijds was, vereiste de SCHRIJF_EXTERNAL_STORAGE toestemming om hun applicatiebestanden naar de externe opslag te schrijven. Wat echter verontrustender was, was het feit dat bijna elke toepassing ook de LEES_EXTERNAL_STORAGE toestemming - gewoon om hun eigen gegevensbestanden te lezen! Dit betekende dat applicaties eenvoudig toegang konden krijgen tot gegevens die overal op de externe opslag waren opgeslagen, en zo'n toestemming werd vaak door de gebruiker verleend omdat het voor veel apps nodig was om gelijk te zijn functie.
Google zag dit duidelijk als problematisch. Het hele idee achter toestemmingsbeheer is om te scheiden waartoe apps wel en geen toegang hebben. Als bijna elke app leestoegang krijgt tot potentieel gevoelige gebruikersgegevens, is deze toestemming zinloos. Daarom besloot Google dat ze een nieuwe aanpak nodig hadden. Dat is waar FUSE in beeld komt.
Bestandssysteem in gebruikersruimte (FUSE)
Vanaf Android 4.4 besloot Google om de virtuele “sdcard”-partitie niet langer als VFAT te mounten. In plaats daarvan begon Google FUSE te gebruiken om FAT32 te emuleren op de virtuele “sdcard”-partitie. Met het sdcard-programma dat aanroept FUSE om mapmachtigingen in FAT-op-sdcard-stijl te emuleren, zouden applicaties toegang kunnen krijgen tot de gegevens die zijn opgeslagen op externe opslag zonder dat er toestemming nodig is. Vanaf API-niveau 19 was READ_EXTERNAL_STORAGE niet langer nodig om toegang te krijgen tot bestanden die zich op externe opslag - op voorwaarde dat de gegevensmap die door de FUSE-daemon is gemaakt, overeenkomt met de pakketnaam van de app. FUSE zou het afhandelen het synthetiseren van de eigenaar, groep en modi van bestanden op externe opslag wanneer een applicatie is geïnstalleerd.
FUSE verschilt van in-kernelmodules omdat het niet-bevoorrechte gebruikers de mogelijkheid biedt virtuele bestandssystemen te schrijven. De reden dat Google FUSE heeft geïmplementeerd is vrij eenvoudig: het deed wat ze wilden en was het al goed begrepen en gedocumenteerd in de Linux-wereld. Om een te citeren Google-ontwikkelaar over de kwestie:
"Omdat FUSE een mooie stabiele API is, is er vrijwel geen onderhoudswerk nodig bij het wisselen tussen kernelversies. Als we zouden migreren naar een in-kernel-oplossing, zouden we ons aanmelden voor het onderhouden van een reeks patches voor elke stabiele kernelversie." -Jeff Sharkey, Software Engineer bij Google
Het werd echter vrij duidelijk dat de overhead van FUSE onder meer een prestatieverlies teweegbracht. De ontwikkelaar met wie ik over deze kwestie sprak, Michal Kowalczyk, heeft een uitstekende blogpost geschreven meer dan een jaar geleden waarin de huidige problemen met FUSE werden beschreven. Meer technische details zijn te lezen op zijn blog, maar ik zal zijn bevindingen (met zijn toestemming) in meer lekentermen beschrijven.
Het probleem met FUSE
In Android gebruikt de “sdcard” gebruikersruimte-daemon FUSE om /dev/fuse te koppelen aan de geëmuleerde externe opslagmap tijdens het opstarten. Daarna peilt de sdcard-daemon het FUSE-apparaat naar eventuele openstaande berichten van de kernel. Als je naar de podcast hebt geluisterd, heb je de heer Lemarchand misschien horen verwijzen naar FUSE die overhead introduceert tijdens I/O-bewerkingen - dit is in essentie wat er gebeurt.
In de echte wereld heeft deze prestatiehit invloed elk bestand opgeslagen op externe opslag.
Probleem #1 - I/O-overhead
Laten we zeggen dat we een eenvoudig tekstbestand maken, genaamd “test.txt”, en dit opslaan in /sdcard/test.txt (wat, laten we Ik wil u eraan herinneren dat het eigenlijk /data/media/0/test.txt is, ervan uitgaande dat de huidige gebruiker de primaire gebruiker op het apparaat). Als we dit bestand zouden willen lezen (command cat), zouden we verwachten dat het systeem 3 opdrachten geeft: openen, lezen en vervolgens sluiten. Zoals de heer Kowalczyk laat zien met behulp van spoor, dat is wat er gebeurt:
Maar omdat het bestand zich op de externe opslag bevindt die wordt beheerd door de sdcard-daemon, moeten er veel extra bewerkingen worden uitgevoerd. Volgens de heer Kowalczyk zijn er in wezen acht extra stappen nodig elk van deze 3 individuele commando's:
- Userspace-applicatie geeft een systeemoproep uit die zal worden afgehandeld door het FUSE-stuurprogramma in de kernel (we zien het in de eerste strace-uitvoer)
- Het FUSE-stuurprogramma in de kernel informeert de gebruikersruimte-daemon (sdcard) over een nieuw verzoek
- Userspace-daemon leest /dev/fuse
- Userspace-daemon parseert opdrachten en herkent bestandsbewerkingen (bijv. open)
- Userspace-daemon voert een systeemaanroep uit naar het daadwerkelijke bestandssysteem (EXT4)
- Kernel verzorgt de fysieke gegevenstoegang en stuurt gegevens terug naar de gebruikersruimte
- Userspace wijzigt (of niet) gegevens en geeft deze opnieuw via /dev/fuse door aan de kernel
- Kernel voltooit de oorspronkelijke systeemaanroep en verplaatst gegevens naar de daadwerkelijke gebruikersruimtetoepassing (in ons voorbeeld cat)
Dit lijkt erop veel van overhead tot slechts één enkele I/O-opdracht die moet worden uitgevoerd. En je zou gelijk hebben. Om dit aan te tonen probeerde de heer Kowalczyk twee verschillende I/O-tests uit: één waarbij een groot bestand werd gekopieerd en de andere waarbij veel kleine bestanden werden gekopieerd. Hij vergeleek de snelheid van FUSE (op de virtuele partitie gemonteerd als FAT32) die deze bewerkingen afhandelde met de snelheid van de kernel (op de gegevenspartitie geformatteerd als EXT4), en hij ontdekte dat de FUSE inderdaad een aanzienlijke bijdrage leverde boven het hoofd.
In de eerste test kopieerde hij onder beide testomstandigheden een bestand van 725 MB. Hij ontdekte dat de FUSE-implementatie grote bestanden overbracht 17% langzamer.
In de tweede test kopieerde hij 10.000 bestanden, elk met een grootte van 5 KB. In dit scenario was de FUSE-implementatie voorbij 40 seconden langzamer om in principe 50 MB aan gegevens te kopiëren.
In de echte wereld heeft deze prestatiehit invloed elk bestand opgeslagen op externe opslag. Dit betekent apps zoals Kaarten die grote bestanden opslaan op /sdcard, Muziek-apps die talloze muziekbestanden opslaan, Camera-apps en foto's, enz. Elke I/O-bewerking die wordt uitgevoerd waarbij de externe opslag betrokken is, wordt beïnvloed door de overhead van FUSE. Maar I/O-overhead is niet het enige probleem met FUSE.
Probleem #2 - Dubbele caching
Het cachen van gegevens is belangrijk voor het verbeteren van de gegevenstoegangsprestaties. Door essentiële stukjes gegevens in het geheugen op te slaan, kan de Linux-kernel die gegevens snel oproepen wanneer dat nodig is. Maar dankzij de manier waarop FUSE is geïmplementeerd, slaat Android de dubbele hoeveelheid cache op die nodig is.
Zoals de heer Kowalczyk laat zien, wordt verwacht dat een bestand van 10 MB in de cache wordt opgeslagen als exact 10 MB, maar in plaats daarvan wordt de cachegrootte vergroot met ongeveer 20 MB. Dit is problematisch op apparaten met minder RAM, omdat de Linux-kernelopslag paginacache gebruikt om gegevens op te slaan geheugen. Dhr. Kowalczyk testte dit dubbele caching-probleem met behulp van deze aanpak:
- Maak een bestand met een bekende grootte (voor testen: 10 MB)
- Kopieer het naar /sdcard
- Verwijder de paginacache
- Maak een momentopname van het gebruik van de paginacache
- Lees het testbestand
- Maak nog een momentopname van het gebruik van de paginacache
Wat hij ontdekte was dat voorafgaand aan zijn test 241 MB door de kernel werd gebruikt voor paginacache. Nadat hij zijn testbestand had gelezen, verwachtte hij dat er 251 MB zou worden gebruikt voor paginacache. In plaats daarvan ontdekte hij dat die kernel gebruikte 263 MB voor paginacache - ongeveer twee keer zoveel als verwacht. De reden dat dit gebeurt is omdat de gegevens eerst in de cache worden opgeslagen door de gebruikerstoepassing die oorspronkelijk de I/O-aanroep heeft uitgegeven (FUSE), en ten tweede door de sdcard-daemon (EXT4 FS).
Probleem #3 - Onvolledige implementatie van FAT32
Er zijn nog twee problemen die voortkomen uit het gebruik van FUSE die FAT32 emuleert en die minder bekend zijn in de Android-gemeenschap.
De eerste betreft onjuiste tijdstempels. Als je ooit een bestand (zoals een foto) hebt overgezet en hebt gemerkt dat de tijdstempel onjuist is, komt dat door de Android-implementatie van FUSE. Deze kwestie heeft bestond voor jaar. Om specifieker te zijn: de kwestie betreft de tijd() systeemoproep waarmee u de toegang en wijzigingstijd van een bestand kunt wijzigen. Helaas hebben oproepen naar de sdcard-daemon als standaardgebruiker niet de juiste toestemming om deze systeemaanroep uit te voeren. Er zijn oplossingen hiervoor, maar deze vereisen wel dat u dit doet root-toegang hebben.
Als je ooit een bestand (zoals een foto) hebt overgezet en hebt gemerkt dat de tijdstempel onjuist is, komt dat door de Android-implementatie van FUSE.
Het volgende probleem is zorgwekkender voor bedrijven die zoiets als een smartSD-kaart. Vóór FUSE konden app-makers de O_DIRECT vlag om te communiceren met een ingebouwde microcontroller in de kaart. Met FUSE hebben ontwikkelaars alleen toegang tot de cacheversie van een bestand en kunnen ze geen opdrachten zien die door een microcontroller zijn verzonden. Dit is problematisch voor sommige bedrijfs-/overheids-/bankapps die communiceren met microSD-kaarten met toegevoegde waarde.
FUSE dumpen voor SDCardFS
Sommige OEM's onderkenden deze problemen al vroeg en gingen op zoek naar een in-kerneloplossing om FUSE te vervangen. Samsung heeft bijvoorbeeld ontwikkeld SDCardFS die is gebaseerd op WrapFS. Deze in-kernel-oplossing emuleert FAT32 net zoals FUSE dat doet, maar ziet af van de I/O-overhead, dubbele caching en andere problemen die ik hierboven heb genoemd. (Ja, laat me dat punt herhalen, deze oplossing die Google nu implementeert is gebaseerd op het werk van Samsung).
Google zelf heeft eindelijk de nadelen van FUSE onderkend en is daarom begonnen met de overstap naar de in-kernel FAT32-emulatielaag, ontwikkeld door Samsung. Het bedrijf, zoals vermeld in de Android-ontwikkelaars backstage podcast, heeft gewerkt aan het beschikbaar maken van SDCardFS voor alle apparaten in een komende versie van de kernel. U kunt momenteel de voortgang van hun zien werken bij AOSP.
Als een Google-ontwikkelaar heeft het eerder uitgelegd, de grootste uitdaging bij het implementeren van een in-kernel-oplossing is hoe de pakketnaam eraan moet worden toegewezen applicatie-ID die nodig is voor een pakket om toegang te krijgen tot zijn eigen gegevens in externe opslag zonder dat deze nodig zijn rechten. Maar die verklaring werd een jaar geleden afgelegd en we hebben het punt bereikt waarop het team SDCardFS hun ‘next big thing’ noemt. Ze hebben al bevestigd dat de gevreesde tijdstempelfout is opgelost dankzij het verlaten van FUSE, dus we kunnen uitkijken naar alle veranderingen die voortkomen uit het verlaten van FUSE.
Misvattingen over het controleren van feiten
Als je zo ver in het artikel bent gekomen, dan een pluim voor het bijhouden van alles tot nu toe! Ik wilde een paar vragen verduidelijken die ik zelf had bij het schrijven van dit artikel:
- SDCardFS heeft niets te maken met echte SD-kaarten. Het wordt zo genoemd omdat het de I/O-toegang voor /sdcard afhandelt. En zoals u zich wellicht herinnert, is /sdcard een verouderd label dat verwijst naar de ‘externe’ opslag van uw apparaat (waar apps hun media opslaan).
- SDCardFS is geen traditioneel bestandssysteem zoals FAT32, EXT4 of F2FS. Het is een stapelbaar wrapper-bestandssysteem dat opdrachten doorgeeft aan de lagere, geëmuleerde bestandssystemen (in dit geval zou het FAT32 zijn op de /sd-kaart).
- Ten aanzien van MTP verandert er niets. U blijft MTP gebruiken om bestanden van/naar uw computer over te dragen (totdat Google een beter protocol heeft gevonden). Maar de tijdstempelfout wordt tenminste opgelost!
- Zoals eerder vermeld, wanneer Google verwijst naar 'Externe opslag', hebben ze het over de (in alle opzichten en doeleinden) interne /sdcard virtuele FAT32-partitie OF ze hebben het over een daadwerkelijke, fysieke, verwijderbare microSD kaart. De terminologie is verwarrend, maar dit is wat ons opvalt.
Conclusie
Door af te stappen van FUSE en een in-kernel FAT32-emulatielaag (SDCardFS) te implementeren, zal Google het aantal aanzienlijke I/O-overhead, het elimineren van dubbele caching en het oplossen van enkele obscure problemen met betrekking tot de FUSE-emulatie van FAT32.
Omdat deze wijzigingen in een kernel worden aangebracht, kunnen ze worden uitgerold zonder dat er een grote nieuwe versie van Android naast zit. Sommige gebruikers verwachten dat deze wijzigingen officieel worden geïmplementeerd in Android 8, maar het is mogelijk voor eventuele toekomstige OTA op een Pixel-apparaat zal de Linux-kernel versie 4.1 komen waar Google aan heeft gewerkt op.
Voor sommigen van jullie is SDCardFS geen nieuw concept. Samsung-apparaten maken er zelfs al jaren gebruik van (zij waren tenslotte degenen die het ontwikkelden). Sinds SDCardFS vorig jaar in AOSP werd geïntroduceerd, hebben sommige aangepaste ROM- en kernelontwikkelaars ervoor gekozen om het in hun werk te implementeren. CyanogenMOD overwoog op een gegeven moment om het te implementeren, maar draaide het terug toen gebruikers problemen ondervonden met hun foto's. Maar hopelijk kunnen Android-gebruikers op alle toekomstige apparaten profiteren van de verbeteringen die zijn geïntroduceerd door het verlaten van FUSE, nu Google de leiding over dit project overneemt.