Tot el curs ha mirat cap al mateix lloc: un servidor amb corrent permanent, memòria de sobres, disc que es pot ampliar i una xarxa que gairebé sempre hi és. I la lliçó anterior va portar aquest model a l'extrem, on les màquines sobren, es creen per API i es llencen quan fan nosa.
Ara mirarem en la direcció contrària, cap a dues famílies de sistemes operatius que comparteixen la mateixa arrel teòrica —processos, planificació, memòria, IPC, protecció— però treballen amb restriccions oposades. El telèfon des del qual un usuari consulta l'API de Meteora executa un nucli Linux, però amb una bateria que s'esgota, sense espai d'intercanvi, amb la xarxa intermitent i amb aplicacions d'origen desconegut que no s'han de poder tocar entre si: aquest conjunt de restriccions ha obligat a reinventar l'IPC, la gestió de memòria i el model de permisos dels mòduls 3 i 5. I les estacions meteorològiques de Meteora —els dispositius encastats que a 01-03 vam decidir que portarien un RTOS— tenen 64 KB de RAM, funcionen mesos amb una pila, no poden demanar una altra unitat quan els falta memòria, i si el mostreig del sensor arriba 10 mil·lisegons tard, la dada està malament, no pas "arriba a poc a poc".
En aquesta lliçó entendràs què afegeix Android sobre Linux i per què, com s'aïlla una aplicació mòbil i per què el sistema la pot matar, què significa de debò "temps real" —que no és velocitat—, com es demostra matemàticament que un conjunt de tasques complirà els seus terminis, i per què una nau a Mart va estar a punt de perdre's per un problema de sincronització que ja coneixes. Acabarem escrivint el microprogramari d'una estació de Meteora sobre FreeRTOS, amb les seves tasques, les seves prioritats justificades i la seva anàlisi de planificabilitat amb xifres concretes.
Contingut
- Què afegeix Android sobre el nucli Linux
- Binder: per què no n'hi havia prou amb l'IPC d'UNIX
- Zygote, ART i el truc de l'arrencada
- Aïllament per aplicació: un UID per app
- Cicle de vida i per què el sistema pot matar una app
- Gestió d'energia: wake locks, Doze i big.LITTLE
- iOS comparat
- Què ha de saber qui dissenya l'API que consumeix l'app
- Temps real: la definició correcta
- Dur i tou, amb conseqüències
- El model de tasques periòdiques
- Rate Monotonic i la fita de Liu i Layland
- EDF i l'anàlisi exacta de temps de resposta
- Inversió de prioritats i el Mars Pathfinder
- Latència d'interrupció, jitter i determinisme
- RTOS reals i Linux amb
PREEMPT_RT - Particularitats dels sistemes encastats
- Cas pràctic: el microprogramari d'una estació de Meteora
- Tancament del mòdul 6
Què afegeix Android sobre el nucli Linux
Android és Linux: el mateix nucli monolític modular de 01-05, els mateixos processos de 02-01, els mateixos namespaces i cgroups de 06-02. Però per damunt del nucli gairebé res no és el que coneixes: no hi ha glibc (fa servir Bionic, més petita), no hi ha systemd (fa servir un init propi amb fitxers .rc), no hi ha X11 ni Wayland, i l'espai d'usuari està dissenyat al voltant de premisses que en un servidor no existeixen.
| Restricció del mòbil | Conseqüència de disseny |
|---|---|
| Bateria limitada | Tot el sistema optimitza energia abans que rendiment sostingut |
| Sense espai d'intercanvi | Quan falta RAM cal matar processos, no paginar a disc |
| Aplicacions d'origen desconegut | Aïllament fort per aplicació, no per usuari |
| Interfície sempre fluida | La tasca d'interfície ha de tenir prioritat gairebé de temps real |
| Xarxa intermitent i cara | Agrupació de feina i tolerància a la desconnexió |
| Maquinari molt heterogeni | Una capa d'abstracció (HAL) entre el marc i els controladors |
Els afegits d'Android sobre el nucli són, en essència, cinc: Binder, un IPC propi implementat com a controlador del nucli; zygote, el procés que precarrega l'entorn d'execució per arrencar apps a l'instant; ART, la màquina d'execució de les aplicacions; la HAL, capa d'abstracció que desacobla el marc d'Android dels controladors del fabricant i que des del Projecte Treble viu en processos separats amb interfícies versionades, cosa que permet actualitzar Android sense que el fabricant refaci els controladors; i un model d'aïllament per aplicació construït sobre els UID de 05-02, però fet servir d'una manera que a UNIX ningú no havia previst.
Binder: per què no n'hi havia prou amb l'IPC d'UNIX
A 03-03 vam veure el catàleg clàssic d'IPC: canonades, FIFO, cues de missatges, memòria compartida, sockets. Android té tot això disponible i tot i així va construir un altre mecanisme. La pregunta interessant és per què.
La resposta és que a Android gairebé tot és una crida entre processos: demanar la ubicació, consultar la bateria, dibuixar a la pantalla, demanar un permís. El patró real no és "enviar un flux de bytes", sinó invocar un mètode d'un objecte que viu en un altre procés i esperar-ne el resultat. I aquest patró exigeix coses que l'IPC clàssic no dona:
| Necessitat | Què ofereix l'IPC clàssic | Què ofereix Binder |
|---|---|---|
| Crida síncrona amb resultat | Cal muntar-la a mà sobre dos canals | Nativa: és el seu model |
| Saber qui crida, de manera fiable | El PID es pot reutilitzar; l'UID cal passar-lo | El nucli injecta UID i PID del cridant |
| Comptar referències a objectes remots | No existeix | Integrat: l'objecte mor quan ningú no el fa servir |
| Còpies de dades | 2 còpies (emissor → nucli → receptor) | 1 còpia, amb memòria mapada |
| Límit de recursos | Cada mecanisme el seu | Pool de fils per procés, amb control |
| Mort de l'interlocutor | Es detecta tard i malament | Notificació de defunció explícita |
La fila decisiva per a la seguretat és la segona. Quan meteo-app demana la ubicació, el servei del sistema necessita saber amb certesa qui pregunta per comprovar si té el permís. Si l'identificador l'enviés el mateix cridant, mentiria. Binder ho resol al nucli: el controlador hi afegeix l'UID i el PID reals de l'emissor a cada transacció, i el receptor els consulta amb Binder.getCallingUid(). És el mateix principi que feia valuós el journald a 05-04 —les metadades les posa qui no pot mentir—, aplicat a l'IPC.
L'altra fila important és la de les còpies. Binder mapa una àrea de memòria del procés receptor i hi escriu directament des del nucli, amb la qual cosa la transacció costa una sola còpia en lloc de dues. Amb desenes de milers de transaccions per segon en un telèfon actiu, aquesta meitat importa en energia i en latència.
El preu és una mida de transacció limitada (de l'ordre d'1 MB compartit per procés), per la qual cosa Binder no serveix per moure dades grans: per a això es passa un descriptor de fitxer de memòria compartida, que Binder sí que sap transferir entre processos igual que feia SCM_RIGHTS sobre sockets UNIX a 03-03.
Zygote, ART i el truc de l'arrencada
Cada aplicació Android s'executa en el seu propi procés, amb la seva pròpia instància de l'entorn d'execució. Arrencar aquest entorn des de zero —carregar les classes del marc, inicialitzar els recursos— costaria centenars de mil·lisegons i uns quants megabytes per aplicació. Amb quaranta aplicacions, seria inviable.
La solució és zygote ("zigot"), i és una aplicació preciosa d'una cosa que ja coneixes. En arrencar el sistema, init llança un procés zygote, que precarrega les classes del marc i els recursos comuns —milers de classes i desenes de megabytes ja inicialitzats— i es queda esperant en un socket. Quan l'usuari obre una aplicació, el gestor d'activitats demana a zygote que faci fork(): el fill hereta tot el precarregat, només ha de carregar el codi propi de l'app, i després crida setuid() amb l'UID de l'aplicació per deixar anar privilegis, exactament el patró de 05-02.
La clau és al fork() i a la còpia en escriure de 02-01 i 02-04: el fill no copia els centenars de megabytes del marc, comparteix les mateixes pàgines físiques amb zygote i amb tots els seus germans. Només quan un procés escriu en una pàgina se'n fa una còpia privada. I com que les classes precarregades són de només lectura a la pràctica, gairebé mai no s'escriuen.
El resultat és doble i espectacular: arrencada d'una app en desenes de mil·lisegons en lloc de centenars, i un estalvi de memòria enorme, perquè quaranta aplicacions comparteixen físicament el mateix marc. Fixa't que és la mateixa idea que KSM a 06-01 i que les capes d'overlayfs a 06-02: compartir el que és idèntic i copiar només en escriure. El truc apareix als tres mons.
Sobre ART (Android Runtime), el rellevant per a aquest curs és la seva evolució, que il·lustra una decisió de sistemes: es va passar d'interpretar (lent però sense cost d'instal·lació) a compilar-ho tot en instal·lar (ràpid però amb instal·lacions llarguíssimes i molt espai) i finalment a un esquema híbrid —s'interpreta al principi, es perfila el que de debò es fa servir, i es compila en segon pla quan el dispositiu està carregant i ociós—. Aquesta darrera condició és pur disseny per a mòbil: la feina cara es fa quan l'energia és gratis.
Aïllament per aplicació: un UID per app
Aquí Android fa una cosa que reinterpreta completament el model de 05-02. En un servidor, l'UID identifica una persona, i diversos programes de la mateixa persona comparteixen UID i es poden tocar els fitxers entre si. En un mòbil només hi ha una persona, així que aquell model no aïlla res.
Android reutilitza el mecanisme amb un altre significat: cada aplicació instal·lada rep el seu propi UID, en el rang 10000 endavant.
adb shell ps -A -o USER,PID,NAME | head
# USER PID NAME
# root 1 init
# system 1842 system_server
# u0_a132 4021 com.meteora.app
# u0_a145 4188 com.altra.app
adb shell ls -la /data/data/com.meteora.app
# drwx------ 4 u0_a132 u0_a132 ... . ← mode 700, de l'UID de l'appQuè demostra. u0_a132 és l'usuari 0, aplicació 132: un UID real del sistema (10132). El directori de dades privat és en mode 700 i pertany a aquell UID. Amb això, els nou bits de permís de 04-06 —el mecanisme més vell del curs— aïllen les aplicacions entre si sense necessitat de res més. És una reutilització brillant d'un mecanisme existent.
L'aïllament complet, però, té quatre capes superposades, i totes et sonen:
| Capa | Mecanisme | De quina lliçó |
|---|---|---|
| Identitat | Un UID per aplicació, dades en mode 700 | 04-06, 05-02 |
| MAC | SELinux en mode obligatori per a tot el sistema | 05-01 |
| Confinament de syscalls | Filtre seccomp per a les aplicacions | 05-01 |
| Permisos d'usuari | Concessió explícita per l'usuari en temps d'execució | Sense equivalent a UNIX |
La quarta capa és la que no té precedent. Els permisos en temps d'execució divideixen l'accés en dos: els permisos normals (internet, vibració) es concedeixen en instal·lar, i els perillosos (ubicació, càmera, micròfon, contactes) els concedeix l'usuari, en el moment de fer-los servir, i els pot revocar. A més poden concedir-se de manera acotada: "només mentre faig servir l'app", "només aquesta vegada", "ubicació aproximada en lloc d'exacta".
Compara-ho amb el model de 05-02: en un servidor, un programa hereta tots els permisos de l'usuari que l'executa. Si executes un script descarregat, aquest script pot llegir tot el que tu puguis llegir. A Android, una aplicació té per defecte zero accés a tot el que és interessant, i l'usuari va concedint capacitats una a una. És el mínim privilegi de 05-01 portat a un model on el programari no és de confiança per defecte —i, sincerament, és un model millor que el de l'escriptori i el del servidor, on continuem executant programes amb tots els nostres privilegis—.
Cicle de vida i per què el sistema pot matar una app
Aquesta és la diferència que més costa a qui ve del servidor: a Android, el sistema pot matar el teu procés en qualsevol moment i no és una fallada, és el funcionament normal.
El motiu és la fila més important de la taula del principi: no hi ha espai d'intercanvi. A 02-04 vam veure que quan falta memòria, Linux pagina a disc i només invoca l'OOM killer com a últim recurs desesperat. En un mòbil, amb memòria flaix de vida limitada i sense partició de swap, aquesta via no existeix. L'única manera de recuperar memòria és alliberar processos sencers.
Així que Android va convertir l'últim recurs en política ordinària. El sistema classifica cada procés segons el que l'usuari percebria si desaparegués:
| Categoria | Què és | Es mata? |
|---|---|---|
| Primer pla | El que l'usuari està fent servir ara | Gairebé mai |
| Visible | Visible però no en focus | Poques vegades |
| De servei | Feina en segon pla (descàrrega, música) | Si cal |
| A la memòria cau | Ja no fa res, es manté per si torna | Constantment |
| Buit | Només el procés, sense components actius | El primer a caure |
I la diferència amb l'OOM killer de 02-04 és qualitativa:
| OOM killer (02-04) | LMKD (Android) | |
|---|---|---|
| Quan actua | Quan ja no hi ha memòria | Abans, per pressió creixent (PSI) |
| Criteri | Heurística oom_score: sobretot la mida |
Importància per a l'usuari |
| Percepció | Un desastre inesperat | Comportament normal i esperat |
| Efecte | Es perd la feina | L'app ha d'haver desat el seu estat |
LMKD (Low Memory Killer Daemon) viu en espai d'usuari i fa servir els indicadors de pressió de memòria (PSI) del nucli per actuar abans que el sistema es degradi: així que la pressió puja, comença a matar processos en memòria cau, dels menys importants cap amunt.
La conseqüència per al desenvolupador és una regla dura: l'estat que no s'ha desat, es perd. Per això el cicle de vida d'una activitat té crides de retorn com onPause() i onStop(), amb una promesa explícita: quan es crida onStop(), el sistema garanteix que has tingut ocasió de desar; a partir d'aquí et pot matar sense més avís. Una app ben escrita persisteix el seu estat a cada transició i el restaura en tornar, de manera que l'usuari no percep que el procés va morir i va tornar a néixer.
Gestió d'energia: wake locks, Doze i big.LITTLE
En un servidor, l'energia no és un recurs a gestionar: s'endolla i ja està. En un mòbil és el recurs escàs principal, i el sistema operatiu el gestiona amb la mateixa serietat amb què gestiona la CPU.
El principi de partida és que l'estat natural del dispositiu és adormit. La CPU, la pantalla, la ràdio i els sensors estan apagats o en baix consum, i només es desperten per un esdeveniment. La pregunta que es fa el sistema no és "com reparteixo la CPU" sinó "com aconsegueixo tornar a dormir com més aviat millor".
- Wake locks. Un component que necessita mantenir la CPU desperta adquireix un wake lock, i mentre n'hi hagi algun d'actiu el sistema no entra en suspensió profunda. És necessari i és la primera causa d'aplicacions que esgoten la bateria: un d'adquirit i no alliberat —per una branca d'error que no passa pel
release()— manté el telèfon despert tota la nit. El patró correcte és adquirir amb temps màxim i alliberar en un blocfinally, amb la disciplina d'un mutex de 03-04. - Doze. Quan el dispositiu fa una estona que està quiet, amb la pantalla apagada i sense carregar, el sistema agrupa tota la feina diferida: suspèn els accessos a xarxa, ajorna alarmes i sincronitzacions i ignora els wake locks, i cada cert temps obre una finestra de manteniment on tot el que hi ha pendent s'executa alhora. És la mateixa lògica d'amortització que les virtqueues de
virtioa 06-01 i NAPI a 02-07: una interrupció per lot en lloc d'una per esdeveniment, perquè despertar la ràdio costa energia fixa i fer-ho un cop per a vint coses costa vint vegades menys. - Restriccions en segon pla. Una app que no és en primer pla no pot iniciar serveis a voluntat. La feina diferida es declara a un planificador del sistema amb restriccions —"quan hi hagi WiFi i el dispositiu estigui carregant"—, i el sistema decideix quan executar-la agrupant-la amb la de les altres aplicacions.
I al nivell més baix, el planificador de 02-02 es torna conscient de l'energia. Els processadors mòbils són big.LITTLE: nuclis grans i ràpids que consumeixen molt al costat de nuclis petits i eficients. El planificador ja no només decideix quan s'executa cada tasca, sinó en quin tipus de nucli, estimant-ne la càrrega i l'exigència de latència: la tasca d'interfície va a un nucli gran perquè l'usuari percep cada mil·lisegon, i una sincronització en segon pla va a un nucli petit perquè ningú no la mira. És una dimensió que el CFS d'un servidor no tenia, perquè allà tots els nuclis són iguals.
iOS comparat
| Aspecte | Android | iOS |
|---|---|---|
| Nucli | Linux (monolític modular) | XNU: híbrid, amb Mach i BSD (01-05) |
| Model | Obert, molts fabricants | Tancat, maquinari i programari del mateix fabricant |
| Instal·lació d'apps | Botiga oficial i fonts externes | Només botiga oficial (amb excepcions regulatòries) |
| Aïllament | Un UID per app + SELinux + seccomp | Sandbox obligatori per app, signada i amb entitlements |
| Llenguatge i execució | Java/Kotlin sobre ART | Swift/Objective-C compilat a natiu |
| Gestió de memòria | Recol·lector d'escombraries + LMKD | Recompte de referències (ARC) + jetsam |
| Segon pla | Serveis i feina amb restriccions | Molt restringit: modes concrets i finestres curtes |
| Actualitzacions | Depenen del fabricant (millor amb Treble) | Directes del fabricant, a tot el parc |
Les dues diferències amb més conseqüències tècniques són la gestió de memòria i el segon pla. ART fa servir recol·lecció d'escombraries, còmoda per al programador però amb pauses i consum d'energia, mentre que iOS fa servir recompte automàtic de referències decidit en compilació, que dona un ús més ajustat i predictible a canvi que el programador trenqui els cicles a mà —d'aquí que iOS hagi funcionat històricament bé amb menys RAM—. I en segon pla iOS és molt més restrictiu: modes concrets, finestres curtes i mort sense contemplacions (jetsam) en passar-se del pressupost de memòria, cosa que dona millor bateria i pitjor flexibilitat, i explica que moltes funcions que a Android fa l'app, a iOS les hagi de fer el servidor amb notificacions push.
Què ha de saber qui dissenya l'API que consumeix l'app
Aquest apartat és el pont entre les dues meitats del curs, perquè qui escriu meteo-api a meteo-01 no veu res de tot l'anterior i tot i així li afecta tot. Tres restriccions del client mòbil han d'aparèixer al disseny del servidor:
1. Agrupar peticions, perquè la ràdio és la despesa. Encendre la ràdio mòbil per transmetre costa energia fixa —a més, la ràdio es queda en estat d'alt consum uns segons després d'acabar, "per si de cas"—. Deu peticions separades de 2 KB poden gastar molt més que una de 20 KB. L'API ha de permetre demanar diverses coses alhora:
En lloc de tres peticions (una per estació) que retornen tots els camps, una sola petició amb selecció de camps. La regla de disseny és: una pantalla, una petició. Si l'app necessita tres crides per pintar una vista, l'API està mal dissenyada per a mòbil.
2. Tolerar la desconnexió, perquè és l'estat normal. Un mòbil perd la xarxa en un ascensor, canvia de WiFi a mòbil a mitja petició i es queda sense cobertura en un túnel. El servidor hi ha d'ajudar:
Operacions idempotents amb una clau d'idempotència, perquè reintentar després d'un temps d'espera esgotat no dupliqui res —sense això, el reintent del client, que és inevitable, crea dades duplicades—; sincronització incremental (?des_de_versio=8421) en lloc de descarregar-ho tot un altre cop, perquè reprendre ha de ser barat; etiquetes d'entitat i respostes 304, perquè revalidar costi bytes i no kilobytes; i errors distingibles, perquè el client necessita saber si ha de reintentar i amb quin retard, i un Retry-After estalvia moltíssima bateria davant d'un bucle de reintents immediats.
3. Cuidar la mida de la resposta, perquè es paga tres vegades: en dades de l'usuari, en temps de ràdio (energia) i en memòria d'anàlisi, en un dispositiu que pot estar a punt que LMKD el mati. Retornar el fitxer diari sencer de 17,3 MB a un mòbil és un error en tres dimensions alhora. El correcte és paginar per defecte, comprimir sempre, oferir agregats en lloc de dades crues —el mòbil vol la mitjana horària, no les 720.000 lectures— i permetre seleccionar camps.
I un advertiment que tanca el cercle amb la lliçó anterior: el mòbil no pot reintentar indefinidament, així que l'elasticitat del servidor no ho salva tot. Si meteo-api triga 8 segons a respondre perquè està escalant en fred, l'app ja ha esgotat el seu temps d'espera i ha gastat ràdio per no res.
Temps real: la definició correcta
Canviem de món. I el primer és desmuntar el malentès que arrossega tothom en començar:
Un sistema de temps real no és un sistema ràpid. És un sistema previsible.
La propietat que defineix el temps real és que la resposta es produeix dins d'un termini garantit, sempre, fins i tot en el pitjor cas. Un sistema que respon en 1 microsegon el 99,99 % de les vegades i en 50 mil·lisegons el 0,01 % restant no és de temps real. Un sistema que respon sempre en 8 mil·lisegons, ni un més, amb un termini de 10 mil·lisegons, sí que ho és, encara que sigui mil vegades més lent que el primer.
D'aquí una conseqüència que sorprèn: en temps real, la correcció d'un resultat inclou l'instant en què es produeix. Un resultat correcte lliurat tard és un resultat incorrecte. I una altra conseqüència pràctica: els sistemes de temps real sacrifiquen rendiment mitjà a canvi de previsibilitat. Es desactiven memòries cau i prediccions, s'eviten estructures de dades amb cost variable, es prohibeix l'assignació dinàmica de memòria... tot el que introdueixi variabilitat, encara que de mitjana fos més ràpid.
Aplicat a Meteora: si l'estació ha de mostrejar el sensor cada 100 ms per calcular mitjanes correctes, el que importa no és que el mostreig trigui 2 microsegons, sinó que passi sempre dins de la seva finestra. Un mostreig que de vegades arriba als 130 ms desplaça la mitjana i corromp la dada, encara que la CPU estigui al 3 %.
Dur i tou, amb conseqüències
Reprenem i ampliem la classificació de 01-03:
| Temps real dur | Temps real tou | |
|---|---|---|
| Incomplir un termini | Fallada del sistema | Degradació de la qualitat |
| Garantia | Demostrada matemàticament abans d'executar | Estadística: "el 99 % sota 20 ms" |
| Dimensionament | Per al pitjor cas absolut | Per al cas típic amb marge |
| Cost | Alt: maquinari sobrat, anàlisi, certificació | Moderat |
| Exemples | Coixí de seguretat, control de vol, frens ABS, marcapassos, control de reactor | Vídeo, àudio, videojocs, telefonia, interfície d'usuari |
La diferència es veu millor en les conseqüències de la fallada. Un coixí de seguretat (dur) s'ha de desplegar entre 15 i 30 ms després de la col·lisió: als 50 ms l'ocupant ja ha impactat i el desplegament causa lesions en lloc d'evitar-les, així que no hi ha "una mica tard", hi ha correcte o mortal. Un reproductor de vídeo (tou) que perd un fotograma mostra una estrebada, molesta i no catastròfica, i la resposta correcta és descartar-lo i continuar. La frenada automàtica (dura) té un termini derivat de la velocitat i la distància, i incomplir-lo és un accident. I l'estació meteorològica és tova, i convé ser honest: un mostreig amb 30 ms de retard degrada lleugerament la mitjana horària i no mor ningú, però si es retarden sistemàticament o es perd l'enviament per blocatge, la dada es corromp i Meteora serveix informació falsa. És tova amb requisits exigents, la categoria més comuna a la indústria.
Hi ha una tercera categoria útil, el temps real ferm: incomplir el termini no trenca res, però el resultat no val per a res —una predicció de trajectòria que arriba després d'haver de decidir—. Es descarta el resultat i s'hi continua.
El model de tasques periòdiques
Per poder demostrar que un sistema complirà els seus terminis cal un model. El clàssic descriu cada tasca τᵢ amb tres números:
| Símbol | Nom | Què és |
|---|---|---|
Tᵢ |
Període | Cada quant s'activa la tasca |
Cᵢ |
Temps de còmput en el pitjor cas (WCET) | El màxim que pot trigar una execució |
Dᵢ |
Termini | Quan ha d'haver acabat des de la seva activació |
L'habitual —i el que assumirem— és Dᵢ = Tᵢ: la tasca ha d'acabar abans de tornar-se a activar.
La utilització d'una tasca és Uᵢ = Cᵢ / Tᵢ, i la del sistema U = Σ Cᵢ/Tᵢ. Una tasca que triga 12 ms i s'executa cada 100 ms utilitza el 12 % de la CPU.
El número difícil és el WCET, i mereix un advertiment seriós. No és "el que he mesurat executant-lo mil vegades", sinó la fita superior absoluta, i determinar-lo és sorprenentment difícil perquè el maquinari modern conspira contra la previsibilitat: les memòries cau fan que la mateixa funció trigui 10 vegades més si les dades no hi són a dins (els 200-300 cicles d'un accés a RAM de 06-01), una fallada de predicció de salts costa desenes de cicles, l'execució fora d'ordre i la precàrrega fan que el temps depengui de l'historial recent, i altres nuclis interfereixen a la memòria cau compartida i al bus de memòria.
Per això els sistemes de temps real dur fan servir sovint maquinari deliberadament simple i predictible —microcontroladors sense memòria cau, o amb memòria de treball dedicada— i per això hi ha eines d'anàlisi estàtica que calculen fites del WCET sobre el binari. En un sistema tou n'hi ha prou amb mesurar el màxim observat i afegir-hi un marge generós (del 30 % al 100 %), que és el que farem amb l'estació.
Rate Monotonic i la fita de Liu i Layland
Rate Monotonic (RM) és un algorisme d'assignació de prioritats fixes amb una regla d'una sola línia:
Com menor és el període, major és la prioritat.
I la seva virtut, demostrada per Liu i Layland el 1973, és que és òptim entre els algorismes de prioritat fixa: si algun esquema de prioritats fixes pot planificar un conjunt de tasques, RM també ho pot fer.
La seva prova de planificabilitat és una condició suficient:
U = Σ (Cᵢ / Tᵢ) ≤ n · (2^(1/n) − 1)
on n és el nombre de tasques. La fita val:
| n | Cota | n | Cota | |
|---|---|---|---|---|
| 1 | 100,0 % | 5 | 74,3 % | |
| 2 | 82,8 % | 10 | 71,8 % | |
| 3 | 78,0 % | ∞ | 69,3 % (ln 2) |
|
| 4 | 75,7 % |
Dues lectures importantíssimes d'aquesta taula. Si la utilització total és per sota de la fita, el sistema complirà tots els seus terminis, garantit, i no cal provar res més. Si és per damunt, la prova no diu res: pot ser que es compleixin igualment, perquè és una condició suficient i no necessària, i cal recórrer a l'anàlisi exacta.
I una conclusió de disseny que cal interioritzar: amb moltes tasques, RM només garanteix el 69,3 % de la CPU. Aquest 30 % restant no és malbaratament: és el preu de la garantia. Qui dimensioni un sistema de temps real dur al 95 % d'utilització no ha entès el problema.
EDF i l'anàlisi exacta de temps de resposta
EDF (Earliest Deadline First) fa servir prioritats dinàmiques: en cada instant executa la tasca el termini absolut de la qual sigui més proper. La seva prova de planificabilitat és d'una simplicitat sorprenent:
U = Σ (Cᵢ / Tᵢ) ≤ 1
I aquí és condició necessària i suficient: si hi cap a la CPU, EDF ho planifica. És òptim entre tots els algorismes d'un sol processador.
| Aspecte | Rate Monotonic | EDF |
|---|---|---|
| Prioritats | Fixes, calculades abans d'executar | Dinàmiques, recalculades en execució |
| Utilització garantida | 69,3 % - 100 % segons n |
100 % |
| Cost en execució | Mínim | Més gran: cal ordenar per termini |
| Previsibilitat davant de sobrecàrrega | Bona: fallen primer les de menor prioritat | Dolenta: efecte dòmino impredictible |
| Implementació | Trivial: prioritats estàtiques | Requereix suport del planificador |
| Ús a la indústria | Dominant | Creixent (SCHED_DEADLINE a Linux) |
La fila de la sobrecàrrega explica per què RM continua dominant en sistemes crítics tot i ser pitjor en utilització. Si el sistema se sobrecarrega —una interrupció imprevista, un WCET mal estimat—, amb RM saps exactament qui falla: les tasques de menor prioritat, és a dir, les de període més llarg, i pots dissenyar perquè siguin les menys importants. Amb EDF, una tasca que es passa del seu termini fa que una altra es passi del seu, i aquella a una altra: la fallada es propaga de manera difícil de predir. En un sistema crític, saber qui fallarà val més que esprémer l'últim 25 % de CPU.
L'anàlisi exacta: temps de resposta
Quan la fita d'RM no n'hi ha prou, es fa servir l'anàlisi de temps de resposta, que calcula el pitjor temps de resposta real de cada tasca:
Rᵢ = Cᵢ + Σ_{j ∈ hp(i)} ⌈Rᵢ / Tⱼ⌉ · Cⱼ
Es llegeix així: el pitjor temps de resposta d'una tasca és el seu propi còmput més tota la interferència de les tasques de major prioritat (hp(i)), on cadascuna interfereix tantes vegades com s'activi durant aquell interval —d'aquí el sostre ⌈ ⌉—. Com que Rᵢ apareix als dos costats, es resol iterant: es comença amb R = Cᵢ i es recalcula fins que el valor no canvia. El sistema és planificable si Rᵢ ≤ Dᵢ per a totes. L'aplicarem amb números reals al cas pràctic.
Inversió de prioritats i el Mars Pathfinder
El 4 de juliol de 1997 la sonda Mars Pathfinder va aterrar a Mart amb èxit. Dies després va començar a reiniciar-se sola, perdent dades científiques a cada reinici. La fallada era en un problema de sincronització que ja coneixes del mòdul 3, i el seu desenllaç és una de les millors històries de l'enginyeria de sistemes.
La inversió de prioritats passa així, amb tres tasques:
- Una tasca de baixa prioritat (
B) adquireix un mutex sobre un recurs compartit. - Una tasca d'alta prioritat (
A) s'activa, intenta adquirir el mateix mutex i es bloqueja. Fins aquí és correcte i esperat. - Apareix una tasca de mitjana prioritat (
M), que no fa servir el mutex. Com que té més prioritat queB, desallotjaB. - Resultat:
Bno avança, així que no deixa anar el mutex, així queAno pot continuar.M, de prioritat mitjana, està bloquejant indirectamentA, de prioritat alta, durant un temps que pot ser arbitràriament llarg.
Al Pathfinder, el repartiment era exactament aquest: una tasca de gestió del bus d'informació (alta prioritat, amb termini), una tasca de dades meteorològiques (baixa prioritat, ASI/MET) que hi compartia un mutex sobre el bus, i una tasca de comunicacions (prioritat mitjana, de llarga durada). Quan es donava la seqüència, la tasca del bus incomplia el seu termini, el watchdog detectava que el sistema no havia fet la seva feina a temps i reiniciava la nau, que és exactament el que un watchdog ha de fer.
Fixa't en la coincidència, que és massa bona per deixar-la passar: la tasca de baixa prioritat que ho va provocar tot era la de dades meteorològiques. L'estació de Meteora que dissenyarem al final té la mateixa estructura de tasques.
Les dues solucions clàssiques:
| Protocol | Com funciona | Avantatge | Inconvenient |
|---|---|---|---|
| Herència de prioritat | Quan A es bloqueja en un mutex que té B, B hereta temporalment la prioritat d'A fins a deixar-lo anar |
Simple, s'activa només quan cal | No preveu interbloquejos; el blocatge es pot encadenar |
| Sostre de prioritat | A cada mutex se li assigna la prioritat més alta de les tasques que el fan servir; qui l'adquireix puja a aquell sostre immediatament | Acota el blocatge a una sola secció crítica i preveu interbloquejos | Cal calcular els sostres abans d'executar |
Amb herència de prioritat, al pas 3 B ja s'està executant amb prioritat alta, així que M no la desallotja: B acaba, deixa anar el mutex i A continua. El blocatge queda acotat a la durada de la secció crítica de B.
I el millor de la història: VxWorks ja tenia herència de prioritat; estava desactivada en aquell mutex per rendiment. L'equip del JPL va reproduir la fallada al laboratori i va enviar a Mart un canvi d'un paràmetre per activar-la. La nau va continuar funcionant.
Tres lliçons que valen per a qualsevol sistema, no només per a naus: la sincronització de 03-04 no és un detall d'implementació, sinó part de l'anàlisi temporal, i un mutex mal fet servir converteix un sistema demostrat en un que falla; els mecanismes de seguretat desactivats "per rendiment" acaben costant cars; i el sistema va funcionar com havia de fer-ho, perquè el watchdog va detectar l'incompliment i va reiniciar, sense la qual cosa la nau s'hauria quedat penjada a Mart sense diagnòstic possible.
Latència d'interrupció, jitter i determinisme
A 02-07 vam veure les interrupcions des de la perspectiva del rendiment. En temps real, el que importa és una altra cosa: quant es triga, en el pitjor cas, des que passa l'esdeveniment físic fins que s'executa el codi que hi ha de respondre.
Aquesta latència d'interrupció es descompon així:
| Component | Què és | Com es redueix |
|---|---|---|
| Latència del maquinari | Detecció i senyalització al controlador | Poc marge |
| Seccions amb interrupcions deshabilitades | El nucli era en una regió crítica i no atén | Nucli expropiable; seccions crítiques curtes |
| Desat de context | Salvar registres i saltar a la rutina | Maquinari; de vegades optimitzat |
| Execució de la rutina | La mateixa atenció de la interrupció | ISR curtíssima; la feina, a una tasca |
| Canvi de context a la tasca | Despertar i planificar la tasca que espera | Planificador expropiable, prioritats correctes |
El jitter és la variació d'aquesta latència entre activacions. I aquí hi ha el punt clau per entendre el temps real: el jitter sol importar més que la latència mitjana. Un sistema amb 500 µs de latència constant és perfectament utilitzable —n'hi ha prou amb comptar amb aquests 500 µs—; un sistema amb 50 µs de mitjana però pics de 5 ms és inservible per a control, perquè cal dimensionar per al pitjor cas i aquell pitjor cas és 5 ms.
Per això les tècniques del temps real van totes en la mateixa direcció —reduir la variabilitat— i sacrifiquen alegrement el rendiment mitjà: rutines d'interrupció mínimes que només senyalen a una tasca, seccions crítiques curtíssimes, prohibició d'assignar memòria dinàmicament en execució (malloc té cost variable i pot fragmentar), estructures de dades de cost acotat, i de vegades memòries cau desactivades o fixades per a les rutes crítiques.
RTOS reals i Linux amb PREEMPT_RT
| Sistema | Tipus | Petjada | Llicència | Ús típic |
|---|---|---|---|---|
| FreeRTOS | Dur, molt lleuger | 6-12 KB | MIT | Microcontroladors, IoT, sensors |
| Zephyr | Dur, modular | 8-100 KB | Apache 2.0 | IoT amb connectivitat, wearables |
| QNX | Dur, microkernel (01-05) | ~1 MB | Comercial | Automoció, medicina, industrial |
| VxWorks | Dur, certificable | ~1 MB | Comercial | Aeroespacial, defensa, ferrocarril |
Linux + PREEMPT_RT |
Tou/ferm, latències de desenes de µs | ~50 MB+ | GPL | Robòtica, àudio, control industrial |
Val la pena aturar-se als dos extrems. FreeRTOS no és un sistema operatiu en el sentit d'aquest curs: és una biblioteca de planificació que s'enllaça amb el teu programa, sense processos, sense protecció de memòria i sense sistema de fitxers; hi ha tasques que comparteixen el mateix espai d'adreces, un planificador expropiatiu per prioritats fixes, cues, semàfors i mutex amb herència de prioritat. Cap en 10 KB perquè gairebé no fa res més, i per això funciona amb 64 KB de RAM. Linux amb PREEMPT_RT és l'altre extrem: un sistema complet al qual se li ha fet expropiable gairebé tot el nucli, convertint els spinlocks en mutex que poden dormir, movent les rutines d'interrupció a fils del nucli amb prioritat —perquè una interrupció no retardi indefinidament una tasca més prioritària— i afegint herència de prioritat als mutex del nucli. El pedaç es va fusionar a la branca principal el 2024, després de més de vint anys.
Amb PREEMPT_RT, latències en el pitjor cas de l'ordre de desenes de microsegons són assolibles, davant dels mil·lisegons d'un Linux normal. Combinat amb SCHED_DEADLINE —la política que ja vam anomenar a 02-02, que implementa EDF amb pressupost: es declaren període, termini i temps d'execució, i el nucli rebutja la tasca si no hi cap—, es cobreix bé el temps real tou i ferm.
Quan n'hi ha prou amb Linux i quan cal un RTOS? N'hi ha prou amb Linux amb PREEMPT_RT quan els terminis són de centenars de microsegons o mil·lisegons, quan cal xarxa, sistema de fitxers o pantalla, i quan el sistema és tou o ferm: robòtica, àudio professional, control industrial de gamma mitjana. Cal un RTOS quan els terminis són de microsegons, quan la memòria es mesura en kilobytes, quan el consum ha de ser mínim o quan cal certificar el sistema (DO-178C en aviònica, ISO 26262 en automoció, IEC 62304 en medicina): certificar un Linux complet és econòmicament inviable, certificar 10 KB de FreeRTOS és factible.
Particularitats dels sistemes encastats
Quatre realitats que canvien la manera de programar:
- Memòria escassa i estàtica. 64 KB de RAM i 512 KB de flaix és normal, i es prohibeix l'assignació dinàmica després de l'arrencada: memòries intermèdies, cues i piles es reserven estàticament amb mides calculades. No és només per l'espai, és per la previsibilitat, perquè
mallocté cost variable i pot fragmentar fins a fallar després de setmanes de funcionament, que és el pitjor moment possible. - Sense MMU als més petits, i arrencada directa. Els microcontroladors de gamma baixa no tenen unitat de gestió de memòria, així que tot el del mòdul 2 sobre memòria virtual no existeix: adreces físiques, sense separació entre tasques, i un punter corrupte pot escriure a la pila d'una altra. Molts porten una MPU més simple, que defineix unes poques regions amb permisos i detecta accessos fora de zona sense traduir adreces. I no hi ha BIOS ni gestor d'arrencada: en reiniciar, el processador llegeix l'adreça de la pila i el vector de reset d'una posició fixa del flaix i salta al codi, executant l'aplicació en desenes de mil·lisegons.
- Watchdog. Un temporitzador de maquinari independent que reinicia el sistema si el programari no el refresca a temps: la darrera xarxa de seguretat davant d'un bucle infinit, un blocatge en un mutex o una corrupció de memòria, i exactament el que va salvar el Pathfinder de quedar-se penjat. La seva regla d'or es viola constantment: s'ha de refrescar només si totes les tasques crítiques són vives, mai des d'un temporitzador cec, perquè llavors només protegeix contra que la CPU s'aturi del tot, que és la fallada menys probable.
Cas pràctic: el microprogramari d'una estació de Meteora
Tanquem el mòdul dissenyant el microprogramari d'una estació meteorològica. Maquinari: microcontrolador Cortex-M4 a 48 MHz, 64 KB de RAM, 512 KB de flaix, sensors per I²C, ràdio NB-IoT per UART, bateria de liti. Sistema: FreeRTOS.
Les tasques i els seus requisits
| Tasca | Període T |
WCET C |
Termini D |
Utilització | Per què aquest període |
|---|---|---|---|---|---|
tasca_mostreig |
100 ms | 12 ms | 100 ms | 12,0 % | 10 Hz per a mitjanes horàries fiables |
tasca_watchdog |
250 ms | 2 ms | 250 ms | 0,8 % | Ha de refrescar abans del timeout d'1 s |
tasca_promitjat |
1.000 ms | 180 ms | 1.000 ms | 18,0 % | Consolida 10 mostres per segon |
tasca_enviament |
5.000 ms | 2.400 ms | 5.000 ms | 48,0 % | La ràdio és cara: agrupar 5 s de dades |
U = 78,8 % |
Assignació de prioritats amb Rate Monotonic
Menor període, major prioritat. A FreeRTOS, número més gran = més prioritària:
| Tasca | T |
Prioritat | Justificació |
|---|---|---|---|
tasca_mostreig |
100 ms | 4 (màxima) | Període més curt; un mostreig tardà corromp la dada |
tasca_watchdog |
250 ms | 3 | S'ha d'executar encara que l'enviament s'allargui |
tasca_promitjat |
1.000 ms | 2 | Càlcul local, sense termini extern |
tasca_enviament |
5.000 ms | 1 (mínima) | La més llarga i la més tolerant: si es retarda, es reintenta |
I fixa't en la propietat valuosa d'RM que vam esmentar: si el sistema se sobrecarrega, la primera a fallar és tasca_enviament, que és precisament la que pot reintentar sense perdre res, perquè les dades són a la memòria intermèdia. La tasca la fallada de la qual seria irreparable —el mostreig— és la més protegida. L'assignació de prioritats per RM coincideix aquí amb la importància funcional, i quan això passa, el disseny és bo.
Prova de planificabilitat
Pas 1: fita de Liu i Layland. Amb n = 4, la fita és 4 · (2^(1/4) − 1) = 4 · 0,1892 = 0,757, és a dir 75,7 %.
La nostra utilització és 78,8 %, que supera la fita. La prova és no concloent: no podem afirmar que sigui planificable, però tampoc que no ho sigui. Cal fer l'anàlisi exacta.
Pas 2: anàlisi de temps de resposta. Apliquem Rᵢ = Cᵢ + Σ_{j∈hp(i)} ⌈Rᵢ/Tⱼ⌉·Cⱼ de major a menor prioritat.
tasca_mostreig(màxima prioritat, sense interferència):R = 12 ms ≤ 100 ms✓tasca_watchdog: interferida pel mostreig.R⁰ = 2→R¹ = 2 + ⌈2/100⌉·12 = 2 + 12 = 14→R² = 2 + ⌈14/100⌉·12 = 14. Convergeix.R = 14 ms ≤ 250 ms✓tasca_promitjat: interferida pel mostreig i el watchdog.R⁰ = 180→R¹ = 180 + ⌈180/100⌉·12 + ⌈180/250⌉·2 = 180 + 24 + 2 = 206R² = 180 + ⌈206/100⌉·12 + ⌈206/250⌉·2 = 180 + 36 + 2 = 218R³ = 180 + ⌈218/100⌉·12 + ⌈218/250⌉·2 = 218. Convergeix.R = 218 ms ≤ 1.000 ms✓tasca_enviament(mínima prioritat, interferida per les tres):R⁰ = 2.400R¹ = 2.400 + ⌈2400/100⌉·12 + ⌈2400/250⌉·2 + ⌈2400/1000⌉·180 = 2.400 + 288 + 20 + 540 = 3.248R² = 2.400 + ⌈3248/100⌉·12 + ⌈3248/250⌉·2 + ⌈3248/1000⌉·180 = 2.400 + 396 + 26 + 720 = 3.542R³ = 2.400 + ⌈3542/100⌉·12 + ⌈3542/250⌉·2 + ⌈3542/1000⌉·180 = 2.400 + 432 + 30 + 720 = 3.582R⁴ = 2.400 + 432 + 30 + 720 = 3.582. Convergeix.R = 3.582 ms ≤ 5.000 ms✓
Conclusió: el sistema és planificable amb Rate Monotonic, encara que la fita de Liu i Layland no ho garantís. El marge de la tasca més ajustada és de 5.000 − 3.582 = 1.418 ms, un 28 % de folgança sobre el seu termini. És una lliçó pràctica de primer ordre: la fita és suficient però no necessària, i descartar un disseny només perquè la supera és un error habitual.
(Amb EDF, U = 78,8 % ≤ 100 % hauria bastat com a prova, sense més càlcul. El preu, com vam veure, és el comportament impredictible davant d'una sobrecàrrega.)
El codi
/* ---------- Recursos compartits, tots ESTÀTICS ---------- */
#define N_MOSTRES 50 /* 5 s a 10 Hz */
typedef struct { /* 24 bytes, com la Lectura de meteo-01 */
uint32_t estacio_id;
uint32_t timestamp;
float temperatura, humitat, pressio;
} Lectura;
static Lectura buffer[N_MOSTRES]; /* reservat en compilació */
static StaticSemaphore_t mem_buffer;
static SemaphoreHandle_t mutex_buffer;
static EventGroupHandle_t vives; /* senyals de "sóc viva" */
#define VIVA_MOSTREIG (1<<0)
#define VIVA_ENVIAMENT (1<<1)
/* ---------- Tasca 1: mostreig. Prioritat 4, T=100 ms ---------- */
void tasca_mostreig(void *p) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
Lectura l;
l.timestamp = rtc_ara();
l.temperatura = i2c_llegir_temp(); /* lectures acotades: I2C */
l.humitat = i2c_llegir_hum(); /* amb timeout, mai bloqueig */
l.pressio = i2c_llegir_pres(); /* indefinit */
/* Secció crítica CURTÍSSIMA: només copiar, mai calcular a dins */
if (xSemaphoreTake(mutex_buffer, pdMS_TO_TICKS(5)) == pdTRUE) {
buffer_inserir(&l);
xSemaphoreGive(mutex_buffer);
} else {
comptador_fallades_mutex++; /* es registra, no es bloqueja */
}
xEventGroupSetBits(vives, VIVA_MOSTREIG);
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(100)); /* període EXACTE */
}
}
/* ---------- Tasca 2: watchdog. Prioritat 3, T=250 ms ---------- */
void tasca_watchdog(void *p) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
/* Només refresca si TOTES les tasques crítiques han donat senyal */
EventBits_t b = xEventGroupClearBits(vives, VIVA_MOSTREIG|VIVA_ENVIAMENT);
if (b & VIVA_MOSTREIG) {
iwdg_refrescar(); /* si no, el maquinari reinicia */
}
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(250));
}
}
/* ---------- Tasca 3: promitjat. Prioritat 2, T=1000 ms ---------- */
void tasca_promitjat(void *p) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
Lectura copia[10];
if (xSemaphoreTake(mutex_buffer, pdMS_TO_TICKS(20)) == pdTRUE) {
buffer_copiar_ultimes(copia, 10); /* copiar a dins */
xSemaphoreGive(mutex_buffer);
calcular_mitjanes(copia, 10); /* calcular FORA del mutex */
}
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(1000));
}
}
/* ---------- Tasca 4: enviament. Prioritat 1, T=5000 ms ---------- */
void tasca_enviament(void *p) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
Lectura lot[N_MOSTRES];
size_t n = 0;
if (xSemaphoreTake(mutex_buffer, pdMS_TO_TICKS(50)) == pdTRUE) {
n = buffer_extreure_tot(lot);
xSemaphoreGive(mutex_buffer);
}
if (n > 0 && !nbiot_enviar(lot, n * sizeof(Lectura))) {
buffer_retornar(lot, n); /* fallada: es reintenta després */
}
xEventGroupSetBits(vives, VIVA_ENVIAMENT);
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(5000));
}
}
/* ---------- Arrencada ---------- */
int main(void) {
hw_init();
mutex_buffer = xSemaphoreCreateMutexStatic(&mem_buffer); /* amb herència */
vives = xEventGroupCreate();
xTaskCreate(tasca_mostreig, "mostreig", 256, NULL, 4, NULL);
xTaskCreate(tasca_watchdog, "watchdog", 128, NULL, 3, NULL);
xTaskCreate(tasca_promitjat, "promitjat", 384, NULL, 2, NULL);
xTaskCreate(tasca_enviament, "enviament", 512, NULL, 1, NULL);
iwdg_init(1000); /* watchdog de maquinari: 1 s */
vTaskStartScheduler(); /* no retorna */
for (;;);
}Les set decisions que fan que això funcioni:
vTaskDelayUntili novTaskDelay.vTaskDelay(100)espera 100 ms des d'ara, així que el període real es converteix en 100 ms més el que va trigar la iteració, i l'error s'acumula: en una hora l'estació hauria perdut desenes de mostres.vTaskDelayUntildesperta en instants absoluts i manté el període exacte, que és justament el que exigeix el temps real.- Tot estàtic.
buffer, la memòria del mutex i les piles es reserven en compilació. Capmallocdesprés de l'arrencada: sense fragmentació, sense cost variable, sense fallades al cap de tres setmanes. - Mutex amb herència de prioritat. FreeRTOS la implementa a
xSemaphoreCreateMutex(no pas als semàfors binaris). És exactament la correcció del Pathfinder: sense ella,tasca_enviamentamb el mutex pres podria ser desallotjada pertasca_promitjati bloquejar el mostreig. - Seccions crítiques curtíssimes. Dins del mutex només es copia; els càlculs i la transmissió —el que és car— passen a fora. Això acota el blocatge que pot patir la tasca de màxima prioritat, que és el que entra a l'anàlisi temporal.
- Temps d'espera a tot arreu. Cada
xSemaphoreTakei cada operació d'I²C té un termini màxim. En temps real està prohibit el blocatge indefinit: és preferible perdre una mostra i registrar-ho que penjar una tasca. - El watchdog comprova de debò. Només refresca si la tasca de mostreig ha donat senyal de vida en l'interval. Un watchdog que es refresca des d'un temporitzador cec només detecta que la CPU s'ha aturat, que és la fallada menys probable de totes.
- Les prioritats coincideixen amb la importància. Per construcció d'RM, la tasca que primer incompliria és la d'enviament, que és l'única que pot reintentar sense perdre dades perquè la memòria intermèdia les conserva.
Un marge que cal respectar
Amb U = 78,8 % queda un 21 % de CPU lliure, que no sobra: absorbeix les interrupcions (que no són al model i roben temps a totes les tasques), els errors d'estimació del WCET i el creixement futur. Si demà s'hi afegeix un sensor de vent amb T = 100 ms i C = 8 ms, la utilització puja al 86,8 % i cal refer l'anàlisi completa, no pas suposar que hi cap. Aquest és, al capdavall, l'hàbit que defineix l'enginyeria de temps real: abans d'afegir una tasca, es demostra que continua complint.
Errors Habituals i Consells
Creure que temps real significa ràpid. És l'error conceptual número u. Significa previsible: un sistema lent i constant és de temps real; un de rapidíssim amb pics ocasionals, no.
Dimensionar amb el cas mitjà. En temps real només compta el pitjor cas. Un WCET mesurat en proves benignes i sense marge és una garantia falsa.
Interpretar malament la fita de Liu i Layland. Superar-la no significa que el sistema no sigui planificable: la prova és suficient, no necessària. Descartar un disseny vàlid per això és tan comú com fer l'anàlisi exacta i descobrir, com aquí, que hi ha un 28 % de folgança.
Fer servir vTaskDelay en una tasca periòdica, o fer feina dins de la rutina d'interrupció. El primer desplaça el període i acumula error: sempre vTaskDelayUntil o el seu equivalent. El segon destrossa la previsibilitat de tot el sistema, perquè tota la latència depèn que les ISR siguin mínimes: senyalar a una tasca i sortir.
Desactivar l'herència de prioritat "per rendiment". És literalment la fallada del Mars Pathfinder: l'estalvi és d'uns cicles i el cost, un sistema que es reinicia sol sense que ningú sàpiga per què.
En mòbil: suposar que el teu procés continuarà viu. Sense swap, el sistema mata processos com a funcionament normal. L'estat no desat a onStop() està perdut.
En mòbil: no alliberar un wake lock a la ruta d'error. És la causa més freqüent d'aplicacions que esgoten la bateria. Adquirir amb temps màxim i alliberar en finally.
Dissenyar l'API com si el client fos un servidor. Un mòbil paga cada petició en bateria, dades i memòria. Una pantalla, una petició; agregats en lloc de dades crues; paginació per defecte; i operacions idempotents, perquè el reintent és inevitable.
Consell: mesura el jitter, no la mitjana. En qualsevol sistema amb requisits temporals, la distribució importa més que la mitjana. Un histograma de latències diu la veritat; una mitjana, gairebé mai.
Consell: escriu l'anàlisi de planificabilitat i desa-la amb el codi. S'ha d'actualitzar cada vegada que s'afegeix una tasca o canvia un període. Un microprogramari de temps real sense aquest document és un microprogramari que ningú no pot modificar amb seguretat.
Exercicis
Exercici 1: redissenyar el conjunt de tasques de l'estació
Meteora vol afegir dues funcions a l'estació: tasca_vent (anemòmetre, T = 50 ms, C = 6 ms) i tasca_diagnostic (autotest i estat de la bateria, T = 10.000 ms, C = 400 ms), mantenint les quatre tasques existents.
(a) Assigna les prioritats amb Rate Monotonic per a les sis tasques i justifica'n l'ordre. (b) Calcula la utilització total i compara-la amb la fita de Liu i Layland per a n = 6. (c) Fes l'anàlisi exacta de temps de resposta per a tasca_enviament i per a tasca_diagnostic, i digues si el sistema és planificable. (d) Si no ho fos, proposa tres canvis de disseny diferents que ho arreglin sense canviar de maquinari, indicant què es perd amb cadascun.
Exercici 2: diagnosticar una inversió de prioritats
Una estació de Meteora es reinicia de manera aleatòria, entre dues i sis vegades al dia, sense patró horari. El registre previ al reinici mostra sempre que l'última operació va ser un enviament per ràdio. Se sap que: tasca_enviament (prioritat 1) pren el mutex de la memòria intermèdia abans de transmetre i el manté durant tota la transmissió, que dura fins a 2.400 ms; tasca_promitjat (prioritat 2) no fa servir el mutex i triga 180 ms; tasca_mostreig (prioritat 4) necessita el mutex per inserir cada lectura, amb un temps d'espera de 5 ms; i el watchdog és a 1 s.
(a) Explica pas a pas la seqüència que provoca el reinici, identificant el paper de cada tasca. (b) Per què és aleatori i no passa sempre? (c) Proposa tres correccions independents, digues quina és la millor i per què. (d) N'hi hauria hagut prou amb l'herència de prioritat per resoldre-ho completament? Raona-ho amb cura.
Exercici 3: dissenyar l'API de Meteora per a consum mòbil
L'app de Meteora mostra una pantalla amb: l'estat actual de les 3 estacions preferides de l'usuari, l'evolució de temperatura de les últimes 24 hores de cadascuna, i un avís si alguna estació no reporta des de fa més d'una hora. La implementació actual fa 7 peticions i descarrega 2,4 MB.
(a) Redissenya l'API per reduir-ho, indicant el nombre de peticions i una estimació de la mida, amb la justificació de cada decisió. (b) Explica quins mecanismes hi afegiries per tolerar la desconnexió i per què cadascun. (c) L'app també envia correccions manuals de lectures errònies: dissenya aquest endpoint de manera que un reintent després d'un temps d'espera esgotat no dupliqui la correcció, i explica el mecanisme. (d) Relaciona cada decisió amb la restricció del mòbil que la motiva (ràdio, bateria, memòria o xarxa intermitent).
Solucions
Solució 1
(a) Prioritats per RM (menor període → major prioritat):
| Tasca | T (ms) |
C (ms) |
Prioritat | U |
|---|---|---|---|---|
tasca_vent |
50 | 6 | 6 | 12,0 % |
tasca_mostreig |
100 | 12 | 5 | 12,0 % |
tasca_watchdog |
250 | 2 | 4 | 0,8 % |
tasca_promitjat |
1.000 | 180 | 3 | 18,0 % |
tasca_enviament |
5.000 | 2.400 | 2 | 48,0 % |
tasca_diagnostic |
10.000 | 400 | 1 | 4,0 % |
L'anemòmetre passa a ser la màxima prioritat per tenir el període més curt. Nota que això és coherent amb la funció: mesurar vent a 20 Hz exigeix regularitat estricta. I el diagnòstic queda l'últim, cosa que també és correcta: és l'única cosa que es pot retardar sense conseqüències.
(b) Utilització i fita. U = 0,12 + 0,12 + 0,008 + 0,18 + 0,48 + 0,04 = 0,948 → 94,8 %.
Fita per a n = 6: 6·(2^(1/6) − 1) = 6·0,1225 = 0,735 → 73,5 %.
U supera àmpliament la fita: no concloent, i aquesta vegada amb molt mala pinta.
(c) Anàlisi exacta.
tasca_enviament (prioritat 2), interferida per vent, mostreig, watchdog i promitjat:
R⁰ = 2.400 → R¹ = 2.400 + ⌈2400/50⌉·6 + ⌈2400/100⌉·12 + ⌈2400/250⌉·2 + ⌈2400/1000⌉·180 = 2.400 + 288 + 288 + 20 + 540 = 3.536 → R² = 2.400 + 426 + 432 + 30 + 720 = 4.008 → R³ = 2.400 + 486 + 492 + 34 + 900 = 4.312 → R⁴ = 2.400 + 522 + 528 + 36 + 900 = 4.386 → R⁵ = 2.400 + 528 + 528 + 36 + 900 = 4.392 → R⁶ = 4.392. Convergeix: R = 4.392 ms ≤ 5.000 ms ✓, amb una folgança de només 608 ms (12 %).
tasca_diagnostic (prioritat mínima), interferida per les cinc:
R⁰ = 400 → R¹ = 400 + 48 + 48 + 4 + 180 + 2.400 = 3.080 → R² = 400 + 372 + 372 + 26 + 720 + 2.400 = 4.290 → R³ = 400 + 516 + 516 + 36 + 900 + 2.400 = 4.768 → R⁴ = 400 + 576 + 576 + 40 + 900 + 2.400 = 4.892 → R⁵ = 400 + 588 + 588 + 40 + 900 + 2.400 = 4.916 → R⁶ = 4.916. Convergeix: R = 4.916 ms ≤ 10.000 ms ✓
El sistema és planificable, però amb un marge incòmode: tasca_enviament té només un 12 % de folgança, i el 5,2 % de CPU lliure no n'hi ha prou per absorbir interrupcions, errors de WCET ni ampliacions. Tècnicament compleix; en enginyeria, no és acceptable.
(d) Tres canvis de disseny:
- Allargar el període d'enviament a 10 s (la
Ud'enviament baixa del 48 % al 24 %; laUtotal, al 70,8 %, per sota fins i tot de la fita). Es perd frescor: les dades arriben al servidor amb fins a 10 s de retard. Per a dades meteorològiques és irrellevant, i a més estalvia bateria en despertar la ràdio la meitat de vegades: és la millor opció, i és un exemple que el requisit temporal més car sovint no està justificat. - Trossejar l'enviament en fragments de 300 ms amb punts de cessió entre ells, de manera que la
Cde la tasca baixi encara que l'operació total duri el mateix. Es guanya moltíssima folgança per a les tasques de menor prioritat; es perd simplicitat i cal gestionar l'estat de l'enviament parcial, amb el risc de duplicar o perdre dades si s'interromp a mitges. - Baixar el mostreig de l'anemòmetre a 100 ms (la
Ude vent del 12 % al 6 %; total 88,8 %). És el canvi de menor benefici i amb pèrdua real de qualitat de dada —el vent és la magnitud més variable de les quatre—, així que només es justificaria si es comprova que 10 Hz són suficients.
Una quarta opció legítima: moure el diagnòstic a un esdeveniment extern en lloc de periòdic —quan el demani el servidor, o un cop al dia en un moment de calma—, cosa que elimina 4 punts d'utilització i tota la seva interferència sense perdre funcionalitat.
Solució 2
(a) La seqüència. És una inversió de prioritats de manual, amb el mateix esquema del Pathfinder:
tasca_enviament (prioritat 1) pren el mutex de la memòria intermèdia i comença a transmetre, mantenint-lo fins a 2.400 ms, que és llarguíssim. Als 100 ms, tasca_mostreig (prioritat 4) intenta prendre'l, no pot i esgota la seva espera de 5 ms: perd la lectura. Llavors tasca_promitjat (prioritat 2), que no fa servir el mutex i té més prioritat que enviament, la desallotja durant 180 ms; mentre està desallotjada, enviament no avança i no deixa anar el mutex, així que mostreig continua fallant a cada activació. Com que mostreig no completa la seva feina, no posa el seu bit VIVA_MOSTREIG, tasca_watchdog ho comprova i correctament no refresca, i en esgotar-se el temporitzador d'1 s el maquinari reinicia l'estació.
El paper de cada tasca: enviament és la baixa que reté el recurs, promitjat és la mitjana que desallotja sense fer servir el recurs —l'ingredient que converteix un blocatge normal en inversió—, mostreig és l'alta víctima, i el watchdog és el detector que fa visible la fallada.
(b) Per què és aleatori. Calen tres coincidències simultànies: que la transmissió sigui llarga (depèn de la cobertura NB-IoT, que varia), que tasca_promitjat s'activi dins d'aquella finestra, i que la suma de desallotjaments mantingui mostreig sense poder marcar el seu bit durant més d'1 s complet. Amb bona cobertura la transmissió dura 400 ms i no hi ha temps; amb cobertura dolenta s'allarga i coincideix. Per això passa entre dues i sis vegades al dia i sense patró horari: depèn de la propagació radioelèctrica, no de l'hora.
(c) Tres correccions independents:
- No mantenir el mutex durant la transmissió. Prendre el mutex, copiar el lot a una memòria intermèdia local, deixar-lo anar immediatament, i transmetre sense el mutex. La secció crítica passa de 2.400 ms a menys d'1 ms.
- Activar l'herència de prioritat al mutex (fer servir
xSemaphoreCreateMutexi no un semàfor binari). Així, quanmostreiges bloqueja,enviamenthereta prioritat 4 ipromitjatja no la pot desallotjar. - Fer servir una memòria intermèdia sense blocatge —un anell de productor únic i consumidor únic amb índexs atòmics—, eliminant el mutex completament entre mostreig i enviament.
La millor és la primera, i per una raó de fons: ataca la causa arrel, que és una secció crítica llarguíssima. És el principi de 03-04 portat a la seva conclusió: dins del mutex, només l'imprescindible. A més és un canvi petit, no depèn de característiques de l'RTOS i fa que el sistema sigui correcte encara que l'herència estigués desactivada. La tercera és elegant i la més ràpida, però és la més fàcil d'implementar malament.
(d) N'hi hauria hagut prou amb l'herència de prioritat? Redueix el problema dràsticament, però no el resol del tot. Amb herència, promitjat deixa de desallotjar enviament, així que desapareix la inversió pròpiament dita i el blocatge queda acotat a la durada de la secció crítica. Però aquella secció crítica continua sent de 2.400 ms, així que tasca_mostreig continuaria sense poder inserir durant fins a 2,4 segons: continuaria perdent ~24 lectures seguides i, amb el watchdog a 1 s, continuaria reiniciant.
La conclusió és important i matisa la lliçó del Pathfinder: l'herència de prioritat acota el blocatge, no l'elimina. Si la secció crítica és més llarga que el termini de la tasca d'alta prioritat, cap quantitat d'herència no salva el disseny. L'arranjament estructural és sempre escurçar la secció crítica.
Solució 3
(a) Redisseny. De 7 peticions i 2,4 MB a 1 petició i uns 15-25 KB comprimits:
GET /v1/panell?estacions=12,17,23&historic=24h&resolucio=15m
&camps=temp,hum&format=agregat
Accept-Encoding: gzip
If-None-Match: "w/panell-12,17,23-8421"Decisions i justificació:
Un sol endpoint compost retorna estat actual, sèrie històrica i avisos de les tres estacions: regla "una pantalla, una petició", que elimina 6 despertars de ràdio, la despesa energètica dominant. S'envien agregats i no dades crues —24 h a 15 minuts són 96 punts per estació, 288 en total, davant de 720.000 lectures; el client pinta una corba de 300 píxels, i enviar-li més resolució que píxels és malbaratament pur—, amb selecció de camps (ni pressió ni metadades que la pantalla no mostra) i compressió obligatòria, perquè el JSON de sèries numèriques comprimeix 5:1 o més. I els avisos es calculen al servidor: que "no reporta des de fa una hora" ho decidís el client obligaria a descarregar marques de temps de tot.
(b) Tolerància a la desconnexió:
ETag + 304 Not Modified, perquè un refresc sense canvis costi centenars de bytes en lloc de 20 KB; sincronització incremental (?des_de_versio=8421), perquè després d'un túnel l'app demani només el que és nou; Retry-After, perquè el servidor indiqui quan reintentar en lloc de deixar l'app en un bucle amb la ràdio encesa; errors distingibles, 4xx (no reintentar) davant de 5xx i 429 (reintentar amb retard exponencial), perquè un client que reintenta un 400 gasta energia eternament per no res; i memòria cau local amb marca de frescor, perquè l'app sigui útil sense xarxa mostrant el que hi havia amb un "actualitzat fa X".
(c) Endpoint idempotent per a correccions:
POST /v1/lectures/correccions
Idempotency-Key: 7f3c1e9a-2b44-4c8d-9f01-6ab2e5d31c07
Content-Type: application/json
{"estacio_id": 17, "timestamp": 1756598400, "temperatura": 21.4,
"motiu": "sensor descalibrat"}Mecanisme. El client genera un UUID per intenció de correcció —no per intent— i el repeteix en tots els reintents d'aquella mateixa correcció. El servidor manté una taula de claus processades amb la seva resposta i un TTL (24-48 h):
- Arriba una petició amb clau. Si la clau no existeix, s'aplica la correcció, es desa
clau → respostaa la mateixa transacció i es retorna201. - Si la clau ja existeix, no s'aplica res i es retorna la resposta desada, amb
200. - Si la clau existeix però està en curs, es retorna
409perquè el client reintenti aviat.
Desar la clau i aplicar la correcció a la mateixa transacció és el que fa correcte el mecanisme: si es fessin per separat, una fallada entre totes dues deixaria la porta oberta al duplicat. El cas que això resol és exactament el que es dona cada dia en un mòbil: el servidor processa la correcció, la resposta es perd en entrar en un túnel, i el client reintenta creient que va fallar.
(d) Relació decisió ↔ restricció:
| Decisió | Restricció del mòbil |
|---|---|
| Una petició en lloc de set | Ràdio i bateria: cada despertar de ràdio té cost fix, més segons de cua en alt consum |
| Agregats i selecció de camps | Memòria (el procés pot morir per LMKD) i dades de l'usuari |
| Compressió | Ràdio (menys temps de transmissió) i dades |
ETag / 304 |
Xarxa intermitent i bateria: revalidar no costa gairebé res |
| Sincronització incremental | Xarxa intermitent: reprendre ha de ser barat |
Retry-After i errors distingibles |
Bateria: evita bucles de reintent amb la ràdio encesa |
| Idempotència | Xarxa intermitent: el reintent és inevitable, així que ha de ser segur |
| Memòria cau local amb frescor | Xarxa intermitent: l'app ha de ser útil sense cobertura |
Conclusió
Aquesta lliçó tanca el mòdul 6, que ha tingut un fil únic: l'aïllament, vist en quatre escales successives.
A 06-01 vam veure l'aïllament més fort: duplicar la màquina sencera. Els criteris de Popek i Goldberg i el seu teorema, el fracàs de x86 amb les seves 17 instruccions sensibles no privilegiades i les tres respostes —traducció binària, paravirtualització i assistència per maquinari amb el mode arrel, la VMCS i el VM exit com a unitat de cost—; KVM convertint Linux en hipervisor, on una VM és un procés i una vCPU és un fil; i la virtualització dels tres recursos del mòdul 2: vCPU amb sobresubscripció i st a top, memòria amb EPT/NPT, ballooning i KSM, i E/S amb l'escala emulació → virtio → SR-IOV.
A 06-02 vam veure l'aïllament més lleuger: no duplicar res i limitar el que un procés veu i consumeix. Els vuit espais de noms —amb user com la millora de seguretat decisiva i chroot com el que mai no va ser seguretat—, els cgroups v2 els fitxers de text dels quals a /sys/fs/cgroup governen CPU, memòria, E/S i PID, la seguretat imprescindible a sobre (capabilities, seccomp, MAC), i overlayfs amb el seu copy-up explicant per què cent contenidors caben on cabia un.
A 06-03 vam veure què queda del sistema operatiu quan la màquina deixa de ser un objecte: bestiar i no mascotes, cloud-init, el servei de metadades i el seu risc, la infraestructura immutable, els sistemes mínims, l'emmagatzematge per capes de latència i cost, i l'orquestració com a sistema operatiu del clúster, on requests i limits són els cgroups de la lliçó anterior i el planificador del clúster és el de 02-02 un nivell més amunt.
I en aquesta darrera lliçó hem vist els dos mons on les restriccions són les contràries. En mòbil, com la manca de swap converteix l'OOM killer en política ordinària (LMKD), com Binder va resoldre el que l'IPC d'UNIX no donava —identitat injectada pel nucli, una sola còpia, recompte de referències—, com zygote aplica fork i còpia en escriure per arrencar apps a l'instant, com un UID per aplicació reutilitza el mecanisme més vell del curs per aïllar programari no fiable, i com l'energia es converteix en el recurs que governa el disseny, amb wake locks, Doze i planificació conscient de big.LITTLE. En temps real, que la propietat no és la velocitat sinó la previsibilitat; el model de tasques periòdiques i el difícil problema del WCET; Rate Monotonic amb la fita de Liu i Layland —suficient però no necessària, com va demostrar la nostra estació amb U = 78,8 % sobre una fita del 75,7 % i tot i així planificable amb 1.418 ms de folgança—; EDF, òptim en utilització però impredictible en sobrecàrrega; la inversió de prioritats del Mars Pathfinder amb la seva tasca de dades meteorològiques, i la lliçó matisada que l'herència acota el blocatge però només escurçar la secció crítica l'elimina; i el microprogramari complet de l'estació, amb les seves prioritats justificades, la seva anàlisi numèrica i les seves set decisions de disseny.
Amb això es tanca la meitat conceptual del curs. Saps què és un sistema operatiu (mòdul 1), com reparteix els recursos (2), com coordina l'execució simultània (3), com organitza la persistència (4), com protegeix (5) i com aïlla (6).
Falta la part que separa qui entén un sistema de qui el pot arreglar a les tres de la matinada. Perquè quan meteo-01 respon lent i no saps per què, cap de les abstraccions que has après no serveix de res si no saps quina ordre executar primer, com llegir-ne la sortida i com passar d'un símptoma vague —"el web va lent"— a una causa concreta —"l'agregador està fent lectures aleatòries de 4 KB i ha saturat la cua del RAID"—. Això ja no és teoria: és mètode, pràctica i unes quantes eines que cal conèixer de memòria.
És el Mòdul 7: Administració i Diagnòstic a la Pràctica, i comença baixant a la interfície per on passa tota la resta: La Línia de Comandes com a Interfície del Sistema.
Fonaments de Sistemes Operatius
Mòdul 1: Introducció als Sistemes Operatius
- Conceptes Bàsics de Sistemes Operatius
- Història i Evolució dels Sistemes Operatius
- Tipus de Sistemes Operatius
- Funcions Principals d'un Sistema Operatiu
- Arquitectura del Nucli: Monolític, Microkernel i Híbrid
- Mode Usuari, Mode Nucli i Crides al Sistema
Mòdul 2: Gestió de Recursos
- Gestió de Processos
- Planificació de la CPU
- Gestió de Memòria
- Memòria Virtual i Paginació
- Gestió d'Emmagatzematge
- Gestió de Dispositius
- Controladors, Interrupcions i Operacions d'E/S
Mòdul 3: Concurrència
- Conceptes de Concurrència
- Fils i Processos
- Comunicació entre Processos (IPC)
- Sincronització i Exclusió Mútua
- Problemes Clàssics de Concurrència
- Interbloquejos: Prevenció, Detecció i Recuperació
Mòdul 4: Estructures de Fitxers
- Sistemes de Fitxers
- Estructures de Directoris
- Particions, Muntatge i Sistema de Fitxers Virtual
- Gestió de Fitxers
- Assignació d'Espai, Journaling i Integritat
- Seguretat i Permisos de Fitxers
Mòdul 5: Protecció i Seguretat del Sistema
- Principis de Protecció i Control d'Accés
- Usuaris, Autenticació i Escalada de Privilegis
- Amenaces Habituals i Enfortiment del Sistema
- Auditoria, Registres i Resposta a Incidents
Mòdul 6: Virtualització i Contenidors
- Virtualització: Hipervisors i Màquines Virtuals
- Contenidors: Namespaces i cgroups
- El Sistema Operatiu al Núvol
- Sistemes Operatius Mòbils i de Temps Real
