Pi 4-logo

Raspberry Pi 4

Baggrund

Jeg fik denne maskine for at lave eksperimenter. Det er ikke planen, at den skal ende som server.


Tilbage til toppen af siden.

Hardware

En Raspberry Pi er en computer på en enkelt printplade. Man kan købe den som en enkel komponent, men jeg fik en 'startpakke' med strømforsyning og, køleelementer og en kasse at bygge den i. Computeren understøtter boot fra et SD-kort, men for mig blev det ødelagt meget hurtigt, så jeg starter nu min Raspberry Pi fra en USB-nøgle.

Min maskine er en Paspberry Pi 4B med 8 GB RAM. Denne models cpu er et 'system på en chip', et SoC fra Broadcom.


Tilbage til toppen af siden.

Styresystem

En Raspberry Pi er indrettet, så den ved start kan downloade og installere et tilpasset styresystem, der er baseret på Debian. Det kan installeres på et SD-kort eller en USB-nøgle. Jeg har udskiftet det med FreeBSD, fordi jeg gerne ville programmere i Rust på maskinen, og Debians version var alt for gammel. Jeg bruger desuden FreeBSD på en anden maskine.

Man kan enten bruge det officielle program Imager, eller overføre et image direkte med programmet dd, som følger med UNIX-agtige systemer..

Uanset hvilket styresystem man vælger, skal det tilpasses efter installation. Typisk vil man ændre sprog og tastatur til dansk og vælge en tidszone. I UNIX-agtige systemer er det kutyme at lade maslintiden være i UTC og så konfigurere, hvilken tidszone, der løbende oversættes til. Da en Raspberry Pi 4 ikke har et ur med batteri, skal man man konfigurere synkronisering med en tidsserver for at få nøjagtig tid på maskinen.


Tilbage til toppen af siden.

ARM-cpu'er

CPU-wn i en Raspberry PI er en ARM64-processor. Den er et eksempel på en RISC-processor med svag ordning af access til hukommelsen. Det har den konsekvens, at det er nemmere at opdage programfejl, der handler o eksempelvis samtidighedskontrol.

Jeg har eksperimenteret med at skrive mine egne synkroniseringsprimitiver i Rust, og det blev meget nemmere at finde problemerne med en ARN-cpu sammenligned med en cpu fra eksempelvis AMD. Det skyldes, at ARM-cpu'en ikke garanterer at alle cpu-kerner ser opdateringer af RAM i samme rækkefølge. For at sikre det, skal der bruges 'atomare' instruktioner, som er understøttet i Rust.


Tilbage til toppen af siden.

Eksperimenter

Jeg har implementeret min egen spinlock. Det kan være en god løsning, hvis det er sjældent at to tråde forsøger at låse den på samme tid. Hvis der er brug for at lave I/O mens programmet holder en lås, er en spinlock en meget dårlig løsning. I forhold til Building our own spinlock har jeg ændret implementationen, så der bruges hurtigere inskruktioner i løkken med 'spin'. Idéen er, at næjes med abruge 'Aquire load', når der er en chance for at gennemføre låsningen.


pub fn lock(&self) -> Guard {
        loop {
                if !self.locked.swap(true, Acquire) {
                        return Guard { lock: self };
                }
                self.contented();
        }
}

#[cold]
fn contented(&self) {
        while self.locked.load(Relaxed) {
                std::hint::spin_loop();
        }
}

// Lås op når Guard-objektet går ud af scope.
impl Drop for Guard {
        fn drop(&mut self) {
                self.locked.store(false, Release);
        }
}

Der er en lille risiko for, at låsen bliver snuppet mellem det tidspunkt, hvor løkken i contented() stopper, og kaldet til swap(). Det er stadig mere effektivt end at kalde swap() hver gang. Mite tests viste meget tydeligt, at en spinlock er ineffektiv, hvis der er flere tråde, der samtidig prøver at låse den. Når der gemmes en værdi med Release, vil denne opdatering og tidligere opdateringer med sikkerhed være synlige for andre tråde inden næste læsning med Acquire. Dette sikrer at denne spinlock ikke kan låses op i utide.

Det skulle være mere effektivt al lave 'relaxed load' inde load med 'Acquire', men mine tests viste, at det var langsommere. En ekstra atomar operation er ikke en fordel selvom data bliver cached. Forklaringen er nok, at en atomar opration forhinderer både compiler og CPU i at lave visse optimeringer. I dette tilfælde er den simple løsning også den bedste.

Næste eksperiment var at lave en semafor og en mutex baseret på semaforen. Fordelen ved sen løsning er, at mutex-implementationen ikke direkte stopper og starter tråde, men uddelegerer det til semaforen. Mutexen håndterer adgangen til de delte data. Det er tilsyneladende ikke væsentligt langsommere end oplægget. Jeg har brugt semaforer og en mutex til at implementere en kø med fast længde, hvor en tråd kan sætte data i kø, mens en anden tager data ud af køen. Der er en semafor til at tælle optagede pladser og en anden til at tælle ledige pladser. Mutexen beskytter den interne status i køen. Jeg har brugt min kø i et program, der sætter delresultater i kø, de behandles færdigt i en anden tråd og resultaterne sættes i en anden kø. Hovedprogrammet læser resultaterne fra denne anden kø, og viser resultaterne på skærmen. Det virker, og to CPU-kerner bliver brugt fuldt ud.


Tilbage til toppen af siden.

Klder


Tilbage til toppen af siden.
Tilbage til hovedsiden.