A la lliçó anterior vam arribar a la idea de la paginació i la vam deixar plantejada: si totes les peces fan la mateixa mida, encaixar-les deixa de ser un problema. Ara toca complir la promesa. Aquesta és la lliçó més densa del mòdul i probablement la més rendible de tot el curs, perquè la memòria virtual és el mecanisme que explica una quantitat enorme de coses que veuràs en producció: per què un procés reserva 400 MB i només n'usa 31, per què la primera petició a meteo-api després d'un desplegament és lenta i les següents no, per què un servidor que comença a paginar deixa de respondre de cop, i per què l'OOM killer tria el que tria.

Veuràs la traducció d'adreces amb números concrets, calcularàs la mida impossible d'una taula plana de 64 bits, mesuraràs quant rendiment depèn de la TLB, seguiràs una fallada de pàgina pas a pas, resoldràs els algorismes de reemplaçament sobre una mateixa cadena de referències i mapejaràs el fitxer 2026-08-31.dat en memòria amb mmap(). Al final sabràs interpretar free -h, vmstat i el rastre de l'OOM killer amb criteri.

Contingut

  1. Pàgines i marcs
  2. Traducció d'adreces, amb un exemple numèric complet
  3. La taula de pàgines i els seus bits de control
  4. Per què una taula plana és impossible en 64 bits
  5. Taules multinivell
  6. La TLB i el temps efectiu d'accés
  7. Pàgines grans (huge pages)
  8. Paginació per demanda i la fallada de pàgina
  9. Algorismes de reemplaçament de pàgines
  10. Assignació de marcs, hiperpaginació i conjunt de treball
  11. Fitxers mapejats i memòria compartida amb mmap()
  12. Copy-on-write, revisitat
  13. Swap, swappiness i l'OOM killer
  14. Mesura: VSZ enfront de RSS, /proc/meminfo, free -h, vmstat

Pàgines i marcs

Els dos termes són la base de tot el vocabulari i convé fixar-los sense ambigüitat:

Pàgina Marc (frame)
On viu Espai lògic del procés Memòria física
Mida 4 KB (típica) 4 KB (la mateixa, obligatòriament)
Quantes n'hi ha Fins a 2^36 a x86-64 RAM / 4 KB
Es numeren des de 0, per procés 0, globalment

Comprova la mida de pàgina del teu sistema:

$ getconf PAGESIZE
4096

I els números de meteo-01:

Espai lògic per procés: 2^47 bytes = 128 TB → 2^35 pàgines
RAM instal·lada: 8 GB = 8.589.934.592 bytes → 2.097.152 marcs

Hi ha trenta-quatre mil milions de vegades més pàgines possibles que marcs disponibles. Aquesta desproporció és exactament el que fa possible la memòria virtual: la immensa majoria de les pàgines no existirà mai, i les que existeixin no han d'estar totes a la RAM alhora.

Traducció d'adreces, amb un exemple numèric complet

Una adreça lògica es parteix en dos camps:

┌────────────────────────┬──────────────────────┐
│  Número de pàgina (p)  │   Desplaçament (d)   │
└────────────────────────┴──────────────────────┘
  • El desplaçament ocupa tants bits com calgui per adreçar dins d'una pàgina. Amb pàgines de 4 KB = 2^12 bytes, són 12 bits.
  • El número de pàgina ocupa la resta.

La traducció té tres passos:

  1. Extreure p i d de l'adreça lògica.
  2. Buscar a la taula de pàgines l'entrada p, que conté el número de marc f.
  3. L'adreça física és f × mida_pàgina + d.

El desplaçament no es tradueix mai. Pàgina i marc fan la mateixa mida, així que la posició dins de la pàgina és idèntica a la posició dins del marc. Només canvia quin bloc, no on dins del bloc.

Exemple complet

Treballem amb un espai lògic petit per poder veure-ho tot: adreces de 16 bits i pàgines de 4 KB.

Adreça lògica: 16 bits
Pàgina: 4 KB = 2^12 → desplaçament de 12 bits
Número de pàgina: 16 − 12 = 4 bits → 16 pàgines (0 a 15)

Taula de pàgines de l'ingestor:

Pàgina Marc Present
0 5
1 9
2 2
3 No
4 7

Traduir l'adreça lògica 0x2A5C.

Pas 1, descompondre:

0x2A5C en binari: 0010 1010 0101 1100
                  └──┘ └────────────┘
                   p        d

p = 0010₂ = 2
d = 1010 0101 1100₂ = 0xA5C = 2652

Comprovació aritmètica, que sol ser més ràpida:

p = 0x2A5C / 4096 = 10844 / 4096 = 2 (divisió entera)
d = 0x2A5C % 4096 = 10844 − 2×4096 = 2652

Pas 2, consultar la taula: pàgina 2 → marc 2, present.

Pas 3, compondre l'adreça física:

física = 2 × 4096 + 2652 = 8192 + 2652 = 10844 = 0x2A5C

Ha coincidit amb la lògica per casualitat (la pàgina 2 és al marc 2). Provem-ne una altra.

Traduir 0x105C:

p = 0x105C / 4096 = 4188 / 4096 = 1
d = 4188 − 4096 = 92

Taula: pàgina 1 → marc 9
física = 9 × 4096 + 92 = 36864 + 92 = 36956 = 0x905C

Fixa't en un detall revelador: 0x105C0x905C. Els tres dígits hexadecimals de la dreta no canvien (05C), perquè són el desplaçament. Només canvia el dígit de l'esquerra: 1 → 9, de pàgina a marc. En hexadecimal la traducció és visualment evident quan la mida de pàgina és potència de 16.

Traduir 0x3200:

p = 3, d = 512
Taula: pàgina 3 → NO PRESENT
→ FALLADA DE PÀGINA

Aquí no hi ha traducció possible. La MMU genera una excepció i el nucli pren el control. Què fa aleshores és l'apartat 8.

La taula de pàgines i els seus bits de control

Cada entrada de la taula de pàgines (PTE, Page Table Entry) ocupa 8 bytes a x86-64 i conté molt més que un número de marc:

Bit Nom Què significa Qui el posa
0 P (Present) La pàgina és a la RAM El nucli
1 R/W 0 = només lectura, 1 = lectura/escriptura El nucli
2 U/S 0 = només mode nucli, 1 = accessible en usuari El nucli
3 PWT Política de memòria cau write-through El nucli
4 PCD Memòria cau deshabilitada (per a E/S mapejada) El nucli
5 A (Accessed) S'ha accedit a la pàgina El maquinari
6 D (Dirty) S'ha escrit a la pàgina El maquinari
7 PS (Page Size) És una pàgina gran (2 MB o 1 GB) El nucli
8 G (Global) No s'invalida en canviar de procés El nucli
12-51 Número de marc Els 40 bits del marc físic El nucli
63 NX (No eXecute) La pàgina no es pot executar El nucli

Els que importen de debò i per què:

Bit P (present). És l'interruptor de tota la memòria virtual. Si val 0, qualsevol accés provoca una fallada de pàgina. El nucli el fa servir per a tres situacions diferents, i les distingeix amb els bits restants de l'entrada, que queden lliures quan P=0: pàgina mai carregada, pàgina expulsada a swap, o adreça simplement no vàlida.

Bits R/W i NX. Aquí és on s'implementa la protecció per regió que vèiem a /proc/<pid>/maps. La regió r-xp del codi té R/W=0 (no escrivible); les regions rw-p de pila i monticle tenen NX=1 (no executable). Aquest parell de bits és la política W^X.

Bits A i D. Són especials perquè els escriu el maquinari, no el sistema operatiu. Cada vegada que la CPU accedeix a una pàgina hi posa A=1; cada vegada que hi escriu hi posa D=1. El nucli els llegeix i els neteja periòdicament. Sense ells seria impossible implementar els algorismes de reemplaçament: el nucli no té manera d'observar cada accés a memòria, així que necessita que el maquinari li deixi aquesta pista.

Bit D (brut). Determina el cost d'expulsar la pàgina. Si D=0, la pàgina no ha canviat des que es va carregar, així que es pot descartar sense més (si torna a caldre es rellegeix del fitxer). Si D=1, cal escriure-la a swap abans de reutilitzar el marc. És exactament la distinció de la columna Dirty de pmap que vam veure a 02-03, i la diferència de cost és de zero enfront de mil·lisegons.

Per què una taula plana és impossible en 64 bits

Fem el càlcul que justifica tota la complexitat que ve després.

A x86-64 s'usen actualment 48 bits d'adreça virtual (els 16 superiors són extensió de signe). Amb pàgines de 4 KB:

Bits per al desplaçament: 12
Bits per al número de pàgina: 48 − 12 = 36
Nombre de pàgines possibles: 2^36 = 68.719.476.736

Mida d'una taula plana:
68.719.476.736 entrades × 8 bytes = 549.755.813.888 bytes = 512 GB

512 GB de taula de pàgines. Per procés. En una màquina de 8 GB amb 180 processos, caldrien 92 TB només per a les taules.

És absurd, i ho és per una raó molt concreta: la taula plana reserva una entrada per a cada pàgina possible, incloses les que no existiran mai. Recorda el mapa de memòria de l'ingestor: el codi és a 0x400000 i la pila a 0x7ffd8b3a1000. Entre tots dos hi ha un abisme de 140 TB d'adreces que no es faran servir mai, i una taula plana necessitaria una entrada per a cadascuna.

El procés real fa servir unes 4.500 pàgines dels 68.000 milions possibles: el 0,0000065 %. L'estructura ha d'aprofitar aquesta dispersió extrema.

Taules multinivell

La solució és fer la taula jeràrquica i dispersa: dividir el número de pàgina en diversos camps, cadascun indexant un nivell, i crear només les taules de nivell inferior que realment calguin.

x86-64 fa servir quatre nivells (cinc a les CPU més recents). El número de pàgina de 36 bits es parteix en quatre camps de 9 bits:

┌────────┬────────┬────────┬────────┬──────────────┐
│ PML4   │  PDPT  │   PD   │   PT   │ Desplaç. 12  │
│ 9 bits │ 9 bits │ 9 bits │ 9 bits │              │
└────────┴────────┴────────┴────────┴──────────────┘

Cada nivell té 2^9 = 512 entrades de 8 bytes = exactament 4.096 bytes, una pàgina. Aquest encaix no és casual: cada taula ocupa just una pàgina, cosa que en simplifica enormement la gestió.

flowchart LR
    CR3["Registre CR3<br/>(per procés)"] --> PML4
    PML4["PML4<br/>512 entrades"] -->|índex 9 bits| PDPT
    PDPT["PDPT<br/>512 entrades"] -->|índex 9 bits| PD
    PD["Directori<br/>512 entrades"] -->|índex 9 bits| PT
    PT["Taula de pàgines<br/>512 entrades"] -->|índex 9 bits| MARC["Marc físic<br/>+ desplaçament"]

L'estalvi és espectacular. Per a un procés que fa servir codi a la part baixa i pila a l'alta:

1 taula PML4:                           4 KB
2 taules PDPT (una per zona):           8 KB
2 taules PD:                            8 KB
~10 taules PT (per a ~5.000 pàgines):  40 KB
                                      ───────
Total:                                  60 KB

60 KB enfront de 512 GB. Un factor de vuit milions.

El preu: una traducció requereix quatre accessos a memòria (un per nivell) més l'accés a les dades. Cinc accessos on abans n'hi havia un. A ~100 ns per accés a RAM, això serien 500 ns per cada lectura de memòria: el sistema seria 5 vegades més lent que sense paginació. Inacceptable.

Aquesta és la raó exacta per la qual existeix la TLB.

La TLB i el temps efectiu d'accés

La TLB (Translation Lookaside Buffer) és una memòria cau associativa, dins de la MMU, que guarda les traduccions pàgina→marc usades recentment. És petita (entre 64 i 1.536 entrades a les CPU modernes) i rapidíssima (menys d'1 ns).

El flux:

  1. La CPU genera una adreça lògica.
  2. La MMU busca el número de pàgina a la TLB.
  3. Encert de TLB: s'obté el marc directament. Cost ≈ 0.
  4. Fallada de TLB: cal recórrer els quatre nivells de taules en memòria i després inserir el resultat a la TLB.

El càlcul del temps efectiu d'accés (TEA) és la fórmula que cal saber fer:

TEA = h × (t_TLB + t_mem) + (1 − h) × (t_TLB + n × t_mem + t_mem)

on h és la taxa d'encerts, t_TLB el temps de consulta de la TLB, t_mem l'accés a memòria i n el nombre de nivells.

Amb valors realistes (t_TLB = 1 ns, t_mem = 100 ns, n = 4):

Taxa d'encerts Càlcul TEA Degradació
100 % 1 + 100 101 ns 1,00×
99 % 0,99×101 + 0,01×501 105 ns 1,04×
95 % 0,95×101 + 0,05×501 121 ns 1,20×
90 % 0,90×101 + 0,10×501 141 ns 1,40×
70 % 0,70×101 + 0,30×501 221 ns 2,19×
50 % 0,50×101 + 0,50×501 301 ns 2,98×

Detall del cas del 99 %:

Encert  (99 %):  1 ns (TLB) + 100 ns (dada)               = 101 ns
Fallada (1 %):   1 ns + 4×100 ns (taules) + 100 ns (dada) = 501 ns
TEA = 0,99 × 101 + 0,01 × 501 = 99,99 + 5,01 = 105,0 ns

La conclusió pràctica és contundent: amb un 99 % d'encerts només perds un 4 %; amb un 70 % perds més del doble de rendiment. La TLB no és una optimització menor, és el que fa viable la paginació.

I per això importa tant el cost del canvi de context que vam calcular a 02-01: en canviar cr3, les entrades de TLB del procés anterior deixen de valer i s'invaliden. El procés entrant arrenca amb la TLB buida i una ràfega de fallades. Els PCID (identificadors de context de procés) ho mitiguen etiquetant cada entrada amb el procés a què pertany, de manera que no calgui buidar-la sencera.

Pots veure les fallades de TLB reals:

$ sudo perf stat -e dTLB-load-misses,dTLB-loads -p 1877 sleep 10

 Performance counter stats for process id '1877':

        12.847.331      dTLB-load-misses    #    0,84% of all dTLB cache accesses
     1.529.204.882      dTLB-loads

      10,003 seconds time elapsed

Un 0,84 % de fallades, és a dir un 99,16 % d'encerts. Perfectament sa. Si hi veiessis un 15 % de fallades, tindries un procés amb un patró d'accés molt dispers, i aquí és on entren les pàgines grans.

Pàgines grans (huge pages)

El problema apareix quan un procés treballa amb conjunts de dades enormes. L'agregador processant un dia sencer de lectures:

Dades d'un dia: 17 MB
Pàgines de 4 KB necessàries: 17.000.000 / 4.096 ≈ 4.150 pàgines
Entrades de TLB disponibles (típic L2 TLB): 1.536

No hi caben. Recórrer els 17 MB provoca fallades de TLB contínues, perquè les traduccions s'expulsen entre elles. I amb 30 dies d'històric serien 124.000 pàgines: la TLB seria inútil.

Les pàgines grans resolen això fent servir una mida de pàgina més gran: 2 MB o 1 GB a x86-64. S'aconsegueix aturant la jerarquia un nivell abans (una entrada del directori de pàgines apunta directament a un bloc de 2 MB en lloc d'a una taula).

4 KB 2 MB 1 GB
Pàgines per a 17 MB 4.150 9 1
Entrades de TLB usades 4.150 9 1
Nivells de traducció 4 3 2
Fragmentació interna mitjana 2 KB 1 MB 512 MB
Granularitat d'expulsió a swap Fina Grossa Inviable

Els 17 MB de l'agregador passen de 4.150 entrades de TLB a 9. Amb això hi caben de sobres i les fallades de TLB pràcticament desapareixen.

A Linux hi ha dues maneres de fer-les servir:

$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never

$ grep -i huge /proc/meminfo
AnonHugePages:    215040 kB
HugePages_Total:       0
HugePages_Free:        0
Hugepagesize:       2048 kB
  • THP (Transparent Huge Pages): el nucli promou automàticament regions grans a pàgines de 2 MB. Els 215 MB d'AnonHugePages indiquen que ja està passant. És còmode, però té un cost conegut: el dimoni khugepaged compacta memòria en segon pla i pot introduir pauses. Per això moltes bases de dades recomanen posar-lo a madvise o never.
  • Huge pages explícites: es reserven en arrencar i l'aplicació les demana expressament. Més control, menys comoditat.

Regla de decisió: les pàgines grans ajuden quan el conjunt de treball és gran i es recorre de manera dispersa; fan nosa quan la memòria és escassa i cal paginar, perquè expulsar 2 MB de cop és molt car.

Paginació per demanda i la fallada de pàgina

Aquí arriba la peça que converteix la paginació en memòria virtual: no cal que totes les pàgines d'un procés siguin a la RAM.

La paginació per demanda consisteix a no carregar res fins que es necessiti. En arrencar meteo-api, el nucli no llegeix els seus 820 KB de codi: crea les entrades amb P=0 i deixa que les fallades de pàgina portin el que calgui. Això explica les dades que vam veure a 02-03: libssl amb 12 KB d'1.024 KB carregats.

Quan la CPU accedeix a una pàgina amb P=0:

sequenceDiagram
    participant P as Procés
    participant M as MMU
    participant K as Nucli
    participant D as Disc
    P->>M: accés a l'adreça 0x3200
    M->>M: consulta la taula: bit P = 0
    M->>K: excepció de fallada de pàgina (#PF)<br/>adreça a CR2
    K->>K: és una adreça vàlida del procés?
    alt Adreça no vàlida
        K->>P: SIGSEGV → violació de segment
    else Adreça vàlida
        K->>K: busca un marc lliure
        alt No hi ha marcs lliures
            K->>K: tria una víctima (algorisme de reemplaçament)
            K->>D: si és bruta, l'escriu a swap
        end
        K->>D: llegeix la pàgina del fitxer o del swap
        D-->>K: dades (0,1 - 10 ms)
        K->>K: actualitza la PTE: marc i P = 1
        K->>P: reintenta la instrucció que ha fallat
    end

Un detall essencial del final: la instrucció que ha fallat es torna a executar des de zero. El procés no se n'assabenta de res. Per a ell, aquell accés a memòria simplement ha trigat molt.

El cost real d'una fallada de pàgina

Aquí és on els números fan mal. Hi ha dos tipus de fallada molt diferents:

Tipus Què passa Cost Exemple
Fallada menor (minor) La pàgina és a la RAM però no mapejada en aquest procés 1-3 µs Memòria cau de pàgines, COW, biblioteca ja carregada
Fallada major (major) Cal llegir-la del disc 0,1-10 ms Primera lectura d'un fitxer, pàgina a swap

La diferència és de tres a quatre ordres de magnitud:

Fallada menor amb SSD NVMe:    ~2 µs
Fallada major amb SSD NVMe:  ~100 µs   (50 vegades més)
Fallada major amb disc dur:    ~8 ms   (4.000 vegades més)

I ara el càlcul que explica per què la taxa de fallades majors ha de ser minúscula. Amb accés normal a memòria de 100 ns i fallada major de 8 ms:

TEA = (1 − p) × 100 ns + p × 8.000.000 ns
Taxa de fallades p TEA Degradació
0 100 ns
1 de cada 1.000.000 108 ns 1,08×
1 de cada 100.000 180 ns 1,8×
1 de cada 10.000 900 ns
1 de cada 1.000 8.100 ns 81×

Amb una fallada major per cada mil accessos, el sistema va 81 vegades més lent. Per mantenir la degradació per sota del 10 % cal menys d'una fallada per cada milió d'accessos.

Això no és una curiositat acadèmica: és exactament el que li passa a un servidor que comença a paginar. No es degrada suaument, cau per un precipici. I és la raó de la hiperpaginació que veurem a l'apartat 10.

Pots veure les fallades dels teus processos:

$ ps -eo pid,min_flt,maj_flt,comm -u meteora
    PID  MINFL  MAJFL COMMAND
   1842  84213      3 ingestor
   1877 291045    112 agregador
   1901 138922      8 meteo-api

Les fallades menors són normals i abundants: formen part del funcionament habitual. Les majors són les que importen: 112 a l'agregador després de 22 hores és perfectament sa. Si en veiessis 400.000, el procés estaria llegint constantment de swap i aquest seria el problema a resoldre.

Algorismes de reemplaçament de pàgines

Quan es produeix una fallada de pàgina i no queden marcs lliures, cal expulsar algú. L'elecció determina quantes fallades hi haurà després.

Resoldrem tots els algorismes sobre la mateixa cadena de referències, que és l'única manera honesta de comparar-los:

Cadena: 7 0 1 2 0 3 0 4 2 3 0 3 2 1 2 0 1 7 0 1
Marcs disponibles: 3

Òptim (OPT)

Expulsa la pàgina que trigarà més a tornar-se a usar. És impossible d'implementar (requereix conèixer el futur), però serveix com a fita inferior amb què comparar.

Ref Marcs Fallada Víctima i per què
7 [7,-,-]
0 [7,0,-]
1 [7,0,1]
2 [2,0,1] 7 (no torna fins al final)
0 [2,0,1] ja hi és
3 [2,0,3] 1 (torna a la posició 14)
0 [2,0,3] ja hi és
4 [2,4,3] 0 (torna més tard que 2 i 3)
2 [2,4,3]
3 [2,4,3]
0 [2,0,3] 4 (no torna mai)
3 [2,0,3]
2 [2,0,3]
1 [2,0,1] 3 (no torna)
2 [2,0,1]
0 [2,0,1]
1 [2,0,1]
7 [7,0,1] 2 (no torna)
0 [7,0,1]
1 [7,0,1]

9 fallades.

FIFO

Expulsa la pàgina que fa més temps que està carregada, sense mirar-ne l'ús.

Ref Marcs (ordre d'arribada) Fallada
7 [7]
0 [7,0]
1 [7,0,1]
2 [0,1,2] ● surt 7
0 [0,1,2]
3 [1,2,3] ● surt 0
0 [2,3,0] ● surt 1
4 [3,0,4] ● surt 2
2 [0,4,2] ● surt 3
3 [4,2,3] ● surt 0
0 [2,3,0] ● surt 4
3 [2,3,0]
2 [2,3,0]
1 [3,0,1] ● surt 2
2 [0,1,2] ● surt 3
0 [0,1,2]
1 [0,1,2]
7 [1,2,7] ● surt 0
0 [2,7,0] ● surt 1
1 [7,0,1] ● surt 2

15 fallades. Un 67 % més que l'òptim.

L'anomalia de Belady

FIFO té un defecte que desafia la intuïció: donar-li més memòria pot augmentar les fallades. Amb aquesta cadena:

Cadena: 1 2 3 4 1 2 5 1 2 3 4 5

Amb 3 marcs:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
Fallada

9 fallades.

Amb 4 marcs:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
Fallada

10 fallades. Més memòria, més fallades.

La causa: FIFO no té la propietat de pila (que el conjunt de pàgines amb n marcs estigui contingut en el de n+1 marcs). En afegir un marc, l'ordre d'expulsió canvia del tot i pot expulsar justament el que anava a caldre. LRU i OPT sí que tenen aquesta propietat i per això estan lliures de l'anomalia.

És la raó principal per la qual FIFO pur no es fa servir en cap sistema real: un algorisme del qual no pots afirmar «amb més RAM anirà millor» és inacceptable.

LRU (menys usada recentment)

Expulsa la pàgina que fa més temps que no es fa servir. Es basa en el principi de localitat temporal: allò que s'ha usat fa poc probablement es tornarà a fer servir.

Ref Marcs (més recent a la dreta) Fallada
7 [7]
0 [7,0]
1 [7,0,1]
2 [0,1,2] ● surt 7
0 [1,2,0]
3 [2,0,3] ● surt 1
0 [2,3,0]
4 [3,0,4] ● surt 2
2 [0,4,2] ● surt 3
3 [4,2,3] ● surt 0
0 [2,3,0] ● surt 4
3 [2,0,3]
2 [0,3,2]
1 [3,2,1] ● surt 0
2 [3,1,2]
0 [1,2,0] ● surt 3
1 [2,0,1]
7 [0,1,7] ● surt 2
0 [1,7,0]
1 [7,0,1]

12 fallades. Entre l'òptim (9) i FIFO (15).

El problema de LRU és el seu cost d'implementació. Requereix ordenar les pàgines per últim accés, i això vol dir actualitzar una estructura a cada accés a memòria. Caldria maquinari que, a cada lectura, mogués una entrada al principi d'una llista de milers d'elements. Cap CPU no ho fa, perquè seria caríssim.

Aproximacions a LRU: segona oportunitat i algorisme del rellotge

Com que LRU exacte és inviable, s'aproxima fent servir el bit A (accedit) que el maquinari sí que manté de franc.

L'algorisme del rellotge organitza els marcs en un cercle amb un punter:

  1. El punter apunta a un marc candidat.
  2. Si el seu bit A = 0, s'expulsa i el punter avança.
  3. Si el seu bit A = 1, se li dona una segona oportunitat: es posa A = 0 i el punter avança al següent, sense expulsar res.
  4. Es repeteix fins a trobar un marc amb A = 0.
        ┌───────┐
   ┌───→│ P3 A=1│───┐
   │    └───────┘   ↓
┌───────┐        ┌───────┐
│ P0 A=0│        │ P4 A=1│
└───────┘        └───────┘
   ↑    ┌───────┐   │
   └────│ P2 A=0│←──┘
        └───────┘
             ↑ punter

La intuïció és exacta: una pàgina amb A=1 s'ha fet servir des de l'última volta del punter, així que probablement se seguirà usant. Una amb A=0 no s'ha tocat en tota una volta: és bona candidata.

Una variant millor fa servir dos bits, A i D, que combinen ús recent i cost d'expulsió:

A D Interpretació Prioritat d'expulsió
0 0 Ni usada ni modificada 1a: la millor víctima, es descarta de franc
0 1 No usada però modificada 2a: cal escriure-la, però no la volen
1 0 Usada, no modificada 3a: es descarta de franc però s'està usant
1 1 Usada i modificada 4a: la pitjor víctima

Linux fa servir una variant refinada: dues llistes LRU (activa i inactiva) per zona de memòria, amb promoció entre elles segons els accessos, més el bit A per a l'envelliment. És LRU aproximat, amb el cost amortitzat a gairebé zero.

Comparació final

Algorisme Fallades Enfront de l'òptim Implementable Anomalia de Belady
Òptim 9 No No
LRU 12 +33 % Només aproximat No
Rellotge (2a oportunitat) ~13 +44 % Sí, barat No
FIFO 15 +67 % Sí, trivial

I aquí hi ha la conclusió pràctica que se sol passar per alt: la diferència entre el millor algorisme possible i un de decent és un 33 %; la diferència entre tenir prou RAM i no tenir-ne és un factor de 81. Optimitzar l'algorisme de reemplaçament importa molt menys que dimensionar bé la memòria.

Assignació de marcs, hiperpaginació i conjunt de treball

Amb 180 processos i 2 milions de marcs, quants marcs li toquen a cada procés?

Assignació equitativa: marcs / processos. Simple i injusta: meteo-api amb 116 MB de dades rebria el mateix que un sshd de 9 MB.

Assignació proporcional: repartir segons la mida de cada procés.

marcs_i = (mida_i / Σ mides) × marcs_totals

I una distinció important:

  • Reemplaçament local: un procés que falla només es pot robar marcs a si mateix. El seu rendiment és predictible però no aprofita la memòria ociosa dels altres.
  • Reemplaçament global: pot robar a qualsevol. Millor ús global, però el rendiment d'un procés depèn del comportament dels altres. Linux fa servir reemplaçament global.

Hiperpaginació (thrashing)

Aquí hi ha el fenomen més important d'aquesta secció. Si un procés no té prou marcs per al seu conjunt de treball, entra en un cicle destructiu:

flowchart TD
    A["Procés amb pocs marcs"] --> B["Fallada de pàgina"]
    B --> C["Expulsa una pàgina<br/>que necessitarà de seguida"]
    C --> D["Es bloqueja esperant el disc"]
    D --> E["La CPU queda ociosa"]
    E --> F["El sistema creu que pot<br/>admetre més processos"]
    F --> G["Menys marcs per procés"]
    G --> B

El bucle es realimenta: com menys CPU es fa servir, més processos s'admeten, i menys memòria queda per a cadascun.

Els símptomes són inconfusibles i val la pena memoritzar-los:

Mètrica Valor en hiperpaginació Per què
Ús de CPU Molt baix (5-15 %) Tots els processos esperen disc
%iowait Molt alt (60-90 %) Només hi ha activitat de disc
Fallades majors Milers per segon Cada accés falla
Columna si/so de vmstat Centenars de MB/s Swap constant en tots dos sentits
Càrrega mitjana Altíssima Molts processos en estat D
Sensació El sistema «no respon» Ni tan sols accepta un ssh

La combinació CPU al 10 % amb càrrega mitjana de 40 és el diagnòstic. Si veiessis això a meteo-01:

$ vmstat 1 3
procs -----------memory---------- ---swap-- -----io---- --system-- ------cpu-----
 r  b   swpd   free  buff  cache   si   so    bi    bo   in    cs  us sy id wa st
 1 24 4194300  22140  1024  81920 48932 51204 62104 51988 8421 21044  4  9  2 85  0
 0 27 4194300  19008  1024  79872 52108 49872 64220 50104 9102 23811  3 11  1 85  0

Les dades parlen soles: 85 % de %wa (espera d'E/S), 27 processos bloquejats a la columna b, i si/so al voltant de 50.000 KB/s en totes dues direccions. El sistema està portant i emportant-se les mateixes pàgines contínuament. No hi ha cap ajust de configuració que salvi això: falta memòria.

El model del conjunt de treball

La solució conceptual la va formular Peter Denning el 1968. El conjunt de treball W(t, Δ) és el conjunt de pàgines referenciades per un procés en les últimes Δ referències.

Δ = 10.000 referències

Referències:  ...2 6 1 5 7 7 7 5 1 6 2 3 4 1 2 3 4 4 4 3 4 4 4 1 3 2 3 4 4 4 4 3...
                └──────── finestra Δ ───────┘
                W = {1, 2, 5, 6, 7}         W = {3, 4}

La regla és simple i potent:

Si Σ (conjunts de treball de tots els processos) > marcs disponibles
   → hi ha hiperpaginació

I quan això passa, l'única solució correcta és suspendre processos —reduir el grau de multiprogramació— perquè els que quedin tinguin el seu conjunt de treball complet. És contraintuïtiu però cert: executar menys processos fa que el sistema avanci més.

Linux no implementa el model de Denning literalment, però la seva pressió de memòria i l'OOM killer persegueixen el mateix objectiu: quan el sistema no pot sostenir-los tots, n'elimina algun en lloc de deixar que ningú avanci.

Fitxers mapejats i memòria compartida amb mmap()

mmap() és una de les crides al sistema més potents d'UNIX: fa que un fitxer aparegui com a memòria.

En lloc d'open + read + copiar a una memòria intermèdia, es mapeja el fitxer a l'espai d'adreces i s'hi accedeix com si fos un array.

/* llegir_lectures.c — compilar: gcc -Wall -o llegir_lectures llegir_lectures.c */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h>

struct Lectura {
    uint32_t estacio_id;
    uint32_t timestamp;
    float    temperatura;
    float    humitat;
    float    pressio;
    uint32_t _reservat;       /* completa els 24 bytes */
};

int main(void) {
    const char *ruta = "/var/lib/meteora/lectures/2026-08-31.dat";

    int fd = open(ruta, O_RDONLY);
    if (fd == -1) { perror("open"); return 1; }

    struct stat st;
    if (fstat(fd, &st) == -1) { perror("fstat"); return 1; }

    size_t n = st.st_size / sizeof(struct Lectura);
    printf("Fitxer de %ld bytes = %zu lectures\n", (long)st.st_size, n);

    /* Mapejar el fitxer sencer en memòria */
    struct Lectura *lectures = mmap(NULL, st.st_size,
                                    PROT_READ, MAP_PRIVATE, fd, 0);
    if (lectures == MAP_FAILED) { perror("mmap"); return 1; }

    close(fd);          /* el mapatge sobreviu al tancament del descriptor */

    /* Recórrer-lo com si fos un array normal */
    double suma = 0.0;
    float maxima = -100.0f;
    for (size_t i = 0; i < n; i++) {
        suma += lectures[i].temperatura;
        if (lectures[i].temperatura > maxima)
            maxima = lectures[i].temperatura;
    }

    printf("Temperatura mitjana: %.2f °C\n", suma / n);
    printf("Temperatura màxima: %.2f °C\n", maxima);

    munmap(lectures, st.st_size);
    return 0;
}
$ ./llegir_lectures
Fitxer de 17280000 bytes = 720000 lectures
Temperatura mitjana: 18.43 °C
Temperatura màxima: 34.70 °C

Què fa cada part i per què importa:

  • mmap(NULL, mida, PROT_READ, MAP_PRIVATE, fd, 0) demana al nucli que associï el fitxer a una regió de l'espai d'adreces. NULL deixa que el nucli triï l'adreça; PROT_READ la fa de només lectura; MAP_PRIVATE significa que les escriptures (si n'hi hagués) serien copy-on-write i no arribarien al fitxer.
  • No es llegeix res del disc en aquest moment. El nucli només crea entrades de taula de pàgines amb P=0. Els 17 MB arriben per demanda, quan el bucle els toqui: cada accés a una pàgina nova provoca una fallada de pàgina que la porta.
  • close(fd) no invalida el mapatge. El mapatge manté la seva pròpia referència al fitxer. És un detall que sorprèn i que permet tancar descriptors sense perdre l'accés.
  • lectures[i].temperatura és aritmètica de punters normal. El compilador genera un accés a memòria; la MMU i el nucli fan la resta. No hi ha cap crida al sistema dins del bucle.

Aquest últim punt és la raó de fons per fer servir mmap. Compara-ho amb l'alternativa de read(), aplicant el que vam calcular a 01-06:

read() en blocs de 4 KB mmap()
Crides al sistema 17.280.000 / 4.096 = 4.219 1
Cost de syscalls a 1 µs 4.219 µs = 4,2 ms 1 µs
Còpies de les dades 2 (disc→memòria cau→memòria intermèdia d'usuari) 1 (disc→memòria cau)
Memòria addicional La memòria intermèdia del procés Cap
Si dos processos llegeixen el mateix fitxer Dues còpies a la RAM Comparteixen les mateixes pàgines
Accés aleatori lseek + read Indexació directa

L'última fila és especialment valuosa per a Meteora: si agregador i meteo-api mapegen tots dos 2026-08-31.dat, les pàgines de la memòria cau del nucli es comparteixen físicament. Un sol joc de 17 MB a la RAM serveix els dos processos.

Quan mmap no convé: per a lectures seqüencials d'una sola passada de fitxers molt grans, read() amb una memòria intermèdia gran pot ser igual o millor, perquè mmap provoca una fallada de pàgina per cada 4 KB (milers d'excepcions), i perquè un fitxer més gran que la RAM mapejat sencer pot provocar pressió de memòria.

mmap per a memòria compartida

Amb MAP_SHARED en lloc de MAP_PRIVATE, les escriptures sí que es propaguen: al fitxer i a tots els processos que el mapegin.

int fd = open("/dev/shm/meteora-cache", O_RDWR | O_CREAT, 0640);
ftruncate(fd, 8 * 1024 * 1024);

void *cache = mmap(NULL, 8 * 1024 * 1024,
                   PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

Això és exactament la regió rw-s- que vam veure al pmap de meteo-api a 02-03: els quatre treballadors comparteixen una única memòria cau de 8 MB. Els mecanismes de comunicació entre processos i la sincronització que això exigeix corresponen a Comunicació entre Processos i Sincronització.

Copy-on-write, revisitat

Ara que coneixes els bits de la taula de pàgines, el COW de fork() que vam veure a 02-01 es pot explicar exactament:

  1. fork() copia les taules de pàgines del pare al fill.
  2. A totes dues còpies, totes les pàgines escrivibles es marquen amb R/W = 0 (només lectura), i el nucli anota internament que són COW.
  3. Tots dos processos apunten als mateixos marcs físics. Zero còpies de dades.
  4. Quan qualsevol dels dos escriu, la MMU detecta R/W=0 i genera una fallada de pàgina de protecció.
  5. El nucli la distingeix d'un error real (comprova que la regió és COW i no de només lectura genuïna), copia aquella única pàgina de 4 KB, l'assigna en exclusiva al que ha escrit i li posa R/W=1.
  6. Si el comptador de referències del marc original baixa a 1, l'altre procés també recupera R/W=1: ja no hi ha res a protegir.

Els números per a meteo-api amb els seus 148 MB:

Còpia ingènua:  148 MB / 20 GB/s = 7,4 ms
COW:            ~37.000 entrades de taula de pàgines ≈ 300 KB
                0,3 MB / 20 GB/s + gestió ≈ 0,5 ms
Factor de millora: ~15×
Si el fill fa execve() immediatament: es copien ~0 pàgines

I ara entens també per què fork() pot semblar barat i després costar: si el fill escriu per tota la memòria heretada, acabaràs pagant la còpia pàgina a pàgina, amb una fallada de pàgina per cada 4 KB. Aquesta és la queixa clàssica contra fork() en processos enormes, i la raó que existeixin alternatives com posix_spawn() i vfork().

Swap, swappiness i l'OOM killer

L'àrea d'intercanvi (swap) és l'espai en disc on es guarden les pàgines anònimes expulsades.

$ swapon --show
NAME      TYPE      SIZE USED PRIO
/dev/sda3 partition   4G 512M   -2

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           7,8Gi       3,1Gi       412Mi       528Mi       4,3Gi       3,9Gi
Swap:          4,0Gi       512Mi       3,5Gi

Interpretació de free -h, que és on gairebé tothom s'equivoca:

Columna Què és Parany habitual
total RAM instal·lada
used Usada pels processos
free Completament sense usar 412 MB no vol dir que falti memòria
buff/cache Memòria cau de pàgines i memòries intermèdies S'allibera a l'instant si cal
available El que un procés nou pot obtenir Aquesta és la columna que importa

Els 412 MB de free alarmen molta gent sense motiu. Els 4,3 GB de buff/cache són pàgines de fitxers que el nucli manté per si tornen a caldre, i són llencables a l'instant. La memòria realment disponible són els 3,9 GB d'available.

RAM lliure no és RAM aprofitada. Un sistema amb memòria lliure està desaprofitant l'oportunitat de fer memòria cau. El correcte és que free sigui baix i available sigui alt.

swappiness

Controla l'agressivitat amb què el nucli expulsa pàgines anònimes enfront de descartar memòria cau de fitxers:

$ cat /proc/sys/vm/swappiness
60
Valor Comportament Adequat per a
0 Swap només per evitar l'OOM killer Bases de dades amb prou RAM
1-10 Molt reticent a fer swap Servidors sensibles a la latència
60 Per defecte, equilibrat Escriptori, ús general
100 Tracta igual anònimes i de fitxer Càrregues amb molta lectura de fitxers

Per a meteo-01 un valor de 10 seria raonable: és preferible descartar memòria cau de pàgines (recuperable amb una lectura seqüencial ràpida) abans que enviar a swap la memòria cau de respostes de meteo-api (que provocaria fallades majors al camí crític de les peticions).

$ sudo sysctl -w vm.swappiness=10
$ echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.d/99-meteora.conf

Un avís important: swappiness=0 no desactiva el swap, i desactivar el swap del tot tampoc no és bona idea. Sense swap, les pàgines anònimes inactives —que en qualsevol sistema són moltes— es queden ocupant RAM eternament, i davant de pressió de memòria el nucli passa directament a l'OOM killer sense tenir alternatives intermèdies.

L'OOM killer

Quan no hi ha memòria ni marcs per expulsar, el nucli activa l'Out Of Memory killer: tria un procés i el mata.

L'elecció es basa en una puntuació:

$ cat /proc/1901/oom_score
187
$ cat /proc/1901/oom_score_adj
0

oom_score és proporcional a la memòria consumida, amb ajustos: penalitza els processos grans i d'usuaris normals, i protegeix els de root i el PID 1. oom_score_adj va de −1000 (no matar-lo mai) a +1000 (matar-lo primer) i és el que pots ajustar tu:

# Protegir l'ingestor: perdre lectures és irreversible
$ echo -900 | sudo tee /proc/1842/oom_score_adj

# Sacrificar primer la còpia de seguretat
$ echo 800 | sudo tee /proc/3901/oom_score_adj

El rastre que deixa al registre:

$ sudo dmesg -T | grep -A4 'Out of memory'
[Sun Aug 31 04:12:33 2026] agregador invoked oom-killer: gfp_mask=0x100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
[Sun Aug 31 04:12:33 2026] Mem-Info:
[Sun Aug 31 04:12:33 2026] active_anon:1842103 inactive_anon:204118 isolated_anon:0
[Sun Aug 31 04:12:33 2026] Tasks state (memory values in pages):
[Sun Aug 31 04:12:33 2026] [   pid ]   uid  tgid total_vm      rss  pgtables_bytes swapents oom_score_adj name
[Sun Aug 31 04:12:33 2026] [   1877]   998  1877  1204832  1180221     9539584        0             0 agregador
[Sun Aug 31 04:12:33 2026] [   1901]   998  1901    37080    33785      274432        0             0 meteo-api
[Sun Aug 31 04:12:33 2026] Out of memory: Killed process 1877 (agregador) total-vm:4819328kB, anon-rss:4720884kB, file-rss:0kB, shmem-rss:0kB, UID:998 pgtables:9316kB oom_score_adj:0

Com llegir-ho línia a línia, que és el que cal saber fer en una guàrdia:

  • agregador invoked oom-killer: qui va provocar l'OOM va ser l'agregador en demanar memòria. No implica necessàriament que en sigui el culpable, encara que aquí ho és.
  • La taula de tasques llista tots els processos amb el seu rss en pàgines. L'agregador té 1.180.221 pàgines × 4 KB = 4,5 GB. meteo-api en té 33.785 × 4 KB = 132 MB.
  • Killed process 1877 (agregador) amb anon-rss:4720884kB: es va triar el que més memòria anònima consumia, que és el criteri habitual.
  • anon-rss enfront de file-rss: els 4,7 GB són anònims, és a dir, memòria dinàmica sense suport en fitxer. Matar el procés els allibera immediatament. Si fos file-rss, matar-lo tot just alliberaria res.

Aquesta xifra de 4,5 GB en un agregador que en condicions normals fa servir 31 MB és el diagnòstic complet: processar un dia sencer carregant-ho tot a memòria en lloc de fer-ho per blocs. La correcció és de disseny —mmap o lectura per blocs—, no de configuració.

Recorda a més el que vam veure a 02-01: quan l'OOM killer mata un procés, aquest mor per SIGKILL (senyal 9), cosa que el teu supervisor detectaria amb WIFSIGNALED(estat) i WTERMSIG(estat) == 9.

Mesura: VSZ enfront de RSS, /proc/meminfo, free -h, vmstat

Tanquem amb les eines i com interpretar-les sense equivocar-se.

$ ps -eo pid,vsz,rss,comm -u meteora
    PID    VSZ   RSS COMMAND
   1842 408212 18204 ingestor
   1877 412308 31456 agregador
   1901 486300 148320 meteo-api
Mètrica Què mesura Quan la fas servir Parany
VSZ Espai d'adreces reservat Gairebé mai Inclou el mai tocat; reservar és de franc
RSS Pàgines realment a la RAM Estimació ràpida Compta les compartides a cada procés
PSS RSS amb les compartides dividides Sumar consum de diversos processos Només a smaps
USS Memòria exclusiva del procés Quant s'allibera en matar-lo Només a smaps

El problema del RSS amb un exemple concret: si meteo-api té 4 treballadors i cadascun reporta 148 MB de RSS, sumar dona 592 MB, però el consum real pot ser 200 MB perquè comparteixen codi, libc i la memòria cau de /dev/shm. PSS ho resol dividint cada pàgina compartida entre els qui la fan servir:

$ sudo awk '/^Pss:/ {suma += $2} END {print suma " kB (PSS)"}' /proc/1901/smaps
94208 kB (PSS)

94 MB reals enfront de 148 MB de RSS. Per comptabilitzar memòria en un servidor amb processos que comparteixen molt, PSS és la mètrica correcta.

/proc/meminfo dona la foto global:

$ grep -E 'MemTotal|MemAvailable|Cached|Dirty|Writeback|AnonPages|Mapped|Slab|SwapTotal|SwapFree' /proc/meminfo
MemTotal:        8122448 kB
MemAvailable:    4089216 kB
Cached:          4198400 kB
Dirty:             28160 kB
Writeback:             0 kB
AnonPages:       3021312 kB
Mapped:           412160 kB
Slab:             298432 kB
SwapTotal:       4194300 kB
SwapFree:        3670012 kB

Les línies que informen de debò:

  • MemAvailable: 3,9 GB. L'única que respon a «quanta memòria puc fer servir?».
  • AnonPages 2,9 GB: memòria anònima dels processos. Només pot anar a swap.
  • Cached 4 GB: pàgines de fitxer. Llencables a l'instant.
  • Dirty 28 MB: modificades però encara no escrites a disc. Si aquesta xifra puja molt, hi ha un coll d'ampolla d'escriptura. El seu buidatge forçat és el que fa fsync().
  • Slab 298 MB: estructures del mateix nucli (inodes, dentries, task_struct). No és memòria de processos.

I vmstat per veure la dinàmica:

$ vmstat 2 3
procs -----------memory---------- ---swap-- -----io---- --system-- ------cpu-----
 r  b   swpd   free  buff  cache   si   so    bi    bo   in    cs  us sy id wa st
 2  0 524288 421904  1024 4198400    0    0   142   288 4211  8877 12  4 83  1  0
 1  0 524288 419872  1024 4199424    0    0    88   412 4402  9104 14  5 80  1  0
 3  1 524288 418112  1024 4200448    0   16   204  1128 5108 10944 16  6 76  2  0

Les columnes crítiques per a memòria:

  • si/so (swap in / swap out, en KB/s): la mètrica més important. Un swpd de 512 MB amb si/so a zero és inofensiu: són pàgines velles expulsades fa temps que ningú no reclama. El greu és si/so sostinguts: aquí sí que hi ha hiperpaginació.
  • b: processos bloquejats en E/S ininterrompible (l'estat D de 02-01).
  • wa: percentatge de CPU esperant E/S.

La regla de diagnòstic que resumeix la lliçó: mira si/so, no swpd. Tenir swap ocupat és normal; tenir swap en moviment constant significa que falta RAM.

Errors Habituals i Consells

Alarmar-se perquè free és baix. La columna correcta és available. Un sistema sa té poca memòria lliure i molta memòria cau, perquè la RAM ociosa és RAM malbaratada.

Confondre swpd amb estar paginant. Que hi hagi 512 MB a swap no diu res per si sol: poden portar dies allà sense que ningú els reclami. El que indica un problema són si/so sostinguts a vmstat.

Desactivar el swap «perquè el sistema no s'alenteixi». Sense swap, el nucli perd la seva eina intermèdia i davant de pressió de memòria passa directament a l'OOM killer. La configuració raonable és tenir swap i abaixar swappiness, no eliminar-lo.

Sumar els RSS de diversos processos. Dona un total inflat perquè les pàgines compartides es compten diverses vegades. Per sumar, fes servir PSS.

Creure que l'OOM killer mata el culpable. Mata el que té la puntuació més alta, que sol ser el més gran. Si l'agregador provoca l'escassetat i meteo-api és el que té més memòria, mor meteo-api. Protegeix el que és crític amb oom_score_adj negatiu.

Activar THP a tot arreu. Les pàgines transparents de 2 MB ajuden càrregues amb conjunts de treball grans, però khugepaged compactant memòria pot introduir pauses de desenes de mil·lisegons. Les bases de dades solen recomanar madvise o never justament per això.

Interpretar un SIGSEGV com un error del sistema. És la MMU fent la seva feina: el procés ha accedit a una adreça sense traducció vàlida o sense els permisos adequats. Compara l'adreça amb /proc/<pid>/maps per saber si va ser un punter corrupte (cau en un forat) o una escriptura en zona de només lectura.

Consell de diagnòstic: davant d'una sospita de problema de memòria, aquest és l'ordre que funciona: free -h (mirar available), vmstat 1 (mirar si/so i wa), ps -eo pid,rss,maj_flt --sort=-rss | head (qui consumeix i qui falla), pmap -x <pid> (quina regió creix) i dmesg -T | grep -i oom (si ja ha mort algú). Cinc ordres i tens el quadre complet.

Exercicis

Exercici 1: traducció d'adreces i mida de taules

Un sistema té adreces lògiques de 32 bits i pàgines de 4 KB.

  1. Quants bits ocupen el número de pàgina i el desplaçament? Quantes pàgines hi ha com a màxim?
  2. Amb aquesta taula de pàgines, tradueix les adreces lògiques 0x00003ABC, 0x00001234 i 0x00006000:
Pàgina Marc Present
0 0x0A
1 0x1F
2 0x03
3 0x2C
4 No
  1. Calcula la mida d'una taula plana amb entrades de 4 bytes. Compara-la amb el cas de 64 bits de l'apartat 4 de la lliçó.
  2. Amb una TLB del 96 % d'encerts, accés a memòria de 80 ns, TLB de 2 ns i 2 nivells de taula, calcula el temps efectiu d'accés.

Exercici 2: comparar algorismes de reemplaçament

L'agregador genera aquesta cadena de referències a pàgines:

1 2 3 4 1 2 5 1 2 3 4 5
  1. Calcula les fallades de pàgina amb 3 marcs per a: òptim, FIFO i LRU. Mostra l'estat dels marcs a cada pas.
  2. Repeteix-ho amb 4 marcs.
  3. Quin algorisme presenta l'anomalia de Belady? Demostra-ho amb els teus números.
  4. Si cada fallada major costa 8 ms, calcula el temps total perdut en cada cas amb 3 marcs.

Exercici 3: diagnosticar un servidor amb problemes de memòria

meteo-01 respon amb lentitud extrema. Reculls aquestes dades:

$ free -h
               total        used        free      shared  buff/cache   available
Mem:           7,8Gi       7,4Gi       102Mi        12Mi       298Mi       118Mi
Swap:          4,0Gi       3,8Gi        204Mi

$ vmstat 2 3
procs -----------memory---------- ---swap-- -----io---- --system-- ------cpu-----
 r  b   swpd   free  buff  cache   si   so    bi    bo   in    cs  us sy id wa st
 0 31 3985408 104448  512 305152 42104 39882 51204 40118 9821 24102  3  8  1 88  0
 1 29 3985408 102112  512 303104 44210 41004 53108 41220 10104 25811  2  9  1 88  0
 0 33 3985408 101888  512 301056 43108 40112 52004 40988 9902 24998  3  8  1 88  0

$ ps -eo pid,rss,maj_flt,comm --sort=-rss | head -5
    PID    RSS  MAJFL COMMAND
   1877 4720884 892104 agregador
   1901 148320  41022 meteo-api
   1842  18204   8104 ingestor
  1. Diagnostica què li passa al sistema. Anomena el fenomen i justifica'l amb almenys quatre dades.
  2. Per què l'ús de CPU és del 3 % si el sistema va lentíssim?
  3. Identifica la causa arrel. Quanta memòria hauria de fer servir l'agregador, sabent que processa un dia de lectures?
  4. Proposa tres solucions: una d'immediata, una de configuració i una de disseny. Indica quina és la correcta.
  5. Escriu el fragment de codi que resoldria el problema d'arrel.

Solucions

Solució 1

1. Descomposició de l'adreça.

Pàgina de 4 KB = 2^12 bytes  →  desplaçament = 12 bits
Número de pàgina = 32 − 12 = 20 bits
Pàgines màximes = 2^20 = 1.048.576

2. Traduccions.

0x00003ABC:

En binari: 0000 0000 0000 0000 0011 | 1010 1011 1100
                  p = 0x00003 = 3   |    d = 0xABC = 2748

Aritmèticament: 0x3ABC = 15036;  15036 / 4096 = 3;  15036 % 4096 = 2748

Taula: pàgina 3 → marc 0x2C = 44, present
física = 44 × 4096 + 2748 = 180.224 + 2.748 = 182.972 = 0x0002CABC

Drecera hexadecimal: els tres dígits de la dreta (ABC) es conserven i els de l'esquerra passen de 00003 a 0002C.

0x00001234:

p = 0x00001 = 1,  d = 0x234 = 564
Taula: pàgina 1 → marc 0x1F = 31
física = 31 × 4096 + 564 = 126.976 + 564 = 127.540 = 0x0001F234

0x00006000:

p = 0x00006 = 6,  d = 0
Taula: la pàgina 6 no existeix (només hi ha entrades 0-4)
→ FALLADA DE PÀGINA per adreça no vàlida
→ El nucli comprova /proc/pid/maps: no pertany a cap regió
→ SIGSEGV: violació de segment

És important distingir aquest cas del de la pàgina 4, que sí que existeix però té Present = No: allà la fallada seria recuperable (el nucli la portaria de swap o del fitxer i reintentaria la instrucció), mentre que aquí és un error genuí del programa.

3. Mida de la taula plana en 32 bits.

2^20 entrades × 4 bytes = 4.194.304 bytes = 4 MB per procés

Comparació amb 64 bits:

32 bits 64 bits (48 usats)
Bits de pàgina 20 36
Entrades 1.048.576 68.719.476.736
Bytes per entrada 4 8
Taula plana 4 MB 512 GB
Amb 180 processos 720 MB 92 TB

I aquí hi ha l'observació clau de l'exercici: 4 MB per procés ja és massa. Amb 180 processos són 720 MB només de taules, gairebé el 9 % dels 8 GB de meteo-01, i la immensa majoria d'aquestes entrades estarien buides. Per això fins i tot els sistemes de 32 bits feien servir taules multinivell (x86 de 32 bits en tenia dos nivells). La taula plana no va ser mai viable; en 64 bits passa d'inviable a directament absurda.

4. Temps efectiu d'accés.

Encert  (96 %):  2 ns (TLB) + 80 ns (dada)                     = 82 ns
Fallada (4 %):   2 ns + 2 × 80 ns (dos nivells) + 80 ns (dada) = 242 ns

TEA = 0,96 × 82 + 0,04 × 242
    = 78,72 + 9,68
    = 88,4 ns

Degradació enfront de l'accés pur (80 ns): 88,4 / 80 = 1,105, un 10,5 %.

Val la pena observar quant pesa aquest 4 % de fallades: aporta 9,68 ns dels 88,4 totals, un 11 % del temps, sent només el 4 % dels accessos. És l'aritmètica típica de les memòries cau, i explica per què millorar del 96 % al 99 % d'encerts val la pena:

TEA al 99 % = 0,99 × 82 + 0,01 × 242 = 81,18 + 2,42 = 83,6 ns  (+4,5 %)

Solució 2

1. Amb 3 marcs.

Òptim (expulsa la que trigarà més a reaparèixer):

Ref 1 2 3 4 1 2 5 1 2 3 4 5
M1 1 1 1 1 1 1 1 1 1 3 3 3
M2 2 2 2 2 2 2 2 2 2 4 4
M3 3 4 4 4 5 5 5 5 5 5
Fallada

Les decisions d'expulsió: a la posició 4 surt el 3 (reapareix a la 10, més tard que 1 i 2); a la 7 surt el 4 (reapareix a la 11); a la 10 surt l'1 (no torna mai); a la 11 surt el 2 (tampoc no torna).

7 fallades.

FIFO:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
M1 1 1 1 4 4 4 5 5 5 5 5 5
M2 2 2 2 1 1 1 1 1 3 3 3
M3 3 3 3 2 2 2 2 2 4 4
Fallada

9 fallades.

LRU:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
M1 1 1 1 4 4 4 5 5 5 3 3 3
M2 2 2 2 1 1 1 1 1 1 4 4
M3 3 3 3 2 2 2 2 2 2 5
Fallada

10 fallades.

2. Amb 4 marcs.

Òptim:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
M1 1 1 1 1 1 1 1 1 1 1 4 4
M2 2 2 2 2 2 2 2 2 2 2 2
M3 3 3 3 3 3 3 3 3 3 3
M4 4 4 4 5 5 5 5 5 5
Fallada

Amb 4 marcs hi caben l'1, el 2, el 3 i el 4 des del principi. En arribar el 5 s'expulsa el 4, perquè reapareix a la posició 11, més tard que 1, 2 i 3. A la posició 10 el 3 ja és resident, així que no hi ha fallada. A l'11 cal portar el 4 i s'expulsa l'1, que no es torna a fer servir.

6 fallades.

FIFO:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
M1 1 1 1 1 1 1 5 5 5 5 4 4
M2 2 2 2 2 2 2 1 1 1 1 5
M3 3 3 3 3 3 3 2 2 2 2
M4 4 4 4 4 4 4 3 3 3
Fallada

10 fallades.

LRU:

Ref 1 2 3 4 1 2 5 1 2 3 4 5
M1 1 1 1 1 1 1 1 1 1 1 1 5
M2 2 2 2 2 2 2 2 2 2 2 2
M3 3 3 3 3 5 5 5 5 4 4
M4 4 4 4 4 4 4 3 3 3
Fallada

Traça de les expulsions: en arribar el 5 (posició 7), l'ordre d'ús és 3, 4, 1, 2, així que surt el 3. A la posició 10 cal el 3 i el menys usat recentment és el 4. A l'11 cal el 4 i surt el 5. A la 12 cal el 5 i surt l'1. Fixa't en el patró: les tres últimes referències fallen perquè LRU acaba d'expulsar justament el que es demana tot seguit.

8 fallades.

3. L'anomalia de Belady.

Algorisme 3 marcs 4 marcs Millora amb més memòria?
Òptim 7 6 Sí (−1)
FIFO 9 10 NO: empitjora (+1)
LRU 10 8 Sí (−2)

FIFO presenta l'anomalia: passar de 3 a 4 marcs augmenta les fallades de 9 a 10.

L'explicació estructural: FIFO no compleix la propietat de pila. Formalment, un algorisme la compleix si el conjunt de pàgines residents amb n marcs és sempre un subconjunt del conjunt amb n+1 marcs. LRU i OPT la compleixen perquè la seva decisió depèn del patró de referències, que no canvia en afegir marcs. FIFO decideix per ordre d'arribada, i aquest ordre s'altera completament en canviar el nombre de marcs: amb 4 marcs, les pàgines 1 i 2 sobreviuen més temps i acaben sent expulsades just abans de tornar-se a fer servir.

És un resultat important perquè trenca una intuïció que semblava segura, i és la raó pràctica per la qual cap sistema real no fa servir FIFO pur: no pots prometre que ampliar la RAM millorarà el rendiment.

4. Temps perdut amb 3 marcs.

Algorisme Fallades Temps perdut Enfront de l'òptim
Òptim 7 7 × 8 ms = 56 ms
FIFO 9 9 × 8 ms = 72 ms +28,6 %
LRU 10 10 × 8 ms = 80 ms +42,9 %

Una observació honesta sobre aquests números: aquí LRU surt pitjor que FIFO, cosa que contradiu la intuïció general. És un artefacte d'aquesta cadena concreta, dissenyada precisament per exhibir l'anomalia de Belady. Amb cadenes reals, que presenten localitat temporal forta, LRU supera clarament FIFO. És un bon recordatori que una cadena de dotze referències no demostra res sobre el comportament general d'un algorisme; per a això calen traces reals de milions de referències.

El que sí que demostra la comparació és l'ordre de magnitud del problema: 12 accessos a memòria que haurien de costar 1,2 microsegons han costat entre 56 i 80 mil·lisegons. Un factor de 50.000. Quan falten marcs, l'algorisme de reemplaçament és el de menys.

Solució 3

1. Diagnòstic: hiperpaginació (thrashing).

Les dades que ho confirmen, una a una:

Evidència Valor Què significa
available 118 Mi de 7,8 Gi Memòria pràcticament exhaurida
Swap usat 3,8 Gi de 4,0 Gi El swap també és ple
si/so ~42.000 / ~40.000 KB/s 40 MB/s en totes dues direccions simultàniament
wa 88 % La CPU no fa res més que esperar disc
b 29-33 processos Gairebé tot el sistema en estat D
MAJFL de l'agregador 892.104 Gairebé un milió de fallades majors

La dada definitiva és si i so alts alhora. Si només hi hagués so, el sistema estaria alliberant memòria de manera ordenada. Que entri i surti alhora a 40 MB/s significa que les mateixes pàgines s'expulsen i es tornen a portar sense parar: el conjunt de treball no cap a la RAM i cada pàgina expulsada es necessita immediatament després. És la definició exacta del bucle de realimentació del diagrama de la lliçó.

2. Per què la CPU està al 3 %.

Perquè no hi ha res a executar. Gairebé tots els processos són en estat D, bloquejats esperant que el disc porti una pàgina. La columna r (executables) marca 0 o 1, mentre que b (bloquejats) marca 31.

Aquest és el patró més enganyós de tots: CPU gairebé ociosa amb el sistema completament aturat. Qui miri només l'ús de CPU conclourà que el servidor està bé i buscarà el problema en un altre lloc. La combinació que cal reconèixer a l'instant és us+sy baixos + wa altíssim + b alt.

Un càlcul que ho dimensiona: 892.104 fallades majors de l'agregador, a uns 100 µs cadascuna amb SSD, són 89 segons d'espera pura. Amb disc mecànic a 8 ms serien ~2 hores de disc.

3. Causa arrel.

L'agregador4.720.884 KB = 4,5 GB de RSS en una màquina de 7,8 GB. Ell sol és el 60 % de la RAM total. Ni meteo-api (148 MB) ni ingestor (18 MB) són rellevants en comparació.

Quant n'hauria de fer servir:

Un dia de lectures: 17 MB (dada del glossari del curs)
Estructures d'agregació:
  Estacions × hores × 5 camps ≈ 50 × 24 × 5 × 8 bytes = 48 KB
Memòries intermèdies de treball, libc, codi:  ~15 MB

Consum raonable: 30-50 MB
Consum real:     4.500 MB
Factor d'excés:  ~100×

I amb 24 bytes per lectura, aquests 4,5 GB equivalen a uns 196 milions de lectures: més de 270 dies de dades. El diagnòstic és immediat: l'agregador està carregant a memòria tot l'històric en lloc del dia que necessita processar, molt probablement per un readdir sobre /var/lib/meteora/lectures/ sense filtre de data, acumulant-ho tot en una estructura en memòria.

4. Tres solucions.

Immediata (recuperar el servidor ara):

$ sudo kill 1877

Allibera 4,5 GB a l'instant. El sistema deixa de paginar en segons. És un pedaç, no una cura: tornarà a passar a la propera execució.

De configuració (contenir el dany):

# Limitar la memòria del servei amb systemd
$ sudo systemctl edit meteora-agregador
[Service]
MemoryMax=512M
MemoryHigh=384M

# Protegir els processos crítics de l'OOM killer
$ echo -900 | sudo tee /proc/1842/oom_score_adj   # ingestor
$ echo -500 | sudo tee /proc/1901/oom_score_adj   # meteo-api

# Reduir la tendència a fer swap
$ sudo sysctl -w vm.swappiness=10

Amb MemoryMax=512M (un límit de cgroup, tema de 06-02), l'agregador que intenti passar de 512 MB mor ell sol sense arrossegar la resta del sistema. Això converteix una caiguda global en una fallada aïllada, que és exactament el que es vol. No arregla l'error, però el conté.

De disseny (la correcta):

Processar per flux, no carregant-ho tot. Dues variants vàlides: llegir per blocs amb read(), o mapejar el fitxer del dia amb mmap() i deixar que la paginació per demanda gestioni la memòria.

5. Codi que ho resol.

Versió amb mmap(), que aplica directament el que hem vist a la lliçó:

/* agregar_dia.c — processa UN dia sense carregar-lo sencer a memòria pròpia */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h>

struct Lectura {
    uint32_t estacio_id;
    uint32_t timestamp;
    float    temperatura;
    float    humitat;
    float    pressio;
    uint32_t _reservat;
};

#define MAX_ESTACIONS 64
#define HORES_DIA     24

struct Acumulador {
    double suma_temp;
    double suma_hum;
    double suma_pres;
    uint32_t n;
};

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "ús: %s AAAA-MM-DD\n", argv[0]);
        return 1;
    }

    char ruta[256];
    snprintf(ruta, sizeof ruta,
             "/var/lib/meteora/lectures/%s.dat", argv[1]);

    int fd = open(ruta, O_RDONLY);
    if (fd == -1) { perror("open"); return 1; }

    struct stat st;
    if (fstat(fd, &st) == -1) { perror("fstat"); close(fd); return 1; }

    struct Lectura *lec = mmap(NULL, st.st_size,
                               PROT_READ, MAP_PRIVATE, fd, 0);
    if (lec == MAP_FAILED) { perror("mmap"); close(fd); return 1; }
    close(fd);

    /* Avís al nucli: el recorrerem en ordre i no el rellegirem */
    madvise(lec, st.st_size, MADV_SEQUENTIAL);

    /* Acumuladors: 64 × 24 × 32 bytes = 48 KB, mida FIXA */
    static struct Acumulador acc[MAX_ESTACIONS][HORES_DIA];
    memset(acc, 0, sizeof acc);

    size_t n = st.st_size / sizeof(struct Lectura);
    for (size_t i = 0; i < n; i++) {
        uint32_t est  = lec[i].estacio_id % MAX_ESTACIONS;
        uint32_t hora = (lec[i].timestamp / 3600) % HORES_DIA;
        struct Acumulador *a = &acc[est][hora];
        a->suma_temp += lec[i].temperatura;
        a->suma_hum  += lec[i].humitat;
        a->suma_pres += lec[i].pressio;
        a->n++;
    }

    munmap(lec, st.st_size);

    for (int e = 0; e < MAX_ESTACIONS; e++)
        for (int h = 0; h < HORES_DIA; h++)
            if (acc[e][h].n > 0)
                printf("%s %02d:00 est=%d n=%u T=%.2f H=%.1f P=%.1f\n",
                       argv[1], h, e, acc[e][h].n,
                       acc[e][h].suma_temp / acc[e][h].n,
                       acc[e][h].suma_hum  / acc[e][h].n,
                       acc[e][h].suma_pres / acc[e][h].n);
    return 0;
}

Per què això resol el problema d'arrel:

  • Un sol dia per execució. La ruta es construeix amb la data rebuda: és impossible carregar l'històric complet per accident.
  • Els acumuladors tenen mida fixa: 64 × 24 × 32 bytes = 48 KB, independent del volum de dades. Aquesta és la clau de l'arranjament: la memòria del procés ja no creix amb l'entrada.
  • mmap no consumeix RSS propi. Les pàgines mapejades pertanyen a la memòria cau de pàgines del nucli, que és llencable sota pressió de memòria (són netes i tenen suport en fitxer, com vam veure a 02-03). Si falta RAM, el nucli les descarta sense escriure res i les rellegeix després. Enfront dels 4,5 GB anònims de la versió anterior —que només podien anar a swap—, això canvia completament el comportament del sistema sota pressió.
  • madvise(MADV_SEQUENTIAL) diu al nucli que l'accés serà seqüencial. El nucli activa lectura anticipada agressiva i descarta abans les pàgines ja recorregudes. És una optimització petita d'escriure i molt efectiva en recorreguts complets.

Consum resultant:

RSS del procés:         ~15 MB (codi + libc + acumuladors)
Memòria cau de pàgines: fins a 17 MB, alliberables a l'instant
Total efectiu:          ~32 MB enfront de 4.500 MB

Un factor de 140 de reducció, i cap pàgina anònima que pugui arrossegar el sistema a la hiperpaginació.

Conclusió

La paginació divideix l'espai lògic en pàgines i la memòria física en marcs de la mateixa mida, i tradueix cada adreça separant número de pàgina i desplaçament —que no es tradueix mai—. Cada entrada de la taula porta el marc i uns bits de control que ho governen tot: P habilita la memòria virtual, R/W i NX implementen W^X, i A i D, que escriu el maquinari, són l'única pista que el nucli té per decidir qui expulsar i a quin cost.

Una taula plana en 64 bits ocuparia 512 GB per procés, així que les taules són multinivell i disperses: quatre nivells de 9 bits, cada taula ocupant exactament una pàgina, i 60 KB reals en lloc de 512 GB. El preu són quatre accessos a memòria per traducció, i per això existeix la TLB: amb un 99 % d'encerts perds un 4 % de rendiment, amb un 70 % perds més del doble. Les pàgines grans redueixen 4.150 entrades de TLB a 9 per als 17 MB de l'agregador, a canvi de fragmentació interna i d'una granularitat d'expulsió grossera.

La paginació per demanda fa que res no es carregui fins que es toca, cosa que explica libc amb només el 44 % del seu codi a la RAM. Una fallada menor costa microsegons i és normal; una fallada major costa mil·lisegons, i amb una per cada mil accessos el sistema va 81 vegades més lent. Els algorismes de reemplaçament —òptim com a fita, FIFO amb la seva anomalia de Belady, LRU inviable en exacte i el rellotge com a aproximació barata fent servir el bit A— es mouen en un marge del 33 %, molt menys del que importa tenir prou RAM. Quan el conjunt de treball no hi cap, apareix la hiperpaginació: CPU al 3 %, wa al 88 % i el sistema aturat.

I tot això es toca amb les mans: mmap() converteix 4.219 crides al sistema en una i permet que dos processos comparteixin físicament el mateix 2026-08-31.dat; el copy-on-write de fork() és simplement R/W=0 més una fallada de protecció; swappiness decideix què se sacrifica abans; i l'OOM killer deixa a dmesg un rastre que es llegeix procés a procés. La regla que resumeix la part operativa: mira available i no free, mira si/so i no swpd, i suma PSS i no RSS.

Amb això tanquem la memòria. Ens queda l'altre gran recurs que el sistema operatiu administra i que ha aparegut a cada fallada major d'aquesta lliçó: l'emmagatzematge. Quant costa realment aquest accés a disc que hem estat comptant en mil·lisegons, per què un SSD canvia totes les regles, com s'ordenen les peticions per minimitzar el moviment del capçal i quina configuració de RAID mereix /var/lib/meteora. És el que veurem a Gestió d'Emmagatzematge.

Fonaments de Sistemes Operatius

Mòdul 1: Introducció als Sistemes Operatius

Mòdul 2: Gestió de Recursos

Mòdul 3: Concurrència

Mòdul 4: Estructures de Fitxers

Mòdul 5: Protecció i Seguretat del Sistema

Mòdul 6: Virtualització i Contenidors

Mòdul 7: Administració i Diagnòstic a la Pràctica

© Copyright 2026. Tots els drets reservats