Etiquetas

,

Uno de las asignaturas que enseño en la UIB es «Sistemas Operativos 2», del segundo semestre del segundo curso de la ingeniería en informática. En esta asignatura enseñamos fundamentalmente gestión de memoria, de E/S y sistemas de ficheros.

La práctica consiste en el desarrollo en C desde cero de un sistema de ficheros basado en i-nodos. Quizás es la práctica más difícil y extensa (aunque son poco más de 2.000 líneas de código en C) que se hayan encontrado, pero tampoco es tan complicada. De hecho uno de los objetivos de la práctica es desmitificar la complejidad (de los conceptos fundamentales, el demonio está en los detalles) de los sistemas de ficheros. Pero nos encontramos con muchos problemas:

  • No saben relacionar lo aprendido en estructuras de datos. Por ejemplo, a esta alturas ya han visto árboles y el recorrido iterativo y recursivo en ellos, sin embargo les cuesta mucho hacerlo para la traducción de bloques lógicos a físicos (que es básicamente recorrer árboles de uno, dos o tres niveles).
  • Falta de práctica de programación. No están acostumbrados a desarrollar programas que sean mayores a un par de cientos de líneas, cuesta mucho que muestren código que «huela» bien, ni siquiera son capaces de sangrarlo correctamente, son incapaces de hacer el diseño del programa.
  • Desconocimiento de C, del compilador, y de su relación con la arquitectura de los ordenadores.

Para que estos problemas no bloqueen todo el desarrollo, el diseño se lo pasamos hecho al nivel de funciones con un programa semanal de qué funciones deben implementar y cómo probarlas. Además les pasamos el código de las funciones de recorrido de las estructuras más complejas.

Aún minimizando los problemas anteriores, el otro gran problema que tienen -y creo que el peor- es la incapacidad para analizar y corregir los errores del código. Aún en las partes más sencillas y obvias, es típico el diálogo:

– Profesor, el programa me falla.

– ¿Dónde está el fallo?

– No lo sé, da un segmentation fault.

– Pero ¿en qué función?.

– No lo sé, lo hace apenas lo ejecutamos.

– Pero ¿no has puesto ni print-efes para saber por donde se cuelga?

– ¿Dónde los pongo?

Es muy fustrante, les pego unos rapapolvos importantes. El ejemplo que doy a continuación es un caso real, del lunes pasado.

Como parte de las pruebas del sistema de fichero, tienen que desarrollar unos programas concurrentes que escriban una estructura de datos en posiciones aleatorias de un fichero. Luego otro programa tiene que recorrer secuencialmente ese fichero y detectar las escrituras, la forma fundamental es verificar que uno de los campos de la estructura sea igual al PID del proceso que escribió en ese fichero.

Un par de alumnos tenían problemas, después un un diálogo muy similar al anterior les indiqué que antes que les mire el código debían indicarme exactamente el sitio donde les daba problemas. Me mostraron el siguiente código:

struct record rec, rec2;
char pathname[256];
...
my_read(pathname, &rec, position, sizeof(rec));
while (rec.pid != pid) {
    position += sizeof(rec);
    my_read(pathname, &rec, position, sizeof(rec));
}
printf("Final del bucle\n");

Me dijeron:

– El problema es que nunca sale del bucle.

Es decir, según ellos, que nunca se cumplía la condición de que rec.pid sea igual a pid. Les indiqué el primer problema, el código no verificaba que se haya llegado al fin del fichero (es decir, que el resultado de my_read() sea mayor que cero). Si no salía de ese bucle, el my_read() debería estar generando errores y muy raro que no salga ningún error. En un momento de la conversación, y después de varios minutos verificando que el fichero estaba bien (el my_read() funciona correctamente), les digo:

– Esto no puede ser, debe haber otro error ¿habéis verificado que el nombre del fichero en pathname sea correcto?

– Ahora que lo dices, en una de las pruebas cuando habilitamos la impresión de errores nos dio «la ruta no existe».

– ¡¡¡¡#$@!!!

Les pedí que impriman el valor de la variable pathname, y en vez de un /dir1/dir2/datos.dat salía /dir1dir2/datos.dat, todo por olvidare de una «/» en el sprintf() para el pathname.

Es decir, el problema no estaba ni el bucle, sino unas cuantas líneas más arriba de él, pero no fueron capaces de darse cuenta de ello. A los pocos minutos los mismos alumnos me dicen:

– Ahora nos da segmentation fault después del bucle, en estas instrucciones, y no sabemos por qué.

Voy a mirar esas líneas:

rec2.pid = rec.pid;
rec2.x = rec.x;
rec2.y = rec.y;
rec2.z = rec.z
...
/* más código complejo */

Era obvio que esas instrucciones, asignación de estructuras idénticas (y sin punteros involucrados) no podían generar un segmentation fault (además de la optimización es obvia y con menos código: memcpy(&rec2, &rec, sizeof(rec)).

– El error no está allí, ¿por qué decís que son esas líneas?

– Porque lo último que imprime es la salida del bucle.

– Pero ¿estáis seguro que es allí y no en el código de más abajo? ¿habéis puesto un printf para verificar?

– No.

:facepalm: (y rapapolvos varios)

Estos casos son los que nos encontramos cada día en el laboratorio y tutorías, ayer mismo me pasó otro. Un grupo me dice:

– Profesor, el programa nos falla, ¿lo puede mirar?

– Pero ¿cuál es el fallo?

– No sabemos, nos da stack smashing detected

– Eso es porque trabajáis con algún puntero local y os estáis saliendo de los límites, por eso el error en la pila. ¿En qué función dónde da eso?

– Hummmm… creemos que en esta, pero aquí no hay nada raro.

Miro la función y detecto el error inmediatamente, les indico que es el «sprintf».

char pathname[11];
sprintf(pathname, "%s/%s/pruebas.dat", dir1, dir2);

– Pero ¿por qué el error? si está bien.

– Porque la longitud del string generado es superior a 11.

– Pero si «pruebas.dat» es menor que 11.

:facepalm: (y enésima explicación de strings, array de caracteres, y rapapolvos varios).

Este segundo ejemplo muestra el desinterés por aprender bien las herramientas que usan (el lenguaje C es muy sencillo), pero también hay una incapacidad de asumir que se cometen errores y de interpretar y analizar el código. Aunque es una técnica muy habitual en programación, son incapaces de reducir el espacio del problema para analizar el código (tan fácil como poner print-efes donde toca). A pesar de las horas que llevan de programación en más de un año y medio de carrera, no tienen siquiera la iniciativa de poner mensajes para así poder llegar exactamente a la instrucción que les está provocando el error. En pocas palabras, carecen de las habilidades básicas para hacer debugs de programas, que a su vez es una habilidad básica de los programadores.

No sé qué estamos haciendo mal en la universidad para que no tengan un dominio básico de debugging. O si el problema lo arrastran de instituto, por ejemplo, no tener iniciativa para resolver por sí solos los problemas, esperan -y casi exigen- que el profesor tenga capacidad adivinatorias superiores que les evite pensar a ellos. O que no hayan tomado conciencia que cualquier profesional necesita conocer las herramientas con las que trabaja.

Después de dos años de una carrera de ingeniería informática no podemos permitir que tengan esas carencias fundamentales, en temas tan básicos. Tenemos problemas de calado en la forma en que los formamos. O quizás deberíamos empezar a tomar no tan en serio lo del «seguimiento personal a los alumnos» del Plan Bolonia y dejar que se apañen solos, que parece que lo seguimos tratando como a niños desvalidos intelectualmente.

En cualquier caso, ya me desahogué, es frustrante. Y perdón a mis alumnos por los rapapolvos, pero es que es muy frustrante (y mejor que se los dé yo, antes que su primer jefecillo 😉 ).

Relacionado (gracias): Why I never give straight answers.