Programación de un manejador para un altavoz

Objetivo

El objetivo del trabajo es que el alumno llegue a conocer cómo se desarrolla un manejador para un dispositivo de caracteres en Linux. Concretamente, por las razones que se explican más adelante, se ha optado por un manejador que gestione un altavoz. Este proyecto va a permitir aprender, al menos de forma básica, diversos aspectos vinculados con este tipo de desarrollo:

Este documento, además de explicar las características del manejador que se pretende desarrollar, incluye información de los diversos aspectos internos de Linux que se requiere conocer para llevar a cabo este proyecto. En cualquier caso, se recomienda la consulta del libro referencia sobre esta temática: Téngase en cuenta que es un libro un poco antiguo (se ocupa de la versión 2.6.10 de Linux) y, por tanto, alguna de la información que contiene ha quedado obsoleta.

Se recomienda también acceder a sitios web que ofrecen la posibilidad de navegar por el código fuente de las distintas versiones de Linux ofreciendo un servicio de referencias cruzadas (Linux Cross Reference) como, por ejemplo, lxr.free-electrons.com o lxr.linux.no.

Plataforma de desarrollo

Se plantean dos posibles plataformas a la hora de afrontar el desarrollo de este manejador de un altavoz: Ambas plataformas incluyen hardware que implementa la funcionalidad de tipo PWM requerida para asegurar la correcta temporización de los sonidos sin necesidad de que el procesador tenga que intervenir en cada paso de la generación de los mismos. En caso de que se desarrolle el código para ambas plataformas, se puede integrar en el mismo manejador usando directivas condicionales del preprocesador para aquellas partes que sean específicas de cada plataforma.

Desarrollo en un computador personal estándar

En esta plataforma se ha optado por usar el altavoz interno del equipo como dispositivo para el que se diseñará el manejador, puesto que tiene el mismo comportamiento en todos los equipos donde está instalado, esta presente en la mayoría de los equipos, es relativamente sencillo y, por último, no tiene una importancia crítica en el sistema (en el sentido de que la manipulación errónea del mismo no pueda comprometer el buen funcionamiento del sistema). Desafortunadamente, aunque durante mucho tiempo el altavoz interno ha sido un componente que ha estado presente en todos los equipos (entre otras funciones, uno de sus usos más relevantes es generar sonidos que indiquen qué tipo de problemas pueden aparecer durante el arranque del equipo), en los últimos años algunos fabricantes han optado por prescindir del mismo.


NOTA: El alumno debe comprobar si su equipo Linux dispone de altavoz interno. Para ello, puede instalar el mandato beep, que interacciona directamente con el altavoz interno, y ejecutarlo, habiendo previamente cargado el manejador Linux de ese dispositivo (módulo pcpkr), que, por defecto, no está instalado.
sudo apt-get install beep
sudo modprobe pcspkr
sudo beep
Si la ejecución de ese mandato no produce ningún sonido, su equipo no dispone de un altavoz interno (al menos, no de uno estándar) y debe notificárselo al profesor encargado de la práctica para buscar una solución a esta situación.

Desarrollo en una Raspberry Pi

Aunque en el caso de esta plataforma concebida para el desarrollo de sistemas empotrados se presenta una gran variedad de posibilidades a la hora de plantear un proyecto práctico (la imaginación es el límite), por mantener la compatibilidad con la plataforma PC, se va a desarrollar también el manejador de un altavoz. El altavoz estará conectado a uno de los pines de tipo GPIO a los que se les puede asociar la funcionalidad PWM disponible en esta plataforma, concretamente, al pin 12 de la placa (identificado en la documentación como GPIO18).


En cualquier caso, para facilitar el desarrollo del proyecto, aunque no se disponga en principio de un equipo con altavoz interno, todas las operaciones relacionadas con interaccionar directamente con el altavoz se ha englobado únicamente en la primera fase del proyecto y el código correspondiente a las mismas se va a incluir en un solo fichero (spkr-io.c). De este fichero, se proporciona una versión inicial tal que la implementación de esas operaciones sobre el hardware del altavoz únicamente imprime un mensaje que especifica qué operación se ha llevado a cabo (por ejemplo, si se ha activado el altavoz o se ha fijado su frecuencia a un determinado valor). De esta forma, aunque en un momento dado se use un equipo que no tenga altavoz, se pueden afrontar las cinco fases restantes comprobando si los mensajes que van imprimiéndose son correctos.

Descripción general del trabajo práctico a realizar

Se plantea desarrollar un módulo que actúe de manejador de un dispositivo de caracteres que proporcione acceso a un altavoz conectado al equipo y que tendrá las siguientes características:

Para facilitar el desarrollo de este manejador se plantean una serie de fases de carácter incremental. Dentro de cada fase, se presentan tres secciones: los conceptos teóricos requeridos por la misma (el bagaje teórico requerido), la funcionalidad a implementar y las pruebas a realizar sobre el software desarrollado (no se trata de pruebas exhaustivas, sino un mínimo conjunto de comprobaciones de la funcionalidad planteada en este enunciado). Se distinguen las siguientes fases:

  1. Gestión del hardware del dispositivo.
  2. Alta, y baja, del dispositivo.
  3. Operaciones de apertura y cierre.
  4. Operación de escritura.
  5. Operación fsync y adaptación a la versión 3.0 de Linux.
  6. Operaciones ioctl.

Primera fase: Gestión del hardware del dispositivo

Antes de presentar esta fase, hay que resaltar que, aunque ha sido identificada como la primera en esta presentación, su implementación puede realizarse en el momento en que se considere oportuno (incluso al final del proyecto), puesto que se puede desarrollar el resto de la práctica sin haberla completado (en vez de sonar el altavoz, aparecerán los mensajes correspondientes). Las fases restantes de la práctica sí deberían realizarse en el orden establecido.

Bagaje requerido

Acceso PIO a los registros del dispositivo

Linux proporciona un API interno para acceder a los puertos de entrada/salida de un dispositivo. Hay diversas funciones dependiendo del tamaño de la información transferida (8, 16 o 32) y si se trata de una lectura (in) o una escritura (out): inb, inw, inl, outb, outw y outl, respectivamente.

Además, existen versiones de esas mismas funciones con un sufijo _p (por ejemplo, inb_p), que realizan una muy breve pausa después de la operación de entrada/salida, que puede ser necesario en algunas arquitecturas.

Acceso MMIO a los registros del dispositivo

Para poder acceder a los registros del dispositivo cuando estos están conectados mediante MMIO a partir de una cierta dirección de memoria física, se requiere asociar un rango de direcciones lógicas a las direcciones físicas correspondientes a esos registros. La función ioremap realiza esta función recibiendo como parámetros el rango de direcciones físicas que se pretende acceder y devolviendo la dirección lógica a partir de la cual se puede acceder a ese rango:
void *ioremap(unsigned long phys_addr, unsigned long size);
Aunque la dirección devuelta puede usarse como un puntero convencional en la mayoría de las plataformas, podría haber algunas en las que no es posible. Por ello, la documentación de Linux propone como una buena práctica usar las siguientes funciones para leer y escribir con ese puntero datos de diversos tamaños: Una posible ventaja adicional de usar estas funciones en lugar de el acceso directo mediante el puntero es que facilita la identificación de qué partes del código manipulan dispositivos de E/S. Asimismo, su uso en una plataforma basada en un procesador ARM, como el caso de una Raspberry Pi, incluye de forma transparente el uso de barreras requerido en este tipo de sistemas.

La sincronización en Linux

Un aspecto fundamental y muy complejo en el desarrollo de un manejador es el control de todos los problemas de concurrencia que pueden presentarse durante la ejecución del mismo, y más si este está destinado a ejecutar en un sistema multiprocesador con un sistema operativo expulsivo, como ocurre con todo el código de Linux. De hecho, el grado de complejidad es tal que es prácticamente imposible verificar de una manera formal la total corrección de un determinado módulo y que el mismo está libre de condiciones de carrera.

Aunque este aspecto se ha estudiado en la parte teórica de la asignatura, en esta sección repasaremos brevemente algunos de los conceptos asociados a este problema, que afectarán al diseño del manejador desde esta primera fase. Puede consultar el capítulo 5 del libro LDD para profundizar en los aspectos presentados en este apartado.

Simplificando un poco el tema, se pueden distinguir dos tipos de escenarios potencialmente conflictivos en lo que se refiere a la concurrencia y dos clases de soluciones. En cuanto a los escenarios conflictivos, se podrían clasificar como:

Con respecto a las posibles soluciones, se podría distinguir entre: Linux ofrece una gran variedad de mecanismos de sincronización de ambos tipos. Dadas las necesidades del proyecto, en esta sección basta con comentar brevemente uno de cada tipo.

Como mecanismo bloqueante (es decir, adecuado para resolver problemas de sincronización entre llamadas), se puede usar el mutex, cuyo modo de operación es el habitual de esta clase de mecanismo.

En cuanto al mecanismo de sincronización basado en espera activa (es decir, adecuado para resolver problemas de sincronización entre una llamada y una interrupción), se van a presentar los spinlocks convencionales (Linux ofrece una variedad de mecanismos de este tipo: spinlocks de lectura/escritura, seqlocks, RCU locks,...). Como último punto con respecto a la sincronización es conveniente resaltar dos aspectos adicionales.

La mejor forma de resolver este problema es evitarlo radicalmente escribiendo código que no requiera secciones críticas. Así, por ejemplo, el tipo de datos kfifo de Linux, que implementa una cola FIFO y que será recomendado para implementar el manejador planteado en este proyecto, está diseñado de manera que no es necesario ningún tipo de sincronización siempre que haya solo un productor y un consumidor.

El spinlock está implementado internamente realizando consultas y actualizaciones atómicas de variables. Cuando una sección crítica solo implica la actualización de una variable escalar, en vez de usar alguno de los mecanismos presentados en esta sección, puede realizarse un acceso atómico a esa variable. La variable debe declararse como atomic_t y usarse las operaciones atómicas que proporciona Linux (atomic_set, atomic_xchg,...).

Aspectos hardware específicos de la plataforma PC

Historia del altavoz interno
El altavoz interno ya estaba presente en el primer PC de IBM (modelo 5150) como único elemento capaz de producir sonidos en el equipo. El modo de operación no ha cambiado desde entonces: la salida del temporizador 2 del sistema está conectada al altavoz lo que permite enviar formas de onda al mismo que generan sonidos con una frecuencia que depende del contador que se haya cargado en ese temporizador. Nótese que con esta solución el software no está involucrado directamente en la generación del sonido (lo cual era un requisito dada la limitada potencia de los procesadores de entonces), restringiéndose su labor a cargar el temporizador con un valor que corresponda a la frecuencia deseada y, a continuación, activar el altavoz, desactivándolo cuando ya no se desee que continúe el sonido.

Este hardware de sonido tenía una calidad y una versatilidad muy limitadas. Aún así, con ingenio y aprovechando ciertas propiedades físicas de estos primeros altavoces, que eran de tipo dinámico (magnet-driven), se idearon técnicas de tipo PWM, que conseguían generar sonidos más complejos, aunque de calidad limitada, sobre este hardware, requiriendo, eso sí, la dedicación exclusiva del procesador para llevarlas a cabo. Estas técnicas dejaron de ser efectivas con la progresiva sustitución de los altavoces dinámicos por los piezoelécticos, que no tenían esas propiedades físicas.

La aparición de las tarjetas de sonido, con una calidad de audio muy superior, eclipsó a los altavoces internos que quedaron relegados básicamente a una importante misión: ser el medio de diagnosticar errores en el arranque del sistema cuando ni siquiera puede estar accesible la pantalla y menos todavía la tarjeta de sonido.

Modo de operación del altavoz interno
En esta sección se revisará brevemente el hardware de este dispositivo. Sin embargo, de forma intencionada, no se entrará en detalle sobre el mismo porque se considera, desde un punto de vista didáctico, que esa labor de búsqueda de información sobre un componente hardware y el estudio detallado de su modo de operación es parte de los conocimientos prácticos que se pretende que alcance el alumno como parte de este proyecto. En cualquier caso se sugieren dos referencias de OSDev que presentan en detalle el hardware involucrado, así como aspectos sobre su programación (PIT y PC Speaker).

Desde su diseño original, el PC dispone de un circuito denominado Programmable Internal Timers (PIT; originalmente, chip 8253/8254, aunque actualmente su funcionalidad está englobada en chips que integran muchas otras funciones adicionales) que ofrece tres temporizadores (denominados también canales): el primero (timer 0), conectado a la línea de interrupción IRQ0 y usado normalmente como temporizador del sistema operativo, el segundo (timer 1), asociado al refresco de la memoria dinámica, y el tercero (timer 2), conectado al altavoz interno. Estos temporizadores pueden actúar como divisores de frecuencia de una entrada de reloj que oscila, por razones históricas, a 1,193182 MHZ (constante PIT_TICK_RATE en el código de Linux). Los dos primeros temporizadores tienen directamente conectada esa señal de entrada, pero para el tercero, que es el que nos interesa, es necesario conectarla explícitamente, como explicaremos en breve.

Para configurar uno de estos temporizadores, en primer lugar, es necesario seleccionar su modo de operación (de un solo disparo, de onda cuadrada,...) escribiendo en su registro de control (puerto 0x43) el valor correspondiente (en nuestro caso, hay que seleccionar el timer 2 y especificar un modo de operación 3, denominado square wave generator, que actúa como un divisor de frecuencia). A continuación, hay que escribir en su registro de datos (puerto 0x42) el valor del contador del temporizador (el divisor de frecuencia). Al tratarse de un puerto de 8 bits y dado que el contador puede tener hasta 16 bits, se pueden necesitar dos escrituras (primero la parte baja del contador y luego la alta) para configurar el contador.

La operación de configuración que acaba de describirse determinará la forma de onda que alimenta el altavoz (el código correspondiente deberá incluirse en la función spkr_set_frequency del fichero spkr-io.c). Sin embargo, no causa que el altavoz comience a sonar. Como se comentó previamente, para activar el altavoz (el código correspondiente deberá incluirse en la función spkr_on del fichero spkr-io.c), es necesario, en primer lugar, conectar el reloj del sistema a la entrada del temporizador 2, lo que requiere escribir un 1 en el bit 1 del puerto 0x61. Sin embargo, eso no es suficiente: la salida del temporizador 2 se mezcla mediante una función AND con el bit 0 del puerto 0x61, por lo que habrá que escribir un 1 en dicho bit para que la forma de onda llegue a la entrada del altavoz. Por tanto, recapitulando, hay que escribir un 1 en los dos bits de menor peso del puerto 0x61. Téngase en cuenta que este puerto, denominado puerto B de control del sistema, está involucrado en la configuración de otros componentes del sistema, por lo que, a la hora de escribirse los dos primeros bits a 1, hay que asegurarse de que los bits restantes permanecen inalterados.

Para desactivar el altavoz, es suficiente con escribir un 0 en cualquiera de esos bits (el código correspondiente deberá incluirse en la función spkr_off del fichero spkr-io.c).

Soporte del altavoz interno en Linux
Linux incluye dos manejadores para el altavoz interno: los módulos pcspkr y snd_pcsp. Ambos permiten enviar al altavoz formas de onda de la frecuencia especificada. El segundo, además, permite gestionar sonidos más complejos usando las técnicas de PWM comentadas previamente y solo tiene sentido usarlo en un sistema sin tarjeta de sonido. Evidentemente, cuando se quiera probar el código escrito, no debe estar cargado ninguno de estos dos módulos, sino el desarrollado como parte del proyecto.

En cuanto al API ofrecido a las aplicaciones, ofrece dos tipos de servicios:

  1. Basados en ioctls dirigidos a la consola (fichero /dev/console). Concretamente, proporciona dos operaciones (man console_ioctl): KDMKTONE, que genera un sonido del periodo (inverso de la frecuencia) y duración en milisegundos especificados por la parte baja y alta del parámetro de la operación (obsérvese las similitudes con el esquema propuesto en este proyecto), y KIOCSOUND, que produce un sonido del periodo especificado que se mantendrá hasta que se genere otro sonido o se realice esta misma operación especificando un valor 0 como parámetro.
  2. Basados en caracteres de escape dirigidos a la consola (fichero /dev/console). Cuando un proceso escribe el carácter ASCII 7 (Control-G, BELL o \a) en la consola, se produce un sonido cuya frecuencia y duración es configurable (man console_codes): la secuencia ESC[10;m] fija en m la frecuencia del sonido, mientras que la secuencia ESC[11;n] establece como n la duración en milisegundos.
Evidentemente, la breve explicación de estas APIs solo tiene carácter didáctico, puesto que no se van a usar en este proyecto, aunque también sirve como comparativa con el API que se propone en este trabajo. Así, por ejemplo, se puede observar que estas APIs, a diferencia de la propuesta en este proyecto, no permiten que la aplicación especifique una secuencia de sonidos con una sola operación.

Aspectos hardware específicos de la plataforma Raspberry Pi

Toda la documentación técnica sobre los dispositivos en esta plataforma se encuentra en BCM2835 ARM Peripherals. Sin embargo, en esta sección se va a intentar proporcionar la información requerida para implementar la funcionalidad planteada en la práctica.

Los dispositivos periféricos de la Raspberry están accesibles por MMIO a partir de una cierta dirección física que depende de los modelos:

Esta plataforma ofrece una colección de pines de tipo GPIO accesibles mediante MMIO algunos de los cuales tienen funcionalidades añadidas, tal como la posibilidad de asociar una señal PWM hardware a la salida de ese pin.

Hay disponibles dos canales PWM que pueden asociarse a distintos pines. Para el proyecto, vamos a usar el canal 1 de PWM (PWM0) asociado al pin 12 (identificado en la documentación como GPIO18), donde se habrá conectado un altavoz. Ambos canales están alimentados por un reloj dedicado en exclusiva a esa labor que se debe configurar para activar la funcionalidad PWM.

Como se detalla más adelante, para abordar la funcionalidad requerida por el proyecto, hay que interaccionar con tres tipos de dispositivos en la Raspberry:

A continuación, se describen los pasos requeridos para activar una señal PWM de las características deseadas en esta plataforma. Nótese que, de forma intencionada, no se especifican todos los detalles de la operación porque se considera, desde un punto de vista didáctico, que esa labor de búsqueda de información sobre un componente hardware y el estudio detallado de su modo de operación es parte de los conocimientos prácticos que se pretende que alcance el alumno como parte de este proyecto.
  1. Activación del reloj PWM. La sección 6.3 de BCM2835 ARM Peripherals explica la configuración de los relojes de propósito general asociados a los pines GPIO. Para el reloj PWM, el modo de operación es el mismo pero aplicado a sus registros de configuración (CM_PWMCLKCTL y CM_PWMCLKDIV) anteriormente presentados. Dada la funcionalidad requerida por el proyecto, bastaría con (los valores por defecto de los otros campos son válidos): Nótese que, dado que la configuración del reloj se va a realizar en el momento de la carga del módulo, no tenemos que preocuparnos de los avisos que aparecen en el manual que aconsejan manipular el reloj solo cuando está parado para evitar generar picos extraños en la señal generada, puesto que en ese momento inicial no está conectado al altavoz.
  2. Configuración de GPIO18 como PWM. La sección 6 de BCM2835 ARM Peripherals explica la configuración de los pines GPIO. Para el proyecto se necesita configurar el pin GPIO18 de manera que quede asociado al canal 1 de PWM (PWM0), que corresponde a la función alternativa 5 (ALT5) vinculada con ese pin, como se aprecia en la tabla que aparece en la sección 6.2 de ese manual. La configuración de GPIO18 como ALT5, como explica dicho manual, requiere modificar el registro GPFSEL1 (cuya dirección física tiene un desplazamiento de 4 bytes con respecto a la zona asociada a la GPIO), que permite configurar la funcionalidad de los pines de GPIO10 a GPIO19, escribiendo un 010 en los bits 24, 25 y 26 de dicho registro. Nótese que para no afectar a la configuración de los otros pines es necesario leer previamente el registro y escribir el nuevo valor del registro modificando solo esos tres bits.
  3. Configuración del canal 1 de PWM. La sección 9 de BCM2835 ARM Peripherals explica la configuración de los canales PWM. Para el proyecto, se va a usar el modo de operación M/S (mark/space), que depende de dos valores: La configuración del canal 1 en esta modalidad M/S requerirá:
Soporte del PWM en Linux
En la versión del núcleo de Linux específica para la Raspberry Pi se incluye el módulo pwm_bcm2835 que actúa como un manejador PWM para el canal 1 asociado al pin GPIO18. Evidentemente, cuando se quiera probar el código escrito, no debe estar cargado este módulo, sino el desarrollado como parte del proyecto.

En cuanto al API ofrecido a las aplicaciones, que no se va a usar en este proyecto, se ofrece a través del directorio /sys/class/pwm/. Previamente, hay que configurar el sistema mediante Device Tree Overlays de manera que el pin GPIO18 quede asociado a PWM0 y el reloj PWM se active. Arrancado el sistema con esta configuración, escribiendo en ficheros del directorio /sys/class/pwm/ se accede a esta funcionalidad.

Funcionalidad a desarrollar

En esta primera fase se desarrollará todo el código de acceso al hardware requerido por el proyecto. En ella se implementará la funcionalidad específica de la plataforma para fijar la frecuencia del altavoz, así como su activación y desactivación (spkr_set_frequency, spkr_on y spkr_off, respectivamente, incluidas en el fichero spkr-io.c). Asimismo, si es necesario incluir algún código específico de la plataforma para la configuración inicial del dispositivo o cuando finaliza el uso del mismo, se incorporará dentro de las funciones spkr_init y spkr_exit, respectivamente.

Funcionalidad a desarrollar en la plataforma PC

Se trata de manipular mediante PIO (funciones inb, outb, etc.) los puertos 0x42 y 0x43 para fijar la frecuencia deseada en la función spkr_set_frequency y el 0x61 para activar y desactivar la llegada de la señal periódica al altavoz en las funciones spkr_on y spkr_off, respectivamente.

Con respecto al cálculo de la frecuencia requerida realizado dentro de la función spkr_set_frequency, hay que determinar cuál es el divisor de frecuencia correspondiente teniendo en cuenta la frecuencia del reloj de entrada al temporizador (constante PIT_TICK_RATE en el código de Linux) y la frecuencia deseada.

En cuanto a las operaciones de activación y desactivación, recuerde que debe leerse previamente el registro 0x61 para mantener el resto de los valores que contiene.

En este punto, ya es necesario afrontar el problema de la sincronización. Cuando se programa el chip 8253/8254 es necesario realizar dos escrituras al registro de datos para enviar primero la parte baja del contador y, a continuación, la parte alta. Por tanto, es necesario crear una sección crítica en el acceso a ese hardware. Hay diversas partes del código de Linux que acceden a ese hardware y usan un spinlock (denominado i8253_lock de tipo raw spinlock, que es más eficiente pero menos robusto que un spinlock convencional, y que se incluye en el fichero linux/i8253.h) para controlar el acceso. El nuevo código desarrollado deberá, por tanto, usar también este mecanismo. Dado que este hardware se accede tanto desde el contexto de una llamada como desde el de una interrupción, hay que usar las funciones que protegen también contra las interrupciones: raw_spin_lock_irqsave y raw_spin_unlock_irqrestore.

Funcionalidad a desarrollar en la plataforma Raspberry Pi

Siguiendo las pautas de configuración para esta plataforma, explicadas previamente, habría que programar la siguiente funcionalidad:

Pruebas

Para probar el software desarrollado en esta fase, dado que todavía no se ha creado el dispositivo como tal, se propone incluir en la función de inicio del módulo una llamada para fijar la frecuencia del altavoz, según el valor recibido en un parámetro entero denominado frecuencia, y, a continuación, una segunda llamada para activarlo. Asimismo, en la llamada de fin del módulo, se realizaría la invocación de la función que desactiva el altavoz. De esta manera, al cargar el módulo, se debería escuchar un sonido de la frecuencia correspondiente al parámetro recibido y al descargarlo debería detenerse la reproducción del mismo.

Segunda fase: Alta y baja del dispositivo

Bagaje requerido

Reserva y liberación de números major y minor

Puede consultar la sección 2 del capítulo 3 del libro LDD para profundizar en los aspectos presentados en este apartado.

Un dispositivo en Linux queda identificado por una pareja de números: el major, que identifica al manejador, y el minor, que identifica al dispositivo concreto entre los que gestiona ese manejador.

El tipo dev_t mantiene un identificador de dispositivo dentro del núcleo. Internamente, como se comentó previamente, está compuesto por los valores major y minor asociados, pudiéndose extraer los mismos del tipo identificador:

	dev_t midispo; 
	.....
	int mj, mn;
	mj=MAJOR(midispo);
	mn=MINOR(midispo);
O viceversa:
	int mj, mn;
	.....
	dev_t midispo; 
	midispo = MKDEV(mj, mn);
Antes de dar de alta un dispositivo de caracteres, hay que reservar sus números major y minor asociados. En caso de que se pretenda que el número major lo elija el propio sistema, como ocurre en este caso, se puede usar la función alloc_chrdev_region (definida en #include <linux/fs.h>) , que devuelve un número negativo en caso de error y que tiene los siguientes parámetros:
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor,
                        unsigned int count, char *name);
  1. Parámetro solo de salida donde nos devuelve el tipo dev_t del primer identificador de dispositivo reservado.
  2. Parámetro de entrada que representa el minor del identificador de dispositivo que queremos reservar (el primero de ellos si se pretenden reservar varios).
  3. Parámetro de entrada que indica cuántos números minor se quieren reservar.
  4. Parámetro de entrada de tipo cadena de caracteres con el nombre del dispositivo.
En caso de que se quiera usar un determinado número major se utiliza en su lugar la función register_chrdev_region.

La operación complementaria a la reserva es la liberación de los números major y minor asociados. Tanto si la reserva se ha hecho con alloc_chrdev_region como si ha sido con register_chrdev_region, la liberación se realiza con la función unregister_chrdev_region, que recibe el primer identificador de dispositivo a liberar, de tipo dev_t, así como cuántos se pretenden liberar.

void unregister_chrdev_region(dev_t first, unsigned int count);

Alta y baja de un dispositivo dentro del núcleo

Puede consultar la sección 4 del capítulo 3 del libro LDD para profundizar en los aspectos presentados en este apartado.

Por ahora, solo hemos reservado un número de identificador de dispositivo formado por la pareja major y minor. A continuación, es necesario "crear un dispositivo" asociado a esos números, es decir, dar de alta dentro del núcleo la estructura de datos interna que representa un dispositivo de caracteres y, dentro de esta estructura, especificar la parte más importante: el conjunto de funciones de acceso (apertura, cierre, lectura, escritura, ...) que proporciona el dispositivo.

El tipo que representa un dispositivo de caracteres dentro de Linux es struct cdev (no confundir con el tipo dev_t, comentado previamente, que guarda un identificador de dispositivo; nótese, sin embargo, que, como es lógico, dentro del tipo struct cdev hay un campo denominado dev de tipo dev_t que almacena el identificador de ese dispositivo), que está definido en #include <linux/cdev.h>.

Para iniciar esa estructura de datos (simplemente, dar valor inicial a sus campos), se puede usar la función cdev_init, que recibe como primer parámetro la dirección de la variable que contiene la estructura de dispositivo que se pretende iniciar, y como segundo una estructura de tipo struct file_operations, que especifica las funciones de servicio del dispositivo (véase sección 3 del capítulo 3 del libro LDD).

void cdev_init(struct cdev *cdev, struct file_operations *fops);

A continuación, se muestra un ejemplo que especifica solamente las operaciones de apertura, cierre y escritura.


static int ejemplo_open(struct inode *inode, struct file *filp) {
	.....

static int ejemplo_release(struct inode *inode, struct file *filp) {
	.....

static ssize_t ejemplo_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
	.....

static struct file_operations ejemplo_fops = {
        .owner =    THIS_MODULE,
        .open =     ejemplo_open,
        .release =  ejemplo_release,
        .write =    ejemplo_write
};
Después de iniciar la estructura que representa al dispositivo, hay que asociarla con los identificadores de dispositivo reservados previamente. Para ello, se usa la función cdev_add:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);
siendo el tercer parámetro igual a 1 en nuestro caso, puesto que queremos dar de alta un único dispositivo.

Finalmente, la operación de baja del dispositivo se lleva a cabo mediante la función cdev_del:

void cdev_del(struct cdev *dev);
Nótese que esta operación siempre hay que hacerla antes de liberar el identificador de dispositivo (unregister_chrdev_region).

Alta de un dispositivo para su uso desde las aplicaciones

Aunque después de dar de alta un dispositivo dentro del núcleo ya está disponible para dar servicio a través de sus funciones de acceso exportadas, para que las aplicaciones de usuario puedan utilizarlo, es necesario crear un fichero especial de tipo dispositivo de caracteres dentro del sistema de ficheros.

Anteriormente a la incorporación del modelo general de dispositivo en Linux y el sysfs, era necesario que el administrador invocara los mandatos mknod necesarios para crear los ficheros especiales requeridos. Con la inclusión de este modelo, el manejador solo debe dar de alta el dispositivo en el sysfs, encargándose el proceso demonio de usuario udevd de la creación automática del fichero especial (en este caso, /dev/intspkr).

El primer paso que se debe llevar a cabo es la creación de una clase en sysfs para el dispositivo gestionado por el manejador usando para ello la llamada class_create:

#include <linux/device.h>

struct class * class_create (struct module *owner, const char *name); 
Como primer parámetro se especificaría THIS_MODULE y en el segundo el nombre que se le quiere dar a esta clase de dispositivos (en este caso, speaker). Después de ejecutar esta llamada, aparecerá la entrada correspondiente en sysfs (/sys/class/speaker/).

A la hora de descargar el módulo habrá que hacer la operación complementaria (class_destroy(struct class * clase)).

Después de crear la clase, hay que dar de alta el dispositivo de esa clase. Para ello, se usa la función device_create:

struct device * device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
Para el ejemplo que nos ocupa, solo son relevantes los siguientes parámetros (para los demás se especificará un valor NULL): En nuestro caso, habrá que hacer una llamada a esta función especificando como nombre de dispositivo intspkr, creándose la entrada /sys/class/speaker/intspkr. Nótese que el demonio de usuario udevd creará automáticamente en /dev la entrada correspondiente con los números major y minor seleccionados:
ls -l /dev/intspkr
crw------- 1 root root 251, 0 Oct  9 09:15 /dev/intspkr
A partir de este momento, las llamadas de apertura, lectura, escritura, cierre, etc. sobre esos ficheros especiales son redirigidas por el sistema operativo a las funciones de acceso correspondientes exportadas por el manejador del dispositivo.

La llamada device_destroy(struct class * class, dev_t devt) da de baja el dispositivo en sysfs.

Funcionalidad a desarrollar

Una vez presentados los conceptos teóricos, se acomete la funcionalidad planteada en esta fase. En primer lugar, hay que reservar para el dispositivo un número major, elegido por el propio núcleo, y un número minor, que corresponde al especificado como parámetro del módulo con el nombre minor (0 si no se recibe ningún parámetro).

Para ello, se debe incluir dentro de la rutina de iniciación del módulo una llamada a alloc_chrdev_region para reservar un dispositivo llamado spkr, cuyo major lo seleccionará el sistema, mientras que el minor corresponderá al valor recibido como parámetro. Asimismo, añada a la rutina de terminación del módulo la llamada a la función unregister_chrdev_region para realizar la liberación correspondiente.

Una vez reservado el número de dispositivo, hay que incluir en la función de carga del módulo la iniciación (cdev_init) y alta (cdev_add) del dispositivo. Asimismo, se debe añadir el código de eliminación del dispositivo (cdev_del) en la rutina de descarga del módulo.

Para dar de alta al dispositivo en sysfs, en la iniciación del módulo se usarán las funciones class_create y device_create. En la rutina de descarga del módulo habrá que invocar a device_destroy y a class_destroy para dar de baja el dispositivo y la clase en sysfs.

En cuanto a las funciones exportadas por el módulo, en esta fase, solo se especificarán las tres operaciones presentadas previamente (apertura, escritura y liberación). Además, dichas funciones solo imprimirán un mensaje con printk mostrando qué función se está invocando. Estos mensajes se deben mantener en la versión final de la práctica.

Para evitar que los programas que usen este dispositivo todavía incompleto se comporten de manera errónea, las funciones de apertura y liberación deberían devolver un 0, para señalar que no ha habido error, y la función de escritura debería devolver el mismo valor que recibe como tercer parámetro (count), para indicar que se han procesado los datos a escribir.

Pruebas

Para comprobar el funcionamiento correcto del módulo, puede añadir un printk que muestre qué major se le ha asignado al dispositivo y usar el mandato dmesg, que muestra el log del núcleo, para poder verificarlo (antes de hacer una prueba, se recomienda vaciar el log, usando dmesg --clear, para facilitar las verificaciones sobre la información impresa). Asimismo, después de la carga pero antes de la descarga, puede comprobar en el fichero /proc/devices que se ha creado una entrada correspondiente al nuevo dispositivo, y que esta desaparece cuando el módulo es descargado. Asimismo, una vez cargado el módulo, debe comprobar que se han creado los ficheros correspondientes en sysfs, así como el fichero especial asociado con el dispositivo:
ls -l /sys/class/speaker/intspkr /dev/intspkr
Pruebe a cargar el módulo, sin especificar el número minor como parámetro y haciéndolo, y compruebe que el fichero especial creado tiene el número de minor correctamente asignado.

A continuación, pruebe a ejecutar el siguiente mandato para comprobar que se activan las funciones de apertura, escritura y cierre:

    $ echo X > /dev/intspkr

Tercera fase: Operaciones de apertura y cierre

Bagaje requerido

Parámetros del open

Puede consultar la sección 3 del capítulo 3 y la sección 5 del capítulo 3 del libro LDD para profundizar en los aspectos presentados en este apartado.

La llamada de apertura recibe dos parámetros:

Aunque no se requiera para este proyecto, a continuación, por motivos didácticos, se explica una estrategia que se utiliza muy frecuentemente en los manejadores que gestionan varios dispositivos.

Como parece lógico, un manejador que gestiona varios dispositivos utilizará algún tipo de estructura de datos para guardar la información de cada uno de ellos. Dentro de esa estructura parece razonable almacenar en un campo de la misma la estructura cdev asociada a ese dispositivo. Por ejemplo:

struct info_mydev {
	.......
	struct cdev mydev_cdev;
	.......
};
Dentro del contexto de la llamada de apertura, se requerirá acceder a la estructura de datos que almacena la información asociada justo con ese dispositivo. Para lograrlo, se puede acceder al campo i_cdev del puntero al inodo recibido como parámetro, que apuntará al campo mydev_cdev de la estructura correspondiente a ese dispositivo en concreto y subir al comienzo de la misma con la macro container_of. Por otro lado, dado que las funciones de lectura y escritura no reciben como parámetro un puntero al inodo pero sí un puntero a la estructura file, puede usarse el campo private_data para dejar disponible esa información a dichas funciones:
static int mydev_open(struct inode *inode, struct file *filp) {
	struct info_mydev *info_dev = container_of(inode->i_cdev,
			struct info_mydev, mydev_cdev);
	filp->private_data = info_dev;
	.......

Funcionalidad a desarrollar

Hay que asegurarse de que en cada momento solo está abierto una vez en modo escritura el fichero. Si se produce una solicitud de apertura en modo escritura estando ya abierto en ese mismo modo, se retornará el error -EBUSY. Nótese que, sin embargo, no habrá ninguna limitación con las aperturas en modo lectura.

Téngase en cuenta que pueden ejecutarse concurrentemente varias activaciones de la operación de apertura (ya sea porque se ejecutan en distinto procesador o, aunque se ejecuten en el mismo, lo hacen de forma intercalada al tratarse de un núcleo expulsivo). Por tanto, se deberá asegurar de que no se producen condiciones de carrera.

Pruebas

A continuación se propone una prueba para verificar si la funcionalidad se ha desarrollado correctamente:

  1. Se arranca en una ventana un escritor y se le deja parado sin escribir nada por la pantalla (si es necesario ejecutarlo con sudo, se usaría el siguiente mandato: sudo sh -c "cat > /dev/intspkr"):
        $ cat > /dev/intspkr
    
  2. En una segunda ventana se ejecuta otro escritor y debe producirse un error:
        $ cat > /dev/intspkr
        bash: /dev/intspkr: Dispositivo o recurso ocupado
    
  3. Se lanza un proceso que abre el fichero en modo lectura para comprobar que no da error:
       $ dd if=/dev/intspkr count=0
    
  4. En la ventana inicial introducimos un Control-D para finalizar el proceso escritor y volvemos a lanzarlo para probar que se ha rehabilitado la posibilidad de abrir el fichero en modo escritura.

Cuarta fase: Operación de escritura

Esta es la fase central del proyecto en la que se desarrolla la funcionalidad principal del manejador. En ella, es necesario gestionar un buffer interno donde se irán almacenando los sonidos generados por la aplicación en espera de que se vayan reproduciendo. Para la implementación de ese buffer que tiene un comportamiento de cola FIFO, se presentan dos opciones: programar una estructura de datos que implemente esa cola o, la opción recomendada, usar un tipo de datos interno de Linux, denominado kfifo, que implementa esa funcionalidad.

Si se opta por la primera alternativa, es necesario conocer como se reserva memoria dinámica en Linux, mientras que si se selecciona la segunda, no es estrictamente necesario conocer este aspecto puesto que esa reserva de memoria se realiza internamente al iniciar una instancia del tipo kfifo. En cualquier caso, a continuación, se va a incluir información sobre cómo se solicita y libera memoria dinámica dentro del núcleo de Linux.

Bagaje requerido

Reserva y liberación de memoria dinámica

Puede consultar la sección 1 del capítulo 8 del libro LDD para profundizar en los aspectos presentados en este apartado.

Las funciones para reservar y liberar memoria dinámica son kmalloc y kfree, cuyas declaraciones son las siguientes:

    #include <linux/slab.h>
    void *kmalloc(size_t size, int flags);
    void kfree(const void *);

Sus prototipos son similares a las funciones correspondientes de la biblioteca de C. La única diferencia está en el parámetro flags, que controla el comportamiento de la reserva. Los tres valores más usados para este parámetro son:

GFP_KERNEL
Reserva espacio para uso del sistema operativo pero asumiendo que esta llamada puede bloquear al proceso invocante si es necesario. Es el que se usa para reservar memoria en el contexto de una llamada al sistema.
GFP_ATOMIC
Reserva espacio para uso del sistema operativo pero asegurando que esta llamada nunca se bloquea. Es el método que se usa para reservar memoria en el ámbito de una rutina de interrupción.
GFP_USER
Reserva espacio para páginas de usuario. Se usa en la rutina de fallo de página para asignar un marco al proceso.
En caso de error, la llamada kmalloc devuelve NULL. Habitualmente, en ese caso se termina la función correspondiente retornando el error -ENOMEM.

Acceso al mapa de usuario del proceso

Puede consultar la sección 7 del capítulo 3 y la sección 1 del capítulo 6 del libro LDD para profundizar en los aspectos presentados en este apartado.

Habitualmente, un manejador necesita acceder al mapa de memoria de usuario para leer información del mismo, en el caso de una escritura, y para escribir información en él, si se trata de una lectura. Para ello, se proporcionan funciones similares a la rutina estándar de C memcpy que permiten copiar datos desde el espacio de usuario al de sistema (copy_from_user) y viceversa (copy_to_user). Asimismo, se ofrecen macros que permite copiar un único dato en ambos sentidos (get_user y put_user, respectivamente).

    #include <asm/uaccess.h>
    unsigned long copy_from_user(void __user *to, const void *from, unsigned long n);
    unsigned long copy_to_user(void *to, const void __user *from, unsigned long n);
    int put_user(datum,ptr);
    int get_user(local,ptr);
En caso de éxito, estas funciones devuelven un 0. Si hay un fallo, que se deberá a que en el rango de direcciones del buffer de usuario hay una o más direcciones inválidas, devuelve un valor distinto de 0 que representa el número de bytes que no se han podido copiar.

Normalmente, si se produce un error, la función correspondiente devuelve el error -EFAULT.

Bloqueo y desbloqueo de procesos

A continuación, se explican algunos conceptos básicos de la gestión de bloqueos y desbloqueos de procesos en Linux.

KFIFO

Linux proporciona una implementación de una cola FIFO, que permite almacenar en la misma elementos de un determinado tamaño (por defecto, cada elemento es de tipo unsigned char, pero puede especificarse que sea de cualquier tipo). El diseño de la misma está cuidadosamente realizado de manera que no requiere ningún tipo de sincronización siempre que solo haya un flujo de ejecución productor y uno consumidor. Puede consultar el API de kfifo y ejemplos de uso.

A continuación, se comentan brevemente algunas de las funciones que se pueden requerir a la hora de llevar a cabo el proyecto:

Gestión de temporizadores

Puede consultar la sección 4 del capítulo 7 del libro LDD para profundizar en los aspectos presentados en este apartado.

El API interno de Linux ofrece una colección de funciones que permiten gestionar temporizadores, es decir, permite solicitar que cuando transcurra una cierta cantidad de tiempo, se ejecute asíncronamente la función asociada al temporizador. A la hora de especificar el tiempo hay que hacerlo usando como unidad el periodo (inverso de la frecuencia) de la interrupción del reloj del sistema (es decir, el tiempo que transcurre entre dos interrupciones de reloj). El tiempo actual se almacena en la variable global jiffies y corresponde al número de interrupciones de reloj que se han producido desde que arrancó el sistema.

El API de gestión de temporizadores ha cambiado a partir de la versión del núcleo 4.15, por lo que, a continuación, se revisa brevemente esta API mostrando los cambios sufridos en la misma.

Es importante resaltar que la rutina del temporizador se ejecuta de manera asíncrona, en el contexto de una interrupción software. Por tanto, para protegerse de la misma en un entorno multiprocesador hay que usar spin_lock_bh para inhibir las interrupciones software en el fragmento de código conflictivo.

Funcionalidad a desarrollar

Como se comentó previamente, esta fase es el meollo del proyecto puesto que en ella hay que implementar toda la lógica del modo de operación desacoplado del manejador.

En cuanto a la operación de escritura, dado que se asume que el manejador puede ejecutar en un sistema multiprocesador con un núcleo expulsivo, para evitar condiciones de carrera (recuerde que la estructura kfifo está diseñada para que funcione con un único productor y un único consumidor, siendo necesaria una sincronización explícita en caso contrario), hay que asegurarse de que se secuencia el procesamiento de las llamadas de escritura (como se verá más adelante, también hay que secuenciar las llamadas fsync tanto entre sí como con respecto a las escrituras).

Tal como se ha explicado previamente, el comportamiento de la llamada de escritura seguirá el rol de productor trabajando de forma desacoplada con el proceso de consumir los datos (la reproducción de sonidos). Esta llamada quedará bloqueada solo si se encuentra la cola llena y terminará en cuanto haya podido copiar en la cola todos los bytes que se pretenden escribir. Esta llamada, solo en caso de que en ese momento no se esté reproduciendo ningún sonido (el dispositivo está parado), programará la reproducción del sonido de turno, activando el altavoz, a no ser que se trate de un silencio. Recuerde que si el tamaño de la escritura no es múltiplo de 4, al no tratarse de un sonido completo, se guardará esa información parcial hasta que una escritura posterior la complete.

La programación del dispositivo implica fijar la frecuencia del altavoz al valor especificado y establecer un temporizador con un valor correspondiente a la duración del sonido. En caso de tratarse de un silencio no hay que fijar la frecuencia sino desactivar el altavoz si estuviera activo en ese momento.

Cuando se cumple el temporizador, su rutina de tratamiento es la que realizará la programación del próximo sonido, siempre que se encuentre que queda alguno almacenado, desactivando el altavoz en caso contrario. Además, desbloqueará a un proceso escritor si el hueco en la cola o bien es suficiente para que el proceso pueda completar su petición, o bien es mayor o igual que el umbral recibido como parámetro. Dado el carácter asíncrono de la rutina de temporización, es necesario establecer los mecanismos de sincronización requeridos entre dicha rutina y la función de escritura.

Como se comentó previamente, este modo desacoplado conlleva algunas dificultades en lo que se refiere a la descarga del módulo (rmmod) puesto que, aunque no haya ningún fichero abierto asociado al manejador, este puede seguir procesando los sonidos almacenados en la cola. Hay que asegurarse, por tanto, en la operación de fin de módulo de que se realiza una desactivación ordenada del dispositivo, deteniendo el temporizador que pudiera estar activo (en un multiprocesador, incluso podría estar ejecutando en ese momento la rutina del temporizador) y silenciando el altavoz.

Pruebas

A continuación se propone una serie de pruebas para verificar si la funcionalidad se ha desarrollado correctamente. Para poder verificar mejor el comportamiento de la práctica, se recomienda imprimir un mensaje a la entrada y a la salida de la función de escritura, al comienzo y al final de la función asociada al temporizador y cada que vez que se va a programar un sonido especificando la frecuencia y duración del mismo. Usando el mandato dmesg podrá visualizar todos esos mensajes. Se proporciona un fichero binario (songs.bin) con la secuencia de sonidos de varias canciones, así como una versión en modo texto del mismo (songs.txt) para facilitar la comprobación del buen comportamiento del código en las pruebas.

  1. Esta prueba comprueba si se procesa correctamente un único sonido (la primera nota de la sintonía de Los Simpsons).
    $ dd if=songs.bin of=/dev/intspkr bs=4 count=1
    
    Compruebe con dmesg que la frecuencia y duración son adecuadas (puede verificarlo comprobando los valores almacenados en la primera línea del fichero songs.txt), y que el altavoz se ha programado correctamente:
    spkr set frequency: 1047
    spkr ON
    spkr OFF
    
  2. En esta prueba se genera el mismo sonido pero realizando dos escrituras de dos bytes. El comportamiento dede ser el mismo que la prueba anterior.
    $ dd if=songs.bin of=/dev/intspkr bs=2 count=2
    
  3. Esta prueba genera los 8 primeros sonidos del fichero songs.bin usando 8 escrituras y dejando tanto el tamaño del buffer como del umbral en sus valores por defecto. La primera escritura debe activar el primer sonido y las restantes deben devolver el control inmediatamente. Con dmesg se debe apreciar que, exceptuando el primer sonido, los demás son programados en el contexto de la rutina de tratamiento del temporizador.
    $ dd if=songs.bin of=/dev/intspkr bs=4 count=8
    
  4. La misma prueba que la anterior pero con una única escritura, que debe completarse inmediatamente.
    $ dd if=songs.bin of=/dev/intspkr bs=32 count=1
    
  5. Esta prueba intenta comprobar que se tratan adecuadamente las pausas o silencios que aparecen en una secuencia. Para ello, se van a generar los 20 primeros sonidos, donde aparecen dos pausas. Debe comprobarse que el altavoz se desactiva al tratar esas pausas y que se reactiva al aparecer nuevamente sonidos convencionales en la secuencia. Se va a probar con escrituras de 4 bytes y con una única escritura:
    $ dd if=songs.bin of=/dev/intspkr bs=4 count=20
    
    $ dd if=songs.bin of=/dev/intspkr bs=80 count=1
    
  6. Esta prueba va a forzar que se llene la cola pero no va a definir ningún umbral. Para llevarla a cabo, se debe cargar el módulo especificando un tamaño de buffer de 32. A continuación, se va ejecutar una prueba que genere 20 sonidos. En primer lugar, con escrituras de 4 bytes:
    dd if=songs.bin of=/dev/intspkr bs=4 count=20
    
    Debe comprobar con dmesg como la llamada de escritura que encuentra la cola llena se bloquea y que, cada vez que completa el procesado de un sonido, se desbloquea al proceso escritor puesto que, al realizar operaciones de 4 bytes, ya tiene sitio en la cola para completar la llamada y ejecutar en paralelo con el procesamiento de los sonidos previos.

    Finalizada la prueba, repítala usando una única escritura de 80 bytes.

    dd if=songs.bin of=/dev/intspkr bs=80 count=1
    
    En este caso, debe comprobar cómo en la operación de escritura se producen solo dos bloqueos.
  7. Esta prueba comprueba el funcionamiento del umbral. Para ello, se repetirá la anterior (programa que genera 20 sonidos) pero especificando un tamaño de umbral de 16 bytes. La prueba que realiza escrituras de 4 bytes no se verá afectada por el cambio pero sí lo estará la que realiza una única escritura de 80 bytes, que se bloqueará tres veces, en lugar de 2.
  8. Aunque se podrían probar múltiples situaciones de error, vamos a centrarnos solo en una: la dirección del buffer de la operación write no es válida y, por tanto, esta llamada debe retornar el error -EFAULT. La prueba consiste simplemente en ejecutar el programa error proporcionado como material de apoyo.
    ./error
    
  9. Esta es una prueba de esfuerzo (usa los valores por defecto de todos los parámetros): se reproduce todo el fichero de canciones usando escrituras de 4 bytes:
    $ dd if=songs.bin of=/dev/intspkr bs=4
    
    y, acabada completamente esa prueba (con la cola vacía), se repite usando escrituras de 4KiB:
    $ dd if=songs.bin of=/dev/intspkr bs=4096
    
  10. En esta prueba, que también usa los valores por defecto de todos los parámetros, se va a comprobar que cuando se descarga el módulo justo después de completarse la aplicación, se hace de forma correcta deteniendo el procesamiento de sonidos y dejando en silencio el altavoz:
    $ dd if=songs.bin of=/dev/intspkr bs=4096 count=1
    $ sleep 1
    $ rmmod spkr
    

Quinta fase: Operación fsync y adaptación a la versión 3.0 de Linux

Bagaje requerido

fsync

La llamada fsync, cuando se usa con un fichero convencional, se asegura de que todos los datos del fichero que están presentes en la cache del sistema de ficheros residente en memoria se escriben en el disco, no completándose hasta que no finalice esa operación de actualización.

En el caso de dispositivos, se usa habitualmente para asegurarse de que todos los datos presentes en el buffer del manejador se han procesado, bloqueando al proceso hasta que eso ocurra.

A continuación, se muestra cómo se incluye en la estructura struct file_operations la función que maneja esta llamada al sistema. Nótese que para la funcionalidad que se plantea en este proyecto, no son significativos los parámetros que recibe esta llamada.

...........................
static int ejemplo_fsync(struct file *filp, loff_t start, loff_t end, int datasync) {
	.....

static struct file_operations ejemplo_fops = {
        .owner =    THIS_MODULE,
	......................
        .fsync =    ejemplo_fsync
};
El prototipo de esta operación ha ido cambiado en las sucesivas versiones de Linux. Así, en la versión 3.0, se usaba el siguiente prototipo (que, a su vez, es diferente del que aparece en la sección 3 del capítulo 3 del libro LDD):
int spkr_fsync(struct file *filp, int datasync);

Dependencia de la versión del núcleo

Puede consultar la sección 4.3 del capítulo 2 del libro LDD para profundizar en los aspectos presentados en este apartado.

El API interno del núcleo va cambiando según va evolucionando Linux. A veces, se trata de cambios importantes que exigen que haya una versión distinta de un módulo para dos versiones diferentes del sistema operativo. Sin embargo, en otras ocasiones, son pequeñas modificaciones que no afectan a la funcionalidad del módulo y que pueden ser resueltas por directivas del preprocesador que establecen distintas alternativas a la hora de compilar el módulo dependiendo del número de versión del núcleo para el que se compila.

Para la funcionalidad que se requiere en el proyecto, es suficiente con conocer las dos siguientes macros:

Usando esas dos macros, se pueden establecer regiones de código de compilación condicional que se adapten a los posibles cambios en el API del núcleo que pueden existir entre diferentes versiones del sistema operativo.

Funcionalidad a desarrollar

La implementación de esta fase requiere la incorporación de una nueva wait queue para permitir que el proceso que realizó la llamada se quede bloqueado esperando hasta que se complete el procesado de todos los sonidos almacenados en la cola FIFO. Al igual que ocurre con la llamada de escritura, mientras el proceso esté bloqueado en esta función, no se procesarán nuevas llamadas de escritura ni al propio fsync.

En esta fase, además, nos aseguraremos de que el código desarrollado también compila para la versión 3.0 de Linux. Esto afecta a tres aspectos del desarrollo:

Pruebas

Con respecto a la compilación del módulo en un núcleo de la versión 3.0, si no dispone del entorno adecuado para probarlo, no se preocupe: se probará una vez entregada la práctica.

En cuanto a la funcionalidad de la operación sync, se plantea la siguiente prueba (nuevamente, se recomienda incluir mensajes al entrar y salir se esta función):

  1. Teniendo el módulo cargado con los valores por defecto, se va a ejecutar el siguiente mandato que hace una llamada fsync justo antes del close (nótese el uso del strace para comprobar que se bloquea la llamada fsync), primero con 20 escrituras de 4 bytes:
    $ strace dd if=songs.bin of=/dev/intspkr bs=4 count=20 conv=fsync
    
    y luego con 1 de 80:
    $ strace dd if=songs.bin of=/dev/intspkr bs=80 count=1 conv=fsync
    

Sexta fase: Operaciones ioctl

Bagaje requerido

ioctl

Puede consultar la sección 1 del capítulo 6 del libro LDD para profundizar en los aspectos presentados en este apartado.

Esta llamada es una especie de cajón de sastre a la que se le puede asignar cualquier funcionalidad del dispositivo que no sea fácil de plasmar como una lectura o una escritura (por ejemplo, rebobinar una cinta o expulsar un medio extraíble).

A continuación, se muestra cómo se incluye en la estructura struct file_operations la función que maneja esta llamada al sistema.

...........................
static long ejemplo_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
	.....

static struct file_operations ejemplo_fops = {
        .owner =    THIS_MODULE,
	......................
        .unlocked_ioctl =  ejemplo_ioctl
};
El segundo parámetro de la llamada permite especificar qué tipo de operación se quiere solicitar (el número identificador que tiene asignado un determinado mandato), mientras que el tercero, que generalmente corresponde a la dirección de memoria de un dato, representa un valor que puede ser de entrada a la llamada, cuando esta quiere modificar de alguna forma el estado del dispositivo, o de salida, en caso de que su objetivo sea obtener información sobre el estado del dispositivo.

Aunque cada manejador podría asignar sus propios números a sus mandatos (mandato 1, mandato 2,...), para controlar mejor los errores, existe un convenio para asignar un número de mandato único en todo el sistema a cada operación. Para ello, ese número se compone a partir de la siguiente información:

Existen macros para facilitar la creación del número identificador de un determinado mandato a partir de toda esa información:

Funcionalidad a desarrollar

En esta sección se van a implementar tres operaciones de tipo ioctl: A la hora de definir los identificadores únicos de cada operación se usará como número mágico el carácter '9'.

Dado que se pretende que estas operaciones pueda hacerlas una aplicación ajena a la que ha solicitado reproducir sonidos y puesto que solo se permite acceso exclusivo en modo escritura, las aplicaciones que pretendan usar estas operaciones deberán abrir el fichero en modo lectura. Además, estas operaciones deben poder realizarse aunque haya un proceso bloqueado en la escritura o en un fsync.

Empecemos por la primera operación: la funcionalidad de enmudecer y desenmudecer. En caso de que se solicite enmudecer y el altavoz esté activado en ese instante, habrá que desactivarlo inmediatamente. De manera complementaria, si se solicita desenmudecer el altavoz y en ese momento debería estar sonando (se está reproduciendo un sonido y no es un silencio), se procederá a su reactivación. Recuerde que mientras está el altavoz enmudecido, se seguirán procesando los sonidos de la forma habitual, de manera que cuando se desenmudezca, se escuchará justo el sonido que esté reproduciéndose en ese instante (no se oirá nada en el caso de que no se esté procesando ningún sonido en ese momento o se esté trabajando con un silencio). Evidentemente, el cambio del estado de esta opción se mantendrá hasta que se modifique explícitamente mediante esta misma operación pero especificando el valor contrario. Deben incluirse los mecanismos de sincronización requeridos por esta llamada.

En cuanto a la segunda operación, simplemente retorna el estado actual de esta opción, correspondiendo con el valor especificado en la última llamada a la operación previa, teniendo en cuenta que el estado inicial es mute off.

Con respecto a la tercera operación de vaciado de la cola, dado que kfifo solo permite que haya un único flujo de ejecución haciendo el rol de consumidor/lector en cada instante, debería delegarse esta operación para que la realice el mismo flujo consumidor que extrae los sonidos de la cola (así, se podría implementar de manera que si se ha solicitado esta operación, cuando se vuelva a la cola para extraer más sonidos, esta se considerará vacía liberando toda la información almacenada en la misma). Téngase en cuenta que esta operación también opera en modo desacoplado en el sentido de que devuelve el control inmediatamente pero el vaciado no se produce hasta que se completa el procesado del sonido en curso. Por tanto, los sonidos encolados por llamadas de escritura posteriores a una operación de vaciado, pero anteriores a que se complete el procesado del sonido actual también son eliminados. Para conseguir un funcionamiento más síncrono, se podría hacer que esta operación no se completase hasta que se lleve a cabo la operación de vaciado, de manera similar al modo de operación de fsync. Sin embargo, este enunciado no plantea implementar este modo más acoplado (aunque el mandato que se usará para las pruebas, denominado reset, consigue ese comportamiento llamando a fsync justo después de este ioctl). Para esta llamada, también deben incluirse los mecanismos de sincronización requeridos por esta llamada.

Pruebas

Antes de comenzar con las pruebas, debe modificar los programas setmute, getmute y reset para incluir en los mismos las definiciones de las operaciones ioctl correspondientes tal como se han definido en el módulo. Recuerde que, como se comentó previamente, el programa reset no solo incluye la llamada ioctl sino que, a continuación, invoca la llamada fsync, lo que asegura que al finalizar el programa el vaciado se ha completado.

A continuación, se proponen las pruebas para realizar una verificación básica de la funcionalidad de esta fase.

  1. Esta prueba intenta comprobar si se gestiona correctamente el estado del mute:
    $ ./getmute 
    mute off
    $ ./setmute 1 
    $ ./getmute 
    mute on
    $ ./setmute 0
    $ ./getmute 
    mute off
    
  2. La siguiente prueba va realizando operaciones que enmudecen y desenmudecen el altavoz en distintas circunstancias (en momentos donde se están reproduciéndos sonidos y en momentos en los que no). Para comprobar si la evolución del estado del altavoz es correcta, se recomienda revisar el log del núcleo con dmesg al final de cada etapa y el borrado del mismo antes de comenzar la siguiente.
    ./setmute 1;  dd if=songs.bin of=/dev/intspkr bs=48 count=1; sleep 1; ./setmute 0; sleep 1; ./setmute 1; echo etapa1; read v; dd if=songs.bin of=/dev/intspkr bs=48 count=1; echo etapa2; read v; ./setmute 0; dd if=songs.bin of=/dev/intspkr bs=48 count=1
    
  3. Esta prueba se centra en el comportamiento de la operación mute cuando coincide con la reproducción de pausas.
    $ ./setmute 0; dd if=songs.bin of=/dev/intspkr bs=40 count=1 skip=1; sleep 1; ./setmute 0
    $ ./setmute 0; dd if=songs.bin of=/dev/intspkr bs=40 count=1 skip=1; sleep 1; ./setmute 1
    $ ./setmute 1; dd if=songs.bin of=/dev/intspkr bs=40 count=1 skip=1; sleep 1; ./setmute 0
    $ ./setmute 1; dd if=songs.bin of=/dev/intspkr bs=40 count=1 skip=1; sleep 1; ./setmute 1
    
  4. Para probar la operación de vaciado, se propone ejecutar la siguiente secuencia de mandatos:
    dd if=songs.bin of=/dev/intspkr bs=80 count=1; ./reset; dd if=songs.bin of=/dev/intspkr bs=80 count=1 skip=1
    
    Debe comprobarse que de los 20 primeros sonidos, debido al reset, solo se procesa uno, mientras que la segunda tanda de 20 sonidos se procesa de forma normal.

Material de apoyo

En este fichero se encuentra el material de apoyo de la práctica. Al descomprimirlo, se encontrará con un directorio denominado SEU que incluye a su vez dos subdirectorios:

Entrega del proyecto

Dentro del plazo establecido para la entrega del proyecto, el alumno debe enviar al responsable de la práctica (fperez@fi.upm.es) un correo que especifique como asunto SEU: primera parte del proyecto y adjunte un fichero ZIP que contenga únicamente lo siguiente: