Lenguajes de Programación

Creando accesores en Python

Python tiene una característica que son las propiedades, pero al no conocerlas bien y ser particulares de este lenguaje, he creado una minúscula aplicación que genera el código para iniciar y acceder (get y set) a las propiedades de un objeto.

Esencialmente, es una función a la que le pasamos el nombre de las propiedades y su valor inicial, por ejemplo:

buildAccessor([
        (‘configfile’,\’/etc/foo.conf\’),
        ])

Y esto nos genera el código que debemos incluir en el constructor, así como los métodos get y set:

self.__configfile = ‘/etc/foo.conf’

        def getConfigfile(self):
                return self.__configfile

        def setConfigfile(self, value):
                self.__configfile=value

Con lo que si hacemos una prueba:

class objectFoo:
        def __init__(self):
                self.__configfile = ‘/etc/foo.conf’

        def getConfigfile(self):
                return self.__configfile

        def setConfigfile(self, value):
                self.__configfile=value

if __name__==‘__main__’:
        test=objectFoo()
        print(‘test.getConfigfile() says \’ + test.getConfigfile() + \’)
        test.setConfigfile(‘/home/linz/.foo’)
        print(‘Now, test.getConfigfile() says \’ + test.getConfigfile() + \’)

Resulta esto:

pablo@golgi:~/Desktop/py-pwgen$ python test_buildAccessor.py
test.getConfigfile() says ‘/etc/foo.conf’
Now test.getConfigfile() says ‘/home/linz/.foo’
pablo@golgi:~/Desktop/py-pwgen$

He decidido ponerle una página estática, por que puede ser que en el futuro amplíe el trabajo, o incluso que lo suba un SVN.

Output
Python

Comments (0)

Permalink

Reserva de memoria en Linux

Ya sabemos que en C las reservas de memoria se hacen mediante la llamada al sistema malloc() y similares, pero cómo opera realmente el kernel es algo que puede no estar del todo claro.

Mediante una prueba de concepto voy a ir mostrando cómo se comporta un kernel Linux estándar, sin configuraciones especiales. Me voy a basar en el concepto de Tamaño del Conjunto Residente (Resident Set Size, RSS).

El Conjunto Residente en el conjunto de páginas de nuestro proceso que se encuentran en memoria física, que será igual o inferior al conjunto de páginas de nuestro proceso (cada página o bien está en memoria física, o bien en el archivo de intercambio).
Hay que recordar que para que puede accederse a una información o ejecutarse código, debe estar en memoria física, y que los algoritmos de gestión de memoria del S.O. tienden, cuando necesitan localizar un nuevo hueco de memoria libre, a enviar las páginas menos usadas al espacio de intercambio.

Inicialmente tenemos un programa que arranca:

#include
#include
#include
#include

void mi_pause(const char *str);

int main(int arg, char **argv)
{
int pagesize=sysconf(_SC_PAGE_SIZE);
int num_pag=10000,i;
char *buffer;
time_t t0;
short d_t=30;
float d_p=0.1;

/*Sin reservar memoria*/
mi_pause("El RSS deberia ser minimo");

Hasta aquí, si vemos su RSS:

pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
352 mem

Hasta este punto, este programa ha sido cargado en memoria por el cargador del sistema operativo, junto con las librerías requeridas si es que no estaban ya cargadas.

Así que el proceso tiene asignadas al menos una serie de páginas donde está el código del programa y otras páginas donde está la pila (el kernel Linux por defecto asigna una pila de 8K a los procesos, y en la arquitectura i386 las páginas son de 4K, luego dos páginas para la pila). Ahora mismo desconozco si tendrá más secciones, aparte de código y pila, pero todas estas páginas suman 352k, tal y como nos indica ps.

Se sigue ejecutando el código:

/*Tras reservar memoria*/
buffer=(char *)malloc(num_pag*pagesize);
if(!buffer) {
perror("Error en malloc()");
exit(EXIT_FAILURE);
}
mi_pause("El RSS deberia seguir siendo minimo");

Ahora reservamos memoria, 10000 páginas en el heap (malloc() reserva en el heap o montículo, que es una tercera sección de memoria). En i386 eso son 40000k. Aparentemente, cabe esperar que el RSS haya subido de 352k que teníamos antes a 40352k, aproximadamente:

pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
388 mem

Pues no, tenemos tan sólo 36k más, 9 páginas. Y las otras 9991, ¿dónde andan?

El comportamiento por defecto del kernel Linux es proporcionar las páginas bajo demanda, es decir, que mientras no vayas a usar la página, ¿para qué molestarse en hacer que esté disponible? (esto se llama deferred page allocation, y pareceser que AIX también disponía de un sistema como éste)

Si tú solicitas –malloc()– la memoria y no has superado el límite que tiene establecido tu usuario, etc., el sistema te dice que sí, que vale, que te da la memoria, pero en realidad aún no ha ido a buscarte una página libre.

Cuando por fin accedes a esa página que es legítimamente tuya, es cuando el S.O. se molesta en recorrer toda la lista de páginas que tiene, encontrar una libre –si la hay, y si no, ya se sabe: elegir una candidata y mandarla al espacio de intercambio para dejar un hueco libre–, marcarla como tuya, y devolver el control a tu proceso.

O resumidamente: que hasta que no accedes a las páginas no son tuyas del todo, y por tanto no pertenecen a tu conjunto residente.

Ventajas:

  • Ahorro de tiempo ante procesos que reservan mucho y usan poco.
  • Ahorro de tiempo en el arranque de un proceso (que es donde se suele reservar memoria), al contrario que durante su ejecución (donde se usa esa memoria y hay que localizarla, si es que llega a usarse).
  • Permite aprovechar al máximo el sistema, a expensas de tener una menor estabilidad. En sistemas de misión crítica esto puede no ser deseable, y de hecho creo que hay una opcion del kernel para desactivar este comportamiento.

Voy a tratar de explicar este último punto con el siguiente escenario:
Sea un sistema con 64M de RAM (y nada más de memoria), si el proceso A reserva 50M (aunque sólo usa 20M de esas 50M), y a continuación el proceso B reserva 30M y usa enteros esos 30M.

En total ambos procesos tratan de reservar 80M, de las cuales la máquina no dispone, así que en principio cabe pensar que no se pueden ejecutar a la vez ambos procesos.

En cambio, con el enfoque que hemos visto, el kernel le localiza 20M al proceso A (aunque le informe de que le ha reservado 50M) y 30M al proceso B, dando un total de 50M localizadas (abarcables por la máquina).

Si ahora el proceso A que sólo usaba 20M y que está conviviendo con B, poco a poco empieza a aumentar su consumo de memoria hasta usar por entero esos 50M, ¿qué ocurre? Pues sencillamente que no hay sitio para los dos procesos (situación Out-of-Memory) y al menos uno debe morir.

¿Quién se encarga de hacer justicia? El OOM Killer ¿Bajo qué política? Eso es más complicado, y en el enlace anterior se muestra un fragmento de código que explica un poco los fundamentos, aunque hay quien discrepa con este sistema.

¿Qué ocurre si mientras A engorda, a B le da tiempo a acabar? Pues todos felices y contentos y hemos podido ejecutar dos procesos que de otra manera no podríamos.

Sigamos ejecutando:

/*Tras acceder a la memoria*/
for(i=0;i
buffer[i*pagesize]=0;
}
mi_pause("Ahora deberia ser maximo");

El código ahora se dedica a poner el primer byte de cada página reservada a 0. Ahora sí que accedemos a las páginas que habíamos solicitado con malloc().

El resultado es que ahora nuestro RSS engorda estrepitosamente:

pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
40384 mem

Ahora sí que hemos hecho la reserva efectiva.

Sigamos ejecutando:

/*Tras acceder sólo al 10% de las páginas durante 30s. mientras otra aplicación accede a mucha memoria*/
printf("Ahora se esta accediendo al %f%% de las paginas reservadas\n",d_p);
printf("– Comprueba el RSS mientras se accede (%hds.) –\n",d_t);
printf("– Correr paralelamente un devorador de memoria ayudara –\n");
t0=time(NULL);
while(time(NULL)
for(i=0;i
buffer[i*pagesize]=0;
}
}

Nos dedicamos, durante 30 segundos, a acceder al primer 10% de las páginas que reservamos, es decir, las primeras 1000 páginas.

¿Cuál es el objetivo de esto? Que si ahora otros procesos requieren grandes cantidades de memoria, podamos ver cómo el S.O. nos quita páginas del conjunto residente y las vaya enviando al espacio de intercambio.

El S.O. va a quitar primero las menos utilizadas, que son ese 90% que no estamos accediendo. Si los otros procesos no requieren demasiada memoria, lo que nos quite probablemente no llegue al 90%.
Si requieren muchísima, puede superarlo, dando lugar a que constantemente haya que estar metiendo y sacando páginas del espacio de intercambio, ya que parte de las que se han metido sí se usan con frecuencia (hiperpaginación o thrashing).

Para comer memoria he hecho devorador.c, que se reserva (y accede durante 20 segundos) 200M de memoria (estoy ejecutando todo esto en una máquina virtual con 256M de RAM):

#include
#include
#include
#include

int main(int argc, char **argv)
{
size_t size=200*1024*1024, offs;
char *buffer;
unsigned short pagesize=sysconf(_SC_PAGE_SIZE);
time_t t0, d_t=20;

buffer=(char *)malloc(size);
if(!buffer){
perror("Error en malloc()");
exit(EXIT_FAILURE);
}

offs=0;
t0=time(NULL);
while(time(NULL)
buffer[offs]=0;
offs=(offs+pagesize)%size;
}

free(buffer);

exit(EXIT_SUCCESS);
}

Si paralelamente a la ejecución del código anterior, ejecutamos el devorador en otra terminal, podemos ver como el RSS decae:

pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
23268 mem
pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
17484 mem

No llega hasta el 10% de los 40000k que (más o menos) teníamos, pero se ve cómo la mitad de las páginas se han ido al espacio de intercambio.

/*Tras liberar memoria*/
free(buffer);
mi_pause("Ahora deberia volver a ser minimo");

exit(EXIT_SUCCESS);
}

void mi_pause(const char *str)
{
if(str)
printf("%s\n",str);

printf("– Comprueba el RSS y dale a intro –\n");
getchar();
}

Y ya para acabar liberamos la memoria reservada, visualizamos cómo disminuye el RSS:

pablo@linux-rt:# ps ax -o rss,ucmd | grep mem
188 mem

y finalizamos el programa.

Espero que con esto se hayan refrescado o dado a conocer algunos conceptos sobre gestión de memoria de los sistemas operativos, y más particularmente sobre el deferred page allocation de Linux, y sus ventajas e inconvenientes.

Ficheros: mem.c y devorador.c

C
Linux
Sistemas operativos

Comments (0)

Permalink

Averiguar el tamaño de página

Una entrada breve, pero que creo que puede ser útil, ya que hasta hace unos años estuve mucho tiempo sin saber cómo hacer esto.

A veces, en ciertas aplicaciones, es útil e incluso necesario saber el tamaño de página que usa la arquitectura sobre la que estamos trabajando. Funciones como mmap y mlock en ocasiones requieren conocer este valor.

Para ello existe la llamada al sistema sysconf (definida en unistd.h). Esta llamada recibe un argumento que indica qué se quiere saber acerca del sistema, por ejemplo, sysconf(_SC_PAGE_SIZE) nos devuelve el tamaño de página. Puede fallar con ciertos argumentos, en cuyo caso devolverá -1 y fijará errno a EINVAL.

La lista de atributos del sistema que se pueden conocer está resumida en la correspondiente página del manual para esta llamada al sistema, desglosada por los estándares a los que pertenecen. Por ejemplo, nos indica que _SC_PAGE_SIZE está soportada por la versión 2 de la Single Unix Specification, así que es posible que en sistemas que no sigan SUSv2 este atributo tenga otro nombre, o haya que leerlo de otra manera.

Como ejemplo plasmo aquí un cacho de código que obtiene este valor, y su ejecución en dos arquitecturas diferentes:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{

  /*Definido en SUSv2*/
  printf("sysconf(_SC_PAGE_SIZE)=%ld\n",
         sysconf(_SC_PAGE_SIZE));

  exit(EXIT_SUCCESS);
}

Compilando y ejecutándolo en una UltraSPARC-250:

-bash-3.00$ ./sysconf-pagesize
sysconf(_SC_PAGE_SIZE)=8192
-bash-3.00$ uname -a
SunOS <xxx> 5.10 Generic_120011-12 sun4u sparc SUNW,Ultra-250

vemos que las páginas son de 8K.

En cambio, en un i686 (realmente un i686 virtualizado sobre vmware, pero eso da igual porque la arquitectura por supuesto no cambia):

-bash-3.00$ ./sysconf-pagesize
sysconf(_SC_PAGE_SIZE)=4096
-bash-3.00$ uname -a
Linux <xxx> 2.6.26-1-686 #1 SMP Sat Jan 10 18:29:31 UTC 2009 i686 GNU/Linux

tenemos páginas de 4K (lo esperable, ¿no?).

C
Sistemas operativos

Comments (0)

Permalink

Python: argumentos identificados en una función

Observemos el siguiente código:

# -*- coding: UTF-8 -*-

def foo(bar=False):
        return ‘"bar" vale "%s"’%(bar)

if __name__==‘__main__’:
        print (‘Cuando no se especifica el argumento, toma el valor por defecto:\n\tfoo() retorna ‘+foo())
        print (‘Cuando sí se especifica el argumento, toma el valor especificado:\n\tfoo(bar=\’baz\’) retorna ‘+foo(bar=‘baz’))

La función foo está definida de manera que recibe un parámetro llamado bar. Tal y como está especificada, si al llamarla no se le pasa ese parámetro, bar tomará el valor por defecto False. Por ejemplo, con la llamada foo(), sin más, bar pasa a ser False, con lo que la función retorna “bar” vale “False”.

Si sí se le pasa ese argumento, la función simplemente usa el valor proporcionado:

>>> import wx_formas
>>> wx_formas.foo(3)
‘"bar" is "3"’
>>> wx_formas.foo(None)
‘"bar" is "None"’
>>> wx_formas.foo([1,2,3])
‘"bar" is "[1, 2, 3]"’
>>>

La ejecución del código original da el siguiente resultado:

Cuando no se especifica el argumento, toma el valor por defecto:
        foo() retorna "bar" vale "False"
Cuando sí se especifica el argumento, toma el valor especificado:
        foo(bar=‘baz’) retorna "bar" vale "baz"

También se le puede pasar un número variable de argumentos a una función, bien con def foo(*tupla_de_argumentos), o bien con def foo(**diccionario_de_argumentos).

En el libro Learning Python, de Mark Lutz, hay más información acerca de esto (capítulo 16, pág. 331 en la tercera edición, de octubre de 2007 en inglés).

Por darle un poco más de chicha al artículo, diré que acabo de encontrar la Py Zine, revista dedicada enteramente a Python.

Python

Comments (3)

Permalink

Aprendiendo Python: ejercicios con excepciones (i)

Del libro Learning Python, de Mark Lutz. Ejercicios de la parte VII, acerca de las excepciones en Python.

El ejercicio 1, aunque están resueltos en el propio libro, pongo mi solución por aquí y la comento:

  • Enunciado: Escribir una función llamada oops que lanza una excepción IndexError. Después, escribir otra función que llame a oops dentro de una estructura try/except para capturar la excepción. ¿Qué ocurre si oops lanzara la excepción KeyError? ¿De dónde vienen los nombres KeyError e IndexError? (Pista: recuerda que todos los identificadores no calificados vienen de uno de los cuatro ámbitos, según la regla LEGB)
  • Mi solución:

    def oops(excepcion):
        raise excepcion

    def caller():
        try:
            oops(KeyError)
        except IndexError:
            print("Received an IndexError exception")
        except KeyError:
            print("Received an KeyError exception")

    if __name__=="__main__":
        caller()

  • La voy a ir comentando:
def oops(excepcion):
    raise excepcion

Define una función llamada oops que recibe un único argumento referenciado por el identificador excepcion. Notar que no se define de qué tipo debe ser el argumento, si lo que hiciera nuestra función funcionara igual con enteros que con floats no tendríamos que programar dos funciones diferentes, nos valdría con la misma.

En el cuerpo de la función, raise lanza una excepción. De esta manera, se consigue que se deje de ejecutar la función oops nada más hacer el raise, y la función que llamó a ésta reciba la excepción.

def caller():
    try:
        oops(KeyError)
    except IndexError:
        print("Received an IndexError exception")
    except KeyError:
        print("Received an KeyError exception")

Define la función caller, que no recibe argumentos. En esta función hay un bloque try/except. En este tipo de bloques se ejecuta el cuerpo del try, y si durante la ejecución se recibe alguna excepción, se comprueba con los bloques except que haya (al menos uno).

En este caso el primer bloque except comprueba si se recibió la excepción IndexError, en cuyo caso ejecuta la sentencia print() que se ve ahí. El print() va con paréntesis porque es la sintaxis exigida por Python 3000 (recién salido del horno), y soportada por versiones anteriores. Hasta ahora había sido habitual verlo como está pero sin esos paréntesis.

Esta función sencillamente llama a la función oops(), y le pasa como parámetro una excepción KeyError. Esa función lanzará entonces una excepción KeyError, que será recibida por el except KeyError de caller() (dado que la llamada a oops() se hizo dentro del try y que existe un except que captura esa excepción). Finalmente imprimirá el mensaje correspondiente según la excepción capturada.

if __name__=="__main__":
    caller()

__name__ es una variable built-in que indica el nombre del módulo que se está ejecutando. Si se trata del programa principal, tiene el valor ‘__main__’. Este código es habitual verlo en módulos, donde suele tener la función de ejecutar una serie de test sobre el propio módulo cuando es ejecutado en vez de cuando es incluido en otro script.

Por otra parte, ya contestando a la última pregunta, KeyError e IndexError son dos identificadores de sendas clases derivadas de la clase Exception. Tanto Exception, como sus derivadas KeyError e IndexError están alojadas en el built-in, es decir, que vienen de por sí en el lenguaje, igual que viene el tipo lista o la función print().

La regla LEGB hace referencia a Local, Enclosing function locals, Global y Built-in. Es decir, que python busca los identificadores de las variables en esos ámbitos, por ese orden: primero en variables locales (los argumentos también son variables locales) dentro de la función, luego en variables locales de funciones dentro de las que está definida la función actual, luego a nivel global, y por último, si no lo encontró en ningún nivel anterior, busca en el built-in.
Así, sí hay una variable global foo y otra local del mismo nombre e una función, al hacerle referencia desde la función, se hará referencia a su variable local, y no a la global.

Nota: la identación de código en WordPress la he hecho con el plugin Code-snippet. Y no es recomendable usar el modo visual con él, ya que se cepilla los espacios/tabulaciones.

Python

Comments (0)

Permalink