8. Software

8.1. Organización del capítulo

El capítulo se encuentra organizado en las siguientes secciones:

Características del software controlador
Resumen de las características técnicas del software controlador.

Driver RW_BAR
Descripción del diseño y la implementación del driver RW_BAR. Incluye una introducción a los tipos de drivers Linux existentes, creación de módulos, transferencia de datos entre dispositivos y apliaciones de usuario, y los detalles de implementación del código del driver.

Herramientas de prueba
Se mencionan las herramientas de prueba desarrolladas específicamente para utilizar junto con el driver.

Recursos PCI del sistema operativo Linux
Enumera las herramientas provistas por el sistema operativo Linux para trabajar con dispositivos conectados en el bus PCI. También hace una breve introducción a la medición precisa del tiempo y los registros de tipo de memoria (MTRR) de los procesadores Pentium II y superiores.

Conclusiones

8.2. Características del software controlador (driver)

Las principales características del driver son las siguientes:

8.3. Driver RW_BAR

La forma correcta de utilizar las funciones provistas por una placa conectada al bus PCI dependen totalmente de la apliación. Para proveer de funcionalidades avanzadas, el driver debe ser desarrollado a medida, tomando en cuenta la aplicación final.
Como el driver debe permitir desarrollar y probar aplicaciones genéricas, el mismo implementa las funciones más básicas, pero imprecindibles, para asegurar su utilidad.

Este capitulo requiere que el lector tenga ciertos conocimientos sobre la arquitectura PCI y el sistema operativo Linux.

8.3.1. Diseño

8.3.1.1. Mapeo de direcciones PCI

La aplicación sintetizada en la placa se comunica con el exterior a través de rangos de direcciones en el espacio PCI. Dichas direcciones son asignadas en el momento de arranque del sistema. La cantidad, tamaño y propiedades de dichas regiones son especificadas por el core PCI a través del comportamiento característico que poseen los BAR.

Se tomaron las siguientes decisiones:

Los espacios de direcciones utilizados por el hardware de la tarjeta se mapean dentro del sistema de archivos reservado para dispositivos (directorio /dev). De esta manera se puede acceder a los espacios de drirecciones de PCI como si fuesen archivos, utilizando las clásicas funciones de acceso a archivos (open, seek, read, write y close).

8.3.1.2. Dispositivo de caracteres

El sistema operativo linux hace distición entre tres tipos de dispositivos:

Los dispositivos de caracteres ¨char¨ son aquellos que puede ser accedidos como un flujo de bytes, o cómo si fuesen un archivo.

Históricamente, los dispositivos de bloques se acceden de a múltiplos de un bloque, dónde un bloque es generalmente 1 Kbyte. En Linux, un dispositivo de bloques puede transferir cualquier cantidad de bytes y no presenta diferencias, en este sentido, con un dispositivo de caracteres. Es importante notar que para albergar un sistema de archivos es fundamental que el dispositivo sea implementado cómo un dispositivo de bloques ya que proveed de ciertos interfaces necesarios para poder montarlo y accederlo.

Los interfaces de red (network interface ) están pensados para intercambiar información con otros sistemas. Están diseñados para enviar y recibir información en forma de paquetes. Al no estar orientados a la transferencia de información en forma de flujo de bytes, no pueden ser accedidos cómo si fuesen archivos y por lo tanto no pueden ser mapeados dentro del sistema de archivos, ni se pueden usar sobre ellos las clásicas funciones de archivos (open, read, write, seek y close).
Un interfaz de red puede, sin embargo, generar una interrupción para ser atendido por el kernel; algo que es imposible para los dispositivos char y los block.

La opción más acorde a las necesidades y objetivos de nuestro diseño es la de un dispositivo de caracteres. Esta elección no impide el uso de interrupciones en la placa, pero debe escribirse código dentro del controlador (driver) que atienda dichas interrupciones y realice las tareas necesarias.

8.3.1.3. Modularización

Las formas existentes de implementar un controlador (driver) de un dispositivo pueden ser:

El segundo método es mucho más cómodo de utilizar en una diversidad de sitaciones, en particular cuándo se está desarrollando dicho driver. Si se desean hacer modificaciones del código, solo basta con descargar el módulo, hacer los cámbios en el código fuente y previa compilación, volverlo a cargar.

También permite distribuir e instalar el driver en un PC que no posea las herramientas de desarrollo necesarias para compilar el kernel.

No hay diferencias entre el código fuente de un controlador modular y uno compilado junto con el kernel.

Los cabezales que deben incluirse para implementar un módulo son:

Otros cabezales utilizados por nuestro driver y de uso frecuente:

También es necesario darle valor a los siguientes macros, cómo se muestra a continuación:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("sebfer@fing.edu.uy - ciro@mondueri.com")

Para poder pasar parámetros al módulo y hacer configuraciones diferentes al momento de cargarlo, se puede utilizar el macros MODULE_PARAM. Para documentar el parámetro, se utiliza el macro MODULE_PARAM_DESC.

MODULE_PARM(rw_bar_major, "i");
MODULE_PARM_DESC(rw_bar_major, "Major node to be assigned. Dynamic allocation if 0 or unspecified");

8.3.1.4. Transferencia de datos con el hardware

Los módulos son ejecutados en lo que se llama espacio de kernel, mientras que las aplicaciones corren en el espacio de usuario. Este concepto es fundamental en la teoría de sistemas operativos.

El rol de un sistema operativo es el de proveer a los programas de acceso al hardware del sistema, en forma consistente y confiable. También debe permitir que los programas operen independientemente y proveer de protecciones para evitar el acceso no autorizado a los recursos de. hardware. Esta tarea es posible si el CPU provee dichas protecciones.

La forma en que los CPU proveen esta proteccion es implementando diferentes modos de operacion, o niveles. Dichos niveles tiene diferentes roles, y algunas operaciones están deshabilitadas en los niveles más bajos. El salto de niveles debe ser realizado a través de ciertos portales. Todos los procesadores modernos tienen al menos dos niveles de protección. En los sistemas operativos del tipo Unix, el kernel es ejecutado en el nivel más alto, dónde todo es permitido, mientras que las aplicaciones son ejecutdas en el nivel más bajo, donde el procesador regual el acceso directo al hardware y el acceso no autorizado a ciertas regiones de memoria.

El espacio de kernel y el espacio de usuario, se refieren no sólo al modo correspondiente de operación del CPU, sino también al hecho que cada modo tiene su propio mapeo de direcciones de memoria, su propio direccionamiento de memoria.

Esta existencia de diferentes espacios de ejecución implica que la transferencia de datos entre las aplicaciones de usuario y el hardware deba ser un proceso de dos pasos.
Primero los datos deben transferirse del espacio de usuario al espacio de kernel, previa reserva de espacio suficiente de memoria de kernel por parte del módulo. Luego que los datos fueron almacenados en este buffer temporal en espacio de kernel, pueden entonces ser transferidos al hardware por parte del código del módulo, que se ejecuta en espacio de kernel.

El kernel provee funciones para hacer dichas transferencias. En el caso de una lectura desde el hardware PCI, las funciones utilizadas serían las siguientes.

El diagrama siguiente muestra los pasos necesarios para hacer la lectura antes mencionada:

memory_spaces.jpg

La función memcpy_fromio trabaja sobre direcciones virtuales de memoria manejadas por el kernel, y no necesariamente con la dirección física real del dispositivo PCI. Esta direccion virtual se asigna mediante la función ioremap.

La función ioremap construye nuevas tablas de páginas de memoria, pero no reserva memoria. El valor devuelto por la función es una direccion virtual especial que puede ser usada para acceder al rango de direcciones físico especificado. Este valor obtenido no puede ser accedido directamente ya que es una dirección especial, y se deben utilizar funciones como readb o memcpy_fromio.

Pero para realizar estas transferencias, el controlador (driver) debe tener garantizado el acceso exclusivo a las regiones de memoria dónde está ubicado el dispositivo de hardware, para prevenir interferencias de otros drivers. Para esto el kernel provee tres funciones:

8.3.2. Implementación

8.3.2.1. Estructura general de un módulo de kernel linux

Para implementar un módulo de kernel se deben implementar dos funciones, una de inicialización y otra de salida. El nombre de las funciones puede ser cualquiera y deben ser asociadas cómo tales pasando sus nombres como parámetros de las funciones module_init y module_exit.

Extracto del código RW_BAR:

/*********************************************************************
 * Hooks de inicializacion y salida del MODULO.
 ********************************************************************/
module_init(rw_bar_init);
module_exit(rw_bar_cleanup);

Las funciones de entrada y de salida deben ser marcadas con los macros __init y __exit para que sean procesadas correctamente por el compilador.

8.3.2.2. Manejo de dispositivos PCI en kernel linux

El kernel linux posee varias funcionalidades que hacen muy sencillo la codificación de un controlador PCI. Entre ellas están la búsqueda automática del dispositivo utilizando su número de fabricante y dispositivo, el registro de las funciones detección y extracción del dispositivo, etc.

Es necesario definir dos estructuras de datos, una con la información necesaria para poder identificar el dispositivo de hardware y la otra con las funciones de software a invocar en el momento de la inicialización y extracción del dispositivo. Se debe invocar el macro MODULE_DEVICE_TABLE para la estructura que contiene la información identificatoria del dispositivo.
A continuación se muestra un extracto del código con las estructuras mencionadas:

/*
 * dispositivos en los que se usa este driver
 */
static struct pci_device_id rw_bar_tbl[] __devinitdata = {
  { MY_DEVICE_VENDOR, MY_DEVICE_ID, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0 },
};
MODULE_DEVICE_TABLE(pci, rw_bar_tbl);

/*
 * estructura del driver PCI
 */
static struct pci_driver rw_bar_driver = {
  name:         "rw_bar",
  probe:                rw_bar_init_one,
  remove:               rw_bar_remove_one,
  id_table:     rw_bar_tbl,
};


El registro de dichas estructuras se realiza desde dentro del código de inicialización del módulo de kernel (rw_bar_init en este caso) utilizando la función pci_module_init. Se comprueba el valor devuelto por dicha función para determinar si el hardware está presente o si hubo algún tipo de error. El código es el siguiente:

  /* 
   * cargo el driver PCI.
   * pci_module_init detecta si el dispositivo especificado en
   * rw_bar_driver esta instalado en el sistema y en dicho caso
   *  ejecuta rw_bar_init_one 
   */
  if ( pci_module_init (&rw_bar_driver) != 0){
    PDEBUG("rw_bar_init: pci_module_init != 0\n");
    return -ENODEV;
  }

La función de inicialización del dispositivo ( rw_bar_init_one ) debe ser marcada con el macro _devinit para que sea correctamente procesada por el compilador. De igual forma, la función de extracción del dispositivo ( _rw_bar_remove_one ) debe ser marcada con el macro __devexit.
Su única función importante es la de almacenar las direcciones de inicio y el tamaño de las regiones de memoria PCI correspondientes a cada BAR, en un par de variables de tipo array. Dichas variables serán accedidas luego desde las funciones de lectura y escritura de datos para identificar en qué dirección se deben acceder los datos de hardware.

A continuación, un extracto de código de la función rw_bar_init_one :

/*
 * Inicializacion del driver PCI (PROBE).
 */
static int __devinit rw_bar_init_one (struct pci_dev *pdev,
                                      const struct pci_device_id *ent)
{

  // hay 6 bars nada mas
  if (used_bars > 5) {
    used_bars = 6;
  }

  // para cada bar leer los valores
  int i;
  for (i = 0; i<used_bars; i++) {
    mem_base[i] = pci_resource_start(pdev, i);
    mem_size[i] = pci_resource_len(pdev, i);
    mem_v_base[i] = 0;

    if ( (mem_base[i] <= 0) || (mem_size[i] <= 0) ) {
      if (i == 0 ) { 
        return -ENODEV;
      }
      continue;
    }
  }
  return 0;
}


8.3.2.3. Funciones de acceso a archivo

Para registrar las funciones que deben ser provistas por un dispositivo de caracteres, se debe:

El primer parámetro de la función register_chrdev es el número mayor de dispositivo a asignar. Si es cero, el sistema asigna un número automáticamente devolviéndolo como resultado de la función. En el código de driver se contemplan las opciones de especificar como parámetro un número mayor de dispositivo, o dejar que el sistema lo seleccione automáticamente.

A continuación se muestran los dos extractos del código correspondientes:

/* 
 * file operations
 */
static struct file_operations rw_bar_fops = {
  read: fop_read,
  write: fop_write,
  poll: fop_poll,
  open: fop_open,
  release: fop_release,
};

...

int __init rw_bar_init(void)
{
   ....
  /*
   * registro del character device (fops)
   */
  result = register_chrdev(rw_bar_major, "rw_bar", &rw_bar_fops);
  if (result < 0) {
    printk(KERN_INFO "rw_bar: can't get major number\n");
    return result;
  }
  if (rw_bar_major == 0)
    rw_bar_major = result; /* dynamic */
}


El trabajo real de este dispositivo es realizado por las funciones fop_read y fop_write. En ellas se hace todo el movimiento de datos entre el dispositivo hardware y el usuario. Son estas las funciones invocadas cuando se accede al dispositivo correspondiente en el sistema de archivos.
Dichas funciones se describen en la siguiente sección.

8.3.2.4. Transferencia de datos desde y hacia el hardware

En la carga del módulo, se reservan las direcciones de memoria utilizadas por cada BAR mediante la función request_mem_region, previo chequeo de su estado mediante la función check_mem_region.
Luego se hace corresponder dichas regiones de memoria del dispositivo PCI a direcciones que pueden ser accedidas desde el kernel mediante la función ioremap. A continuación un extracto de la función de carga del módulo dónde está contenido dicho procedimiento:

  /*
   * Reservo la region fisica de memoria correspondiente a cada BAR
   */
  for (i = 0; i<used_bars; i++) {
    if (check_mem_region(mem_base[i], mem_size[i])) {
      printk("drivername: memory already in use\n");
      return -EBUSY;
    }
    request_mem_region(mem_base[i], mem_size[i], "rw_bar");

    /*
     * Mapear memoria fisica dispositivo a memoria virtual
     */
    mem_v_base[i] = ioremap(mem_base[i], mem_size[i]);
  }

La transferencia de datos es realizada por las funciones fop_read y fop_write.
Describimos unicamente la implementación de la función fop_read por su simetría característica.

  1. reservar temporalmente memoria en espacio de kernel para hacer la transferencia ( kmalloc )
  2. hacer la transferencia desde el hardware a el espeacio temporal de memoria reservado ( memcpy_fromio )
  3. transferir desde el espacio temporal a el espacio de memoria de usuario ( copy_to_user )
  4. liberar la memoria reservada ( kfree )

Es importante recalcar que el uso del parámetro GFP_KERNEL en la llamada a la función de reserva de memoria kmalloc puede hacer que el proceso sea puesto en espera por el kernel debido a la falta de memoria libre y por lo tanto su contexto debe ser reentrante. En caso de estar intentando obtener memoria desde una interrupción o timer, el parámetro adecuado es GFP_ATOMIC que jamás es puesto en espera y falla si directamente si no hay memoria libre.

A continuación se muestra la función fop_read simplificada (sin código de debug y estadísticas).

static ssize_t fop_read (struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
  int retval;
  unsigned char *kbuf, *ptr;
  void *add;
  int bar_to_read = MINOR(filp->f_dentry->d_inode->i_rdev);

  /* si quiero leer mas del tamanio disponible, achico count al
   *  tamanio max de ese bloque de memoria */
  if (count > mem_size[bar_to_read]) 
    /* mem_size_1 es el largo o el largo menos 1? */
    count = mem_size[bar_to_read] - 1;

  if (count < 0) return 0;

  /*
   * reserva de espacio temporal de memoria para hacer la transferencia
   */
  kbuf = kmalloc(count, GFP_KERNEL);
  if (!kbuf) return -ENOMEM;
  ptr=kbuf;
  retval=count;

  /*
   * Cambiar a nuestro espacio de direcciones virtual mapeado.
   */
  add = mem_v_base[bar_to_read] + *f_pos;

  /*
   * copia desde el dispositivo desde la direccion virtual al buffer temporal
   */ 
  memcpy_fromio(ptr, add, count);


  /*
   * copia desde buffer temporal al espacio de memoria de usuario
   */ 
  if (retval > 0)
    copy_to_user(buf, kbuf, retval);
  kfree(kbuf);
  *f_pos += retval;
  return retval;
}


La función fop_read recibe los siguientes parámetros:

La función fop_read debe devolver como resultado la cantidad de bytes que fueron efectivamente leidos.

8.3.2.5. Uso de /proc para estadísticas

Para poder desplegar estadísticas de funcionamiento del hardware, se optó por la funcionalidad provista en el subdirectorio especial /proc.
Los archivos ahí presentes pueden ser accedidos cómo si fuesen archivos de caracteres permitiendo obtener información o configurar parámetros del módulo.
Se implementó el despliegue de datos estadísticos sobre la transferencia de datos desde y hacia el hardware. La información es recopilada inmediatamente antes y después de la ejecución de las funciones que hacen la transferencia desde y hacia el harware ( memcpy_toio y memcpy_fromio ) y almacenada en variables globales.

El código necesario para registrar un archivo dentro del subdirectorio especial /proc se invoca dentro de la carga del módulo y se muestra a continuación:

  /*
   * registro de entrada proc/drivers/iiepci
   */
  create_proc_read_entry("driver/iiepci",
                         0 /* default mode */,
                         NULL /*parent dir */,
                         rw_bar_read_procmem,
                         NULL /* client data */);

En este caso la función que realiza el trabajo de desplegar los datos se llama rw_bar_read_procmem . A continuación se muestra un extracto de dicha función:

int rw_bar_read_procmem(char *buf, char **start, off_t offset, 
                        int count, int *eof, void *data)
{
  int len = 0;

  len += sprintf(buf+len, "\n--------------- IEEPCI Stats ----------------\n");
  /* memcopy */
  len += sprintf(buf+len, "-- MEMCOPY - WRITE ----------------\n");
  len += sprintf(buf+len, "total bytes    : %li\n", st_mc_w_total_bytes);
  len += sprintf(buf+len, "total tsc loops: %li\n", st_mc_w_total_tsc_loops);
  ...

  /* ultima */
  *eof = 1;
  return len;
}


Para almacenar los datos sobre la performance, se modifca el contenido de variables globales en los momentos inmediatamente enteriores y posteriores a la ejecución de la trasnsferencia de datos correspondientes a la lectura o escritura. A continuación se muestra un extracto del código correspondiente a la operación de escritura fop_write :

  rdtscl(st_mc_w_last_tsc_start);
  memcpy_toio(add, ptr, count);
  rdtscl(st_mc_w_last_tsc_end);

La primer llamada a la función rdtscl almacena el valor inicial del contador TSC del cpu (explicado más adelante en la sección de medición de tiempo) en una variable global.
Luego se realiza la transferencia hacia el hardware mediante la función memcpy_toio.
Inmediatamente después se almacena el valor final del contador TSC en otra variable global.

En el momento de desplegar la información estadística, se realizan los cálculos correspndientes, ya sea para calcular el tiempo o la tasa de transferencia, utilizando un par de macros que hacen más sencillo el código ( los macros son LOOPS2USECS y KBYTESPERSEC y están detallados al final de esta sección )

8.3.2.6. Compilación del driver

Para compilar el driver se deben utilizar los siguientes parámetros y símbolos:

CFLAGS = -D__KERNEL__ -DMODULE -Wall -O

El símbolo __KERNEL__ habilita muchas funciones útiles dentro de los cabezales de kernel. El símbolo MODULE debe ser incluido para todos aquellos drivers que no sean compilados dentro del kernel. El parámetro -Wall habilita el despliegue de todos los mensajes de adertencia del compilador. El parámetro -O es necesario porque muchas funciones están declaradas inline en los cabezales y el el compilador gcc no expande las funciones inline a menos que la optimización esté habilitada.

Junto con el código fuente del driver se incluye un archivo Makefile para facilitar el proceso de compilación.

8.3.2.7. Instalación y desinstalación del driver

Para instalar el módulo se utilza el programa insmod , al que se le pasan como parámetros el nombre del módulo, y los parámetros extra que este requiera. En nuestro caso es necesario especificar la cantidad de espacios de memoria PCI utilizados (cantidad de BAR).
Un ejemplo de la carga del módulo utilizando tres espacios de memoria PCI sería:

/sbin/insmod -f ./rw_bar.o "used_bars=3" 

Para simplificar la tarea de determinar la cantidad de espacios de memoria PCI utilizados por el dispositivo se utiliza un script ( load_rw_bar.sh ), que hace uso de la herramienta lspci explicada más adelante. También en este script se crean los nodos en el sistema de archivos para acceder a cada espacio de memoria PCI.

Las partes más importantes del script serían:

#!/bin/bash

# variables utilizadas 
vendor_id="1172"
device_id="abba"
module="rw_bar"
device="rw_bar"
mode="664"

# obtengo cantidad de espacios de memoria utilizados
used_bars = `lspci -v -d $vendor_id:$device_id | grep Memory | wc -l`

# instalo el módulo
/sbin/insmod -f ./$module.o "used_bars=$used_bars" $* || exit 1

# obtengo el major mode asignado al driver recién cargado
major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"`

# creo los nodos en el sistema de archivos /dev
for (( I=0; $used_bars-$I; I=$I+1 )); do
   mknod /dev/${device}$I c $major $I
   chgrp $group /dev/${device}$I
   chmod $mode  /dev/${device}$I
done;

La descarga del driver utiliza el programa rmmod para desinstalar el módulo, y luego elimina los nodos correspondientes:

#!/bin/sh

# variables utilizadas
vendor_id="1172"
device_id="abba"
module="rw_bar"
device="rw_bar"

# desinstalar el módulo
/sbin/rmmod $module $* || exit 1

# eliminar todos los nodos creados
rm -f /dev/${device}? 

8.4. Herramientas de prueba

La prueba del driver resulta bastante sencilla, ya que los espacios de memoria pueden ser accedidos tal como si fuesen archivos convencionales.

Se desarrollaron un par de scripts basados en el lenguaje Perl, uno de lectura y otro de escritura. Ambos scripts permiten manipular un byte en particular, ubicado en cualquier posicion dentro del espacio de memoria, utilizando un cierto offset o desplazamiento a partir del primer byte.

8.4.1. roff

El script roff permite leer desde un archivo una cierta cantidad de bytes especificada, a partir de un cierto desplazamiento deseado. El comando posee ayuda en linea.

Un ejemplo de utilización sería:

./roff.pl -C -o 2 -c 30 /dev/rw_bar1

Se leen 30 bytes (parámetro -c) a partir del byte 2 (parámetro -o) del archivo /dev/rw_bar1, y la salida se presenta en pantalla en forma hexadecimal y ascii en forma de columnas (parámetro -C).

El funcionamiento del programa se concentra en el siguiente extracto de código:

open(RW,$dev) || die "El archivo de entrada no existe\n";

# avanzo el offset deseado
if ($opt_o) {
    sysseek(RW,$offset, 1);
}

# preparo salida con hexdump si se solicita
if ($opt_C) {
    $pid = open(WRITEME, "| hexdump -C");
}

my $totalread = 0;
my $buf;

while(sysread(RW, $buf, $buffsize) and ($totalread < $count)){
    if ($opt_C) {
        print WRITEME $buf;
    } elsif ($opt_x) {
        print unpack("H*",$buf);
    } else {
        print $buf;
    }
    $totalread += length($buf);
}

if ($opt_C) {
    close(WRITEME);
}
close(RW);

Es importante mencionar que se utilizan las funciones sysseek y sysread de modo de evitar el uso de los buffers de entrada y salida, y poder controlar efectivamente el tamaño de bytes leídos en cada operación.

8.4.2. woff

El script woff permite escribir a un archivo una cierta cantidad de bytes, a partir de un cierto desplazamiento deseado. Los datos a escribir son recibidos por la entrada estándar del programa.

Un ejemplo sería el siguiente:

echo "testing" | ./woff.pl -o 2 /dev/rw_bar1

En este caso se escribe el texto "testing" al archivo /dev/rw_bar1, tomando como posición inicial de escritura el byte 2 dentro del archivo (parámetro -o). La cantidad de caracteres leída de la entrada debe ser especificada por el parámetro -c.
El uso del parámetro -x permite escribir cualquier valor deseado, representado en dos caracteres ASCII en forma hexadecimial. Por ejemplo, el siguiente ejemplo escribe el numero 255 en el primer byte del archivo /dev/rw_bar1:

echo ff | ./woff.pl -x -c 2 /dev/rw_bar1

En caso de utilizarse los parámetros -c y -x juntos, -c indica la cantidad de caracteres leídos de la entrada, que estar representados en ASCII hexadecimal, corresponden a la mitad de los que se van a escribir en el archivo de salida. El ejemplo anterior lee dos caracteres pero escribe un solo byte.

El funcionamiento del programa se resume en las siguientes líneas de código:

# abro el archivo de salida
open(RW,"+<$dev") || die "ERROR: el archivo de salida no existe\n";

# avanzo hasta el offset deseado
if ($opt_o) {
    sysseek(RW,$offset, 1);
}

my $totalwrite = 0;

while(read(STDIN, $buf, $buffsize) and ($totalwrite < $count) ){
    if ($opt_x) {
        # proceso la entrada como representacion ascii de hexa
        $writedata = pack("H*",$buf);
    } else {
        $writedata = $buf;
    }
    
    # utilizo llamada de escritura no buffereada
    $totalwrite += syswrite(RW, $writedata, length($writedata));
}

close(RW);

Es importante mencionar que se utilizan las funciones sysseek y sysread de modo de evitar el uso de los buffers de entrada y salida.

8.5. Recursos PCI del sistema operativo Linux

8.5.1. Disponibilidad de código fuente

El mejor recurso que posee el sistema operativo Linux para el desarrollo de controladores de hardware (drivers), es la disponibilidad de absolutamente todo su código fuente. La facilidad de poder entender a fondo cómo está implementada cada función utilizada, da otro dimensión al desarrollo de software. No es necesario hacer suposiciones ni estar adivinando el porqué de algunos comportamientos aparentemente arbitrarios. Basta con tomarse el tiempo y leer el código.

También la existencia de muchos drivers funcionales que forman parte de la distribución del sistema operativo permite aprender de dichos ejemplos.

8.5.2. Herramientas para dispositivos PCI

Linux posee abundantes herramientas para el desarrollo, testeo y debugging de hardware PCI. Mencionamos a coninuación algunas de las más utilizadas en nuestro proyecto.

8.5.2.1. /proc/pci

El archivo especial /proc/pci contiene un listado de todos los dispositivos PCI presentes en el sistema e información específica de cada uno. Entre la información desplegada se encuentra:

Es muy util para obtener información rápidamente.

8.5.2.2. /proc/bus/pci

El sistema de archivos especial /proc/bus/pci permite obtener y modificar las configuraciones de los dispositivos PCI presentes. Su uso directo no es sencillo y es recomendable utilizar herrameintas para modificar estos valores, como ser setpci (mencionado más adelante).

8.5.2.3. pciutils

El paquete pciutils (que forma parte de la mayoría de las distribuciones de kernel 2.4) contiene dos aplicaciones extremadamente útiles: lspci y setpci .
Dichas aplicaciones permiten inspeccionar y configurar los dispositivos conectados al bus PCI, y realizan la mayoría de sus operaciones a través del sistema de archivos /proc/bus/pci

La aplicación lspci despliega información sobre todos los buses PCI presentes en el sistema, y los dispositivos que estén conectados en ellos.

La aplicación setpci permite obtener y modificar valores de configuración de los dispositivos PCI conectados. Esta apliación permite hacer cambios en los registros de configuración de un dispositivo en forma muy sencilla. Fue utilizada extensivamente en las primeras etapas de desarrollo del core PCI.

8.5.3. Medición del tiempo

En ciertos casos es necesario poder medir intervalos de tiempo de ejecución. En nuestro proyecto fue necesario medir intervalos de tiempo para poder obtener valores sobre la performance del sistema.

Si los intervalos a medir no son demasiado pequeños, se puede utilizar el valor de la variable jiffies. Este contador es incrementado cada vez que se genera una interrupción de timer. La frecuencia de interrupción de timer es dependiente de la plataforma y está defifida en por el valor de la constante HZ .
Hay que tomar en cuenta que el contador de jiffies normalmente hace overflow cada aproximadamente 16 meses de ejcución.

Si el intervalo a medir es muy pequeño o se necesita mucha precisión en los números, se pueden utilizar recursos dependientes de cada plataforma, como ser el uso de algún registro específico del procesador. La mayoría de los CPU modernos incluyn un contador de alta resolución que es incrementado en cada ciclo de reloj. Este contador permite medir intervalos en forma precisa.
En el caso de la arquitectura x86, el registro se denomina TSC (timestamp conuter), y fue introducido con la linea de procesadores Penium. Es un registro de 64 bits que cuenta los ciclos de reloj del procesador. Puede ser leído indistintamente desde el espacio de kernel y desde el espacio de usuario.

Es necesario incluir el cabezal asm/msr.h para poder accederlo con los siguientes macros:

Para dar una idea, en un procesador de 1GHz, la mitad baja de 32 bits del registro hace overflow cada 4.25 segundos.

Para obtener el tiempo en segundos al que corresponden tantos ciclos de procesador, se puede utilizar la variable current_cpu_data.loops_per_jiffy, que es el resultado de un calculo llamado bogomips que se realiza al iniciar el sistema. Esta variable indica cuantos ciclos de CPU hay en un jiffy, que es 1/HZ segundos (en la arquitectura x86, generalmente, un jiffy equivale a una centésima de segundo).

Un macro util para realizar la conversión de ciclos de cpu (loops) a segundos es el siguiente:

/* loops to usecs */
#define LOOPS2USECS(loops) \ 
   (((double)loops* ((double)1000000/((double)HZ* \ 
   (double)current_cpu_data.loops_per_jiffy))))

Un macro que facilita el cálculo de la tasa de transferencia es el siguiente:

#define KBYTESPERSEC(bytes, loops) \ 
   ((((double)(bytes)/(double)loops)* \ 
   (HZ*current_cpu_data.loops_per_jiffy))/((double)1024

8.5.4. MTRR

En la familia de procesadores Intel P6 (Pentium Pro, Pentium II, Pentium III y posteriores) exiten registros llamados MTRR (Memory Type Range Registers) que son utilizados para controlar el acceso por parte del procesador a ciertos rangos de memoria. Estos permiten que el procesador optimice las operaciones para los diferentes tipos de memoria accedidos, como ser RAM, ROM, frame buffers, dispositivos mapeados en memoria, etc.

Esto es de gran utilidad cuando se tiene un dispositivo con grandes areas de memoria para ser accedidas, ubicado en el bus PCI o en el AGP y su manejo de los accesos es desordenado. Es de particular uso para aquellos dispositivos que poseen frame buffers, en especial las tarjetas de video.
Generalmente los servidores X normalmente modifican estos registros.

Mediante la configuración de dichas areas de memoria en modo write-combining (WC) se permite que las transferencias en el bus sean agrupadas antes de ser enviadas, aumentando la performance de las escrituras hasta en 2 veces o más.
La memoria configurada como de tipo write-combining no es cacheable en los caches L1 o L2, lo cual es coherente con el uso que se le quiere dar, ya que no tiene sentido guardar informacion en un cache de datos que pertenencen a un dispositivo de entrada-salida.

Los procesadores Cyrix 6x86, AMD K6-3, AMD Athlon, también poseen registros que proporcionan un funcionamiento similar.

La configuración de dichos registros se hace modificando el archivo especial /proc/mtrr.

Un ejemplo de configuración es el siguiente:

echo "base=0xf8000000 size=0x400000 type=write-combining" > /proc/mtrr

Dicha configuración hace que el rango de memoria del espacio PCI desde la dirección 0xf8000000 hasta la 0xf8400000 trabajen en modo write-combining.

Esto se comprueba mediante el siguiente comando:

cat /proc/mtrr

reg00: base=0x00000000 (   0MB), size= 128MB: write-back, count=1
reg01: base=0xf8000000 (3968MB), size=   4MB: write-combining, count=1


Para elmininar, por ejemplo , la configuración del registro 1 se debe ejecutar el siguiente comando:

echo "disable=1" > /proc/mtrr

8.6. Conclusiones

Los objetivos planteados fueron alcanzados, ya que se logró desarrollar un driver PCI para el sistema operativo Linux.

El proceso permitió descrubrir la infinidad de herramientas existentes para el desarrollo y prueba de dispositivos PCI en dicho sistema operativo, amparado por las licencias de libre distribución.

Attachment: Action: Size: Date: Who: Comment:
memory_spaces.jpg action 86604 03 Dec 2003 - 18:16 CiroMondueri memory spaces en una lectura