1. La unidad de todo lo demás
Este bloque recorre, uno por tema, los servicios que un sistema operativo ofrece: procesos, memoria, archivos, dispositivos y protección. De cada uno damos una vista de conjunto; los cursos siguientes de la ruta los estudian a fondo. Empezamos por los procesos porque los otros cuatro se definen en relación con ellos.
Un proceso es un programa en ejecución, junto con todos los recursos que el sistema operativo le asignó: su memoria, sus archivos abiertos, su identidad y su turno de procesador.
La primera mitad de la definición la establecimos en el tema 9: el programa es un archivo, pasivo; el proceso es ese programa cargado y avanzando. La segunda mitad es la que importa ahora. Para el kernel, el proceso es la unidad de asignación: no reparte la CPU entre programas, sino entre procesos; no concede memoria a un usuario, sino a un proceso; no comprueba los permisos de una persona, sino los del proceso que hace el pedido.
Es también la unidad de aislamiento. La ilusión de la máquina propia que describimos en el tema 2 —un procesador y una memoria para cada programa— es exactamente lo que el kernel le construye a cada proceso. Y por eso un error en uno termina con ese proceso y no con los demás.
2. De qué está hecho un proceso
A lo largo del curso fuimos encontrando las piezas por separado. Reunidas, son estas:
| Componente | Qué es | Lo vimos en |
|---|---|---|
| Espacio de direcciones | Su memoria: código, datos, heap, bibliotecas y pila. | Tema 9; lo ampliamos en el 27 |
| Contexto de CPU | El contenido de los registros, que se guarda cuando pierde el procesador. | Temas 6 y 19 |
| Pila de kernel | La pila que usa el kernel cuando trabaja en nombre de este proceso. | Tema 12 |
| Archivos abiertos | La tabla de sus descriptores: el 0, el 1, el 2 y los que haya abierto. | Tema 14; lo ampliamos en el 28 |
| Identidad | El usuario y el grupo en cuyo nombre actúa. | Tema 12; lo ampliamos en el 30 |
| Identificadores | Su PID y el de su padre. | Tema 14 |
| Estado y contabilidad | Si está en ejecución, dormido o detenido; cuánta CPU consumió. | Tema 19 |
| Entorno | El directorio actual, las variables de entorno, los argumentos con que se lo lanzó. | — |
Todo eso cabe en la estructura task_struct cuyo extracto leímos en el tema 11, y en las que ella enlaza. Allí dijimos que, para el kernel, un proceso es esa estructura. La tabla anterior es su índice.
Hay una división útil entre las filas. El espacio de direcciones y los archivos abiertos son recursos: cosas que el proceso posee. El contexto de CPU y la pila son ejecución: el punto por donde va. La distinción reaparecerá en la sección 12, porque un proceso puede tener varios flujos de ejecución sobre los mismos recursos.
3. Identidad y parentesco
Cada proceso tiene un número que lo identifica mientras existe, el PID. El kernel los asigna en orden creciente; cuando llega al máximo, vuelve a empezar por los que quedaron libres.
$ echo $$ # el PID de esta shell
4102
$ cat /proc/sys/kernel/pid_max # el mayor PID posible
4194304
$ ps -o pid,ppid,user,comm
PID PPID USER COMMAND
4102 3310 diego bash
8104 4102 diego ps
Un PID identifica a un proceso solo durante su vida. Una vez terminado, el número queda disponible y con el tiempo se asigna a otro, sin relación con el anterior. Por eso un PID anotado ayer no sirve hoy.
La columna PPID es el identificador del padre: el proceso que creó a este. Todo proceso tiene exactamente uno, y de ahí resulta el árbol que recorrimos en el tema 22, con el proceso 1 en la raíz. La salida lo muestra en pequeño: ps es hijo de la shell, que a su vez es hija de la terminal.
El parentesco no es decorativo. El padre es quien recibe la noticia cuando un hijo termina, y el hijo hereda del padre casi todo lo que es al nacer: el usuario, el directorio actual, las variables de entorno y los archivos abiertos. Por eso un programa lanzado desde una terminal escribe en esa terminal sin haber abierto nada: sus descriptores 0, 1 y 2 son los de la shell.
4. Cómo nace: fork y exec
En el tema 16 vimos con strace que la shell ejecuta un comando mediante tres llamadas: crear, reemplazar, esperar. Ahora las usamos nosotros. En Unix, crear un proceso y cargar un programa son operaciones separadas:
| Función | Qué hace | Después de la llamada |
|---|---|---|
fork() | Crea un proceso nuevo, copia casi exacta del que llama. | Hay dos procesos ejecutando el mismo programa en el mismo punto. |
exec…() | Reemplaza el programa del proceso actual por otro. | Sigue habiendo un solo proceso, con el mismo PID y otro contenido. |
wait(), waitpid() | Espera a que un hijo termine, y obtiene cómo terminó. | El padre continúa, con el resultado del hijo. |
fork es la función más extraña que tiene C: se la llama una vez y retorna dos, una en cada proceso. Los dos continúan en la línea siguiente, con las mismas variables y los mismos valores. Lo único que los distingue es lo que fork les devuelve: al hijo, cero; al padre, el PID del hijo.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
printf("Antes de fork: soy el proceso %d\n", (int)getpid());
pid_t pid = fork(); /* a partir de aquí hay dos procesos */
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) { /* esto lo ejecuta solo el hijo */
printf("Hijo: mi PID es %d y mi padre es %d\n", (int)getpid(), (int)getppid());
return 7;
}
int estado; /* y esto, solo el padre */
waitpid(pid, &estado, 0);
printf("Padre: mi PID es %d; mi hijo %d termino con codigo %d\n",
(int)getpid(), (int)pid, WEXITSTATUS(estado));
return 0;
}
$ gcc -Wall -Wextra -g hijo.c -o hijo
$ ./hijo
Antes de fork: soy el proceso 8120
Hijo: mi PID es 8121 y mi padre es 8120
Padre: mi PID es 8120; mi hijo 8121 termino con codigo 7
Un solo programa, y dos procesos que toman ramas distintas del mismo if. El hijo devuelve 7 desde main; el kernel guarda ese valor, y el padre lo recupera con waitpid. Es el código de salida, el número que la shell deja en $?.
Copiar un proceso entero parece un derroche, sobre todo si el hijo va a reemplazar su programa enseguida. No lo es, por el mecanismo que anticipamos en el tema 18: el kernel no copia la memoria, sino que hace que padre e hijo compartan las mismas páginas, marcadas como de solo lectura. Solo cuando uno de los dos escribe en una página se produce un fallo de página, y el kernel duplica esa página y ninguna otra. La técnica se llama copia al escribir.
Prueba ./hijo | cat. La línea «Antes de fork» aparece dos veces, aunque el printf se ejecutó una sola. Es el tema 15 combinado con este. Con la salida en una tubería, el texto queda en el búfer de stdio, que es memoria del proceso. fork copia la memoria, búfer incluido, y cada proceso vacía su copia al terminar. La regla práctica es llamar a fflush(stdout) antes de fork.
5. Una shell en treinta líneas
Un hijo que ejecuta el mismo programa que el padre es útil en algunos casos. Lo habitual es que cargue otro, con alguna función de la familia exec. El siguiente programa recibe como argumentos un comando, lo ejecuta en un proceso hijo e informa cómo terminó. Es, reducido a su esqueleto, lo que hace una shell con cada línea que escribimos.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
if (argc < 2) {
fprintf(stderr, "uso: %s programa [argumentos]\n", argv[0]);
return 1;
}
pid_t pid = fork(); /* 1. crear */
if (pid == -1) {
perror("fork");
return 1;
}
if (pid == 0) {
execvp(argv[1], &argv[1]); /* 2. reemplazar: el hijo pasa a ser otro programa */
perror(argv[1]); /* solo se llega aquí si execvp falló */
return 127;
}
int estado;
waitpid(pid, &estado, 0); /* 3. esperar */
if (WIFEXITED(estado))
printf("[%d termino con codigo %d]\n", (int)pid, WEXITSTATUS(estado));
else if (WIFSIGNALED(estado))
printf("[%d fue finalizado por la senal %d]\n", (int)pid, WTERMSIG(estado));
return 0;
}
$ gcc -Wall -Wextra -g lanzar.c -o lanzar
$ ./lanzar echo hola
hola
[8140 termino con codigo 0]
$ ./lanzar ls /noexiste
ls: cannot access '/noexiste': No such file or directory
[8143 termino con codigo 2]
$ ./lanzar ./segfault # el programa del tema 18
Antes de escribir
[8147 fue finalizado por la senal 11]
$ ./lanzar noexiste
noexiste: No such file or directory
[8150 termino con codigo 127]
Las cuatro ejecuciones muestran las maneras en que puede terminar un proceso, y aclaran cosas que habíamos visto desde afuera:
| Cómo termina | Lo que recibe el padre | Lo que la shell deja en $? |
|---|---|---|
Por su voluntad, con exit o volviendo de main | Un código entre 0 y 255. Por convención, 0 es éxito. | Ese código |
| Finalizado por una señal | El número de la señal | 128 más ese número |
| El programa no pudo ejecutarse | El código que eligió el hijo tras fallar exec: por convención, 127 | 127 |
La tercera ejecución explica de dónde salían, en el tema 18, el mensaje Segmentation fault y el 139. El kernel no escribe ese mensaje, y el programa caído tampoco. Lo escribe la shell, que al volver de waitpid descubre que su hijo no terminó por su cuenta sino por la señal 11, y lo informa. Nuestro programa hizo lo mismo con otras palabras.
Separar la creación del reemplazo tiene una ventaja que justifica el diseño. Entre fork y exec, el hijo todavía ejecuta código del padre, pero ya es otro proceso: puede cambiar sus propios archivos abiertos, su directorio o su usuario sin afectar al padre, y el programa que cargue a continuación heredará esos cambios. Así implementa la shell las redirecciones, como veremos en el tema 28. En Windows, que crea el proceso y carga el programa en una sola operación, todas esas opciones deben pasarse como parámetros de la llamada; es la diferencia que señalamos en el tema 25.
6. Zombis y huérfanos
Cuando un proceso termina, el kernel libera su memoria y cierra sus archivos. No puede eliminar todavía su entrada en la tabla de procesos, porque allí está guardado el código de salida, y el padre aún no lo recogió. En ese intervalo, el proceso está muerto y su registro sigue existiendo. Se lo llama zombi.
Normalmente el intervalo dura microsegundos. Se alarga cuando el padre no llama a wait:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid == 0)
return 0; /* el hijo termina enseguida */
printf("Hijo %d terminado. El padre, %d, no lo recoge.\n", (int)pid, (int)getpid());
sleep(30); /* el padre sigue, sin llamar a wait */
return 0;
}
$ gcc -Wall -Wextra -g zombi.c -o zombi
$ ./zombi &
Hijo 8161 terminado. El padre, 8160, no lo recoge.
$ ps -o pid,ppid,stat,comm -C zombi
PID PPID STAT COMMAND
8160 4102 S zombi
8161 8160 Z zombi <defunct>
El hijo figura con estado Z y la marca <defunct>. No consume memoria ni procesador: es solo una entrada en una tabla. Por eso kill -9 no puede con él; no hay nada que finalizar. Desaparece cuando el padre lo recoge o cuando el padre termina. Un zombi suelto es inofensivo; miles de ellos indican un programa que crea hijos y no los espera, y pueden agotar los PID disponibles.
El caso opuesto es el del padre que termina primero. El hijo queda huérfano, y como todo proceso debe tener un padre, el kernel le asigna uno adoptivo:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
if (fork() == 0) {
printf("Hijo: mi padre es %d\n", (int)getppid());
sleep(3); /* para entonces, el padre ya terminó */
printf("Hijo: ahora mi padre es %d\n", (int)getppid());
return 0;
}
sleep(1);
return 0; /* el padre termina sin esperar al hijo */
}
$ gcc -Wall -Wextra -g huerfano.c -o huerfano
$ ./huerfano
Hijo: mi padre es 8170
$ Hijo: ahora mi padre es 2011
El indicador de la shell reaparece al segundo, porque el proceso que ella esperaba terminó. Dos segundos después, el hijo, que sigue vivo, informa que cambió de padre. El adoptante tradicional es el proceso 1; en una sesión gráfica actual suele ser el systemd del usuario, el 2011 que vimos en el árbol del tema 22. En cualquiera de los dos casos, es un proceso que recoge a todos sus hijos cuando terminan, de modo que los huérfanos no quedan como zombis. Es la tercera particularidad del proceso 1 que dejamos pendiente entonces.
7. Los estados de un proceso
Un proceso no está siempre ejecutándose. En el tema 19 distinguimos al que tiene la CPU, al que espera su turno y al que duerme hasta que ocurra algo. El modelo completo tiene cinco estados:
| Transición | Ocurre cuando | La provoca |
|---|---|---|
| 1. Nuevo → Listo | El kernel terminó de crearlo. | Una llamada fork |
| 2. Listo → En ejecución | El planificador le da un procesador. | El kernel |
| 3. En ejecución → Listo | El kernel le quita el procesador para dárselo a otro. | Una interrupción, casi siempre la del temporizador |
| 4. En ejecución → Bloqueado | Pide algo que no puede completarse de inmediato. | Una llamada al sistema: read, sleep, wait |
| 5. Bloqueado → Listo | Ocurre lo que esperaba. | Una interrupción: llegó el dato, venció la pausa |
| 6. En ejecución → Terminado | Llama a exit o recibe una señal que lo finaliza. | El propio proceso, o el kernel |
Las transiciones 3 y 4 son los dos tipos de cambio de contexto que contamos en el tema 19: la 3 es el involuntario, y la 4, el voluntario. Y hay dos ausencias que conviene notar. No existe una flecha de Bloqueado a En ejecución: un proceso que despierta no recupera el procesador, solo vuelve a la fila. Y no existe una de Listo a Bloqueado: para pedir algo hay que estar ejecutándose.
Linux muestra el estado de cada proceso con una letra, en la columna STAT de ps y en la columna S de top:
| Letra | Nombre | Corresponde a |
|---|---|---|
R | Running | En ejecución o listo: Linux no los distingue con letras distintas. |
S | Sleeping | Bloqueado, a la espera de un suceso; una señal puede despertarlo. |
D | Uninterruptible | Bloqueado en una operación que no admite interrupción, normalmente de disco. |
T | Stopped | Detenido por una señal, hasta que otra lo reanude. |
Z | Zombie | Terminado, y todavía no recogido por su padre. |
I | Idle | Un hilo del kernel sin trabajo. |
En cualquier instante, casi todos los procesos de un sistema están en S. Es lo normal, y lo deseable: un proceso dormido no cuesta nada. Solo unos pocos compiten de verdad por los procesadores.
8. Leer ps
ps toma una fotografía de los procesos en un instante. Sin opciones muestra solo los de la terminal actual. La forma más usada para verlos todos es ps aux:
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 168940 13312 ? Ss 09:14 0:02 /sbin/init
root 2 0.0 0.0 0 0 ? S 09:14 0:00 [kthreadd]
root 388 0.0 0.1 66820 18432 ? S<s 09:14 0:01 /usr/lib/systemd/systemd-journald
root 1042 0.0 0.0 9508 2816 ? Ss 09:14 0:00 /usr/sbin/cron -f -P
diego 4102 0.0 0.0 11420 5632 pts/0 Ss 10:31 0:00 bash
diego 8201 0.0 0.0 12980 4480 pts/0 R+ 12:04 0:00 ps aux
| Columna | Significado |
|---|---|
USER | El usuario en cuyo nombre actúa el proceso. |
PID | Su identificador. |
%CPU | El tiempo de CPU que usó, como porcentaje del tiempo que lleva existiendo. Es un promedio de toda su vida, no un valor instantáneo. |
%MEM | Su memoria física, como porcentaje de la del equipo. |
VSZ | El tamaño de su espacio de direcciones, en KB: lo que tiene prometido. |
RSS | La memoria física que ocupa de verdad, en KB. |
TTY | La terminal a la que está asociado. Un ? indica que no tiene: es un servicio. |
STAT | El estado, con la letra de la sección anterior y marcas adicionales. |
START, TIME | Cuándo se inició, y cuánto tiempo de CPU acumuló. |
COMMAND | El comando con sus argumentos. Entre corchetes, los hilos del kernel. |
La diferencia entre VSZ y RSS es la del tema 18: la memoria se promete al pedirla y se entrega al usarla. El proceso 1 tiene 168 MB de direcciones reservadas y ocupa 13. El hilo del kernel muestra ceros porque no tiene espacio de direcciones propio. Lo vemos en el tema 27.
Las marcas que acompañan a la letra de estado dan información adicional: s indica el primer proceso de una sesión; +, que está en primer plano en su terminal; < y N, prioridad más alta o más baja que la normal; l, que tiene varios hilos.
La última línea tiene una gracia: el único proceso en estado R es el propio ps. Para tomar la fotografía tiene que estar ejecutándose, y en un equipo tranquilo es el único que lo está.
Unas pocas variantes cubren casi todas las necesidades:
| Comando | Muestra |
|---|---|
ps -ef | Todos los procesos, con el PPID. |
ps -ef --forest | Lo mismo, dibujando el árbol de parentesco. |
ps -eo pid,ppid,stat,comm | Solo las columnas elegidas. |
ps aux --sort=-%mem | head | Los que más memoria ocupan. |
ps -C nombre | Los procesos de un programa determinado. |
pgrep nombre | Solo sus PID, para usar en otro comando. |
9. Leer top
top muestra lo mismo que ps, actualizado cada tres segundos y ordenado por uso de CPU, con un resumen del sistema en la parte superior:
$ top
top - 12:06:41 up 2:52, 2 users, load average: 0.42, 0.31, 0.27
Tasks: 287 total, 1 running, 285 sleeping, 0 stopped, 1 zombie
%Cpu(s): 3.1 us, 1.2 sy, 0.0 ni, 95.4 id, 0.2 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 15871.5 total, 7064.6 free, 4301.2 used, 4912.3 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 11570.3 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2204 diego 20 0 5120340 412300 142112 S 2.3 2.5 4:12.08 gnome-shell
3310 diego 20 0 812440 61200 44820 S 0.7 0.4 0:21.55 gnome-terminal-
8240 diego 20 0 14112 5760 3584 R 0.3 0.0 0:00.04 top
Las cinco líneas del encabezado resumen los temas del curso. La tercera la explicamos en el tema 12, y las de memoria son asunto del tema 27. Quedan las dos primeras:
| Dato | Significado |
|---|---|
up 2:52 | El tiempo transcurrido desde el arranque. |
load average | La carga media: la cantidad promedio de procesos que querían ejecutarse, en el último minuto, en los últimos cinco y en los últimos quince. |
Tasks: 287 total | Cuántos procesos existen. |
1 running | Cuántos están en estado R. |
285 sleeping | Cuántos están dormidos. Casi todos. |
1 zombie | Cuántos esperan ser recogidos por su padre. |
La carga media es el indicador más citado y el peor entendido. Cuenta los procesos en estado R, estén ejecutándose o esperando turno, y en Linux también los que están en D. Por sí solo, el número no dice nada: hay que compararlo con la cantidad de procesadores, que informa nproc.
| Carga, en un equipo de 8 procesadores | Interpretación |
|---|---|
| 0,4 | El equipo está casi ocioso. |
| 8 | Todos los procesadores ocupados, y nadie espera. |
| 20 | En promedio, doce procesos esperan turno o esperan al disco. El equipo está saturado. |
Los tres valores juntos dan la tendencia: si el del último minuto supera al de los quince, la carga está subiendo.
En la tabla de procesos, las columnas VIRT y RES son las VSZ y RSS de ps; NI es el valor de cortesía del tema 23, y PR, la prioridad que el kernel calcula a partir de él. La diferencia importante está en %CPU: en top es el uso durante el último intervalo, no el promedio de toda la vida del proceso. Un valor de 100 equivale a un procesador completo, de modo que un programa con varios hilos puede mostrar más.
top es interactivo. Estas son las teclas más útiles:
| Tecla | Efecto |
|---|---|
P, M | Ordenar por uso de CPU o de memoria. |
1 | Mostrar una línea por procesador en lugar del promedio. |
H | Mostrar los hilos por separado. |
k | Enviar una señal a un proceso. |
q | Salir. |
10. Señales: hablarle a un proceso
Las señales aparecieron ya en tres contextos: las que el kernel envía tras una excepción, en el tema 18; la que avisa al padre de que un hijo terminó, en el tema 16; y las que usa systemd para detener un servicio, en el tema 22. Son un mismo mecanismo: un aviso numerado que el kernel entrega a un proceso, y que este puede atender con una función propia, ignorar, o dejar que tenga su efecto por defecto.
Además del kernel, puede enviarlas un usuario a sus propios procesos, con el comando kill. El nombre engaña: sirve para enviar cualquier señal, no solo las que finalizan.
| Señal | Número | Cómo se envía | Efecto por defecto | ¿Puede atenderla el programa? |
|---|---|---|---|---|
SIGINT | 2 | Ctrl+C | Terminar | Sí |
SIGTERM | 15 | kill PID | Terminar | Sí |
SIGKILL | 9 | kill -9 PID | Terminar | No |
SIGTSTP | 20 | Ctrl+Z | Detener | Sí |
SIGSTOP | 19 | kill -STOP PID | Detener | No |
SIGCONT | 18 | kill -CONT PID | Reanudar | Sí |
SIGHUP | 1 | Al cerrarse la terminal | Terminar | Sí |
La última columna distingue dos clases. SIGTERM es un pedido: el programa puede atenderla para guardar su trabajo y cerrar ordenadamente. SIGKILL no llega nunca al programa: la ejecuta el kernel, que elimina el proceso sin darle oportunidad de nada. De ahí la regla de buena práctica:
$ sleep 300 &
[1] 8230
$ kill 8230 # primero pedirlo: SIGTERM
[1]+ Terminated sleep 300
$ sleep 300 &
[1] 8233
$ kill -9 8233 # solo si no responde: SIGKILL
[1]+ Killed sleep 300
Es la secuencia que sigue systemd al detener un servicio o apagar el equipo: pedir, esperar un plazo, y forzar. Usar kill -9 como primer recurso es una mala costumbre; un programa finalizado así puede dejar archivos a medio escribir. Los mensajes Terminated y Killed los escribe la shell, por el mismo camino del tema 18: es el padre, y se entera por waitpid.
Un usuario solo puede enviar señales a sus propios procesos; el administrador, a cualquiera. Para indicar el destinatario por nombre en lugar de por PID están pkill y killall. Cómo se escribe un manejador y qué se puede hacer dentro lo vimos en esbozo en el tema 18, y se estudia en el curso de procesos.
11. Primer y segundo plano
Una shell ejecuta normalmente un comando y espera a que termine: está bloqueada en wait, como en el diagrama de la sección 5. Ese comando está en primer plano, y es el que recibe lo que se escribe en el teclado. Añadir & al final cambia una sola cosa: la shell no espera. El comando queda en segundo plano y la shell vuelve a mostrar su indicador. Lo hicimos varias veces en el curso, con bucle y con zombi.
$ sleep 300 # en primer plano: la shell espera
^Z # Ctrl+Z: el kernel le envía SIGTSTP
[1]+ Stopped sleep 300
$ ps -o pid,stat,comm -C sleep
PID STAT COMMAND
8251 T sleep
$ bg # reanudarlo en segundo plano: SIGCONT
[1]+ sleep 300 &
$ jobs
[1]+ Running sleep 300 &
$ fg # traerlo al primer plano
sleep 300
^C # Ctrl+C: SIGINT
La secuencia recorre tres estados del diagrama y tres señales de la tabla. Ctrl+Z no es una función de la shell: es el kernel, que al recibir esa combinación de teclas en la terminal envía SIGTSTP al proceso que está en primer plano, y lo deja en estado T. bg y fg le envían SIGCONT. Y Ctrl+C es SIGINT.
| Comando o tecla | Efecto |
|---|---|
comando & | Lanzarlo en segundo plano. |
| Ctrl+Z | Detener el que está en primer plano. |
jobs | Listar los trabajos de esta shell. |
bg, fg | Reanudar un trabajo en segundo o en primer plano. |
kill %1 | Enviar una señal al trabajo número 1. |
Los trabajos pertenecen a la shell que los lanzó. Si se cierra la terminal, el kernel les envía SIGHUP y terminan. Para que un programa siga corriendo después hay que lanzarlo con nohup o, mejor, convertirlo en un servicio, como hicimos en el tema 22.
12. Procesos e hilos
En la sección 2 separamos los recursos de un proceso de su ejecución. Esa separación tiene consecuencias. Nada obliga a que haya un solo flujo de ejecución por conjunto de recursos.
Un hilo (thread) es un flujo de ejecución dentro de un proceso. Tiene su propio contador de programa, sus registros y su pila, y comparte con los demás hilos del proceso la memoria, los archivos abiertos y la identidad.
| Dos procesos | Dos hilos de un proceso | |
|---|---|---|
| Memoria | Separada: cada uno la suya | Compartida: ven las mismas variables |
| Archivos abiertos | Cada uno su tabla | La misma |
| Registros y pila | Propios | Propios |
| Comunicarse entre sí | Requiere un mecanismo del kernel | Basta con usar la memoria común |
| Un error en uno | No afecta al otro | Puede corromper al otro, y una señal fatal termina con todos |
| Cambiar de uno a otro | Más costoso: cambia el espacio de direcciones | Más barato |
Los hilos permiten que un programa aproveche varios núcleos a la vez, o que atienda la interfaz mientras hace un cálculo largo. El precio es el que anunciamos al final del tema 19: al compartir memoria, dos hilos pueden modificar el mismo dato a la vez, y aparecen los errores de concurrencia.
En Linux, la distinción es más tenue de lo que sugiere la tabla. El kernel planifica tareas, y tanto un proceso como un hilo son tareas creadas con la misma llamada, clone, que recibe una lista de lo que la nueva tarea debe compartir con la que la crea. Si no comparte nada, es un proceso; si comparte memoria y archivos, es un hilo. Es la llamada que apareció en la traza del tema 16, y por eso cada hilo tiene su propio identificador, el que devolvía gettid en el tema 14.
$ ps -o pid,nlwp,comm -p 2204,4102
PID NLWP COMMAND
2204 14 gnome-shell
4102 1 bash
La columna NLWP es la cantidad de hilos. El entorno gráfico tiene catorce; la shell, uno. ps -eLf los lista por separado. Los hilos y su sincronización son el tema central del curso de concurrencia.
13. Un directorio por proceso
Todo lo que ps y top muestran lo obtienen del mismo lugar. Por cada proceso existente, el kernel presenta en /proc un directorio cuyo nombre es el PID:
$ ls /proc | grep -c '^[0-9]' # un directorio por proceso
287
$ ls -l /proc/$$/exe /proc/$$/cwd
lrwxrwxrwx 1 diego diego 0 oct 12 12:20 /proc/4102/cwd -> /home/diego/so
lrwxrwxrwx 1 diego diego 0 oct 12 12:20 /proc/4102/exe -> /usr/bin/bash
$ tr '\0' ' ' < /proc/1042/cmdline; echo
/usr/sbin/cron -f -P
| Archivo | Contenido | Componente de la sección 2 |
|---|---|---|
status | Nombre, estado, PID, padre, usuario, memoria, cambios de contexto. | Identificadores, estado, contabilidad |
cmdline | Los argumentos con que se lo lanzó, separados por un byte cero. | Entorno |
exe | Un enlace al archivo ejecutable. | — |
cwd | Un enlace a su directorio actual. | Entorno |
environ | Sus variables de entorno. | Entorno |
maps | El mapa de su memoria. | Espacio de direcciones |
fd/ | Un enlace por cada descriptor abierto. | Archivos abiertos |
task/ | Un subdirectorio por cada hilo. | Ejecución |
Varios de estos archivos ya los usamos: status en los temas 11 y 19, maps en el 9, fd en un ejercicio del 25. La tercera columna muestra que el directorio es la tabla de la sección 2 hecha visible: cada componente de un proceso tiene su archivo. El conteo coincide con el total de top, y no por casualidad: top obtiene su número recorriendo este directorio.
Recorremos /proc entero en el tema 31. Antes, el tema 27 se ocupa del componente más voluminoso de un proceso: su memoria.
14. Resumen
- Un proceso es un programa en ejecución junto con los recursos que el sistema le asignó. Es la unidad a la que el kernel reparte la CPU y la memoria, y la que aísla.
- Está formado por un espacio de direcciones, un contexto de CPU, archivos abiertos, una identidad, unos identificadores, un estado y un entorno.
- Cada proceso tiene un PID, válido mientras vive, y un padre, del que hereda usuario, directorio y archivos abiertos.
forkcrea una copia del proceso que llama y retorna en los dos.execreemplaza el programa sin cambiar de proceso.waitespera a un hijo y obtiene su resultado.- Un proceso termina con un código de salida, o finalizado por una señal. El padre es quien se entera, y la shell lo refleja en
$?. - Un zombi es un proceso terminado cuyo padre aún no recogió el resultado. Un huérfano es uno cuyo padre terminó; lo adopta el proceso 1 o el gestor de la sesión.
- Los estados son nuevo, listo, en ejecución, bloqueado y terminado. Linux los muestra con letras:
R,S,D,T,Z. psda una fotografía de los procesos;top, una vista continua con un resumen del sistema.- La carga media es la cantidad promedio de procesos que quieren ejecutarse, y se interpreta comparándola con la cantidad de procesadores.
- Las señales son avisos numerados.
SIGTERMpide terminar y puede atenderse;SIGKILLla ejecuta el kernel sin consultar. &, Ctrl+Z,bgyfgpasan un trabajo entre el primer y el segundo plano.- Un hilo es un flujo de ejecución que comparte memoria y archivos con los demás hilos de su proceso.
- Cada proceso tiene un directorio en
/proc, del que obtienen sus datospsytop.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿En qué sentido el proceso es la «unidad de asignación» del sistema operativo?
El kernel reparte la CPU, concede la memoria, abre los archivos y comprueba los permisos por proceso. Todo recurso pertenece a un proceso, y todo pedido lo hace un proceso.
2. Menciona cinco cosas que forman parte de un proceso además de su código.
Su espacio de direcciones, con datos, heap y pila; el contenido de sus registros; sus archivos abiertos; el usuario en cuyo nombre actúa; y su PID y el de su padre. También su estado, su directorio actual y sus variables de entorno.
3. ¿Qué hereda un proceso de su padre?
El usuario, el directorio actual, las variables de entorno y los archivos abiertos, entre otras cosas. Por eso un programa lanzado desde una terminal puede escribir en ella sin abrirla.
4. ¿Qué devuelve fork, y a quién?
Retorna dos veces. Al proceso hijo le devuelve 0. Al padre le devuelve el PID del hijo, o −1 si no se pudo crear.
5. Después de un exec exitoso, ¿cambia el PID del proceso?
No. exec reemplaza el programa dentro del mismo proceso: cambian el código y la memoria, y se conservan el PID, el padre y los archivos abiertos.
6. En lanzar.c, ¿por qué hay un perror después de execvp, si exec reemplaza el programa?
Porque esa línea solo se alcanza si execvp falló, por ejemplo porque el programa no existe. Si tiene éxito, el código que sigue ya no está: fue reemplazado.
7. ¿Quién escribe «Segmentation fault» cuando un programa falla, y cómo se entera?
La shell. Es el proceso padre, y al volver de waitpid comprueba que su hijo no terminó por su cuenta, sino por la señal 11. Escribe el mensaje y deja 139 en $?.
8. ¿Por qué no se puede eliminar un zombi con kill -9?
Porque ya está terminado: no tiene memoria ni ejecución, solo una entrada en la tabla de procesos con su código de salida. Desaparece cuando su padre lo recoge con wait, o cuando el padre termina.
9. ¿Qué ocurre con un proceso cuyo padre termina antes que él?
Queda huérfano, y el kernel le asigna como padre al proceso 1 o al gestor de la sesión del usuario, que lo recogerá cuando termine.
10. Un proceso bloqueado en un read recibe por fin sus datos. ¿A qué estado pasa?
A listo, no a en ejecución. Queda en condiciones de ejecutarse, y recibe el procesador cuando el planificador lo elige.
11. En ps aux, un proceso muestra VSZ de 2 GB y RSS de 30 MB. ¿Cuánta memoria ocupa?
Treinta megabytes de memoria física. Los 2 GB son el tamaño de su espacio de direcciones: lo que tiene reservado, la mayor parte del cual nunca usó.
12. Una carga media de 6, ¿es alta?
Depende de la cantidad de procesadores. En un equipo de 16 es moderada; en uno de 2 significa que, en promedio, cuatro procesos esperan turno.
13. ¿Qué diferencia hay entre SIGTERM y SIGKILL?
SIGTERM se entrega al programa, que puede atenderla para cerrar ordenadamente. SIGKILL no llega al programa: el kernel elimina el proceso de inmediato. Por eso conviene enviar primero la primera.
14. ¿Qué comparten dos hilos de un mismo proceso, y qué tiene cada uno por separado?
Comparten la memoria, los archivos abiertos y la identidad. Cada uno tiene su contador de programa, sus registros y su pila.
Para practicar
- Ejecuta
./hijoy luego./hijo | cat. Explica la diferencia y corrígela con una llamada afflush. - Modifica
hijo.cpara que el proceso cree tres hijos, y que cada uno termine con un código distinto. Haz que el padre los espere a todos e informe cada código. - Usa
lanzarcon./division,./ilegaly./pausa, del tema 18. ¿Qué número de señal informa en cada caso? Compáralo con la tabla de ese tema. - Ejecuta
strace -f -e trace=clone,execve,wait4,exit_group ./lanzar echo holay relaciona cada línea con una delanzar.c. - Lanza
./zombi &y busca al zombi entop: ¿qué línea del encabezado cambia? Intenta eliminarlo conkill -9. Después finaliza al padre y comprueba qué pasa con el hijo. - Ejecuta
ps -eo stat | cut -c1 | sort | uniq -c. ¿Cuántos procesos hay en cada estado? ¿Cuál es el que está enR? - Lanza tantas copias de
bucle, del tema 19, como procesadores tengas más dos. Observa durante un minuto la carga media entopy el valor al que tiende. - Con
sleep 300en primer plano, reproduce la sesión de la sección 11. Luego envíaleSIGSTOPySIGCONTconkilldesde otra terminal, mirando su estado conps. - Elige un proceso de tu sesión y, usando solo archivos de
/proc/PID, averigua su ejecutable, su directorio actual, su padre y cuántos hilos tiene. - Busca con
ps -eo pid,nlwp,comm --sort=-nlwp | headlos programas con más hilos de tu sistema.