Más allá del htop: E-Cores y P-cores de forma gráfica en Alder Lake i5-12600K, como cambian de core los procesos en linux.

Desde que instalé mi i5-12600, he estado obsesionado con entender cómo gestiona Linux su arquitectura híbrida. En herramientas como htop o btop, vemos porcentajes de uso que se actualizan cada pocos segundos.
Hoy vamos a bajar al nivel del kernel para ver cómo el scheduler de Linux (el orquestador de procesos) mueve los hilos entre núcleos de alto rendimiento (P-Cores) y de eficiencia (E-Cores).

Como vemos en la imagen ( HTOP )Core 6, 8 y 11 están usando el 100 % del CPUPero, ¿qué pasa realmente en la escala de microsegundos?
Así que hace días estoy trasteando para ver esto más a nivel del kernel, ver cómo se orquesta esto a nivel del scheduler de linux
Cuando vemos htop, vemos carga de CPU y unas barritas que solo nos dicen cuánta carga hay y cuánto CPU estamos usando, esperas y demás, pero a un nivel más bajo, ¿qué sucede? ¿Cada cuánto cambia un proceso de CORE?
Miremos una gráfica que he generado, para entender mejor, básicamente se han capturado los IN y OUT de los procesos, o sea cuando un proceso salta de un nucleo a otro -> https://docs.kernel.org/trace/rv/monitor_sched.html
Como se gestionan cientos de procesos internos, he filtrado la misma traza con algunos procesos para que se vea mejor el salto entre otros cores y no se vean simplemente colores por todo lado:
Filtrados los siguientes procesos en ejecución al generarse la gráfica:
Chrome en ejecución
Stress-ng lanzado con --cpu 2
Xorg ( las X )
macro que gestiona Mate
IPC IO es interno de chrome
Antes que nada:
P-Cores de 0 a 11
E-Cores de 12 a 15

Recordemos: no es el USO de CPU, es el cambio de CPU de un proceso.
Vemos cómo Chrome está "bailando" en los E-cores principalmente, ya que está sin hacer nada. Stress-ng (en verde) es un proceso que consume mucho y, si lo vemos en el top, aparece siempre consumiendo. Se ve cómo el scheduler lo mueve constantemente de core y, al tener 2 hilos, puede estar en 2 cores a la vez, pero con un constante cambio y manteniéndose solo en los P-CORES.
Esta gráfica por sí sola ya deja claro cómo funcionan el Hardware Feedback Interface (HFI) de Intel y el EEVDF scheduler de Linux, trabajando juntos para decidir en qué cores dejar los procesos.
Veamos la misma gráfica añadiendo un backup de borg que estaba excluido de la gráfica inicial:

Borg tiene varias fases: no consume mucho CPU, analiza, deduplica, comprime, etc. Se ve que entra tanto en P-cores como en E-cores, mayormente en P-cores cuando hace el trabajo pesado. El backup no tardó mucho, solo tenía que deduplicar alguna cosilla del día.
Aquí tienes la corrección de este bloque. He pulido la puntuación y las tildes para que la aclaración se entienda perfectamente, manteniendo tu estilo directo:
Los 8 procesos con más "switches"
Ahora, veamos los 8 procesos que más switch han hecho en los 60 segundos en los que capturamos la actividad.
Aclaración: En la siguiente gráfica se han excluido: swapper, kworker, perf, rcu_preempt, migration e idle. Si no se hace este filtro, estos procesos internos lo acaparan todo, ya que se la pasan "bailando" todo el rato.

Curiosamente, no vemos a stress-ng en este ranking aunque estuvo consumiendo el 100 % de CPU de 2 cores. Esto es porque no estamos midiendo el USO de CPU, sino las veces que un proceso cambia de core.
Por eso vemos a Xorg primero (el que se encarga de generar la imagen, ratón, teclado y todo lo que se ve en el monitor) y luego a Marco, el gestor de ventanas de Mate.
Son dos de los procesos que siempre van a estar ahí y son los que más "bailan" entre cores, aunque no sean los que más consumen en este caso. Esto nos dice lo siguiente: cuanto más ligero sea el escritorio, mejor. Es lo que más actividad genera en el CPU de lejos, por encima de stress-ng, que estuvo 60 segundos consumiendo el 200 % de CPU pero no bailó tanto como Xorg y Marco.
Mola mucho ver esto, así que vamos a dejar stress-ng de lado y voy a lanzar otros procesos para comparar:
p-cores y e-cores y taskset.
No solo quiero ver cómo baila el CPU, sino que taskset me haga caso: si quiero ejecutar un proceso pesado en un E-core, que esto sea posible y no baile ni un milímetro.
Por lo tanto: zstd con 3 hilos, un tar.gz monohilo, taskset con un zip y Handbrake convirtiendo un vídeo. Vamos a ver cuánto "baila" todo esto:
zstd -T3 code/maks.tar -o code/maks.zst &
tar -czf code/maks2.tar.gz maks/ &
taskset -c 15 zip -r code/maks_backup.zip maks/ &
+ Handbrake ( HBNVoy a capturar datos durante 1 minuto y que Handbrake comience a trabajar a los 20 segundos de la captura; o sea, 20 segundos después de que inicien los compresores. Y ya que estamos, ¿voy a lanzar un stress-ng al final?
Igual me pasé con lo de los 60 segundos:
[ perf record: Woken up 1345 times to write data ]
[ perf record: Captured and wrote 340,251 MB perf.data (2774424 samples) ]
Y aquí la gráfica; me costó entenderlo y llegué a pensar que estaba mal el script con el que generé la gráfica. Lo revisé, miré el fichero de log y lo comprendí todo. La verdad es que "mola mucho" ver todo esto.
Procesos pesados cambiando de core en linux:
Interacciones totales:
ghb 866775
kworker/u64 208646
zstd 179582
tar 63317
mate-terminal 21906
gzip 8642
zip 3700
kworker/6 512
kworker/14 411
kworker/2 407
kworker/u65 380
kworker/0 378
kworker/4 353
kworker/13 337
kworker/8 326He incluido los kworkers; luego vemos por qué son tan importantes.
He modificado un poco la gráfica y subo dos versiones: una que incluye el kworker (que mete mucho ruido) y otra sin él.
NOTA: la gráfica no se ve muy fina, hay que ajustar el generador, pero es por que en esas lineas hay repartidas más de 1 millón de puntitos

¿Ven exceso de kworker ? recordemos antiguos POST de este blog, mi equipo tiene Btrfs sobre LUKS2 y esto añade una capa de complejidad que el scheduler de Linux debe orquestar, ya vimos como bajamos de +200K iops a solo 85k IOPS en otras pruebas. En las gráficas se ve cómo los procesos saltan mucho a los kworkers esperando IO, el disco es bastante rápido pero el kernel está cifrando datos sobre la marcha y gestionando un sistema de archivos Copy-on-Write. Es el precio que pagamos por tener seguridad y snapshots, y aquí pueden ver cómo se traduce en milisegundos de espera en la CPU.
Sin kworker

ZSTD fue el que menos E-cores tocó.
Entendamos algunas cosas y por qué es importante el kworker. Primero veamos qué pasa con zip: lo hemos forzado a trabajar en el core 15 y ahí se quedó, pero tiene 3,700 "cambios" en 60 segundos.
Esto es lo que hace zip:
kworker/15:3-mm 24085 [015] 9001.700277: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9001.988274: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9001.988280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.116274: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.116280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.244275: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.244284: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.308280: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.308286: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.436273: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.436280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.596273: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.596280: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.656272: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.656334: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.692272: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
kworker/15:3-mm 24085 [015] 9002.692276: sched:sched_switch: kworker/15:3:24085 [120] I ==> zip:24873 [120]
zip 24873 [015] 9002.916274: sched:sched_switch: zip:24873 [120] R ==> kworker/15:3:24085 [120]
Aquí vamos a entender algunas cosas interesantes. Todos los que administramos sistemas siempre nos hemos preguntado qué hace kworker, ya que siempre hay procesos de kworker con un número. Supongo que todos lo hemos buscado en Google; aquí pego una de las primeras respuestas que salen, que me parece la explicación más simple y directa:
https://unix.stackexchange.com/questions/436671/difference-between-kworker-n-and-events-n
Cito "kworker" is a placeholder process for kernel worker threads, which perform most of the actual processing for the kernel, especially in cases where there are interrupts, timers, I/O, etc."
Ayuda en operaciones de disco, entre otras cosas, y el ejemplo de las entradas y salidas de zip en el core 15 es un caso claro.
kworker/15 significa que este kworker es el que trabaja en el core 15 y no sale de ahí (el número es el core; cada núcleo tiene su "ayudante"). zip está procesando (comprimiendo), pero el kworker ayuda con las operaciones de lectura y escritura de disco: (leer) -> comprimir -> (escribir).
Esto aclara algunas dudas: zip trabaja solo en el core 15, pero ha tenido 3.700 salidas/entradas para dar paso al kworker. Podemos intuir que las salidas son para esperar respuesta del disco.
En cambio tar baila por todos los cores: más de 67 000 cambios, tanto de núcleo como de estado:
swapper 0 [014] 8988.887349: sched:sched_switch: swapper/14:0 [120] R ==> tar:24871 [120]
tar 24871 [014] 8988.887373: sched:sched_switch: tar:24871 [120] S ==> swapper/14:0 [120]
swapper 0 [008] 8988.888172: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
tar 24871 [008] 8988.888186: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
swapper 0 [008] 8988.888836: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
tar 24871 [008] 8988.888845: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
swapper 0 [008] 8988.889639: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
tar 24871 [008] 8988.889651: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
swapper 0 [008] 8988.890322: sched:sched_switch: swapper/8:0 [120] R ==> tar:24871 [120]
tar 24871 [008] 8988.890346: sched:sched_switch: tar:24871 [120] S ==> swapper/8:0 [120]
swapper 0 [000] 8988.891058: sched:sched_switch: swapper/0:0 [120] R ==> tar:24871 [120]
tar 24871 [000] 8988.891071: sched:sched_switch: tar:24871 [120] S ==> swapper/0:0 [120]
swapper 0 [012] 8988.891880: sched:sched_switch: swapper/12:0 [120] R ==> tar:24871 [120]
tar 24871 [012] 8988.891905: sched:sched_switch: tar:24871 [120] S ==> swapper/12:0 [120]
swapper 0 [000] 8988.892682: sched:sched_switch: swapper/0:0 [120] R ==> tar:24871 [120]Si nos fijamos en la columna 8, el proceso pasa a suspended (suspendido); entra swapper mientras espera y así 67 mil veces, entre cambios de estado y brincos de un core a otro.
El motivo de este cambio de estado tan brutal son las operaciones de I/O. La CPU va muy rápido y el disco va muy lento; el kernel tiene una eficiencia brutal y esos micro y milisegundos de suspended controlan la temperatura y evitan el colapso. O sea, en ese pequeñísimo tiempo en el que tar espera respuesta del disco, el kernel suspende el proceso, parece una tontería, pero ante millones de operaciones, esas pausas y cambios de estado, ahorran energía, evitan calor etc
Para el que lo quiera ver mejor:
Columna 3: El número de core.
Columna 8: El estado.
S (Interruptible sleep): Significa que está esperando un evento.
R (Running): El proceso está ejecutándose o listo para ello.
Estos estados son algo básico en Linux; es lo que vemos siempre en top, ps, etc.
Esta entrada explica los estados: https://www.supportpro.com/blog/process-status-ps/
Copio y pego aquí lo básico.
D Uninterruptible sleep (usually IO) a blocked state. The process waits for a hardware condition and cannot handle any signal
R Running Process is either running or ready to run
S Interruptible sleep (waiting for an event to complete) a Blocked state of a process and waiting for an event or signal from another process
T Stopped, either by a job control signal or because it is being traced Process is stopped or halted and can be restarted by some other process
X dead (should never be seen)
Z Defunct (“zombie”) process, terminated but not reaped by its parent process terminated, but the information is still there in the process table.Pues eso es la columna 8, constante cambio de R a S, trabajando y esperando datos mayormente del disco.
Luego esta ghb que es Handbrake codificando un pequeño vídeo de 5 minutos:
swapper 0 [001] 8998.232422: sched:sched_switch: swapper/1:0 [120] R ==> ghb:25086 [120]
swapper 0 [003] 8998.232425: sched:sched_switch: swapper/3:0 [120] R ==> ghb:25087 [120]
swapper 0 [008] 8998.232429: sched:sched_switch: swapper/8:0 [120] R ==> ghb:25088 [120]
swapper 0 [004] 8998.232429: sched:sched_switch: swapper/4:0 [120] R ==> ghb:25089 [120]
ghb 25002 [012] 8998.232439: sched:sched_switch: ghb:25002 [120] R ==> ghb:25092 [120]
ghb 25092 [012] 8998.232445: sched:sched_switch: ghb:25092 [120] S ==> ghb:25090 [120]
ghb 24975 [006] 8998.232965: sched:sched_switch: ghb:24975 [120] S ==> swapper/6:0 [120]
ghb 24970 [013] 8998.233344: sched:sched_switch: ghb:24970 [120] R ==> kworker/u64:20:24473 [120]
kworker/u64:20- 24473 [013] 8998.233359: sched:sched_switch: kworker/u64:20:24473 [120] I ==> ghb:24970 [120]
ghb 24976 [014] 8998.233368: sched:sched_switch: ghb:24976 [120] R ==> Xorg:2594 [120]
Xorg 2594 [014] 8998.233417: sched:sched_switch: Xorg:2594 [120] S ==> ghb:24976 [120]
ghb 25089 [004] 8998.233894: sched:sched_switch: ghb:25089 [120] S ==> swapper/4:0 [120]
ghb 24976 [014] 8998.234110: sched:sched_switch: ghb:24976 [120] S ==> marco:3214 [120]
ghb 25085 [007] 8998.234328: sched:sched_switch: ghb:25085 [120] S ==> swapper/7:0 [120]
ghb 24978 [011] 8998.234741: sched:sched_switch: ghb:24978 [120] S ==> swapper/11:0 [120]
ghb 25090 [012] 8998.235155: sched:sched_switch: ghb:25090 [120] S ==> ghb:25002 [120]
swapper 0 [014] 8998.235162: sched:sched_switch: swapper/14:0 [120] R ==> ghb:25092 [120]
swapper 0 [007] 8998.235165: sched:sched_switch: swapper/7:0 [120] R ==> ghb:25093 [120]
ghb 24970 [013] 8998.235165: sched:sched_switch: ghb:24970 [120] S ==> swapper/13:0 [120]
swapper 0 [011] 8998.235166: sched:sched_switch: swapper/11:0 [120] R ==> ghb:25095 [120]
swapper 0 [004] 8998.235170: sched:sched_switch: swapper/4:0 [120] R ==> ghb:25094 [120]
swapper 0 [013] 8998.235171: sched:sched_switch: swapper/13:0 [120] R ==> ghb:25097 [120]
Aquí no vamos a explicar nada ya, creo que esta todo dicho, pero como ha tenido 870 mil saltos de core me entro la curiosidad a ver cuanto salto y en que cores estuvo:
grep "ghb" salida.perf | awk '{print $3}' | tr -d '[]' | sort -n | uniq -c
90523 001
100340 002
72286 003
94990 004
78124 005
98588 006
82533 007
95119 008
79904 009
94229 010
76483 011
122725 012
110969 013
100647 014
2969 015
Me resulta llamativo: ha bailado casi por igual en todos los cores. No exactamente igual, pero bailó mucho, menos en el core 15, que tenía a zip ahí agarrado y el kernel ha respetado eso. Como zip no bailaba, el núcleo no quedaba libre y GHB no pudo pillar ese core casi nunca. Supongo que las interacciones que ha tenido son de cuando zip entraba en estado S; o sea, se dormía esos momentos en los que esperaba al disco.
Por último, para que se vean mejor las gráficas, las divido por proceso; así hay menos ruido:
Solo ZIP con taskset: (No se ejecuta en ningún core más que el 15, tal como pedimos).

Solo zstd con opción de 3 hilos, generó mucho baile pero acabo en 30 segundos

HandBrake multi hilo, casi no toca el core 15 en el que tenemos ZIP, como ya vimos.

Y tar que hace lo que le da la gana, muchas lecturas y escrituras, estaba trabajando en una carpeta con 244 mil archivos cuando se capturo la gráfica.

¿Que hizo el resto?
Hemos visto cosas interesantes: con varios procesos que "chupan" CPU y un proceso enganchado al core 15, ¿qué pasó con los demás?
Veamos, durante esos 60 segundos, cuáles fueron los procesos que más veces tuvieron operaciones de switch:

Sí llama la atención AV, es de libav es parte de handbrake, debí incluir ambos en el mismo saco.
Cargas altas de trabajo y los e-cores y p-cores en linux
Muchos puntos y mucha gráfica, pero ¿ves procesos en todos los cores, no?
El asunto aquí es que Linux usa todos los cores, para eso están ahí. No es que los E-cores no ejecuten cargas de trabajo pesadas; los E-cores están ahí para consumir menos electricidad y que tengas que pagar menos a la compañía eléctrica, solo para eso. El sistema prioriza la velocidad: cuando necesitas potencia, consumes, y cuando el sistema puede ejecutar procesos ligeros en E-cores, lo hace ahí para ahorrar energía.
Hagamos un test más con stress solamente y veamos si, durante 60 segundos, un proceso multihilo que ocupe el 100 % de CPU de 3 cores toca algún E-core.
La primera imagen de esta entrada (el htop con stress --cpu 3), desde el lado del kernel se ve así:

643 saltos de un core a otro y alguna otra operación
Toca los e-cores, pero solo al inicio y mucho menos que a los p-cores, concreta mente ha estado en cada core:
~# grep -w "stress" stress.perf | awk '{print $3}' | tr -d '[]' | sort -n | uniq -c
78 000
68 001
122 002
42 003
116 004
102 005
138 006
110 007
152 008
44 009
102 010
86 011
30 012
24 013
30 014
42 015
NOTA: El script que genera las gráficas lo ha generado la IA en Python, ya que mis conocimientos de Python son algo limitados y no había otra forma de generarlos de manera sencilla.
Es muy interesante ver los procesos a este nivel; ya nos queda algo más claro cómo funcionan los E-cores y P-cores, aunque podemos profundizar más aún. Podemos entrar en los ciclos de CPU durante la carga para ver cuánto se usan realmente los E-cores (Atom) o los cores "de toda la vida":

Y no solo eso, podemos hacer otro POST para ver cuándo la CPU espera al disco y ver qué sucede ahí en mayor profundidad.
Todo esto, si bien es interesante o te puede parecer aburrido, es una forma de entender el sistema y analizar mejor por qué puede ir lento; la mayoría de las veces no es el CPU, es la espera del disco, tal como hablamos en aquel otro hilo de webs con WordPress que van lentas por estar esperando al disco siempre...
En fin, eso es todo.
EDITADO --- Añadidas 2 gráficas más con algunas mejoras:
Ejemplo gráfico, XORG vz CC1
He compilado el kernel 6.18.8 y he aprovechado a tomar una muestra mientras compila
Para los curiosos, vemos esto compilando un kernel usando todos los cores:

E-cores y p-cores en linux durante una compilación
Arrancado el 6.18.8 en principio algo mejorado con los modulos de intel y demás, veamos esto
He puesto a compilar el kernel especificando -j 4 ( usar solo 4 cores )
Y el resultado es bastante interesante:

Aquí es donde realmente vemos los saltos de CPU que da el compilador. Los puntos de color azul oscuro no tocan los E-cores; dan saltos en los P-cores, pero tampoco demasiados. Hablamos de que se han hecho 2.308 saltos por parte de cc1, una cifra bastante alejada de Xorg, del que ya hemos hablado y que siempre está activado. Este último ha estado en los E-cores la mayor parte del tiempo, sin consumir muchos recursos.
El salto de Xorg a los P-cores se entiende en esos momentos donde hacía falta más "poder", sobre todo abriendo y moviendo ventanas de un lado a otro cuando tuve algo de actividad mirando cosas.
Ante la duda, vemos a cc1 junto con otros procesos que hay abiertos (Xorg, Marco —el gestor de Mate—, Chrome, mate-terminal...), donde se ve más claro aún que los E-cores están trabajando constantemente:

Al final, ha hecho bien Intel en poner 4 Atom en un lugar donde solo habría sitio para un core normal (por el tamaño físico en el CPU), y se ve que hacen bien su trabajo.
Estamos hablando de que un E-core consume unos 5W a máxima potencia y un P-core unos 25W. Y aquí estaban los E-cores en "idle", haciendo nada, pero con más actividad y más procesos que los P-cores que estaban al 100 %.