En djupgående utforskning av SDCardFS, Googles ersättare för FUSE, och hur dess implementering kommer att minska I/O-overhead.
För flera månader sedan lade Google till något som heter "SDCardFS” till de officiella AOSP-grenarna för Linux-kärnan. På den tiden märktes flytten endast av vissa kärnutvecklare, men annars flög under radarn hos de flesta användare. Ingen överraskning med tanke på det faktum att de flesta användare, inklusive jag själv, inte riktigt vet vad som händer under huven på Android OS och dess kärna.
Men det senaste avsnittet av Android-utvecklare Backstage podcast förnyat intresse för detta ämne. Podcasten, värd av Chet Haase (en senior mjukvaruingenjör på Google), utforskade de senaste och kommande ändringarna som gjorts i kärnan. På programmet var en Linux-kärnutvecklare som arbetade på Android-teamet - Rom Lemarchand. Duon diskuterade i första hand vilka ändringar som gjordes för att tillgodose A/B-uppdateringar, men under de sista 5 minuterna av avsnittet pratade Lemarchand om "nästa stora sak" som hans team arbetade med - SDCardFS.
Jag måste erkänna att jag lärde mig om existensen av SDCardFS efter att ha lyssnat på denna podcast. Naturligtvis var jag inte ensam om att intressera mig för detta ämne, som en senaste Reddit-tråden har visat. Jag var dock inte nöjd med den grundläggande förklaringen som erbjöds i podcasten, och i ett försök att skingra en del av Desinformation spreds runt, jag gjorde lite egen forskning och pratade med några experter med relevant kunskap om materia.
Stort tack till mjukvaruutvecklaren Michal Kowalczyk för att han bidrog med sin kunskap till den här artikeln och för att han tog sig tid att svara på mina frågor.
"Extern" är verkligen intern
Direkt från början finns det säkert några missuppfattningar som vi måste reda ut - annars kommer resten av artikeln att bli väldigt förvirrande. Det är användbart att diskutera historien om SD-kort och Android-telefoner.
Under Android-telefonernas tidiga dagar förlitade sig nästan alla enheter på att använda sina microSD-kort för lagring. Detta berodde på det faktum att telefoner på den tiden levererades med minimal intern lagringskapacitet. Men SD-kort som används för att lagra applikationer ger ofta inte en fantastisk användarupplevelse, åtminstone jämfört med den hastighet med vilken internt flashminne kan läsa/skriva data. Därför blev den ökande användningen av SD-kort för extern datalagring ett problem för användarupplevelsen för Google.
På grund av den tidiga spridningen av SD-kort som externa lagringsenheter, baserades Androids lagringsnamnkonventioner kring det faktum att varje enhet hade en faktisk, fysisk microSD-kortplats. Men även på enheter som inte innehöll en SD-kortplats användes /sdcard-etiketten fortfarande för att peka på det faktiska interna lagringschippet. Mer förvirrande är det faktum att enheter som använde både ett fysiskt SD-kort och ett lagringschip med hög kapacitet ofta skulle namnge sina partitioner baserade på SD-kortet. Till exempel, i dessa enheter skulle /sdcard-monteringspunkten hänvisa till det faktiska interna lagringschippet, medan något som /storage/sdcard1 skulle hänvisa till det fysiska externa kortet.
Således, även om microSD-kortet praktiskt taget anses vara extern lagring, resulterade namngivningskonventionen i att "SDCard" höll sig kvar långt efter någon faktisk användning av ett fysiskt kort. Denna förvirring med lagring gav också en del huvudbry för applikationsutvecklare på grund av det faktum att applikationsdata och dess media var åtskilda mellan de två partitionerna.
Det låga lagringsutrymmet hos tidiga interna lagringschips resulterade i att användare frustrerande upptäckte att de inte längre kunde installera applikationer (på grund av att /data-partitionen är full). Samtidigt förpassades deras microSD-kort med större kapacitet till att endast innehålla media (som foton, musik och filmer). Användare som surfade på våra forum förr i tiden kanske kommer ihåg dessa namn: Link2SD och Apps2SD. Dessa var (rot)lösningar som gjorde det möjligt för användare att installera sina applikationer och dess data på det fysiska SD-kortet. Men dessa var långt ifrån perfekta lösningar, så Google var tvungen att träda in.
Kända, Google drog ur kontakten på SD-kort mycket tidigt. Nexus One förblir den enda Nexus-enheten med en microSD-kortplats (och det kommer det för alltid att vara eftersom Nexus-märket i praktiken är dött). Med Nexus S fanns det nu bara en enhetlig partition för lagring av all applikationsdata och media - /data-partitionen. Det som en gång var känt som /sdcard-monteringspunkten hänvisade helt enkelt till ett virtuellt filsystem (implementerat under SÄKRING protokoll som diskuteras nedan) som finns i datapartitionen - /data/media/0.
För att upprätthålla kompatibilitet och minska förvirring använde Google fortfarande denna numera virtuella "sdcard"-partition för att hålla media. Men nu när den här virtuella "sdcard"-partitionen faktiskt var placerad i /data, skulle allt som lagras i den räknas mot lagringsutrymmet på det interna lagringschippet. Således var det upp till OEM: er att överväga hur mycket utrymme som skulle allokeras till applikationer (/data) kontra media (/data/media).
Google hoppades på att tillverkarna skulle följa deras exempel och bli av med SD-kort. Tack och lov kunde telefontillverkare med tiden köpa dessa komponenter med högre kapacitet samtidigt som de förblev kostnadseffektiva, så behovet av SD-kort började bli tunt. Men namnkonventionerna har bestått för att minska mängden ansträngning som utvecklare och OEM-tillverkare skulle behöva göra för att anpassa sig. När vi för närvarande hänvisar till "extern lagring" syftar vi på antingen en av två saker: det faktiska flyttbara microSD-kortet eller den virtuella "SDCard"-partitionen som finns i /data/media. Den senare av dessa, praktiskt taget, är faktiskt intern lagring, men Googles namnkonvention skiljer den åt på grund av att denna data är tillgänglig för användaren (som när den är ansluten till datorn).
När vi för närvarande hänvisar till "extern lagring" syftar vi på antingen en av två saker: det faktiska flyttbara microSD-kortet eller den virtuella "SDCard"-partitionen som finns i /data/media.
Historien om Androids virtuella filsystem
Nu när "sdcard" behandlas som ett virtuellt filsystem, innebar det att det kunde formateras som vilket filsystem som helst som Google ville ha. Från och med Nexus S och Android 2.3 valde Google att formatera "sdcard" som VFAT (virtuellt FAT). Detta drag var vettigt på den tiden, eftersom montering av VFAT skulle tillåta nästan vilken dator som helst att komma åt data som lagras på din telefon. Det fanns dock två stora problem med denna initiala implementering.
Den första gäller i första hand slutanvändaren (dig). För att ansluta din enhet till din dator skulle du använda USB-masslagringsläge för att överföra data. Detta krävde dock att Android-enheten avmonterade den virtuella partitionen innan datorn kunde komma åt data. Om en användare ville använda sin enhet medan den var ansluten, skulle många saker visa sig vara otillgängliga.
De införandet av Media Transfer Protocol (MTP) löste detta första problem. När den är ansluten ser din dator din enhet som en "medialagringsenhet". Den begär en lista över filer från din telefon och MTP returnerar en lista med filer som datorn kan ladda ner från enheten. När en fil begärs att raderas, skickar MTP ett kommando för att ta bort den begärda filen från lagringen. Till skillnad från USB-masslagringsläge som faktiskt monterar "sd-kortet", tillåter MTP användaren att fortsätta använda sin enhet medan den är ansluten. Dessutom spelar filsystemet som finns på Android-telefonen inte längre någon roll för att datorn ska känna igen filerna på enheten.
För det andra var det faktum att VFAT inte tillhandahöll den typ av robust behörighetshantering som Google behövde. Tidigt skulle många applikationsutvecklare behandla "sdcard" som en dumpningsplats för deras applikationsdata, utan en enhetlig känsla för var de skulle lagra sina filer. Många program skulle helt enkelt skapa en mapp med dess appnamn och lagra dess filer där.
Nästan varje applikation där ute vid den tiden krävde WRITE_EXTERNAL_STORAGE behörighet att skriva sina programfiler till den externa lagringen. Men det som var mer oroande var det faktum att nästan varje applikation också krävde READ_EXTERNAL_STORAGE tillstånd - bara att läsa sina egna datafiler! Detta innebar att applikationer enkelt kunde få tillgång till data lagrad var som helst på den externa lagringen, och ett sådant tillstånd beviljades ofta av användaren eftersom det krävdes för många appar att jämnas ut fungera.
Google såg helt klart detta som problematiskt. Hela idén bakom behörighetshantering är att separera vad appar kan och inte har tillgång till. Om nästan varje app beviljas läsåtkomst till potentiellt känslig användardata, är tillståndet meningslöst. Därför beslutade Google att de behövde ett nytt tillvägagångssätt. Det är där FUSE kommer in.
Filsystem i användarutrymme (FUSE)
Från och med Android 4.4 beslutade Google att inte längre montera den virtuella "sdcard"-partitionen som VFAT. Istället började Google använda FUSE för att emulera FAT32 på den virtuella "sdcard"-partitionen. Med sdcard-programmet som ringer FUSE för att emulera katalogbehörigheter i FAT-on-sdcard-stil, kan applikationer börja komma åt dess data lagrade på extern lagring utan att kräva några tillstånd. Från och med API-nivå 19 krävdes faktiskt inte längre READ_EXTERNAL_STORAGE för att komma åt filer som finns på extern lagring - förutsatt att datamappen skapad av FUSE-demonen matchar appens paketnamn. FUSE skulle klara syntetisera ägare, grupp och lägen för filer på extern lagring när en applikation är installerad.
FUSE skiljer sig från moduler i kärnan eftersom det tillåter icke-privilegierade användare att skriva virtuella filsystem. Anledningen till att Google implementerade FUSE är ganska enkel - det gjorde vad de ville och var redan väl förstådd och dokumenterad i Linux-världen. För att citera a Google-utvecklare i frågan:
"Eftersom FUSE är ett trevligt stabilt API, krävs det i princip inget underhållsarbete när man flyttar mellan kärnversioner. Om vi migrerade till en lösning i kärnan skulle vi registrera oss för att underhålla en uppsättning patchar för varje stabil kärnversion." -Jeff Sharkey, mjukvaruingenjör på Google
Det höll dock på att bli ganska tydligt att FUSEs overhead introducerade en hit i prestanda bland andra frågor. Utvecklaren jag pratade med angående denna fråga, Michal Kowalczyk, skrev ett utmärkt blogginlägg över ett år sedan med detaljerade uppgifter om aktuella problem med FUSE. Mer tekniska detaljer finns att läsa på hans blogg, men jag kommer att beskriva hans fynd (med hans tillåtelse) i mer lekmannatermer.
Problemet med FUSE
I Android använder "sdcard"-användarutrymmesdemonen FUSE för att montera /dev/fuse till den emulerade externa lagringskatalogen vid uppstart. Efter det kontrollerar sdcard-demonen FUSE-enheten efter eventuella väntande meddelanden från kärnan. Om du lyssnade på podcasten kanske du har hört Mr. Lemarchand hänvisa till FUSE som introducerar overhead under I/O-operationer - här är i huvudsak vad som händer.
I den verkliga världen påverkar denna prestationshit några fil lagrad på extern lagring.
Problem #1 - I/O Overhead
Låt oss säga att vi skapar en enkel textfil, kallad "test.txt", och lagrar den i /sdcard/test.txt (som låt oss jag påminner dig, är faktiskt /data/media/0/test.txt förutsatt att den nuvarande användaren är den primära användaren på enhet). Om vi ville läsa (command cat) den här filen, skulle vi förvänta oss att systemet skulle utfärda 3 kommandon: öppna, läs och stäng sedan. I själva verket, som Mr. Kowalczyk visar att använda strace, det är vad som händer:
Men eftersom filen finns på den externa lagringen som hanteras av sdcard-demonen, finns det många ytterligare operationer som måste utföras. Enligt Mr. Kowalczyk behövs det i huvudsak 8 ytterligare steg vart och ett av dessa 3 individuella kommandon:
- Userspace-applikationen utfärdar systemanrop som kommer att hanteras av FUSE-drivrutinen i kärnan (vi ser det i den första strace-utgången)
- FUSE-drivrutinen i kärnan meddelar userspace daemon (sdcard) om ny begäran
- Userspace-demonen läser /dev/fuse
- Userspace daemon analyserar kommandot och känner igen filoperation (ex. öppen)
- Userspace-demonen utfärdar systemanrop till det faktiska filsystemet (EXT4)
- Kernel hanterar fysisk dataåtkomst och skickar data tillbaka till användarutrymmet
- Userspace modifierar (eller inte) data och skickar den genom /dev/fuse till kärnan igen
- Kernel slutför det ursprungliga systemanropet och flyttar data till den faktiska användarutrymmesapplikationen (i vårt exempel cat)
Det här verkar som mycket av overhead bara till ett enda I/O-kommando som ska köras. Och du skulle ha rätt. För att visa detta försökte Mr. Kowalczyk två olika I/O-tester: ett som involverade kopiering av en stor fil och det andra med att kopiera massor av små filer. Han jämförde hastigheten för FUSE (på den virtuella partitionen monterad som FAT32) som hanterar dessa operationer med kärnan (på datapartitionen formaterad som EXT4), och han fann att FUSE verkligen bidrog avsevärt över huvudet.
I det första testet kopierade han en 725MB fil under båda testförhållandena. Han fann att FUSE-implementationen överförde stora filer 17% långsammare.
I det andra testet kopierade han 10 000 filer - var och en av dem 5 KB i storlek. I det här scenariot var implementeringen av FUSE över 40 sekunder långsammare att kopiera i princip 50 MB data.
I den verkliga världen påverkar denna prestationshit några fil lagrad på extern lagring. Detta innebär appar som kartor som lagrar stora filer på /sdcard, musikappar som lagrar massor av musikfiler, kameraappar och foton, etc. Varje I/O-operation som utförs som involverar den externa lagringen påverkas av FUSE: s overhead. Men I/O-overhead är inte det enda problemet med FUSE.
Problem #2 - Dubbel cachelagring
Cachning av data är viktigt för att förbättra prestanda för dataåtkomst. Genom att lagra viktiga databitar i minnet kan Linux-kärnan snabbt återkalla dessa data när det behövs. Men på grund av hur FUSE är implementerat lagrar Android dubbelt så mycket cache som behövs.
Som Mr. Kowalczyk visar, förväntas en 10 MB fil sparas i cache som exakt 10 MB, men istället upp till cachestorlek med cirka 20 MB. Detta är problematiskt på enheter med mindre RAM-minne, eftersom Linux-kärnalagren använder sidcache för att lagra data i minne. Herr Kowalczyk testade detta problem med dubbla cachelagringar med detta tillvägagångssätt:
- Skapa en fil med en känd storlek (för testning, 10 MB)
- Kopiera det till /sdcard
- Släpp sidcachen
- Ta en ögonblicksbild av sidans cacheanvändning
- Läs testfilen
- Ta en annan ögonblicksbild av sidans cacheanvändning
Vad han fann var att innan hans test användes 241 MB av kärnan för sidcache. När han väl läst sin testfil förväntade han sig att se 251 MB använda för sidcache. Istället upptäckte han att den kärnan använde 263 MB för sidcache - ca dubbelt så mycket som förväntat. Anledningen till att detta inträffar är att data först cachelagras av användarprogrammet som ursprungligen utfärdade I/O-anropet (FUSE), och sedan av sdcard-demonen (EXT4 FS).
Problem #3 - ofullständig implementering av FAT32
Det finns ytterligare två problem som härrör från användningen av FUSE som emulerar FAT32 som är mindre kända i Android-gemenskapen.
Den första innebär felaktiga tidsstämplar. Om du någonsin har överfört en fil (som ett foto) och märkt att tidsstämpeln är felaktig, beror det på Androids implementering av FUSE. Denna fråga har funnits för år. För att vara mer specifik handlar frågan om utime() systemanrop som låter dig ändra åtkomst- och ändringstiden för en fil. Tyvärr har anrop som görs till sdcard-demonen som standardanvändare inte rätt behörighet att utföra detta systemanrop. Det finns lösningar för detta, men de kräver att du gör det har root-åtkomst.
Om du någonsin har överfört en fil (som ett foto) och märkt att tidsstämpeln är felaktig, beror det på Androids implementering av FUSE.
Nästa problem är mer oroande för företag som använder något som en smartSD-kort. Före FUSE kunde apptillverkare övervaka O_DIRECT flagga för att kunna kommunicera med en inbäddad mikrokontroller i kortet. Med FUSE kan utvecklare bara komma åt den cachade versionen av en fil och kan inte se några kommandon som skickas av en mikrokontroller. Detta är problematiskt för vissa företags-/stats-/bankappar som kommunicerar med värdeadderande microSD-kort.
Dumpar SÄKRING för SDCardFS
Vissa OEM-tillverkare insåg tidigt dessa problem och började leta efter en lösning i kärnan för att ersätta FUSE. Samsung utvecklade till exempel SDCardFS som är baserad på WrapFS. Denna lösning i kärnan emulerar FAT32 precis som FUSE gör, men avstår från I/O-overhead, dubbel cachning och andra problem som jag har nämnt ovan. (Ja, låt mig upprepa den punkten, den här lösningen som Google nu implementerar är baserad på Samsungs arbete).
Google själva har äntligen erkänt nackdelarna som är förknippade med FUSE, vilket är anledningen till att de har börjat gå mot kärnan FAT32-emuleringsskiktet utvecklat av Samsung. Företaget, som nämns i Android-utvecklare Backstage podcast, har arbetat med att göra SDCardFS tillgängligt för alla enheter i en kommande version av kärnan. Du kan för närvarande se deras framsteg arbeta i AOSP.
Som en Googles utvecklare förklarade tidigare, den största utmaningen med att implementera en lösning i kärnan är hur man mappar paketnamnet till applikations-ID som krävs för att ett paket ska få åtkomst till sin egen data i extern lagring utan att kräva någon behörigheter. Men det uttalandet gjordes för ett år sedan, och vi har nått den punkt där teamet kallar SDCardFS sin "nästa stora grej." De har redan bekräftat att fruktade tidsstämpelfel har åtgärdats, tack vare att vi flyttat bort från FUSE, så vi kan se fram emot att se alla förändringar som fördes fram i och med att FUSE övergavs.
Faktakontroll missuppfattningar
Om du har kommit så här långt in i artikeln, beröm för att du hängt med i allt så här långt! Jag ville förtydliga några frågor jag själv hade när jag skrev den här artikeln:
- SDCardFS har inget med riktiga SD-kort att göra. Den heter bara som sådan eftersom den hanterar I/O-åtkomst för /sdcard. Och som du kanske minns är /sdcard en föråldrad etikett som hänvisar till den "externa" lagringen av din enhet (där appar lagrar sina media).
- SDCardFS är inte ett traditionellt filsystem som FAT32, EXT4 eller F2FS. Det är ett staplingsbart omslagsfilsystem som skickar kommandon till de lägre, emulerade filsystemen (i det här fallet skulle det vara FAT32 på /sd-kortet).
- Ingenting kommer att förändras med avseende på MTP. Du kommer att fortsätta använda MTP för att överföra filer till/från din dator (tills Google bestämmer sig för ett bättre protokoll). Men åtminstone tidsstämpelfelet kommer att åtgärdas!
- Som nämnts tidigare, när Google hänvisar till "extern lagring" talar de antingen om (för all del och ändamål) intern /sdcard virtuell FAT32-partition ELLER de talar om en faktisk, fysisk, flyttbar microSD kort. Terminologin är förvirrande, men det är vad vi drabbas av.
Slutsats
Genom att gå bort från FUSE och implementera ett FAT32-emuleringslager i kärnan (SDCardFS) kommer Google att minska betydande I/O-overhead, eliminerar dubbel cachning och löser några oklara problem relaterade till dess FUSE: s emulering av FAT32.
Eftersom dessa ändringar kommer att göras i en kärna, kan de rullas ut utan en större ny version av Android vid sidan av den. Vissa användare förväntar sig att se dessa ändringar officiellt implementerade i Android 8, men det är möjligt för eventuell framtida OTA på en Pixel-enhet för att ta med Linux-kärnan version 4.1 som Google har arbetat med på.
För vissa av er är SDCardFS inget nytt koncept. Faktum är att Samsung-enheter har använt det i flera år (de var trots allt de som utvecklade det). Ända sedan SDCardFS introducerades i AOSP förra året har vissa anpassade ROM- och kärnutvecklare valt att implementera det i sitt arbete. CyanogenMOD övervägde vid ett tillfälle att implementera det, men rullade tillbaka det när användare stötte på problem med sina foton. Men förhoppningsvis när Google tar makten över detta projekt, kan Android-användare på alla framtida enheter dra nytta av de förbättringar som infördes med att överge FUSE.