&Notepad

Will Crichton – szeptember 9, 2018
Van egy kis bajom a “rendszerprogramozás” kifejezéssel. Számomra mindig úgy tűnt, hogy szükségtelenül egyesít két elképzelést: az alacsony szintű programozást (a gép implementációs részleteivel való foglalkozás) és a rendszertervezést (egymással együttműködő komponensek összetett halmazának létrehozása és kezelése). Miért van ez így? Mióta igaz ez? És mit nyerhetnénk a rendszerek fogalmának újradefiniálásával?

1970-es évek: Az összeszerelés továbbfejlesztése

Ússzuk vissza a modern számítógépes rendszerek eredetéig, hogy megértsük, hogyan alakult ki a fogalom. Nem tudom, ki alkotta meg eredetileg a kifejezést, de a kereséseim azt sugallják, hogy a “számítógépes rendszerek” meghatározására irányuló komoly erőfeszítések a 70-es évek eleje körül kezdődtek. A Systems Programming Languages (Bergeron1 et al. 1972) című könyvben a szerzők azt írják:

A rendszerprogram alprogramok integrált halmaza, amelyek együttesen nagyobb egészet alkotnak, mint a részek összege, és meghaladnak bizonyos méret és/vagy komplexitás küszöbértéket. Tipikus példák a többprogramozásra, fordításra, szimulációra, információkezelésre,és időmegosztásra szolgáló rendszerek. A következő a tulajdonságok részleges halmaza,amelyek közül néhány nem rendszerekben is megtalálható, és nem mindegyiknek kell jelen lennie egy adott rendszerben.

  1. A megoldandó probléma tág természetű, sok,és általában igen változatos részproblémából áll.
  2. A rendszerprogramot valószínűleg más szoftverek és alkalmazási programok támogatására használják, de lehet maga is egy teljes alkalmazási csomag.
  3. Folyamatos “termelési” használatra tervezték, nem pedig egyetlen alkalmazási probléma egyszeri megoldására.
  4. Valószínűleg folyamatosan fejlődik az általa támogatott funkciók száma és típusai tekintetében.
  5. A rendszerprogram bizonyos fegyelmet vagy struktúrát igényel, mind a modulokon belül, mind a modulok között (azaz “kommunikáció”), és általában egynél több személy tervezi és valósítja meg.

Ez a meghatározás meglehetősen elfogadható – a számítógépes rendszerek nagyszabásúak, hosszú ideig használatosak és időben változóak. Míg azonban ez a definíció nagyrészt leíró jellegű, a dokumentum egyik kulcsgondolata előíró jellegű: az alacsony szintű nyelvek és a rendszernyelvek szétválasztása mellett érvel (akkoriban az assembly-t a FORTRAN-nal szembeállítva).

A rendszerprogramozási nyelv célja, hogy olyan nyelvet biztosítson, amely a “bitforgatás” szempontjainak túlzott figyelembevétele nélkül használható, ugyanakkor olyan kódot generál, amely nem érezhetően rosszabb, mint a kézzel generált kód. Egy ilyen nyelvnek egyesítenie kell a magas szintű nyelvek tömörségét és olvashatóságát a hely- és időhatékonysággal, valamint az assembler nyelven elérhető gépi és operációs rendszereszközök “elérésének” képességével. A tervezési, írási és hibakeresési időnek minimalizálódnia kell anélkül, hogy a rendszer erőforrásainak felesleges terhelése nélkül.

A CMU kutatói ugyanebben az időben publikálták a BLISS: A Language for Systems Programming (Wulf et al. 1972) című könyvet, amelyet a következőképpen írnak le:

A BLISS-t “implementációs nyelvnek” nevezzük, bár elismerjük, hogy ez a kifejezés kissé kétértelmű, mivel feltehetően minden számítógépes nyelv valaminek az implementálására szolgál. Számunkra ez a kifejezés olyan általános célú, magasabb szintű nyelvre utal, amelyben az elsődleges hangsúlyt egy konkrét alkalmazásra helyezték, nevezetesen nagy, sorozatgyártású szoftverrendszerek írására egy adott gépre. A speciális célú nyelvek, mint például a fordító-fordítók, nem tartoznak ebbe a kategóriába, és nem is feltétlenül feltételezzük, hogy ezeknek a nyelveknek gépfüggetlennek kell lenniük. Definíciónkban a “megvalósítás” szót hangsúlyozzuk, és nem használtunk olyan szavakat, mint a “tervezés” és a “dokumentáció”. Nem feltétlenül várjuk el, hogy egy implementációs nyelv megfelelő eszköz legyen egy nagy rendszer kezdeti tervének kifejezésére, vagy a rendszer kizárólagos dokumentálására. Az olyan fogalmak, mint a gépfüggetlenség, a tervezés és a megvalósítás azonos jelöléssel történő kifejezése, az öndokumentáció és mások, egyértelműen kívánatos célok, és olyan kritériumok, amelyek alapján a különböző nyelveket értékeltük.

A szerzők itt szembeállítják az “implementációs nyelv” elképzelését, amely magasabb szintű, mint az assembly, de alacsonyabb szintű, mint a “tervezési nyelv”. Ez ellenkezik az előző dolgozatban szereplő definícióval, amely amellett érvel, hogy egy rendszer tervezése és egy rendszer implementálása külön nyelveket igényel.

Mindkét dolgozat kutatási műalkotás vagy érdekérvényesítés. Az utolsó figyelembe veendő bejegyzés (szintén 1972-ből, egy termékeny évből!) a Systems Programming (Donovan 1972), egy oktatási szöveg a rendszerprogramozás tanulására.

Mi a rendszerprogramozás? A számítógépet úgy képzelhetjük el, mint valamiféle fenevadat, amely minden parancsnak engedelmeskedik. Azt mondják, hogy a számítógépek alapvetően fémből készült emberek, vagy fordítva, az emberek húsból és vérből készült számítógépek. Ha azonban közelebb kerülünk a számítógépekhez, láthatjuk, hogy azok alapvetően gépek, amelyek nagyon specifikus és primitív utasításokat követnek. A számítógépek kezdeti időszakában az emberek primitív utasításokat jelölő be- és kikapcsolókkal kommunikáltak velük. Hamarosan az emberek bonyolultabb utasításokat akartak adni. Például azt akarták mondani, hogy X = 30 * Y; ha Y = 10, akkor mennyi X? A mai számítógépek nem képesek megérteni az ilyen nyelvet rendszerprogramok nélkül. A rendszerprogramokat (pl. fordítóprogramok, betöltők, makroprocesszorok, operációs rendszerek) azért fejlesztették ki, hogy a számítógépek jobban alkalmazkodjanak a felhasználók igényeihez. Továbbá az emberek több segítséget akartak a programjaik elkészítésének mechanikájában.

Meg tetszik, hogy ez a definíció emlékeztet minket arra, hogy a rendszerek az emberek szolgálatában állnak, még akkor is, ha csak infrastruktúráról van szó, amely nincs közvetlenül kitéve a végfelhasználónak.

1990-es évek: A 70-es és 80-as években úgy tűnik, hogy a legtöbb kutató a rendszerprogramozást általában az assembly programozással ellentétesnek tekintette. Egyszerűen nem voltak más jó eszközök a rendszerek építésére. (Nem vagyok benne biztos, hogy a Lisp hol volt ebben az egészben? Az általam olvasott források egyike sem említette a Lispet, bár homályosan tudom, hogy Lisp gépek léteztek, bár rövid ideig.)

A 90-es évek közepén azonban a programozási nyelvekben jelentős változás következett be a dinamikusan tipizált szkriptnyelvek megjelenésével. A korábbi shell szkriptrendszereket, mint például a Bash, továbbfejlesztve olyan nyelvek, mint a Perl (1987), a Tcl (1988), a Python (1990), a Ruby (1995), a PHP (1995) és a Javascript (1995) törtek utat maguknak a mainstreambe. Ez a folyamat a “Scripting” című befolyásos cikkben csúcsosodott ki: Magasabb szintű programozás a 21. században” (Ousterhout 1998). Ez megfogalmazta “Ousterhout dichotómiáját” a “rendszerprogramozási nyelvek” és a “szkriptnyelvek” között.”

A szkriptnyelveket más feladatokra tervezték, mint a rendszerprogramozási nyelveket, és ez alapvető különbségeket eredményez a nyelvek között. A rendszerprogramozási nyelveket adatszerkezetek és algoritmusok nulláról való felépítésére tervezték, a legprimitívebb számítógépes elemekből, például a memória szavaiból kiindulva. Ezzel szemben a szkriptnyelveket ragasztásra tervezték: feltételezik egy sor nagy teljesítményű komponens meglétét, és elsősorban a komponensek összekapcsolására szolgálnak. A rendszerprogramozási nyelvek erősen tipizáltak, hogy segítsék a komplexitás kezelését, míg a szkriptnyelvek tipizálás nélküliek, hogy egyszerűsítsék a komponensek közötti kapcsolatokat és gyors alkalmazásfejlesztést biztosítsanak. Számos közelmúltbeli tendencia, például a gyorsabb gépek, a jobb szkriptnyelvek, a grafikus felhasználói felületek és a komponensarchitektúrák növekvő jelentősége, valamint az internet növekedése jelentősen megnövelte a szkriptnyelvek alkalmazhatóságát.

A technikai szinten Ousterhout a szkriptnyelveket és a rendszereket a típusbiztonság és az utasításonkénti utasítások tengelye mentén állította szembe egymással, ahogyan az a fentiekben látható. Tervezési szinten jellemezte az egyes nyelvosztályok új szerepeit: a rendszerprogramozás a komponensek létrehozására, a szkriptelés pedig ezek összeragasztására szolgál.

Ez idő tájt kezdtek népszerűvé válni a statikusan tipizált, de szemétgyűjtő nyelvek is. A Java (1995) és a C# (2000) a ma ismert titánokká váltak. Bár ez a kettő hagyományosan nem tekinthető “rendszerprogramozási nyelvnek”, a világ számos legnagyobb szoftverrendszerének megtervezéséhez használták őket. Ousterhout még kifejezetten meg is említette, hogy “a most formálódó internetes világban a Java-t rendszerprogramozásra használják.”

2010-es évek: A határok elmosódnak

Az elmúlt évtizedben a szkriptnyelvek és a rendszerprogramozási nyelvek közötti határvonal kezdett elmosódni. Az olyan cégek, mint a Dropbox, meglepően nagy és skálázható rendszereket tudtak építeni pusztán Python nyelven. A Javascriptet valós idejű, összetett felhasználói felületek megjelenítésére használják weboldalak milliárdjain. A fokozatos tipizálás egyre nagyobb teret nyert a Pythonban, a Javascriptben és más szkriptnyelvekben, lehetővé téve a “prototípus” kódból a “gyártósori” kódba való átmenetet a statikus típusinformációk fokozatos hozzáadásával.

Ugyanakkor a statikus nyelvek (pl. a Java HotSpot) és a dinamikus nyelvek (pl. a Lua LuaJIT, a Javascript V8, a Python PyPy) JIT-fordítóira fordított hatalmas mérnöki erőforrások versenyképessé tették teljesítményüket a hagyományos rendszerprogramozási nyelvekkel (C, C++). Az olyan nagyméretű elosztott rendszerek, mint a Spark, Scala2 nyelven íródnak. Az olyan új programozási nyelvek, mint a Julia, a Swift és a Go tovább feszegetik a szemétgyűjtő nyelvek teljesítményének határait.

A Systems Programming in 2014 and Beyond (Rendszerprogramozás 2014-ben és azon túl) elnevezésű panelben a mai önazonos rendszerszintű nyelvek legnagyobb elméi vettek részt: Bjarne Stroustrup (a C++ megalkotója), Rob Pike (a Go megalkotója), Andrei Alexandrescu (D fejlesztő) és Niko Matsakis (Rust fejlesztő). Arra a kérdésre, hogy “mi a rendszerprogramozási nyelv 2014-ben”, azt válaszolták (szerkesztett átirat):

  • Niko Matsakis: Kliensoldali alkalmazások írása. A szöges ellentéte annak, amire a Go-t tervezték. Ezekben az alkalmazásokban nagy késleltetési igényeid vannak, magas biztonsági követelmények, egy csomó olyan követelmény, ami a szerveroldalon nem merül fel.
  • Bjarne Stroustrup: A rendszerprogramozás abból a területből jött, ahol hardverrel kellett foglalkozni, majd az alkalmazások egyre bonyolultabbá váltak. A komplexitással kell foglalkoznod. Ha jelentős erőforrás-korlátozással kapcsolatos problémáid vannak, akkor a rendszerprogramozás területén vagy. Ha finomabb ellenőrzésre van szükséged, akkor szintén a rendszerprogramozás területén vagy. A korlátok határozzák meg, hogy rendszerprogramozásról van-e szó. Kifogytál a memóriából? Kifutsz az időből?
  • Rob Pike: Ezt kissé sajnálom, mert sokan azt feltételezték, hogy ez egy operációs rendszereket író nyelv. Inkább szerveríró nyelvnek kellett volna neveznünk, aminek valójában gondoltuk. Most már úgy tudom, hogy ez egy felhő-infrastruktúra nyelv. Egy másik definíció szerint a rendszerprogramozás az a dolog, ami a felhőben fut.
  • Andrei Alexandrescu: Van néhány lakmuszpapír tesztem annak ellenőrzésére, hogy valami rendszerprogramozási nyelv-e. Egy rendszerprogramozási nyelvnek lehetővé kell tennie, hogy saját memóriaallokátort írhass benne. Képesnek kell lennie arra, hogy egy számot mutatóvá hamisítson, hiszen a hardver így működik.

A rendszerprogramozás akkor a nagy teljesítményről szól? Erőforrás korlátozásokról? Hardveres irányítás? Felhő infrastruktúra? Úgy tűnik, nagyjából úgy tűnik, hogy a C, C++, Rust és D kategóriába tartozó nyelveket a géptől való absztrakciós szintjük alapján különböztetik meg. Ezek a nyelvek feltárják a mögöttes hardver részleteit, mint például a memória kiosztása/elrendezése és a finomszemcsés erőforrás-kezelés.

Egy másik módja a gondolkodásnak: ha van egy hatékonysági problémád, mekkora szabadságod van a megoldásához? Az alacsony szintű programozási nyelvek csodálatos része az, hogy amikor azonosítasz egy hatékonysági hiányosságot, hatalmadban áll a szűk keresztmetszetet megszüntetni a gépi részletek gondos ellenőrzésével. Vektorizáld ezt az utasítást, méretezd át ezt az adatstruktúrát, hogy a gyorsítótárban maradjon, és így tovább. Ugyanúgy, ahogy a statikus típusok nagyobb biztonságot3 nyújtanak, mint például “ez a két dolog, amit megpróbálok hozzáadni, biztosan egész számok”, az alacsony szintű nyelvek nagyobb biztonságot nyújtanak abban, hogy “ez a kód az általam megadott módon fog futni a gépen.”

Ezzel szemben az értelmezett nyelvek optimalizálása egy abszolút dzsungel. Hihetetlenül nehéz tudni, hogy a futtatási idő következetesen úgy fogja-e végrehajtani a kódodat, ahogyan elvárod. Pontosan ugyanez a probléma az autoparalelizáló fordítóprogramokkal is – “az autovektorizáció nem programozási modell” (lásd Az ispc története). Ez olyan, mintha Pythonban írnál egy interfészt, és arra gondolnál, hogy “nos, nagyon remélem, hogy aki meghívja ezt a függvényt, az egy int-et ad nekem.”

Most:

Ez visszavezet az eredeti panaszomhoz. Amit sokan rendszerprogramozásnak hívnak, azt én csak alacsony szintű programozásnak gondolom – a gép részleteinek feltárása. De akkor mi a helyzet a rendszerekkel? Emlékezzünk vissza az 1972-es definíciónkra:

  1. A megoldandó probléma tág természetű, sok, és általában igen változatos részproblémából áll.
  2. A rendszerprogramot valószínűleg más szoftverek és alkalmazási programok támogatására használják, de lehet maga is egy teljes alkalmazási csomag.
  3. Folyamatos “termelési” használatra tervezték, nem pedig egyetlen alkalmazási probléma egyszeri megoldására.
  4. Valószínűleg folyamatosan fejlődik az általa támogatott funkciók száma és típusai tekintetében.
  5. A rendszerprogram bizonyos fegyelmet vagy struktúrát igényel mind a modulokon belül, mind a modulok között (azaz “kommunikáció”), és általában egynél több személy tervezi és valósítja meg.

Ezek sokkal inkább tűnnek szoftverfejlesztési kérdéseknek (modularitás, újrafelhasználás, kódfejlődés), mint alacsony szintű teljesítménykérdéseknek. Ami azt jelenti, hogy minden olyan programozási nyelv, amely kiemelten kezeli ezeket a problémákat, rendszerprogramozási nyelv! Ez még mindig nem jelenti azt, hogy minden nyelv rendszerprogramozási nyelv. A dinamikus programozási nyelvek vitathatóan még mindig messze vannak a rendszernyelvektől, mivel a dinamikus típusok és az olyan idiómák, mint a “bocsánatot kérj, ne engedélyt” nem kedveznek a jó kódminőségnek.

Mire jutunk tehát ezzel a definícióval? Itt van egy forró tipp: az olyan funkcionális nyelvek, mint az OCaml és a Haskell sokkal inkább rendszerorientáltak, mint az olyan alacsony szintű nyelvek, mint a C vagy a C++. Amikor az egyetemi hallgatóknak rendszerprogramozást tanítunk, olyan funkcionális programozási alapelveket kell beépítenünk, mint a megváltoztathatatlanság értéke, a gazdag típusrendszerek hatása az interfésztervezés javítására és a magasabb rendű függvények hasznossága. Az iskoláknak mind a rendszerprogramozást, mind az alacsony szintű programozást tanítaniuk kellene.

Az általunk javasoltak szerint van-e különbség a rendszerprogramozás és a jó szoftverfejlesztés között? Nem igazán, de itt az a probléma, hogy a szoftvertervezést és az alacsony szintű programozást gyakran elszigetelten tanítják. Míg a legtöbb szoftvermérnöki órán általában Java-központú “írj jó interfészeket és teszteket”, a diákoknak azt is meg kellene tanítanunk, hogyan tervezzenek olyan rendszereket, amelyeknek jelentős erőforrás-korlátozásai vannak. Talán azért nevezzük az alacsony szintű programozást “rendszereknek”, mert a legérdekesebb szoftverrendszerek közül sok alacsony szintű (pl. adatbázisok, hálózatok, operációs rendszerek stb.). Mivel az alacsony szintű rendszereknek sok korlátja van, a tervezőiknek kreatív gondolkodásra van szükségük.

Egy másik keretezés szerint az alacsony szintű programozóknak arra kell törekedniük, hogy megértsék, milyen rendszertervezési ötleteket lehet adaptálni a modern hardverek valóságához. Úgy gondolom, hogy a Rust közösség rendkívül innovatív volt ebben a tekintetben, azt vizsgálva, hogy a jó szoftvertervezési/funkcionális programozási elveket hogyan lehet alkalmazni alacsony szintű problémákra (pl. futures, hibakezelés, vagy persze memóriabiztonság).

Összefoglalva, amit mi “rendszerprogramozásnak” hívunk, azt szerintem “alacsony szintű programozásnak” kellene hívni. A számítógépes rendszertervezés mint terület túl fontos ahhoz, hogy ne legyen saját neve. E két gondolat egyértelmű szétválasztása nagyobb fogalmi tisztaságot biztosít a programozási nyelvek tervezésének terében, és megnyitja az ajtót a két tér közötti meglátások megosztása előtt is: hogyan tervezhetjük a rendszert a gép köré, és fordítva?

Kérlek, küldd a hozzászólásokat a postaládámba a [email protected] vagy a Hacker News címre.

  1. Klassz tény: a cikk két szerzője, R. Bergeron és Andy Van Dam alapító tagjai a grafikai közösségnek és a SIGGRAPH konferenciának. Egy folyamatos minta része, ahol a grafikusok meghatározzák a trendeket a rendszertervezésben, lásd a GPGPU eredetét.

  2. Kényszeres link a Scalability-re! De milyen áron?.

  3. A statikus típusok valójában 100%-os garanciát jelentenek (vagy a pénzedet visszakapod), de a gyakorlatban a legtöbb nyelv megenged némi Obj.varázslatot.

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.