Pasos recomendados luego de instalar PostgreSQL en un sistema operativo linux:

Por lo general para empezar a usar un PostreSQL con algun cliente para browsear la base de datos (ej: pgAdmin3) necesitamos crear un usuario para poder autenticarnos contra el motor de base de datos. Esto podemos hacerlo de la siguiente manera:

Desde nuestro usuario hacemos:

salaboy$ su

para ganar acceso de root (administrador del sistema) y luego vamos a necesitar pasarnos al usuario postgre con:

root# su postgre

Y luego para crear usuarios ejecutamos la siguiente linea:

createuser -P -s -e salaboy

Esto nos creara un nuevo usuario de base de datos que tendra las siguientes caracteristicas:

  • Sera superusuario (-s)
  • Nos pedira la password inmediatamente (-P)

Con esto ya tenemos el usuario y la contrasenia con la que accederemos desde el cliente. Ahora nos falta solo un paso para que la configuracion este completa. Nos falta indicarle al motor en que ip y puerto estara escuchando cuando levante y falta aclararle al motor desde que ips se podran acceder al mismo.

Para esto editamos 2 archivos (en mi Ubuntu Hardy):

$ vi /etc/postgresql/8.3/main/pg_hba.conf

y

$vi /etc/postgresql/8.3/main/postgresql.conf

En el primero pg_hba.conf vamos a especificar que IPs van a poder acceder al motor cambiando la siguiente linea:

# IPv4 local connections:
host    all         all        127.0.0.1/24         md5

por:

# IPv4 local connections:
host    all         all        0.0.0.0/0         md5

eso es si queremos que todos los usuarios del planeta puedan conectarse. Si solo queremos que se conecten los usuarios de nuestra red privada la linea quedaria asi:

# IPv4 local connections:
host    all         all        192.168.0.0/24         md5

Esto es si nos encontraramos en la red 192.168.0.x

En el segundo archivo (postgresql.conf) vamos a indicarle al motor en que IP o IPs va a ponerse a escuchar cuando el mismo levante. Para esto modificamos la siguientes lineas:

listen_addresses = ‘localhost’        # what IP address(es) to listen on;
# comma-separated list of addresses;
# defaults to ‘localhost’, ‘*’ = all
# (change requires restart)
port = 5432                                    # (change requires restart)

por:

listen_addresses = ‘*’        # what IP address(es) to listen on;
# comma-separated list of addresses;
# defaults to ‘localhost’, ‘*’ = all
# (change requires restart)
port = 5432                                    # (change requires restart)

En este caso estamos indicando que el motor va a estar escuchando en el puerto 5432 y en todas las IPs que tenga nuestro servidor (esto es localhost, y todas las de las placas o IPs virtuales que tengamos)

Espero que sirva.. mas que nada lo hice para documentar el proceso.. Si alguien tiene algun otro paso recomendado de seguridad o de configuracion mas avanzada. Porfavor postee un comentario! se agredece!

Con el siguiente comando creamos un backup de la base de datos especificada en el archivo especificado en la line.
/usr/bin/pg_dump -i -h <host> -p <port> -U <user> -F c -b -D -v -f “<filename>.backup” “<database>”

Ya faltan menos de 40 dias para el realease oficial de Hardy Heron! La nueva version de Ubuntu!

Para los que no puedan esperar aca les dejo los repositorios todavia en beta para que puedan hacer su distro upgrade!

/etc/apt/source.list:

deb http://archive.ubuntu.com/ubuntu/ hardy main

Cuando quieran saber si tienen algun problema de DNS

Para consultar como resuleve una direccion un servidor de DNS utilicen el siguiente comando:

host <direccion> <servidor de DNS>

eJ: host www.google.com 200.69.193.1

Lo que les devuelve algo como lo siguiente:

Using domain server:
Name: 200.69.193.1
Address: 200.69.193.1#53
Aliases:

www.google.com is an alias for www.l.google.com.
www.l.google.com has address 64.233.161.103
www.l.google.com has address 64.233.161.104
www.l.google.com has address 64.233.161.147
www.l.google.com has address 64.233.161.99

Tambien poseemos el comando dig que nos brinda mucha mas informacion sobre la resolucion de dominios

A pesar de que algunos dispositivos puede ser controlados usando solo sus regiones de I/O, la mayoria de los dispositivos son un poco mas complicados que eso. Los dispositivos tienen que lidiar con el mundo exterior, que seguido incluye cosas como spinning disks, moving tapes, cables a lugares distantes, y mucho mas.

Ya que la mayoria de las veces el procesador queda a la espera de eventos externos, tiene que haber una manera de que el dispositivo le haga saber al procesador que algo ha sucedido.
Esta forma por supuesto son las interrupciones. Una interrupcion es una senial que el hardware puede mandar para captar la atencion del procesador.
Por lo general un dirver registra su manejador de interrupciones para las interrupciones de su dispositivo y sabe como manejarlas bien cuando estas llegan.

Hay que notar que las interrupueciones por naturaleza corren concurrentemente con otro codigo.

INSTALANDO EL MENJADOR DE INTERRUPCIONES
Tenemos que configurar un manejador de interrupciones en el sistema, ya que si al kernel no se le ha dicho que estamos esperando interrupciones, el simplemente las ackea y las ignora.

Las lineas de interrupciones son un mecanismo presiado y limitado (ya que solo hay 15 o 16 lineas). Por lo cual se espera que el modulo pida un canal de interrupciones (o IRQ, interrupt request) antes de usarlo y luego cuando termine lo suelte. Y en muchas ocasiones se espera que los modulos compartan las lineas de interrupciones con otros drivers. Las siguientes funciones, declaradas en
implementan la registracion de interrupciones

int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void*, struct pt_regs*) unsigned long flags, const char *dev_name, void *dev_id);

FLAGS:
• SA_INTERRUPT: fast interrupts -> son ejecutadas con todas las interrupciones deshabilitadas
• SA_SHIRQ: seniala que las interrupciones puede ser compartidas
• SA_SAMPLE_RANDOM: indica que las interrupciones contribuyen al pool de entropia del sistema.

El lugar correcto para llamar a request_irq es cuando el dispositivo es abierto por primera vez, antes de que el hardware sea instruido a generar interrupciones. Y el lugar para llamar a free_irq es la ultima vez que el dispositivo es cerrado, luego de que al hardware se le ha dicho que no interrumpa al procesador. La desventaja de esta tecnica es que necesitamos llevar la cuenta de cuantas veces abrimos el device asi podemos saber cuando las interrupciones pueden ser desactivadas.
(llamar a request_irq cuando inicializamos el modulo es en vano, y mas si no compartimos el canal de interrupciones, ya que el numero de interrupciones es limitada y estariamos limitando a otros dispositivos a usarlas)

Tambien tenemos( en la familia i386 y x86_64) una funcion para consultar la disponibilidad de lineas de interrupciones es:
int can_request_irq(unsigned int irq, unsigned long flags);

LA INTERFACE /proc:
cada vez que una interrupcion de hardware llega al procesador, un contador interno es incrementado, proveyendo una manera de revisar si el dispositivo esta funcionando como se espera (/proc/interrupts)

COPIAR EJEMPLO!!!!!!!!!!!!!!!!!!!!!!!!!!

AUTO DETECTANDO EL NUMERO DE IRQ
Uno de los problemas mas complicados a la hora de inicializacion de un driver es determinar que linea de IRQ va a ser usada por el dispositivo. El driver necesita esta informacion en orden de instalar el manejador, aunque el programador puede pedirle al usuario el numero de IRQ en tiempo de load del modulo, esto no deberia ser asi ya que el usuario no deberia saber este numero, por lo tanto la auto deteccion del numero de interrupcion es un requerimiento basico para la ubicacion del driver.

Hay algunos dispositivos avanzados que anuncian que IRQ van a usar. En estos casos debemos leer un byte de status en los puertos de I/O o en el espacio de configuracion PCI.

Desafortunadamente, no todos los dispositivos anuncian que IRQ van a usuar, por lo tanto, vamos a tener que probar que IRQ usa. Esta tecnica es bastante sencilla, el driver le dice al dispositivo que genere interrupciones y ve que pasa. Si todo va bien solo una linea de IRQ se activa.

Hay 2 formas de probar:
• Kernel assisted probing: el kernel ofrece formas low-level de probing para los numeros de interrupciones. Esta funcion solo funciona para interrupciones no compartidas, pero la mayoria del hardware es capaz de trabajar en modo de interrupciones compartidas, y esta funcion nos ayuda aunque sea a encontrar el numero de interrupcion actualmente configurada. Esta funcionalidad consta de 2 funciones:
? unsigned long probe_irq_on(void); ->devuelve una mask de las interrupciones desasingnadas. Luego de esta llamada tenemos que hacer que el dispositivo genere al menos una interrupciones
? int probe_irq_off(unsigned long): -> luego de que el dispositivo genra una interrupcion, el driver llama a esta funcion pasandosel la bit mask devuelta por probe_irq_on. Esta funcion devuelve el numero de interrupciones que ocurrieron entre probe_irq_on y el probe_irq_off. Si devuelve 0 es que no ocurrio ninguna, Si mas de una interrupcion ocurrio devuelve un nro negativo ya que la deteccion es ambigua.
? EJEMPLO!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
• PROBING A MANOPLA: Nosotros podemos hacerlo a mano sin mucho lio. Es raro tener que hacerlo asi pero vamos a ver la filosofia. El mecanismo es el mismo que el descripto antes: habilitamos todas las interrupciones no usadas, luegos vemos que pasa. Nosotros podemos hacer uso de nuestro conocimiento sobre el dispositivo. Por lo general podemos configurar que un dispositivo elija de un set de 3 o 4, con esto reducimos el numero de pruebas. Para esto tenemos que conocer por adelantado estos posibles numeros.
? EJEMPLO!!!!!!!!!!!!!!!!!!!!!!!

MANEJADORES RAPIDOS Y LENTOS:
Viejas versiones de linux distinguian entre fast y slow interrupts. Una eran las que se atendian realmente rapidas y las otras eran las que demoraban en atenderse. En kernel modernos, esta diferencia desaparecio. Solo queda una: fast interrupt (SA_INTERRUPT flag set) se ejecutan con todas las otras interrupciones deshabilitadas en el procesador actual. Hay que notar que los otros procesadores pueden seguir manejando IRQ, pero nunca veremos a 2 procesadores manejando la misma IRQ al mismo tiempo.

IMPLEMENTANDO UN HANDLER:
Hasta ahora hemos visto como registrar un manejador de interrupciones pero no como escribir uno. Pero la verdad es que no hay nada raro en escribir uno, es codigo C normal. Lo unico peculiar es que el handler corre en tiempo de interrupciones y por eso tiene algunas restricciones de lo que puede hacer y lo que no.
• Un handler no puede transferir datos desde o hacia el espacio de usuario, porno no se ejecuta en contexto de un proceso.
• Los handlers tampoco pueden hacer nada que duerma, algo como llamar a wait_event. Allocar memoria con otra que no sea GFP_ATOMIC, o bloquear un semaforo.
• Finalmente los handlers no puede llamar a schedulle.

El rol de un handler de interrupciones es dar feedback al dispositivo acerca de la interrupciones que se recibio y leer o escribir datos segun el significado de la IRQ.

Una tarea tipica es despertar procesos que estan durmiendo esperando una senial que los desperara, por ejemplo el arribo de datos.

El programador debe ser cuidadoso al escribir rutinas que se ejecuten en una minima cantidad de tiempo. Ya que si se requiere realizar muchos computos, el mejor approach es el uso de tasklets o workqueues.

EJEMPLO!!!!!!!!!!!!!!!!!!!!!!!

ARGUMENTOS Y VALOR DE RETORNO DEL HANDLER:
3 argumentos son pasados: IRQ, dev_id, y regs.
• IRQ -> numero de irq, util para el log
• *dev_id -> algun dato del cliente, es el void * que se le pasa a request_irq
• struct pt_regs *regs -> es raramente usada. COntiene un snapshot del contexto del procesador antes de entrar aen el codigo de la interrupcion

EL handler debe devolver un valor indicando si habia una interrupciones para manejar. Si el handler encuentra el device, entonces, se necesita atencion y se devuelve IRQ_HANDLED. Sino IRQ_NONE.

HABILITANDO Y DESHABILITANDO INTERRUPCIONES:
Hay veces donde el device driver debe bloquear la interrupcion por un (esperemos que corto) periodo de tiempo. Pasa seguido que las interrupciones deben ser bloqueadas cuando tenemos un spinlock para evitar deadlocks. Hay formas de deshabilitar las interrupciones sin involucramos con los spinlocks. Pero hay que notar que deshabilitar las interrupciones es algo muy fuera de lo comun, y no deberia usarse nunca como mecanismo de mutex.

DESHABILITANDO UNA SOLA INTERRUPCION:
Hay veces (muy pocas) donde necesitamos deshabilitar una linea especifica de interrrupciones. Para esto el kernel ofrece un set de funciones. (). Hay que notar que no podemos deshabilitar lineas compartidas (y en los sistemas modenos son las mas normales). void disable_irq(int irq)

DESHABILITANDO TODAS LAS IRQ
En el k 2.6 podemos deshabilitar todas las irq del procesador actual:
void local_irq_save(unsigned long flags); -> deshabilita y guarda el estado anterior de las flags
void local_irq_disable(void); -> no guarda el estado

Para volver a habilitar:
void local_irq_restore(flags)
void local_irq_enable();

TOP y BOTOM HALVES:
Uno de los mayores problemas en el manejo de IRQ es cuando tenemos que llevar a cabo tareas muy largas en el handler, ya que cuando el manejador se ejecuta se bloquea las entrantes.
Hay 2 necesidades (trabajo y velocidad) y etan en conflicto. Y queda como tarea del programador dar prioridades.
En linux este problema se resuleve dividiendo al handler en dos partes:
• Top -> request_irq -> es la que recibe la irq y la schedullea para ejecutar la tarea en un tiempo no atomico y seguro. (se ejecuta con todas las irq desactivadas)
• Bottom -> es schedullado para ejecutarse luego cuando sea seguro(se ejecuta con todas las irq activadas)

TASKLETS !!!!!!!!!

WORKQUEUES!!!!!!!!!!

 

Hasta ahora solo hemos usado kmalloc y kfree para la allocacion y liberacion de memoria. Sin embargo el kernel ofrece un set completo de primitivvas para la allocacion de memoria.

LA HISTORIA REAL DE KMALLOC:
El motor de allocacion Kmalloc es una herramienta poderosa y facil de aprender ya que es similar a malloc. Esta funcion es rapida (pero bloquea) y no limpia la memoria que obtiene.

EL ARGUMENTO FLAG:
si recordamos el prototipo de kmalloc:
#include<linux/slab.h>
void *kmalloc(size_t size, int flags)
El primer argumento es el tamanio del bloque a allocar. El segundo argumento son las flags de allocacion, que son muy interesantes porque controlan el comportamiento de kmallloc.
La flag mas utilizada es GFP_KERNEL., que significa que la allocacion *internamente realizada por la llamada a __get_free_pages) es realizada en medio de un proceso corriendo en el kernel. En otras palabras esto significa que la funcion llamante ejecuta un syscall en el medio de un proceso.
Usar GFP_KERNEL significa que kmalloc pone al proceso actual a dormir a la espera de una pagina cuando es llamado en situaciones donde ha poca memoria disponible. Por lo tanto una funcion que alloca memoria debe ser reentrante y no debe estar corriendo en un contexto atomico.

Mientras el proceso duerme, el kernel realiza las acciones necesarias para encontrar memoria libre, y esto lo logra flusheando los buffers a disco o swapeando memoria de algun proceso de usuario.

A persar de todo esto GFP_KERNEL no siempre es la flag mas adecuada. Puede pasar que el proceso actual este corriendo en un contexto atomico y no pueda ponerse a dormir, entonces debemos usar GFP_ATOMIC. Con lo cual el kernel trata siempre de tener algunas paginas libres en orden de poder satisfacer las allocaciones atomicas.

ZONAS DE MEMORIA:

El kernel de linux conoce como minimo 3 zonas de memoria
• Memoria DMA-capable
• Memoria Normal
• High Memory

Cuando las allocaciones suceden por lo general en la memoria normal, seteando alguna flag podemos cambiar a una zona diferente.

La memoria en la zona DMA-capable (direct memory access) es memoria que vive en un rango preferencial donde los perifericos puede realizar dma access.

High memory es un mecanismo usado para permitir el acceso a (relativamente) grandes cantidades de memoria en sistemas de 32 bits.

Cuando una nueva pagina es allocada para completar un request de allocacion de memoria, el kernel construye una lista de las zonas que puede usar para buscar paginas libres. Esta busqueda se limita dependiendo de las flags (__GPF_DMA o __GPF_HIGHMEM)

EL ARGUMENTO SIZE:

El kernel administra la memoria fisica del sistema, que esta disponible solo en chucks de tamanio de paginas. Como resultado kmalloc a la hora de implementacion luce diferente a malloc. Ya que el kernel usa una tecnica de allocacion orientada a paginas para obtener el mejor uso de la ram del sistema. Linux maneja la allocacion de memoria creando un pool de objetos de memoria de un tamanio fijo. Entonces los request de allicacion son manejados yendo al pool que tiene suficientes objetos, agarrando todo un chunk y devolviendoselo a que lo habia pedido.
Una cosa que hay que tener en cuenta es que el kernel puede allocar solo ciertos bytes array de tamanio fijo predefinidos. POr esto si nosotros pedimos una cantidad arbitraria de memoria probablemente obtengamos mas (hasta casi 2 veces mas es posible)

LOOK ASIDE CACHE: (en vez de usar kmalloc)

Por lo general un device driver termina allocando muchos objetos del mismo tamanio una y otra vez. Dado que el kernel ya mantiene un set de pools de objetos del mismo tamanio, por que no agregamos un pool para estos objetos del device driver? En realidad el kernel ya implementa una solucion que nos permite crear este tipo de pool que son llamados look aside cache.

El administrador de cache en el kernel de linux se suele llamar “SLAB ALLOCATOR” por esta razon las funciones y los types se encuentran declaradas en <linux/slab.h>. El slab allocator implementa caches de tipo kmen_cache_t, que son creadas con la funcion:

kmem_cache_t *kmem_cache_create(const char *name, size_t size, size_t offset, unsigned long flags, void (*constructor) (void*,kmem_cache_t *, unsigned long flags), void (*destructor)(..));

Esta funcion crea nuevos objetos de cache que puede contener cualquier numero de areas de memoria todas del mismo tamanio, especificado por el argumento size.

UNa vez que creamos el cache de objetos, podemos allocar objetos en el llamando:

void *kmem_cache_alloc(kmem_cache_t *cache,int flags)
cache es el creado anteriormente y flags son las mismas que a kmalloc.

Cuando queremos liberar a un objeto de la cache:
void kmem_cache_free(kmem_cache_t *cache, const void *obj);

y cuando queremos destruir la cache:
int kmem_cache_destroy(kmem_cache_t *cache);

POOLS DE MEMORIA:

Hay lugares en el kernel donde la allocacion de memoria no puede falla. Una manera de garantizar las allocacion en ess sitauciones son los mempools. Un mempool es un look aside cache que trata de mantener siempre una lista de memoria libre para usar en casos de emergencia. Los mempools son de tipo mempool_t (<linux/mempool.h>) y creamos uno con:

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data);
donde min_nr es el numero minimo que siempre tiene que tratar de tener libre. y las fn son las que realmente se encargan de la allocacion. Donde los prototipos de estas son:
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data) donde pool_data es el mismo puntero que se le pasa a mempool_create

typedef void *(mempool_free_t)(void *element, void *pool_dada);

Si necesitamos algun comportamiento especial para la allocacion de memoria escribimos estas funciones, pero por lo general dejamos al slab allocator del kernel que haga esta tarea por nosotros. Ya hay 2 funciones que cumplen con el prototipo de mempool_alloc_t y mempool_free_t que se llaman mempool_alloc_slab y mempool_free_slab:

EJ:
cache=kmem_cache_create(….);
pool=mempool_create(my_pool_minimun, mempool_alloc_slab, mempool_free_slab, cache);

Una vez que el pool se creo los objetos se allocan y liberan con:

void *mempool_alloc(mempool_t *pool, int gfp_mask)
void mempool_free(void *element, mempool_t *pool)

Lo bueno de los memory pools es que podemos resizearlos con:
int mempool_resize(mempool_t *pool, int new_min_nr, int gfp_mask);

GET_FREE_PAGES Y SU PANDILLA:

Si nuestro modulo necesita allocar grandes chunks de memoria, es mejor usar una tecnica orientada a paginas. Para allocar paginas tenemos las siguientes funciones:
get_zeroed_page(unsigned int flags) -> nos devuelve un puntero a una nueva pagina llena de 0s
__get_free_page(unisigned int flags) -> igual pero no limpia la memoria
__get_free_pages(flags, unsigned int order) -> cantidad de paginas que pedimos order=0 => 1 pag, order = 3 => 8 pag, nos devuelve paginas contiguas

VMALLOC Y SU PANDILLA -> demasiado loco

Los drivers del mundo real por lo general tienen que lidiar con muchas cosas como timing, administracion de memoria, acceso a hardware, y demases. En este capitulo se vera coom lidiar con problemas de timing. Tratar con cuestiones de tiempo involucra las siguientes tareas, listanes en orden creciente de complejidad:
• Medir laspos de tiempo y comprara tiempos
• Conocer el tiempo actual (wall clock)
• Restrasar operaciones por un cierto periodo de tiempo
• Planificar funciones asincronas ocurran en otro momento

MIDIENDO LAPSOS DE TIEMPO:
EL kernel conoce el paso del tiempo por medio de los timers interrupts. Las interrupciones de timer son generadas por el sistema (hardware de timming) en intervalos regulares. Estos intervalos son programados en tiempo de booteo por el kernel dependiendo del valor de la variable HZ, que es dependiente de la arquitectura. La mayoria de las plataformas corren a 100 o a 1000 interrupciones por segundo. Como regla general, a pesar de conocer el valor de HZ, nunca debemos usar este valor cuando programamos y diseniamos un driver.

Cada vez que una interrupcion de timer ocurre, el kernel aumenta un contador interno. Este contador es inicializado a cero cuando el sistema bootea. entonces este numero representa el numero de tick de nuestro reloj desde que se booteo por ultima vez. Este contador es una variable de 64 bits y es llamada jiffies_64. Sin embargo por lo general accedemos desde nuestro driver a una variable llamada jiffies que es un unsigned long.

USANDO EL CONTADOR JIFFIES:
cuando necesitamos calcular un timestamp futuro tenemos que leer el contador como se muestra en el siguiente ejemplo:

#include <linux/jiffies.h>
j=jiffies;
stamp_1=j+HZ -> 1 segundo en el futuro
stamp_half=j+HZ/2 -> medio segundo en el futuro
stamp_n=j+n*HZ/1000 -> n milisegundos en el futuro

Para comprar valores guardados de jiffies tenemos que usar alguna de las siguientes macros:

#include <linux/jiffies.h>
int time_after(unsigned long a, unsigned long b)
int time_before(unsigned long a, unsigned long b)
int time_after_eq(unsigned long a, unsigned long b)
int time_before_eq(unsigned long a, unsigned long b)

Estos macros funcionan convirtiendo los valores a signed longs y luego restandolos y comparando los resultados:

diff=(long)t2 – (long)t1;

Y podemos convertir los jiffies a milisegundos con:
msec=diff *1000/HZ

Pero aveces necesitamos intercambiar representaciones de tiempo al espacio de usuario. Y lo hacemos con la estructura Timeval y TimeSpec. Entonces el kernel provee 4 macros de conversion:
time_spec_to_jiffies, jiffies_to_time_spec, time_val_to_jiffies y jiffies_to_timeval

Acceder a la variable de 64 bits no es tan facil como acceder a jiffies (neceistamos usar => u64 get_jiffies_64(void))

Finalmente notemos que la frecuencia del clock por lo general se encuentra oculta del espacio de usuario, el unico lugar visible de HZ para los usuarios es en /proc/interrupts

REGISTROS ESPECIFICOS DE PROCESADOR
Cuando necesitamos medir intervalos de tiempo muy cortos o necesitamos mucha presicion, podemos recurrir a recursos dependientes de la plataforma, donde estariamos eligiendo presicion sobre portabilidad del codigo.

La mayoria de los procesadores modernos incluyen un registro de contador que es incrementado una vez por cada ciclo de CPU.

El registro de contador mas conocido es TSC (TimeStamp Counter) que se puede leer desde el espacio de kernel y desde el espacio de usuario.

Luego de inlcuir <asm/msr.h> (Machine specific registers) podemos usar las siguientes macros:

rdtsc(low32, high32) -> lee el valor de los 64 bits en 2 variables
rdtscl(low32) -> lee la parte menos significativa
rdtscll(var64) -> lee todo y lo pone en un long

Leyendo la parte menos significativa de tsc nos alcanza para los usos mas comunes de TSC. a que una CPU de 1GHz overflowea la variable cada 4.2 segundos, y si estamos necesitando presicion es raro que lleguemos a los 4.2 segundos.

Otras plataformas ofrecen otras funcionalidades y el kernel ofrece una que es independiente de la plataforma.

#include <linux/timex.h>
cycles_t get_cycles(void);

La ultima cosas a tener en cuenta sobre los timestamps counter es que no estan sincronizados a travez de los procesadores en un sistema SMP. Para estar seguro de obtener un valor coherente, debemos deshabilitar el preemption en el codigo que esta consultando al contador.

CONOCIENDO EL TIEMPO ACTUAL:
Es raro que un driver necesite saber la hora del dia, expresada en meses, dias, minutos, … ya que esta informacion por lo general se maneja a nivel de usuario.
A pasar de esto el kernel nos da una funcion que tranforma una fecha/hora a jiffies:

long mktime(unsigned int year, mon, day, hour, min, seg);

Hay veces que necesitamos lidiar con timestamps absolutos y para esto usamos la funcion gettimeofday. Que cuando es llamada nos llena un puntero a una struct timeval.

void do_gettimeofday(struct timeval *tv);

DEMORANDO EJECUCIONES:

Los device drivers por lo general necesitan demorar la ejecucion de un pedazo de codigo en particular por un periodo de tiempo, usualmente para permitir que el hardware realice alguna tarea. En esta seccion cubriremos diferentes tecnicas para lograr demoras (delays)

LONG DELAYS:
Periodos largos -> mas de un clock tick

Empezamos con las tecnicas mas simples y vamos hacia las mas avanzadas

BUSY WAITING (espera ocupada): (trabaja mirando el contador de jiffies)

Si queremos demorar la ejecucion multiples clock ticks, la manera mas simple , pero no recomendada, de hacerlo es un loop que monitore el contado de jiffies. Por lo general se ve de la siguiente manera:
while(time_before(jiffies, j1)) -> j1 es hasta donde queremos hacer el delay
cpu_relax(); -> le decimos al sistema que no estamos haciendo mucho uso del procesador

Este codigo es tecnicamente correcto (y funciona bien para lo que se disenio), pero este loop degrada la performance del sistema. Sino configuramos a nuestro kernel para operaciones preemptives, este loop bloquea al procedor por la duracion del delay. (o sea hasta que lleguemos a j1)

CEDIENDO EL PROCESADOR: (tambien trabaja mirando el contado de jiffies)

Como vimos recien busy waiting realiza una gran carga para el sistema, vamos a tratar de buscar una mejor tecnica. Y el primer cambio que se viene a la mente es explicitamente ceder el CPU cuando no estamos interesados en el. Esto lo logramos llamando a la funcion schedule();
while(time_before(jiffies,j1))
schedule();
Esto tampoco sigue siendo optimo. Ya que el proceso actual no hace nada mas que soltar el CPU, y seguir estando en la cola de procesos a ejecutar. Por lo tanto si es el unico proceso con estado running, el scheduler lo llama y el vuelve a la cola y asi sucesivamente.

Este problema se pone peor en un sistema ocupado, ya que el driver puede terminar esperando mas de lo esperado. Ya que una vez que el proceso suelta el CPU con la llamada scheduller, no hay garantias de que el proceso vuelva al cpu en un tiempo calculable. Por lo tanto vemos que llamar a schedulle no es una manera segura para dar solucion a este problema.

TIMEOUTS: (trabaja pidiendole al kernel)
La mejor manera de implementar un delay es pidiendole al kernel que lo haga por nosotros. hay 2 formas de setear timeouts basados en jiffies, dependiendo si el driver espera por otros eventos o no.
Si nuestro driver usa una wait_queue_head para epserar algun otro evento, pero queremos estar seguros de que el proceso correra dentro de un cierto periodo de tiempo, podemos usar las ya vistas wait_event_taimeout y wait_event_interruptible_timeout. Estas dos funciones duermen en la cola de espera, pero vuelve a ejecucion cuando el timeout espresado en jiffies expira.
Estas 2 funciones fueron diseniadas con los drivers de hardware en mente, ya que la ejecucion puede ser resumida en 2 formas:
• Alguien llama a wake up en la cola
• el tiempo expira
Para acomodarnos a la situacion donde no se espera ningun otro evento el kernel nos ofrece schedule_timeout(), con esto nos ahorramos la declaracion y el uso de una wait_queue_head.
#include <linux/sched.h>
signed long schedule_timeout(signed long timeout) -> nro de jiffies a demorar.
La unica restriccion de schedule timeout es que tenemos que setear el proceso antes de llamarlo:

set_current_state(TASK_INTERRUPTIBLE);
schedulle_timeout(delay);

DELAYS CORTOS:
Cuando un driver de dispositivo necesita lidiar con latencias de hardware, los delays involucrados son por lo general unas cuantas docenas de milisigundos como mucho.
En este caso, basarnos en los ticks de reloj no es la manera de afrontar las cosas.

PAra esto el kernel nos provee de las siguientes funciones:

#include <linux/delay.h)
void ndelay(unsigned long nsecs); ->nano segs
void udelay(unsigned long usecs); ->micro segs
void mdelay(unsigned long msecs); -> mili segs
Estas 3 funciones anteriores son busy waiting
Y se proponen 3 que no lo son:

void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millesecs);
void ssleep(unsigned int seconds);

 

Los dispositivos reales ofrecen muchas mas funciones que read y write sincronos. Vamos a analizar algunos de los conceptos para entender como escribir un fully featured char device driver. Vamos a empezar con la syscall ioctl que es una interfaz comun para el control de dispositivos. Luego veremos varias formas de mantenernos sinconizados con el usuario (espacio de usuario). Y para finalizar veremos como poner a un proceso a dormir y luego despertarlo, tambien veremos como implementar I/O sin bloqueo.

IOCTL:
La mayoria de los drivers necesita la habilidad de varios tipos de controles de hardware via el device driver.(cosas aparte de read y write). Muchas veces el usuario va a requerir por ejemplo, cerrar la puerta del dispositivo, expulsar el disco, reportar un error, cambiar el baud rate, etc. Estas operaciones por lo general son soportadas por el metodo IOCTL, que implementa la syscall del mismo nombre.

En espacio de usuario el prototipo de este metodo es:
int ioctl(int fd, unsigned long cmd, …)
Los puntos suspensivos no son varargs, es solo para evitar el type check del compilador. Suele ser un char *argp. Y cmd es el comando que queremos ejecutar.

En el espacio de kernel el metodo ioctl difiere un poco del de espacio de usuario:

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

Los punteros inode y filp corresponde al file descriptor pasado en el metodo del usuario, el parametro cdm se pasa sin cambios y el parametro opcional se pasa en forma de unsigned long. Como era de esperarse la mayoria de las implementaciones de ioctl consiste en un gran switch que elige el comportamiente segun el cmd enviado. Cada comando tiene un numero distinto, que se relaciones a nombres mediante definiciones de preprocesador. Estos por lo general se declaran en un .h aparte.

ELIGIENDO LOS COMANDOS IOCTL:

Debemos escoger los numeros que se corresponderan con los comandos. Lo que uno normalmente haria seria escoger numeros empezando del 0,1,… pero no!!! ya que los ioctl deben ser unicos en todo el sistema. Entonces tenemos que seguir una convencion. Que indica que el numero se componera con los siguiente:
• type(magic number): hay que elegir un numero que no este siendo usado. Para esto miramos en ioctl-numbers.txt
• Number: numero en orden secuencial
• Direction: la direccion de transferencia de datos (_IOC_NONE,_IOC_READ, _IOC_WRITE …)
• Size: el tamanio de los datos de usuario

Como es normal los desarrolladores del kernel hay desarrollado macros para la decodificacion de los numeros:
_IOC_DIR(nro), _IOC_TYPE(nro), _IOC_NR(nro), _IOC_SIZE(nro)
• *********COPIAR EJEMPLO DE SCULL******************

VALOR DE RETORNO:

Como la implementacion por lo general es un swtich surge la pregunta de cual es la accion del valor default del switch. Muchas implementaciones devuelven -EINVAL (Invalid Argument), que tiene sentido, pero el standard propone la devolucion de -ENOTTY (inappropiate ioctl for device)

COMANDOS PREDEFINIDOS:

A pesar de que las sys calls ioctl por lo general se usa para actuar sobre dispositivos, hay un set de comandos que son reconocidos por el kernel. Hay que notar que estos comandos tienen prioridad sobre los que nosotros definimos, por eso si elegimos un mismo ioctl number el nuestro nunca se llamara.

Estos comandos predefinidos se dividen en 3 grupos:
• Aquellos que pueden ser tratados en cualquier archivo (regular device, fifo, o socket)
• Aquellos que pueden ser tratados solo en archivos regulares
• Aquellos especificos al tipo de fs
Los escritores de driver solo se interesan en el primer grupo que tiene el magic number T. Los siguientes comando ioctl estan predefinidos para cualquier archivo:

• ***********LISTA

USANDO LOS ARGUMENTOS de IOCTL

Otra cosa importante a ver es como usamos el argumento extra del metodo ioctl. Que si es un entero lo podemos usar facilmente, pero si es un puntero tenemos que tener cuidado. Cuando un puntero se refiere al espacio de usuario, debemos asegurar que la direccion es valida. Para esto usamos la funcion llamada access_ok: (<asm/uaccess.h>):

int access_ok(int type, const void *addr, unsigned long size)

Donde el primer argumento debe ser VERIFY_READ o VERIFY_WRITE. *addr contiene la direccion de espacio de usuario y size es un byte count.

Esta funcion no realiza un trabajo completo de verificar el accesso a memoria, solo chequea si la memoria referenciada en esa region es razonablemente accesible. Access_ok asegura que la direccion no apunta a memoria del espacio de kernel.

CAPACIDADES Y OPERACIONES RESTRINGIDAS:

El acceso a un dispisitivo esta controlado por los permisos de los device files, y los drivers no estan normalmente involuctrados en el checkeo de permisos. Hay casos donde los usuarios tienen permisos de escritura/lectura pero algunas otras operaciones las tienen restrigidas. En estos casos el driver debe realizar algunos chequeos adicionales en pos de saber si el usuario puede  realizar la operacion requerida.

BLOCKING I/O

Hasta ahora hemos visto como implementar read y write, pero hemos salteado una cuestion importante: como responde el driver si no puede satisfacer inmediatamente el request???
Por ejemplo: si viene una llamda a read y no hay datos para leer, pero se esperan en el futuro. En un caso asi el driver debe bloquear al proceso y ponerlo a dormir hasta que el request pueda continuar.

INTRODUCCION A SLEEPING:

Que significa que un proceso duerma? cuando un proceso es puesto a dormir, es marcado con un estado especial y removido de la cola del scheduller. Hasta que el estado no sea cambiado por alguien, el proceso no va a ser schedulado y por lo tanto no correra.

Hay que tener una reglas claras para hacer que el codigo duerma de una manera segura.

Primera regla: “Nunca dormir si estamos en un contexto atomico”
Lo que se traduce en no podemos dormir cuando:
- Tenemos un spinlock, un seqlock, o un RCU lock.
- Tenemos las interrupciones desactivadas
- Podemos dormir cuando tenemos un semaforo pero hay que tener cuidado ya que podemos bloquear al mismo proceso que nos despierta.

Otra cosa que debemos recordar es que cuando nos despertamos, nunca vamos a saber cuanto tiempo estuvimos fuera del cpu o que ha cambiado en este tiempo. Puede que otro porceso que haya estado durmiendo por el mismo evento se haya despertado y nos haya robado los recursos que nosotros necesitamos y debemos volver a dormir. Por lo tanto cuando nos despertamos debemos que chequear el estado del contexto y de los recursos que nos interesan.

Otra cosa relevante es que el proceso no puede dormir a menos que este seguro de que alguien lo va a despertar. El codigo encargado de la despertacion tiene que ser capaz de encontrar al proceso a despertar.

Para esto existen las wait queues! que son como listas de procesos todos esperando por un mismo evento.

En linux las wait queues son manejadas en el termino de wait_queue_head_t  (<linux/wait.h>)

estaticamente: declare_wait_queue_head(name)

dinamicamente:
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);

SLEEPING SIMPLE:

La manera mas simple de domir en el kernel de linus es la macro llamada wait_event, que combina el manjo de los detalles de sleeping con el chequeo de la condicion por la que el proceso esta esperando. Hay 4 formas de wait_event:
- wait_event(queue, condition)
- wait_event_interuptible(queue,condition)
- wait_event_timeout(queue,condition,timeout)
- wait_event_interruptible_timeout(queue,condition,timeout)

donde queue es la wait_queue_head_t y condition es una expresion booleana que se evalua antes y despues de dormir.
En las timeout se espera un tiempo medido en jiffies y si no ha ocurrido el evento se devuelve el valor resultado de la expresion booleana.

Por otra parte tenemos la funcion de wake_up:

void wake_up(wait_queue_head_t *queue)
void wake_up_interruptible(wait_queue_head_t *queue)

Hay que notar que estas dos funciones despiertan a todos los procesos que estan en la cola.

• **********COPIAR EJEMPLO!!!!

OPERACIONES BLOQUEANTES Y NO BLOQUEANTES:

Un ultimpo punto a ver es cuando ponemos a un proceso a dormir. Hay veces cuando implementamos las coas de manera correcta se requiere que las operaciones no bloqueen, a pesar de que esto signifique que no se termine la tarea.
Tambien tenemos la situacion donde el proceso llamante nos informa que no quiere que ocurra un bloqueo, sin importar si su I/O puede progresar o no. Para explicitar esto usamos O_NONBLOCK (open non block) en filp->f_flags y se especifica en tiempo de OPEN.
EN caso de operaciones bloqueantes (son por defecto bloqueantes) el siguiente comportamiento debe implementarse en orden de seguir los stadares semanticos:
• Si un proceso llama a read pero no hay datos (todavia), el proceso debe bloquear. El preceo debe despertar tan pronto como lleguen los datos, y los datos son devueltos al llamante, a pesar de que los datos sean menos de los expresados con el argumento count.
• Si un proceso llama a write y no hay espacio en el buffer el proceso debe bloquear, y debe ponerse en otra cola que la usada por read. Cuado algunos datos sean escritos al dispositivo, y hay espacio en el buffer, el proceso se despierta y write se comleta con exito. a pesar de que puede no haber escrito todos los datos especificados en count.

EL comportamiendo de read y write es distinto si se especifica O_NONBLOCK. En este caso se devuelve -EAGAIN (TRY AGAIN). Las operaciones no bloqueantes devuelven un valor inmediatamente. Solo read y write son afectados por O_NONBLOCK

EJEMPLO DE BLOQUEO I/O

!!!!!!!!PONER IMAGENES

CONCEPTOS AVANZADOS DE SLEEPING

Hay situaciones donde se necesita tener un conocimiento profundo de como funcionan las colas de espera en linux.

COMO DUERME UN PROCESO:

Si miramos dentro de <linux/wait.h> podemos ver que la estructura de datos wait_queue_head es muy simple. Consiste en un spinlock y una lista linkeada. Lo que va en esa lista son las wait queue entries. Que estan declaradas con el tipo wait_queue_t. Esta estructura contiene informacion acerca de los procesos que estan durmiendo y exactamente como hay que despertarlos.

El primer paso para poner a un proceso a dormir es la allocacion y la inicializacion de una wait_queue_t structure, seguido por la adicion a la wait_queue correspondiente. Cuando esta todo en su lugar , cualquiera que sea el encargado de despertar al proceso podra encontrarlo y realizar su tarea.

El siguiente paso es cambiar el estado del proceso a “Durmiendo”, hay varios estados definidos en <linux/sched.h>. TASK_RUNNING es que el proceso puede correr, lo cual no significa que este corriendo en ningun tiempo especifico. Y hay dos para indicar que se esta durmiendo TASK_INTERRUPTIBLE y TASK_UNINTERRUPTIBLE.

Para cambiar el estado usamos: void set_current_state(int new_state) -> no se donde llamamos esto?

Con este codigo solo cambiamos el estado del proceso, y esto solo no pone el proceso a dormir directamente. Con esto logramos que el scheduler cambie la forma de tratar al proceso, pero todavia no cedemos el procesador.

Ceder el procesador es el ultimo paso, pero una una cosa que hacer antes, y esto es chequear que la condicion para dormir sea todavia cierta. Esto nos ahorra generar una condicion de corrida, ya que si la condicion se cuelve cierta (o sea no se debe dormir) justo cuando  estamos por ceder el procesador vamos a perder el wake up y podemos dormir para siempre.
Entonces : if(!condition)
schedule(); -> cedemos el procesador ya que el proceso tiene el estado de durmiendo.

Si el if no es cierto y no se llama a schedule tenemos que realizar una limpieza, cambiar el estado del proceso a TASK_RUNNING y borrarlo de la cola.
Si entra a schedule esta limpieza no es necesaria, ya que para salir del schedule necesita que alguien le cambie el estado.

MANUAL SLEEP: no va no????

EXCLUSIVE WAITS:
Como hemos visto cuando un proceso llama a wake up en una wait_queue_head, todos los procesos que estan esperando se ponen en TASK_RUNNING. En muchos casos este es el comportamiento correcto. En otros casos, en cambio, es posible saber de ante mano que un solo proceso de los que seran despertados va a ser el que va a obtener un recurso deseado, y el resto van a dormir de vuelta. Cada uno de estos procesos peleara por el recurso en cuestion y explicitamente volveran a dormir. Entonces si el numero de porcesos en una wait queue es grande, va a haber una rafaga de procesos peleando por el recurso y esto degrada significativamente la performance del sistema.
En respuesta a esto, los desarrolladores del kernel han desarrollado exclusive waits. Una exclusive wait actua como un sleep normal, con dos diferencias importantes:
• Cuando una wait queue entry tiene la WQ_FLAG_EXCLUSIVE seteada, es agregada al final de la wait_queue. Y las que no la tienen seteada al principio.
• Cuando un wake_up es llamado en una wait_queue, para luego de despertar al primer proceso que tenga WQ_FLAG_EXCLUSIVE seteada.

El resultado final es que los procesos marcados con exclusive wait son despertados de a uno a la vez, en una manera ordenada, y el kernel sigue despertando a todos los procesos que no estan marcados.
Para emplear exclusive waits en un driver hay que considerar dos condiciones:
• Esperamos que haya mucha pelea por recursos.
• Despertar un solo procesos es suficiente para completar el consumo de un recursos cuando se pone disponible.

Poner a un proceso en un wait interruptible es tan facil como:

void prepare_to_wait_exclusive(wait_queue_head_t *queue, wait_queue *wait, int state);

Esto lo llamamos en vez de prepare_to_wait, y setea el flag de exclusive y lo agrega al final de la lista.
Hay que no tar que no se puede hacer un exclusive wait con wait_event y sus variantes.

DETALLES DE WAKING UP:
El comportamiento resultante cuando un proceso es despertado es controlado por una funcion en la wait_queue entry. y es mucho mas complejo de lo que se vio antes. El comportamiento default de wake up setea el estado del proceso en TASK_RUNNABLE y posiblemente realiza un cambio de contexto si el proceso tiene prioridad alta. Hay una lista de otros wait que probablemente nunca necesitemos.

 

Concurrencia es cuando el sistema trata de hacer mas de una cosa a la vez. Y el manejo de esta concurrencia es uno de los core problems en la programacion de Sistemas Operativos.
Los bugs relacionados con la concurrencia son los mas faciles de crear y los mas dificiles de encontrar.

En los primeros kernel habian relativamente pocas fuentes de concurrencia (ya que SMP no era soportado) y la unica causa de ejecuciones concurrentes era el uso de interrupciones de hardware.
Pero en respuesta al nuevo hardware y las nuevas aplicaciones el kernel evoluciono y muchas cosas pasan simultaneamente.

Condiciones de corrida, por otro lado, son el resultado de accesos no controlados a datos compartidos. Estos accesos no controlados producen resultados inesperados (ya que por ejemplo un proceso puede pisar los datos de otro)

CONCURRENCIA Y SU MANEJO:

En los sistemas linux actuales, hay un gran numero de fuentes de concurrencia y por esto posibles condiciones de corrida.

Hay que tener en cuenta de que el codigo de kernel es preemptible (es decir, que el codigo del driver puede perder el procesador en cualquier momento y el proceso que lo reemplaza puede estar corriendo en nuestro driver tambien)

Tambien las interrupciones (eventos asincronos) pueden causar ejecuciones concurrentes de nuestro codigo.

En el mundo hotplugueable de hoy en dia, nuestro dispositivo puede desaparecer mientras estamos trabajando con el.

Entonces como hace el programador de drivers para evitar la creacion de caos absoluto?

• Usa primitivas de kernel para el control de concurrencia
• Tiene en cuenta algunos principios basico anti concurrencia

Las condiciones de corrida, como habiamos dicho antes, vienen como resultado del acceso compartido de los recursos. Cuando dos hilos de ejecucion tienen que trabajar con la misma estructura de datos (o recurso de hardware) la posibilidad de mezcla existe. Entonces primera regla para el disenio de nuestro driver: “Evitar los recursos compratidos cuando sea posible”.
La aplicacion mas obvia de esta regla es evitar el uso de variables globales.

El problema es que este compartimiento es por lo general requerido. Los recursos de hardware, por naturaleza, son compoartiudos y los recursos de software or lo general tiene que estar disponibles para mas de un hilo de ejecucion.

Hay que tener en mente que las variables globales no son la unica manera de compartir datos, cada vez que nuestro codigo pasa un puntero a otra parte del kernel, estamos potencialmente creando una situacion de comportacion. “Compartir es un hecho de la vida”.

REGLA DURA DEL COMPARTIMIENTO DE RECURSOS:
“Cada vez que un recurso (hard o soft) es compartido a travez de un hilo de ejecucion, y existe la posibilidad de que un hilo pueda encontrar un estado inconsistente del recurso, debemos explicitamente administrar el acceso al recurso”.

La tecnica mas comun para la administracion de acceso es llamada LOCKING o EXCLUSION MUTUA.

Antes de ver estos temas otra regla importante:
“CUando codigo de kernel crea un objeto que va a ser compartido en cualquier otra parte del kernel, este objeto debe continuar existiendo (y funcionando correctamente) hasta que no haya mas referencias exgternas a el”.

Esta regla trae consigo 2 requerimientos:
• Ningun objeto puede hacerse disponible hasta que este en un estado de correcto funcionamiento.
• Todas las referencias a estos objetos deben ser trackeadas(ref count) (en la mayoria de los casos el kernel se encarga de las referencias pero hay excepciones)

SEMAFOROS Y MUTEXES:

La meta de agregar locking a nuestra aplicacion es que las operaciones sobre nuestras estructuras de datos sean atomicas. Para esto tenemos que definir y marcar las secciones criticas (que son las secciones de codigo que pueden ser ejecutadas por solo un thread a la vez).
Pero no todas estas secciones criticas son iguales. Por esto el kernel provee diferentes primitivas para diferentes necesidades.
En esta contexto usamos mucho la frase “go to sleep”. en los casos donde un proceso llega a un punto dende no puede seguir su flijo de proceso, “se va a dormir” (o se bloquea) cediendo el procesador a alguien mas hasta que en algun punto en el futuro el pueda seguir haciendo su trabajo y se despierte. Por lo general los procesos duermen cuando se quedan esperando que operaciones de I/O se completen. A medida que vamos entrando en las profundidades del kernel veremos bastantes situaciones donde no podemos dormir.

Los semaforos son un concepto bien entendido en las ciencias de la computacion. Como nucleo tenemos que un semaforo es un solo entero combinado con un par de funciones que son tipicamente llamadas P (down) y V (up).

Cuando un semaforo es usado para exclusion mutua – mantener multiples procesos corriendo con una seccion critica simultaneamente – el valor inicial se setea en 1. Este semaforo puede ser posieido por un solo proceso o hilo en un tiempo determinado (esto es llamado MUTEX).

IMPLEMENTACION DE SEMAFOROS EN LINUX (<asm/semaphore.h>)

El tipo mas utilizado es struct semaphore e inicializamos esta struct con:

void sema_init(strcut semaphore *sem, int val);

Donde val es el valor inicial del semaforo.

Para hacer esto mas facil el kernel provee un set de funciones y macros. Por ejemplo para la inicializacion:

declare_mutex(name) -> setea al semaforo de nombre name en 1
declare_mutex_locked(name) ->setea al semaforo de nombre name en 0

Si los mutex van a ser inicializados en runtime:

void init_mutex(struct semaphore *sem)
void init_mutex_locked(struct semaphore *sem)

Luego tenemos la funcion down que decremente el valor del semaforo, esta funcion viene en 3 versiones:

void down(struct semaphore *sem)
void down_interruptible(struct semaphore *sem)
voide down_trylock(struct semaphore *sem)

down decremente el valor del semaforo y espera tanto como deba esperar. down_interruptible hace lo mismo pero la operacion es interumpible (esta es la que mas vamos a usar). Permite al usuario (proceso en el user-space) interrumpir el proceso. “No debemos usar operaciones no interumpibles a menos que no tengamos alternativa”. (las operaciones no interrumpibles son buenas para crear procesos inmatables (dreaded “D state” cuando tiramos un ps)).  Pero ojo que usar down_interruptible requiere un cuidado extra. Ya que si la operacion es interrumpida,se devuelve un valor que no es cero y el que llamo a la funcion deja de poseer el semaforo. POr lo tanto un uso adecuado de down_interruptible requiere siempre que revisemos el valor de retorno y actuemos acordemente en base a el.

La version final de down_trylock nunca duerme, si el semaforo no esta disponible a la hora de llamarlo, down_trylock devuelve inmediantamente un valor no cero.

Una vez que un hilo ha exitosamente llamado a algun version de down. se dice que ese proceso tiene el semaforo. Este hilo ahora tiene el poder de acceder a la seccion critica protegida por el semaforo. Cuando las operaciones que requieren exclusion se completaron el semaforo tiene que ser devuelto con:

void up(struct semaphore *sem)

La clave de usar bien semaforos esta en especificar bien que vamos a proteger.

Semaphore in Scull

SEMAFOROS DE LECTURA/ESCRITURA:

Los semaforos proveen exclusion mutua para todos los llamantes, sin importar que es lo que el hilo quiera hacer. La mayoria de las tareas se pueden dividir en 2 tipos:
• Lectura de los datos protegidos
• Cambios (o escritura) de los datos protegidos

Por lo tanto es posible tener muchos lectores concurrentes, mientras nadie trate de hacer ningun cambio. Hacinedo esto optimizamos la performance, ya que las tareas de solo lectura pueden hacer su trabajo en paralelo sin tener que esperar a que otros lectores salgan de la seccion critica.
Para esto linux provee un tipo espacial de semaforo llamado rwsem (<linux/rwsem.h>)
Y tenemos las funciones para actuar sobre el:

Inicializacion
void init_rwsem(struct rw_semaphore *sem)
Lectura
void down_read(struct rw_semaphore *sem)
int down_read_trylock(struct rw_semaphore *sem)
void up_read(struct rw_semaphore *sem)
y las funciones para escritura
void down_write(struct rw_semaphore *sem)
int down_write_trylock(struct rw_semaphore *sem)
void up_write(struct rw_semaphore *sem)
void downgrade_write(struct rw_semaphore *sem)

Entonces rwsem permite que un escritor o una cantidad ilimitada lectores tenga el semaforo. Los escritores tienen prioridad. Cuando un escritor entra a la seccion critica ningun lector podra acceder. Por esto rwsem es usado cuando el acceso para escritura son por periodos cortos y ocurren casi nunca y tenemos muchos accesos de lectura.

COMPLETIONS:

Un patron comun en la programacion del kernel involucra inicial alguna actividad fuera del hilo actual y luego esperar a que esta actividad se complete. Esta actividad puede ser tanto la creacion de un nuevo hilo de kernel o un proceso de espacio de usuario, un request a algun proceso existent, o alguna accion sobre algun hardware. EN esto casos puede ser tentador usar un semaforo para sincronizar las dos tareas:

EJ:

struct semaphore sem;
init_mutex_locked(&sem);
start_external_task(&sem);
down(&sem);

Luego en external task se llama a up(&sem) cuando se termina esa tarea.
Como se puede ver los semaforos no son la mejor herramienta para usae en esta situacion. ya que en un uso normal el codigo que trata de obtener un semaforo lo encuentra siempre disponible. Si hay una disputa por este semaforo la performas va a sufrir y el esquema de lockeo tendra que ser replanteado. POr eso los semaforos han sido fuertemente optimizados para estos casos de disponiblidad. Cuando usamos para comunica finalizacion de tareas como mostramos antes, el hilo que llama a down siempre tiene que esperar y con esto la performance tambien sufre.

Para solucionar estos problemas surge COMPLETIONS que son un mecanismo liviano que cumple con una sola tarea: “permitir a un hilo decirle a otro que el trabajo esta hecho”. Para usar esta funcionalidad (<linux/completion.h>):

declare_completion(my_completion)
o si queremos crearla e inicializarla dinamicamente:
struct completion my_completion;
init_completion(&my_completion);

Y esperar por la completitud de una tarea es facil:
void wait_for_completion(struct completion *c);

Hay que notar que esta funcion realiza una espera ininterrumplible. Si nuestro codigo llama a esta funcion y nunca nadie termina la tarea, el resultado es un proceso inmatable. POr el otro lado la completacion de una tarea se seniala llamando:

void complete(struct completion *c)
void complete_all(struct completion *c)

Estas dos funciones se comportan distinto si mas de un hilo esta esperando por el mismo evento de completacion.
“complete” despierta a solo uno de los hilos que esta esperando.

SPINLOCKS:

Los semaforos son una herramienta util para exclusiones mutuas, pero no son las unicas herramientas provistas por el kernel. Por esto la mayoria del locking implementado en el kernel esta implementado con un mecanismo llamado spinlock. A diferencia de los semaforos son usado en codigo donde NO SE PUEDE DORMIR, como por ejemplo manejadores de interrupciones. Cuando son correctamente usados, los spinlocks ofrecen mejor performance que los semaforos. Pero ellos traen todo un set de restricciones para su uso.

Spinlock es un concepto simple: es un dispositivo para exclusion mutua que puede tener solo 2 valores (locked y unlocked). usualmente esta implementado como un solo bit en un valor entero.
EL codigo interesado en tomar un lock en particular revisa este bit; si el lock esta disponible, se setea el bit de locked y el codigo continua en la seccion critica. En cambio si el lock ha sido tomado por algien mas el codigo entra en un loop donde repetitivamente revisa el bit de lock hasta que este disponible. Este loop es la parte spin de spinlock.
La operacion de “test y set” debe ser atomica asi un solo hilo puede obtener el lock, a pesar de que varios esten loopeando al mismo tiempo.

INTRODUCCION A LA API DE SPINLOCK (<linux/spinlock.h>)

El tipo importante aca es: spinlock_t y para inicializarlo usamos:
spinlock_t my_lock=spin_lock_unlocked;
o en runtime
void spin_lock_init(spinlock_t *lock);

Y luego antes de entrar en una seccion critica hacemos:
void spin_lock(spinlock_t *lock)

Esta llamada tambien genera esperas ininterrumpibles. Una vez que llamamos spin_lock se va a loopear hasta que el lock este disponible.

Para liberar el lock hacemos:
void spin_unlock(spinlock_t *lock)

SPINLOCKS Y CONTEXTOS ATOMICOS:

• REGLA DE ORO: ” cualquier codigo que tiene un spinlock debe ser atomico. no puede dormir, no puede ceder el procesador por ninguna razon, excepto por las interrupciones (a veces ni siquiera)”
• Cada vez que el codigo del kernel obtiene un spinlock se desactiva el preemption en ese procesador.
Hay que evitar dormir mientras tenemos un spinlock y esto puede ser dificil. Por lo tanto escribir codigo que se ejecuta cuando tenemos un spinlock requiere presetar atencion a cada funcion que llamamos.

Example IRQ

FUNCIONES DE SPINLOCK:
void spin_lock(spinlock_t *lock)

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)
Esta funcion desabilita las interrupciones de hard y soft en el procesador local. Las flags representan al estado anterior de las interrupciones que se deshabilitan

void spin_lock_irq(spinlock_t *lock)

void spin_lock_bh(spinlock_t *lock)
Esta funcion deshabilita solo las interrupciones de soft

Luego tenemos las mismas funciones de unlock

SPINLOCKS LECUTRA/ESCRITURA: exactamente igual que los semaforos. la misma idea..

LOCKING TRAPS:

El concepto y la utilizacion de locking es dificil de entender bien. La administracion de la concurrencia es bastante complicada y hay muchos errores que podemos cometer. Entonces vamos a ver que cosas pueden ir mal.

REGLAS AMBIGUAS:

Un esquema de locking requiere reglas claras y explicitas. Cuando creamos un recurso que puede ser accedido concurrentemente, debemos definir que lock va a controlar el acceso a dicho recurso. Para que nuestro locking funcione bien, tenemos que escribir funciones que tengan el conocimiento de que el que las llamo ya tiene el/los lock(s) relevantes. (sino seguro que causamos un deadlock)

REGLAS DE ORDEN PARA EL LOCKEO

En sistemas con una gran cantidad de locks no es raro tener que obtener mas de un lock a la misma vez. Pero tomar multiples locks a la misma vez puede ser peligroso. POr ejemplo: si tenemos 2 locks: LOCK1 y LOCK2 y el codigo necesita obtener los dos al mismo tiempo podemos llegar a tener un deadlock. Imaginemos que un hilo toma el lock1 mientras que otro hilo toma el lock2. Luego ambos tratan de obtener el que no tiene y ahi se produce un deadlock.
La solucion a este problema es por lo general simple: “cuando debemos obtener multiples locks, siempre debemos adquirirlos en el mismo orden”.

ALTERNATIVAS A LOCKING:

ALGORITMOS LOCK-FREE

A veces podemos re pensar nuestros algoritmos para evitar el uso de locking. En casos donde hay multiples lectores y un solo escritor casi siempre se puede trabajar sin locking. Si el escritor tiene conciencia de que la estructura de datos siempre sea consistente para los lectores, podemos crear una estructura de datos libre de locks.

Una estructura de datos que puede ser usada para tareas de productor/consumidor sin locks son los buffers circulares. Este algoritmos involucra a un productor que pone datos en un extremos de un array, mientras que el consumidor saca del otro extremo. Cuando se llega al fin del array el productor vuelve al principio. Por esto un buffer circular requiere de un array y dos valores que indican donde va el nuevo valor y que valor debe ser removido a continuacion (uno para la lectura y otro para la escritura). El productor es el unico hilo que tiene permiso para modificar el valor del indice de escritura y la posicion donde apunta en el array. Mientras el productor escriba un nuevo valor en el buffer antes de cambiar el valor del indice. El/los lector(es) siempre veran un buffer consistente. Por otro lado el lector es el unico que puede modificar el indice de lectura y acceder al valor que apunta. Con un poco de cuidado de que ninguno de los 2 punteros sobre escriba al otro, el productor y el consumido pueden acceder al buffer concurrentemente sin condiciones de corrida

Locking Free

VARIABLES ATOMICAS:

A veces un recurso compartido es simplemente un entero. Y a este entero queremos incrementarlo con: nro++;  donde esta operacion requeriria locking. Pero todo el esquema de locking parece mucho para manejar solo un entero. Para esto existen la variables de tipo entero atomicas:
atomic_t -> (<asm/atomic.h>)
Entonces tenemos las funciones:
atomic_set(atomic_t *v,int i), atomic_read(..), atomic_add(), atomic_sub(), atomic_inc(), atomic_dec();

OPERACIONES DE BITS:
Atomic_t es bueno para enteros pero poco performante cuando tenemos que manipular bits. Entonces tenemos las mismas funciones que para atomic pero para bit…

SEQLOCKS:

El kernel 2.6 contiene un par de nuevos mecanismos para intentar proveer acceso rapido y sin locks a recursos compartidos. SEQLOCKS funciona en situaciones donde el recurso a proteger es pequenio, simple y frecuentemente accedido, y donde el acceso de escritura es raro pero debe ser rapido. Escencialmente, funciona permitiendo a los lectores el libre acceso a los recursos pero requiriendo que los lectores revisen las posibles colisiones con los escritores, y cuando una colision ocurre, los lectores deben reintentar su acceso.
Los SEQLOCKS generalmente no pueden ser usados para proteger estructuras con datos que contengan punteros, ya que los lectores podrian estar siguiendo punteros invalidos mientras el escritor esta cambiando la estructura de datos.