1. El punto de llegada
En el tema 1, el primer programa fue este:
#include <stdio.h>
int main(void)
{
printf("Hola, sistema operativo\n");
return 0;
}
Estas son cuatro líneas de cuenta, la herramienta del tema 41. Al comienzo del curso habrían sido un jeroglífico; ahora cada pieza tiene una razón:
ssize_t n = read(fd, buf, sizeof buf);
if (n < 0) {
if (errno == EINTR)
continue;
return errno;
}
if (n == 0)
break;
contador_alimentar(&k, buf, (size_t)n);
| Pieza | Lo que ahora sabes que hace, y por qué está | Tema |
|---|---|---|
read(fd, …) | Una llamada al sistema sobre un descriptor, un número entero que sirve igual para un archivo, una tubería o la terminal. | 38 |
ssize_t n | Un tipo con signo, porque el resultado puede ser un tamaño (positivo), el fin de archivo (cero) o un error (negativo). | 5, 38 |
sizeof buf | El tamaño del arreglo, sin números mágicos. Funciona porque buf sigue siendo un arreglo aquí; dentro de una función que lo recibiera como parámetro sería el tamaño de un puntero. | 17 |
n < 0 y errno | La convención de las llamadas al sistema: un valor de retorno que avisa del fallo y una variable que dice por qué. | 37 |
EINTR y continue | Una señal puede interrumpir la lectura sin que haya error de verdad: se reintenta. | 37 |
return errno | Una biblioteca no imprime ni sale: devuelve el código a quien la llama, que decide el mensaje y el código de salida. | 37, 41 |
n == 0 | El fin de archivo no es un error: es cero, y se distingue de -1. | 38 |
&k | La dirección de una estructura local, para que otra función la modifique (paso de punteros). | 15, 16, 24 |
(size_t)n | Una conversión explícita de con signo a sin signo, hecha después de comprobar que n no es negativo, para que el compilador no avise (-Wconversion). | 7, 40 |
| Lo que hay alrededor | Está dentro de una biblioteca estática, se compila con un Makefile que sabe de dependencias, y lo vigilan pruebas unitarias, una comparación con wc y los sanitizadores. | 32, 36, 40, 41 |
Entre un programa y el otro no hay una lista de funciones memorizadas: hay un modelo. Sabes lo que es una dirección y qué ocurre cuando se la usa mal, de dónde sale la memoria de cada variable, qué hace cada etapa de gcc y el enlazador, qué significa que una llamada falle y cómo detectarlo, y cómo averiguar la causa de un error con un depurador. Con eso, el código en C de otras personas deja de ser una caja negra, y el propio deja de ser una apuesta.
Las habilidades del curso se apilan, cada una sobre las anteriores:
cuenta, en la cima, usa todas a la vez.2. El curso en diez respuestas
Cada bloque respondió una pregunta. Estas son las respuestas, en una frase cada una:
| Bloque | La pregunta | La respuesta |
|---|---|---|
| 1. Del código fuente al programa Temas 1 a 4 | ¿Por qué C, y qué pasa entre el fuente y el ejecutable? | Porque el kernel y los comandos están escritos en C, que da control directo a cambio de disciplina; y porque gcc son cuatro etapas (preprocesador, compilador, ensamblador, enlazador), cada una con sus errores y sus herramientas. |
| 2. Tipos, operadores y control Temas 5 a 10 | ¿Cómo se representan y combinan los datos? | Con tipos de tamaño conocido, con o sin signo, cuyas operaciones pueden desbordarse o convertirse sin avisar; y con if, switch y bucles, printf para salir y fgets para entrar. |
| 3. Funciones y organización Temas 11 a 14 | ¿Cómo se organiza un programa? | En funciones que reciben copias de sus parámetros y viven en marcos de la pila, repartidas en archivos .h y .c con extern y static, y con un preprocesador cuyas macros tienen trampas. |
| 4. Punteros Temas 15 a 20 | ¿Qué es un puntero y para qué sirve? | Una dirección con tipo. Permite modificar datos del llamador, recorrer arreglos con aritmética, apuntar a otros punteros, proteger con const y llamar funciones por medio de punteros a funciones. |
| 5. Cadenas y arreglos Temas 21 a 23 | ¿Cómo se maneja el texto sin desbordar nada? | Como arreglos de bytes terminados en un nulo, con snprintf, strtol y strtok_r en lugar de las funciones que no miran el tamaño; y con matrices, argv y getopt. |
| 6. Estructuras y tipos propios Temas 24 a 26 | ¿Cómo se define un dato compuesto? | Con struct, cuyo tamaño real incluye relleno de alineación; y con uniones, enumeraciones y typedef. |
| 7. Memoria dinámica Temas 27 a 30 | ¿Quién reserva la memoria, y quién la libera? | El programa, con malloc y compañía, comprobando cada pedido y liberando cada bloque una vez; los errores típicos (fugas, uso tras liberar, doble liberación, desbordes) se cazan con Valgrind y AddressSanitizer, y se evitan dando a cada bloque un dueño claro. |
| 8. Compilación, enlazado y bibliotecas Temas 31 a 36 | ¿Cómo se arma un proyecto de varios archivos? | Con archivos objeto y símbolos que el enlazador resuelve; bibliotecas estáticas (ar, el orden importa) y dinámicas (-fPIC, soname, rpath); carga a pedido con dlopen; y make con dependencias automáticas y un directorio por variante. |
| 9. Errores, archivos y depuración Temas 37 a 40 | ¿Cómo se falla bien, y cómo se encuentra un error? | Comprobando cada llamada y usando errno y códigos de salida con sentido; sabiendo que entre el programa y el disco hay búferes; con gdb para investigar; y con avisos, assert, sanitizadores, análisis estático y strace para que los errores aparezcan antes. |
| 10. Integración y cierre Temas 41 y 42 | ¿Cómo se construye una herramienta de verdad? | Escribiendo antes la especificación; separando una biblioteca de un programa; probando en dos niveles, unitario y contra una referencia; y compilando en tres modos (debug, san y release). |
3. Las ideas que sostienen todo
La sintaxis se olvida, y se vuelve a buscar. Estas diez ideas no se olvidan, porque explican por qué C es como es y por qué los programas en C bien hechos se parecen entre sí. Si alguna no resulta evidente, el tema indicado es el que conviene repasar.
| Idea | Lo que explica | Tema |
|---|---|---|
| C da control y no protege. No comprueba los límites de un arreglo, ni que un puntero apunte a algo válido, ni que un número no se desborde. | Por qué un error de memoria puede «funcionar» durante meses, y por qué las herramientas de detección (sanitizadores, avisos, análisis) son parte del lenguaje en la práctica. | 1, 29, 40 |
| Un puntero es una dirección con tipo. El tipo dice cuántos bytes se leen y cuánto avanza al sumarle uno. | La aritmética de punteros, p[i] como *(p + i), void * y los casts, y los punteros a funciones. | 15, 18, 20 |
| Un arreglo no recuerda su tamaño. Al pasarlo a una función se convierte en un puntero al primer elemento. | Por qué sizeof da otra cosa dentro de la función, por qué el tamaño viaja aparte (char *dst, size_t cap), y de dónde sale casi todo desbordamiento. | 17, 21, 22 |
| Cada dato vive en algún lugar y tiene su tiempo de vida. Pila, datos estáticos o montículo, según cómo se declare. | Por qué no se devuelve la dirección de una variable local, qué significa static y por qué la memoria dinámica necesita un dueño. | 12, 27, 28, 30 |
| El comportamiento indefinido no avisa. El estándar deja sin definir muchos errores, y el programa puede hacer cualquier cosa, incluso lo que parecía correcto. | El entero con signo que se desborda, el índice fuera de rango, el uso tras liberar, y por qué «a mí me funciona» no es una prueba. | 7, 29, 40 |
| Compilar y enlazar son pasos distintos. El compilador traduce cada archivo por separado; el enlazador junta y resuelve los símbolos. | Los errores undefined reference y multiple definition, el orden de las bibliotecas en la línea de gcc y qué puede cambiar en un .h sin recompilar todo. | 4, 31, 32 |
| Una biblioteca se elige al compilar o al ejecutar. Estática, se copia al programa; dinámica, se busca al arrancar; cargada a pedido, la abre el propio programa. | Por qué un programa que compiló bien puede no arrancar, cómo lo busca el cargador, qué es el soname y para qué sirve dlopen. | 32, 33, 34 |
Toda llamada puede fallar, y hay que preguntar. Cada convención tiene su forma de avisar, y errno solo vale justo después del fallo. | Los valores de retorno, perror y strerror, los códigos de salida, y el error de comprobar el resultado de fopen pero no el de fclose. | 28, 37, 38 |
Entre el programa y el disco hay búferes. stdio acumula, el núcleo guarda en caché, y cada capa tiene su orden y su momento. | La salida que cambia de orden en una tubería, los datos que se pierden si el programa aborta, y por qué leer byte a byte es mucho más lento. | 38 |
| Lo que no se prueba no funciona. Los errores viven en los bordes y en los datos del tamaño del mundo real. | Las pruebas de casos límite, los sanitizadores en cada ejecución de prueba, la comparación contra una referencia, y los tres modos de compilación. | 39, 40, 41 |
Las cuatro últimas son la diferencia entre un programa que funciona y uno en el que se puede confiar. Ninguna depende de C: valen para cualquier programa que vaya a ejecutarse sin que nadie lo mire, y las volverás a encontrar en los servicios del sistema y en cualquier lenguaje que uses después.
4. Los patrones que se repiten
A lo largo del curso, las mismas formas aparecieron en programas muy distintos. Reconocerlas es lo que permite escribir uno nuevo sin empezar de cero, y leer uno ajeno sin perderse.
| Patrón | Dónde apareció |
|---|---|
| El encabezado de siempre. | #define _POSIX_C_SOURCE 200809L, los #include ordenados, y la compilación con -std=c17 -Wall -Wextra: en todos los ejemplos completos del curso. |
| Un puntero y su tamaño, siempre juntos. | char *dst, size_t cap, const int *v, size_t n, void *datos, size_t n: las funciones de cadenas del bloque 5, las estructuras dinámicas del bloque 7, y la biblioteca de cuenta. |
| Comprobar y no seguir si falló. | if (p == NULL) tras malloc, if (fd < 0) tras open, if (!f) tras fopen, y también tras fclose y fflush. |
| Un dueño para cada bloque. | Quien reserva sabe quién libera: las funciones nuevo y liberar de las listas del tema 30, y la liberación en orden inverso al de reserva. |
La limpieza con goto. | Una etiqueta al final de la función que libera los recursos en orden inverso, el uso de goto que casi todos aceptan (tema 9). |
| El bucle de lectura. | while ((n = read(…)) != 0), con -1 como error (y EINTR que se reintenta) y 0 como fin: cpsys del tema 38 y contar_fd del 41. |
Una interfaz mínima en el .h. | Solo lo público en el encabezado, con guardas de inclusión; lo demás static en el .c: los temas 13, 32 y 41. |
| El código de error como valor de retorno. | 0 o un valor de errno, sin imprimir ni salir: el contrato de las bibliotecas, desde copiar_nombre del tema 40 hasta contar_ruta del 41. |
Mensajes por stderr y códigos de salida con sentido. | perror, fprintf(stderr, …) y las constantes de sysexits.h: copiar (tema 37) y cuenta (tema 41). |
| Un directorio de compilación por modo. | build/debug, build/release, build/san, con dependencias automáticas (-MMD -MP) y requisito de solo orden para el directorio: los temas 36 a 41. |
| Los casos de borde. | Tamaño cero, uno, justo, uno de más, nulo, vacío, archivo mayor que el búfer: copiar_nombre (tema 40) y las pruebas de cuenta (tema 41). |
5. Los errores, ahora con explicación
Los mensajes de gcc, del enlazador, del cargador y del sistema son breves y, al principio, crípticos. Pero casi siempre dicen exactamente qué pasó. Al comienzo del curso eran frases en inglés; ahora cada una remite a un mecanismo conocido.
| El mensaje | Lo que significa | Tema |
|---|---|---|
Segmentation fault (código 139) | El programa accedió a una dirección que no le corresponde: un puntero nulo, sin inicializar o ya liberado. gdb se detiene en la línea del fallo y bt muestra cómo se llegó. | 27, 39 |
undefined reference to `f' | El enlazador no encontró la definición: falta un archivo objeto o una biblioteca, o está antes de lo que la necesita en la línea de gcc. | 31, 32 |
multiple definition of `f' | Dos archivos definen el mismo símbolo, a menudo por definir una variable o función en un .h sin static. | 13, 31 |
cannot find -lxxx | El enlazador no halla libxxx.so ni libxxx.a en los directorios de -L ni en los del sistema. | 32 |
error while loading shared libraries: libxxx.so.1: cannot open shared object file | El programa compiló, pero el cargador no encuentra la biblioteca al ejecutarlo: -L es del enlazador. Se resuelve con rpath, LD_LIBRARY_PATH o ldconfig. | 33 |
undefined symbol: f al hacer dlopen | Al plugin le falta algo: el programa principal no exportó el símbolo (-rdynamic), o el plugin usa una función que nadie define. | 34 |
Makefile:2: *** missing separator. Stop. | Una línea de la receta empieza con espacios y no con un tabulador. | 35 |
No rule to make target 'x.h', needed by 'x.o' | Una dependencia que no existe: típicamente un encabezado borrado que un .d viejo todavía nombra (falta -MP). | 35, 36 |
make: 'prog' is up to date y el binario no cambió | make no lee los #include: el encabezado no era un requisito. | 35, 36 |
No such file or directory, Permission denied, Is a directory | Los mensajes de strerror para ENOENT, EACCES y EISDIR: el código que una llamada dejó en errno. | 37 |
Bad file descriptor | Se usó un descriptor cerrado o inválido, a menudo después de un fclose o un doble cierre. | 38 |
*** stack smashing detected ***, *** buffer overflow detected *** | El canario de la pila o FORTIFY_SOURCE detectaron un desborde y abortaron (código 134): hay un error de memoria que corregir. | 40 |
Assertion `…' failed y código 134 | Se violó una condición que el programador declaró imposible. El mensaje trae archivo, línea y función. | 14, 40 |
runtime error: signed integer overflow, index 5 out of bounds | Los informes de UBSan: comportamiento indefinido detectado en el momento exacto. | 40 |
ERROR: AddressSanitizer: heap-buffer-overflow, heap-use-after-free | Los informes de AddressSanitizer: se escribió fuera de un bloque o se usó después de liberarlo, con la línea y el origen del bloque. | 29 |
warning: comparison of integer expressions of different signedness | Se comparó un entero con signo con uno sin signo: el negativo se convierte en un número enorme. | 7, 40 |
<optimized out> en gdb | El programa se compiló con optimización y la variable ya no existe como tal. Se depura con -g -O0. | 39 |
| Ningún mensaje, y código 0 | Funcionó. O el programa es de los que no comprueban los errores: por eso los últimos temas. | 37 |
La costumbre que resume la tabla es la del tema 4: leer el mensaje completo, con su archivo y su línea, antes de cambiar nada; y compilar con -Wall -Wextra para que el compilador diga lo que sospecha antes de que el programa se ejecute.
6. La caja de herramientas
El inventario del curso, ordenado por lo que se quiere hacer. No es para memorizar: es para saber que la herramienta existe, y dónde está explicada.
| Para… | Herramientas | Temas |
|---|---|---|
| Empezar | gcc -std=c17 -Wall -Wextra, el manual (man 2, man 3), un editor | 1, 2, 3 |
| Ver las etapas de la compilación | gcc -E, -S, -c, -v | 4, 14 |
| Datos y operaciones | Tipos de stdint.h, sizeof, operadores de bits, printf, fgets, strtol | 5 a 10, 22 |
| Organizar el código | Funciones, .h y .c, static, extern, macros | 11 a 14 |
| Punteros | &, *, aritmética, const, punteros a funciones, qsort | 15 a 20 |
| Cadenas y estructuras | string.h, snprintf, strtok_r, getopt, struct, union, enum, typedef, offsetof | 21 a 26 |
| Memoria dinámica | malloc, calloc, realloc, free, /proc/self/maps, Valgrind, AddressSanitizer | 27 a 30 |
| Inspeccionar binarios y bibliotecas | nm, objdump, readelf, ldd, ar, file | 31 a 33 |
| Crear bibliotecas | ar rcs, -fPIC -shared, -Wl,-soname, -Wl,-rpath, ldconfig, dlopen, dlsym | 32 a 34 |
| Construir | make, reglas de patrón, -MMD -MP, .PHONY, install, -j | 35, 36 |
| Manejar errores | errno, perror, strerror, exit, sysexits.h, err.h | 37 |
| Archivos | fopen, fgets, getline, fread; open, read, write, lseek; setvbuf, fflush, fsync | 38 |
| Depurar | gdb: break, run, next, step, bt, print, x, watch, volcados de memoria | 39 |
| Prevenir y detectar | -Wshadow -Wconversion -Werror, assert, -fsanitize=address,undefined,thread, -fanalyzer, cppcheck, strace | 29, 40 |
| Hacer una herramienta | Biblioteca y programa separados, pruebas unitarias y de integración, tres modos de compilación | 41 |
7. Lo que quedó en tu equipo
Si hiciste los ejemplos, tienes ahora una colección de programas y de plantillas que siguen siendo útiles, y que forman entre todos una pequeña base de trabajo:
| Pieza | Sirve para | Tema |
|---|---|---|
| El entorno de trabajo | gcc, make, gdb, valgrind y strace instalados y verificados. | 2 |
libbasico, estática y dinámica | El ejemplo completo de una biblioteca con ar, con soname y enlaces simbólicos, con rpath y con una caché propia de ldconfig. | 32, 33 |
| El anfitrión de plugins | Un programa que carga módulos con dlopen y les ofrece funciones con -rdynamic. | 34 |
El Makefile de proyecto | Dependencias automáticas, directorio por modo, test, install, help y salida limpia: la plantilla para cualquier proyecto en C. | 36 |
copiar y cpsys | Copiadores de archivos con comprobación de cada paso y códigos de salida, con stdio y con llamadas al sistema. | 37, 38 |
Los guiones de gdb | Sesiones que se repiten: puntos de ruptura con commands para trazar sin recompilar. | 39 |
| Los programas de prueba de errores | Desbordes, comportamiento indefinido y carreras de datos para ver cada defensa en acción. | 40 |
cuenta y sus pruebas | Una herramienta completa, con biblioteca, Makefile, pruebas unitarias y de integración: el modelo para la próxima. | 41 |
Conviene guardarlo todo en un repositorio de git, con su historia. Es el hábito que convierte ejercicios sueltos en un portafolio, y lo que hace falta para que alguien más (o tú, dentro de un año) pueda construirlo con un solo make.
8. Lo que quedó sin ver
Un curso elige. Varios temas se nombraron y se dejaron a un lado, a propósito, para no perder el hilo. Conviene saber cuáles son:
| Quedó afuera | Lo que vimos de eso | Dónde seguir |
|---|---|---|
| Procesos e hilos | El mapa de memoria de un proceso (tema 27), exit y los códigos de salida (37), un -pthread y un contador con carrera de datos (37, 40). | El curso siguiente, Procesos e Hilos: fork, exec, wait, señales, pthread_create y el mutex. |
| La sincronización | Una carrera de datos detectada con ThreadSanitizer (tema 40). | Los cursos de Concurrencia y Sincronización: mutex, variables de condición, semáforos, memoria atómica. |
| Los directorios y los metadatos | Archivos con huecos y permisos con umask (tema 38), stat -c desde la terminal. | opendir, readdir, stat, mmap, enlaces: el curso de Sistemas de Archivos y Entrada/Salida. |
| Las redes | Los descriptores sirven igual para un archivo que para una tubería (tema 38). | socket, bind, connect y el curso de Redes desde el Sistema Operativo. |
| Los caracteres anchos y Unicode | Que cuenta cuenta bytes y que un wc moderno interpreta UTF-8 (tema 41). | setlocale, mbrtowc, wchar_t y las bibliotecas de texto como ICU. |
| Las construcciones más nuevas del lenguaje | Que el curso usa C17 y señala dónde C23 cambia algo (tema 1). | Los estándares C11 y C23: atómicos, hilos del lenguaje, constexpr, nullptr, [[…]]. |
| Otros sistemas de construcción | Un Makefile escrito a mano, con todo lo que hace falta entender. | CMake y Meson, que generan los archivos de construcción para proyectos con varias plataformas. |
| Los marcos de pruebas y el fuzzing | Pruebas escritas a mano y la comparación con una referencia (tema 41). | Marcos como Unity o CMocka, y herramientas de fuzzing como AFL++ y libFuzzer, que generan entradas hostiles. |
| C++ | Nada, a propósito. | Un lenguaje distinto que este curso no cubre: comparte la sintaxis y poco más. |
9. La ruta: once cursos más
Este curso es el cuarto de una serie de quince. Enseñó el lenguaje del sistema: con él se escribieron los programas que el curso de Bash solo podía llamar. Los que siguen bajan, curso a curso, hacia lo que el sistema operativo hace por dentro, y casi todos retoman algo que aquí se usó sin explicar cómo funciona.
Nivel inicial
| Curso | Se ocupa de | Lo tocamos en |
|---|---|---|
| 1. Fundamentos de Sistemas Operativos | Qué hace el kernel, y qué ocurre entre un programa y la máquina. | Temas 1, 4 y 27 |
| 2. Terminal y Línea de Comandos en Linux | Los comandos, las tuberías y los permisos que este curso dio por sabidos. | Temas 2, 38 y 41 |
| 3. Automatización con Scripts en Bash | Unir programas con scripts, con la misma disciplina de errores y códigos de salida. | Temas 36, 37 y 41 |
Nivel intermedio
| Curso | Profundiza en | Lo tocamos en |
|---|---|---|
| 5. Procesos e Hilos | Cómo se crea un proceso, qué recibe un hijo, qué es una señal, y cómo se crean y se esperan los hilos. | Temas 27, 37, 38 y 40 |
| 6. Planificación de la CPU | Cómo decide el kernel quién usa el procesador, y qué significan la prioridad y la carga. | Temas 38 y 41 (medir tiempos) |
| 7. Concurrencia y Sincronización | Las carreras entre procesos e hilos, y los candados que las evitan. | Temas 37 y 40 |
| 8. Gestión de Memoria | La memoria virtual, la caché y los límites de memoria de un proceso. | Temas 27 a 29 |
| 9. Sistemas de Archivos y Entrada/Salida | Inodos, enlaces, directorios y la corrupción de los datos. | Tema 38 |
| 10. Comunicación entre Procesos | Las tuberías, los descriptores y la memoria compartida por dentro. | Tema 38 |
Nivel avanzado
| Curso | Profundiza en | Lo tocamos en |
|---|---|---|
| 11. Redes desde el Sistema Operativo | Conexiones, puertos, TLS y nombres, vistos desde el kernel. | Tema 38 |
| 12. Seguridad en Sistemas Operativos | Privilegios, secretos, claves y la entrada que no hay que creer. | Temas 22, 29 y 40 |
| 13. Virtualización y Contenedores | Máquinas dentro de máquinas, y entornos aislados y reproducibles. | Tema 32 (binarios autosuficientes) |
| 14. Rendimiento, Depuración y Observabilidad | Medir, encontrar el cuello de botella y entender un sistema en producción. | Temas 39 y 40 |
| 15. Construcción de un Sistema Operativo desde Cero | Escribir un kernel mínimo: compilarlo, enlazarlo y arrancarlo. | Temas 27, 31 y 36 |
Un kernel está escrito en C, y el último curso de la serie lo vuelve a usar: sin la biblioteca estándar, con el enlazador configurado a mano y los errores sin ninguna red. Todo lo del bloque 8 del curso, el enlazador, los símbolos y las bibliotecas, es el vocabulario de esa etapa.
10. El próximo paso: Procesos e Hilos
El curso que sigue es Procesos e Hilos. Hasta aquí, cada programa fue un proceso solitario: se lo lanza, hace su trabajo y termina. El curso siguiente enseña cómo un programa crea otros, cómo se comunican con el que los lanzó, cómo se espera a que terminen, y cómo un solo proceso puede hacer varias cosas a la vez con hilos. Sus ejemplos están escritos en C, y lo que el curso actual dejó listo se retoma casi tema por tema:
| Lo vimos aquí, en C | Allí se estudia, desde el sistema operativo |
|---|---|
/proc/self/maps y el mapa de memoria de un proceso, del tema 27. | La imagen de un proceso en memoria: código, datos, bss, montículo y pila (tema 2 de Procesos e Hilos). |
read, write y strace, de los temas 38 y 40. | Modo usuario, modo núcleo y llamadas al sistema, siguiéndolas con strace (tema 10). |
El búfer de stdio y la salida que cambia de orden en una tubería, del tema 38. | Lo que hereda el hijo de fork, incluido el printf que aparece dos veces por el búfer (tema 12). |
Los descriptores, close(1) y la redirección, del tema 38. | fork y exec, y cómo preparar al hijo con dup2 antes de ejecutar un programa (tema 17). |
exit, _exit, atexit y los códigos de salida 0 a 255, del tema 37. | Terminar un proceso y esperarlo con wait y waitpid, interpretando el estado con WIFEXITED y WEXITSTATUS (temas 20 y 21). |
Los códigos 128 + n y las señales SIGSEGV y SIGABRT, del tema 37; EINTR y sigaction. | Controlar procesos con señales: kill, SIGTERM, SIGCHLD (tema 23). |
Compilar con -pthread y la carrera de datos con ThreadSanitizer, de los temas 37 y 40. | Crear y esperar hilos con pthread_create y pthread_join (tema 30), y las condiciones de carrera con el primer pthread_mutex (tema 34). |
Que cada hilo tiene su propio errno, del tema 37. | Los datos propios de cada hilo, _Thread_local, y por qué errno es distinto en cada hilo (tema 35). |
El proyecto cuenta, con una biblioteca que no tiene estado global, del tema 41. | Procesar archivos en paralelo, con varios procesos y con varios hilos, midiendo y comparando (tema 42). |
La diferencia con este curso es de ámbito. Aquí un programa pedía cosas al sistema y esperaba la respuesta; allí un programa crea otros programas, y el sistema decide cuándo corre cada uno. Es más difícil de razonar, porque el orden en que ocurren las cosas deja de ser el del código, y es justamente por eso que importan las herramientas que aprendiste: gdb para detenerse en medio, strace para ver qué llama cada proceso, los sanitizadores para encontrar las carreras, y las pruebas con casos de borde.
Llegas con ventaja. Quien empieza procesos e hilos sin saber manejar errores, sin entender los búferes de stdio ni la memoria de un proceso tiene que aprender todo a la vez, y tropieza con los efectos antes de conocer las causas. Tú ya sabes qué hace cada una de esas piezas por separado.
11. Cómo seguir practicando
C se aprende como cualquier lenguaje: escribiendo y leyendo mucho. Algunas maneras, de menor a mayor esfuerzo:
- Reescribir las herramientas pequeñas de Unix.
cat,head,tail,grep,hexdump,cp,sort,wc(ya lo hiciste concuenta). Cada una tiene una referencia contra la cual comparar, y cada una obliga a pensar en archivos, errores y casos de borde. - Compilar siempre con todas las defensas.
-Wall -Wextra -Wshadow -Wconversiony un modo con-fsanitize=address,undefined. Cada aviso que no entiendas es un tema para repasar. - Escribir una biblioteca de estructuras de datos (lista, tabla de dispersión, arreglo dinámico) con su archivo de pruebas, sus casos de borde y los sanitizadores en cada ejecución. Es el mejor ejercicio de memoria que existe.
- Leer código de otros. Hay proyectos pequeños y muy bien escritos: las herramientas del sistema (
coreutils, BusyBox), SQLite, o una biblioteca comozlib. Leer cómo organizan las cabeceras, los errores y la memoria enseña más que muchos libros. - Depurar con
gdbsin avergonzarse. Cuando algo falle, antes de ponerprintf, probarrun,bt,print. La práctica diaria es lo que lo vuelve natural. - Contribuir a un proyecto de código abierto con un arreglo pequeño: un aviso del compilador, una prueba que falta, un error de documentación. Es la manera de ver C de producción.
Para quien quiera textos de consulta más amplios:
| Texto | Autor | Sirve para |
|---|---|---|
| The C Programming Language | Brian Kernighan y Dennis Ritchie | El texto clásico, breve y denso, del propio creador del lenguaje. Se lee mejor después de este curso. |
| C Programming: A Modern Approach | K. N. King | Un curso completo y muy cuidadoso, con muchos ejercicios. |
| Modern C | Jens Gustedt | C moderno (C11 y C17) escrito por un participante del estándar; gratuito en línea. |
| The Linux Programming Interface | Michael Kerrisk | La referencia sobre la programación de sistemas en Linux: archivos, procesos, señales, hilos, redes. |
| Advanced Programming in the UNIX Environment | W. Richard Stevens y Stephen Rago | El clásico de la programación de sistemas Unix, con más historia y portabilidad. |
| SEI CERT C Coding Standard | Software Engineering Institute (CERT) | Reglas y recomendaciones para escribir C seguro, con ejemplos de código que no cumple y que cumple. |
Y antes que cualquier libro, la documentación que ya está en tu máquina: man 2 para las llamadas al sistema, man 3 para las funciones de la biblioteca de C, info make y el manual de gdb, que ahora puedes leer con soltura.
12. El curso, en doce frases
- C es el lenguaje del sistema: da control directo sobre la máquina y a cambio pide al programador la disciplina que otros lenguajes ponen por él.
- El camino del fuente al ejecutable son cuatro etapas, y cada error pertenece a una: preprocesador, compilador, ensamblador o enlazador.
- Los tipos tienen un tamaño y un signo; las operaciones pueden desbordarse o convertirse sin avisar, y el comportamiento indefinido no se anuncia.
- Una función recibe copias, vive en un marco de la pila y se declara en un
.h; lo interno se esconde constatic. - Un puntero es una dirección con tipo; un arreglo, al pasarlo a una función, se convierte en un puntero y pierde su tamaño, que debe viajar aparte.
- Una cadena es un arreglo de bytes terminado en un nulo, y las funciones que no miran el tamaño son la causa de casi todo desbordamiento.
- Una estructura tiene relleno por alineación; una unión, una enumeración y un
typedefdan forma y nombre a los datos propios. - La memoria dinámica se pide, se comprueba y se libera una sola vez, con un dueño claro para cada bloque; Valgrind y AddressSanitizer encuentran los errores que el compilador no ve.
- Un proyecto se arma con archivos objeto y bibliotecas estáticas o dinámicas, y
makereconstruye solo lo que cambió, con las dependencias que el compilador informa. - Toda llamada puede fallar: se comprueba el valor de retorno, se guarda
errnoenseguida, se escribe un mensaje porstderry se sale con un código con sentido. - Entre el programa y el disco hay búferes: lo que no se vacía puede perderse, y
fclosees una operación que también falla. - Una herramienta de verdad se especifica antes, se organiza en biblioteca y programa, se prueba en sus bordes y contra una referencia, y se compila con avisos estrictos, sanitizadores y
gdba mano.
13. Autoevaluación final
Estas preguntas no corresponden a un tema: cada una pide relacionar varios. Si puedes responderlas sin consultar, el objetivo del curso está cumplido.
1. ¿Por qué sizeof(v) da 8 dentro de una función que recibe int v[10], y qué se hace para conocer el tamaño?
Porque el parámetro int v[10] es en realidad un int *v: al pasar un arreglo a una función se convierte en un puntero a su primer elemento, y sizeof mide el puntero (8 bytes en 64 bits). El tamaño real se pasa aparte, como un parámetro size_t n; esa pareja de puntero y tamaño es el patrón de toda función que recorre datos.
2. ¿Qué diferencia hay entre char *s = "hola"; y char s[] = "hola";?
El primero es un puntero a un literal que está en memoria de solo lectura: modificarlo (s[0] = 'H') cae con Segmentation fault, y -Wwrite-strings pide declararlo const char *. El segundo es un arreglo en la pila que contiene una copia de los cinco bytes, y se puede modificar.
3. ¿Por qué p = realloc(p, n); es peligroso y cuál es la forma correcta?
Si realloc falla, devuelve NULL pero el bloque original sigue vivo: al asignar el resultado a p se pierde la única referencia a ese bloque (una fuga) y se continúa con un puntero nulo. Se usa un puntero temporal: nuevo = realloc(p, n);, se comprueba, y solo si no es NULL se asigna p = nuevo.
4. gcc prog.c -lm funciona, pero gcc -lm prog.c da undefined reference. ¿Por qué?
Porque el enlazador recorre las bibliotecas una sola vez, en el orden en que aparecen, y solo extrae lo que en ese momento está pendiente. Con -lm antes de prog.c no hay nada pendiente al llegar a la biblioteca, y después ya no vuelve a mirarla. La regla: los objetos primero y cada biblioteca después de todo lo que la usa.
5. El programa compiló y enlazó, pero al ejecutarlo dice cannot open shared object file. ¿Qué pasó y cómo se resuelve?
El enlazador encontró la biblioteca gracias a -L, pero eso no se guarda en el ejecutable; el cargador la busca por su cuenta (RUNPATH, LD_LIBRARY_PATH, la caché de ldconfig, los directorios del sistema) y no la halla. Se resuelve con LD_LIBRARY_PATH para probar, con -Wl,-rpath,'$ORIGIN/…' para que el programa lleve su propia ruta, o instalándola y ejecutando ldconfig.
6. Cambiaste un valor en un .h y make dice up to date. ¿Por qué, y cuál es la solución completa?
Porque make no lee los #include: solo sabe lo que el Makefile declara. La solución es que el compilador genere la lista de encabezados de cada objeto (-MMD -MP, que escribe un .d) y que el Makefile los cargue con -include. -MP evita el error No rule to make target cuando se borra un encabezado.
7. ¿Cuándo tiene significado errno y qué hay que hacer con él?
Solo justo después de una llamada cuyo valor de retorno indicó un fallo (una llamada exitosa puede cambiarlo y ninguna lo pone a cero). Hay que copiarlo a una variable local de inmediato, porque cualquier otra llamada, incluso de limpieza, puede pisarlo. Para funciones ambiguas como strtol se pone errno = 0 antes de llamar. Y las funciones pthread_* y posix_memalign no lo usan: devuelven el código.
8. read devolvió menos bytes de los pedidos. ¿Es un error?
No. read puede devolver menos de lo pedido sin que haya error ni fin de archivo, sobre todo con tuberías, sockets o la terminal. Un valor positivo es la cantidad leída (se usa esa y no la pedida), 0 es el fin de archivo y -1 es un error (con EINTR se reintenta). Se lee en un bucle, y lo mismo vale para write.
9. Un programa muestra sus mensajes en orden en la terminal, pero desordenados con | cat. ¿Por qué?
Porque stdout es de líneas en una terminal (cada línea sale al instante) y de búfer completo en una tubería (el texto espera al final), mientras que stderr y las llamadas a write no tienen ese búfer. Se corrige con setvbuf(stdout, NULL, _IOLBF, 0), con fflush, o usando una sola capa.
10. Un programa termina con Segmentation fault. ¿Cómo lo investigas?
Se ejecuta bajo gdb (compilado con -g -O0): se detiene en la señal SIGSEGV, en la línea del fallo. Con bt se ve la pila de llamadas, con frame N se cambia de marco y con print se examinan los punteros; casi siempre uno es nulo o inválido, y la causa suele estar en un marco anterior. Si la caída ocurrió en otro lugar, se habilita ulimit -c unlimited y se abre el volcado de memoria con gdb ./prog core.
11. ¿Cuándo se usa assert y cuándo un manejo de error normal?
assert para condiciones que, por construcción, nunca deberían ocurrir (errores del programador, invariantes internas). Manejo de error normal para lo que puede ocurrir: entradas del usuario, archivos, memoria, red. Un assert desaparece con -DNDEBUG, junto con todo lo que contenga, por lo que nunca debe tener efectos laterales ni validar datos externos.
12. ¿Qué detectan AddressSanitizer, UBSan y ThreadSanitizer, y por qué no basta con uno?
AddressSanitizer: errores de memoria (desbordes, uso tras liberar, doble liberación, fugas). UBSan: comportamiento indefinido (desbordamiento de enteros con signo, desplazamientos, índices, divisiones por cero, punteros nulos). ThreadSanitizer: carreras de datos entre hilos. Cada uno inserta comprobaciones distintas, ASan y UBSan se combinan, TSan va aparte, y ninguno ve lo que la ejecución no recorre; por eso se suman al análisis estático, los avisos y las pruebas de borde.
13. ¿Qué significan los códigos de salida 127, 134 y 139 de un programa ejecutado desde la shell?
127: la shell no encontró la orden. 134: murió por la señal 6 (128 + 6), SIGABRT, de abort, una aserción o un canario de pila. 139: murió por la señal 11 (128 + 11), SIGSEGV, un acceso inválido a memoria. Los valores por encima de 128 indican, en general, una señal y no un exit del programa.
14. Una estructura con campos de 4 y de 2 bytes pesa 8 bytes y no 6. ¿Por qué?
Por la alineación: el compilador coloca cada campo en una dirección múltiplo de su tamaño y añade relleno al final para que el tamaño total sea múltiplo de la mayor alineación, de modo que los elementos de un arreglo de esas estructuras sigan alineados. Se ve con sizeof y offsetof, y se vuelve visible al volcar la estructura a un archivo con fwrite: los dos bytes extra aparecen como ceros.
15. Un programa copia un archivo a un disco lleno y termina con código 0. ¿Qué se olvidó?
Comprobar el resultado de fclose (o de fflush) en el archivo de salida. stdio acumula lo escrito en un búfer y no lo entrega al sistema hasta cerrar o vaciar: fwrite «tiene éxito» al copiar al búfer y el error ENOSPC aparece recién en fclose. Si no se comprueba, el programa dice «éxito» sin haber copiado nada.
Para terminar
- Vuelve al tema 1 y reescribe
hola.ccomo una herramienta: una opción-n NOMBREcongetopt, su ayuda, un mensaje de error porstderrcon código desysexits.hsi falta un argumento, unMakefilecon los tres modos y una prueba de integración. ¿Cuántos temas del curso usaste? - Elige las tres ideas de la sección 3 que te resulten menos claras, y repasa los temas indicados.
- Recorre la tabla de errores de la sección 5 y provoca cada uno a propósito en un programa de prueba, comprobando el mensaje y el código de salida.
- Pasa por los sanitizadores y por
-fanalyzertodos los programas que escribiste durante el curso, y corrige lo que señalen. Anota qué errores encontraste. - Toma una herramienta pequeña de Unix que uses todos los días (
head,tail,hexdump…) y construye tu propia versión siguiendo el modelo del tema 41: especificación, biblioteca, programa, pruebas contra la referencia, Makefile y los tres modos. - Lee la primera página del curso siguiente, Procesos e Hilos, y anota qué conceptos de este curso necesitarás en cada bloque.
Aquí termina Programación en C para Sistemas Operativos. Empezó con un saludo impreso por un programa de una sola función, y termina con una herramienta que cuenta, se prueba contra una referencia, se compila en tres modos y avisa de sus errores con un código que otro programa puede entender. Lo que cambió no es la cantidad de código sino lo que sabes de él: de dónde sale cada byte de memoria, qué ocurre en cada etapa de la compilación, cómo falla una llamada al sistema y cómo averiguar por qué. Con eso, los programas dejan de ser un misterio y empieza lo que sigue: hacerlos nacer, convivir y trabajar juntos.