Hace muchos años, la programación multihilo solo servía para darnos a los usuarios la sensación de multitarea y que el ordenador pudiera respondeer cuando interactuábamos con él. Normalmente sólo teníamos un único núcleo de CPU y solo podía ejecutar una cosa en un momento del tiempo determinado. Aunque el sistema operativo se encargaba de ir cambiando las tareas que iba ejecutando varias veces por segundo, y por eso podíamos estar viendo una página web y al mismo tiempo chateando con otro programa, copiando un archivo, etc. Esta multitarea también nos ayudaba mucho cuando una aplicación estaba esperando un dato del disco o de un dispositivo (ratón, teclado, red...), ya que estos dispositivos son mucho más lentos que la CPU del ordenador. Es decir, mientras la CPU puede procesar millones de operaciones por segundo, pedir un dato a un disco duro, puede tardar lo mismo que tardarían varios miles de operaciones, y si el dato viene de Internet, puede tardar mucho más. Así que los sistemas operativos multitarea, suspenden la ejecución de dicha aplicación hasta que el dato esté listo, y así aprovechan los ciclos de procesador que podían haberse perdido para hacer cosas más productivas como la descompresión de fotogramas de un vídeo de gatitos.

Eso sí, si en una CPU monohilo ejecutamos dos tareas que requieren CPU de forma intensiva como la compresión de vídeo, renderizado 3D o aplicación de filtros sobre imágenes, nos daremos cuenta de que el tiempo total de la ejecución de las tres tareas es aproximadamente el mismo que ejecutar una tarea detrás de otra.

Pero en los tiempos que corren, es muy común encontrar procesadores con varios núcleos, incluso con varios hilos por núcleo. Al tener varios núcleos, podremos ejecutar varios programas y normalmente el sistema operativo se encarga de colocarlos en uno u otro núcleo, de forma que, ya que nuestro procesador puede ejecutar varias cosas a la vez, aprovechemos esa potencia para terminar las tareas antes.
Aunque ahora el problema suele ser que las aplicaciones no están pensadas para ejecutarse en varios hilos. Bueno, actualmente muchas aplicaciones pueden aprovechar la potencia de los procesadores modernos, como compresores de vídeo, software de minería de criptomonedas, tratamiento de imágenes etc. Pero, muchos programas comunes de búsqueda de datos en ficheros, compresión/descompresión, y muchos de nuestros scripts en Bash o Python no están preparados. 


Ejecución de tareas en paralelo en Bash

Cuando, en Bash queremos ejecutar dos tareas a la vez, siempre podemos ejecutarlo con un & al final, volveremos a nuestro shell para ejecutar más cosas mientras el programa inicial se está ejecutando:

$ programa &
19238
$ … ejecuto más cosas …


O también podemos ejecutarlo normalmente, pulsar Control+Z (^Z). El programa se detendrá, si escribimos en la consola "fg" el programa seguirá en primer plano (foreground), pero si ponemos "bg" el programa seguirá en segundo plano (background), y mientras éste está corriendo, podremos ejecutar más cosas:

$ programa
…
^Z
[1]+ Stopped   programa
… ejecuto más cosas …
$ fg
programa
… programa sigue ejecutándose …
^Z
[1]+ Stopped    programa
$ bg
[1]+ programa &
… puedo ejecutar lo que quiera …


GNU Parallel

Con esto puede ser suficiente para muchos de nosotros pero, ¿y si queremos utilizar más hilos? Es un poco pesado ejecutar varios programas con sus argumentos y una & al final. ¿O queremos que los argumentos vayan cambiando según la ejecución? ¿O incluso queremos ejecutar cientos de tareas pero que se vayan repartiendo la CPU de 2 en 2, de 4 en 4 o más dependiendo del número de núcleos que tengamos? Para eso, y mucho más, tenemos la utilidad GNU Parallel.
Un ejemplo típico es crear un programa que calcule dígitos del número PI con un hilo de CPU, es muy fácil con bc:

#!/bin/bash
echo "scale=4000; a(1)*4" | bc -l

Lo salvamos como pi.sh y le damos permiso de ejecución con:

$ chmod +x pi.sh


Este programa, ejecutado en mi ordenador tarda unos 11 segundos en terminar. Suponiendo que nuestro equipo tenga 2 núcleos, podemos hacer:

$ time ./pi.sh & time ./pi.sh


Veremos que tarda también 11 segundos (más o menos). Esto lo podemos hacer con GNU Parallel de esta forma:

$ time seq 2 | parallel -n0 ./pi.sh

Donde se ejecutará el comando pi.sh dos veces y de forma simultánea. Pero, aunque esta orden pueda parecer complicada, podemos jugar un poco con ella, para ejecutar pi 4 veces de forma simultánea:

$ time seq 4 | parallel -n0 ./pi.sh

Si nuestra CPU tiene 4 núcleos, seguirá tardando 11 segundos, en realizar cuatro tareas, pero si nuestra CPU tiene 2 núcleos, tardará más de 22 segundos. Ya que las tareas utilizan mucho la CPU y es un trabajo extra para el sistema operativo intercalarlas para que se ejecuten a la vez. Pero, imaginémonos que queremos ejecutar 4 veces pi, pero que se repartan de 2 en 2, para ello podemos hacer:

$ time seq 4 | parallel -n0 -j2 ./pi.sh

Si queremos, podemos ejecutar un gestor de tareas o el comando "top" con el que veremos qué tareas están en ejecución cada vez.


Varias tareas con argumentos

Ahora vamos a modificar pi.sh así:

#!/bin/bash
DECIMALES=$1
echo "scale=$1; a(1)*4" | bc -l

De modo que tengamos que introducir el número de dígitos que queremos de Pi. Así ejecutaremos pi.sh de forma concurrente pero con parámetros diferentes en cada ejecución. Podemos jacer lo siguiente:

$ time echo "1000 2000 3000 4000" | tr ' ' '\n' | parallel ./pi.sh

O si queremos que solo haya dos ejecuciones de pi.sh simultáneas:

$ time echo "1000 2000 3000 4000" | tr ' ' '\n' | parallel -j2 ./pi.sh

Nota: El uso de tr es porque parallel necesita que los argumentos vayan uno por línea. Si los argumentos los tenemos en un archivo y uno por línea podríamos hacer:

$ cat argumentos | parallel -j2 ./pi.sh


Combinaciones de argumentos

Con GNU Parallel podemos incluso combinar argumentos. En este ejemplo, cambiamos de base varios números binarios a base 10

$ parallel echo -n {1}{2}{3}{4} = \;'echo "ibase=2;{1}{2}{3}{4}" | bc' ::: 0 1 ::: 0 1 ::: 0 1 ::: 0 1
0000 =0
0001 =1
0010 =2
0011 =3
0100 =4
0101 =5
0110 =6
0111 =7
1000 =8
1001 =9
1010 =10
1011 =11
1100 =12
1101 =13
1110 =14
1111 =15

Y como vemos, GNU Parallel, además de proporcionarnos una forma sencilla de ejecutar programas simultáneamente, nos proporciona también una forma de combinar argumentos y generar resultados muy potente. Utilizando {1}, {2},...{n} dependiendo del número de argumento que estemos tratando

Convirtiendo masivamente archivos de JPG a PNG

Uno de mis usos preferidos, es la conversión automática de archivos, o tratamiento de imágenes con ImageMagick. Además, podemos automatizar esta conversión para realizarla en cientos de archivos y despreocuparnos del ordenador ya que él se encarga de hacer el trabajo. Una tarea muy sencilla es convertir un archivo de JPG a PNG con convert. Pero, ¿si tenemos una carpeta llena de archivos? Podemos ejecutar convert en todos ellos, y hacerlo uno detrás de otro, pero podríamos hacerlo, por ejemplo de 2 en 2 o de 4 en 4 dependiendo dee nuestra CPU, incluso podríamos subir a 3 o 5 porque la conversión de archivos implica lecturas y escrituras y es tiempo que podemos estar procesando datos (los archivos convertidos entrarán en el directorio convertidas):

$ find -maxdepth 1 -name*.jpg’ | parallel --progress -j3 convert -verbose {} convertidas/{/.}.png

Fijaos cómo en el segundo argumento le quito la extensión para llamarla png. Podría variar el número del argumento -j según mis preferencias. Además, he introducido --progress que nos indicará el progreso de la tarea.
Como último ejemplo, nos basaremos en el anterior. Pero esta vez, utilizaremos zenity para presentar una barra de progreso de manera gráfica. ¡Porque, que sea terminal no significa que no sea visual!

$ find -maxdepth 1 -name*.jpg’ | parallel --bar convert {} convertidas/{/.}.png 2> >(zenity --progress --auto-kill)

¡Espero que les haya resultado útil!

Viernes, 15 Junio 2018 11:29

Conociendo tu CPU desde el terminal

El mundo de la electrónica estuvo separado por un tiempo de la programación cuando solo se creaban circuitos cableados, pero con la aparición de la electrónica programable (con memorias), el software y la electrónica se han vuelto casi uno. El hardware sin software no tendría demasiada utilidad más que para adornar y el software sin un hardware que lo ejecute tampoco tendría sentido alguno. Por eso me he decidido en este tutorial a unir ambos mundos y exponer una serie de trucos, comandos y maneras de optener información del dispositivo de hardware más importante, el cerebro que procesa toda la información: el microprocesador o CPU (Central Processing Unit).

Ya sabéis que existen muchos programas para obtener información de la CPU y del hardware en general, como hardinfo, etc. Pero no es eso lo que busco con este tutorial, sino describir una serie de comandos y ficheros donde encontrar mucha información del microprocesador e incluso comprender un poco mejor cómo está relacionado con el kernel Linux... Y me gustaría hacerlo de una forma muy directa y sencilla para cualquier nivel de usuario, ya que no quiero entrar en demasiados detalles y explicar ciertos directorios del sistema como /proc, etc. Así que, ¡vamos al grano!

1-Listar información de la CPU

Para ello, se puede usar un comando similar a lsusb que usamos comúnmente para los dispositivos USB presentes en nuestro sistema, pero en este caso el comando es el siguiente:

lscpu

Y con él conseguiremos una salida algo más amigable de la información que está contenida en el fichero /proc/cpuinfo. De hecho, si nos dirigimos a este mismo fichero y vemos su contenido haciendo uso de un editor de texto o simplemente del concatenador:

cat /proc/cpuinfo

Obtendremos un resultado similar, la única diferencia es que en el fichero separa las los núcleos y se muestra la información repetida tantas veces como núcleos tenga nuestra CPU, ya que el kernel de Linux en el caso de los multinúcleo no los ve como un único dispositivo.

Por supuesto, también podréis conseguir información de la CPU y mucho más usando comandos como estos dos: 

sudo dmidecode
hardinfo

En el caso del segundo, necesitarás instalar el paquete correspondiente, ya que no viene instalado por defecto en las distros. En el caso del primero, puedes usar la opción -t seguida del número de entrada de la tabla de la que deseas extraer la información. Por ejemplo, con -t 11 podríamos extraer la información de entrada la tabla DMI tipo 11...

Más información que podemos extraer, pues por ejemplo, si tenemos instalado cpuid, se puede ver el CPUID, es decir, una serie de información extraída de los registros de la CPU con esta instrucción específica que suelen implementar los x86 (¡ojo! Solo en los x86, ya que otras familias o arquitecturas suelen tener mecanismos diferentes, como eFUSE de IBM, etc.):

cpuid

De hecho, existe una biblioteca (cpuid.h que se puede usando este fichero de cabecera desde un código C, por ejemplo, con #include <cpuid.h>) para los que os interese la programación, que hace uso de esta instrucción de la ISA de estos microprocesadores para conseguir información. Dicho fichero de cabecera o header permite hacer uso en el código fuente de una serie de funciones para conseguir la información qu enecesitamos. Otra forma es usar directamente una inserción de código ASM para invocarla (tipo asm(...); ) o directamente usar solo código ensamblador...

 2-Ficheros del sistema

Una vez que hemos visto algunos comandos y formas de extraer información de nuestra CPU, vamos a seguir viendo algunos ficheros muy interesantes que no solo nos van a servir información de la CPU, sino que quizás nos ayuden a entender mejor cómo funciona la CPU y cómo la hace funcionar el propio kernel Linux. Recuerda que el kernel del S.O. es precisamente la interfaz entre el hardware y el software de usuario.

Por ejemplo, con el concatenador podemos ver la información del fichero del directorio /proc donde el kernel almacena info de la CPU:

cat /proc/cpuinfo

Bien, para los menos expertos, vamos a ir desglosando la información que se nos ofrece en la salida. Para ello, pongo un ejemplo concreto que he obtenido de uno PC y entre paréntesis voy a ir explicando lo que significa cada cosa:

processor    : 0   (número CPU, que en este caso no es un MP o sistema multiprocesador con varios sockets como los servers,...)
vendor_id    : AuthenticAMD  (ID del diseñador de la CPU, en este caso AMD)
cpu family    : 22   (nº microarquitectura de la que desciende, los diseñadores bautizan con codenames, en este caso Jaguar)
model        : 0 (dentro de la microarquitectura AMD Jaguar, este modelo es el 0, con núcleos Kabini)
model name    : AMD A4-5000 APU with Radeon(TM) HD Graphics  (nombre comercial del microprocesador o APU en este caso)
stepping    : 1  (modificaciones que se realizan durante la fabricación del chip, puede haber otroA4-5000 con otro stepping)
microcode    : 0x700010b  (versión del microcódigo que la unidad de control está usando. Se puede actualizar por el firmware)
cpu MHz        : 800.000   (la frecuencia de reloj a la que está funcionando actualmente, no la nominal)
cache size    : 2048 KB (tamaño de la memoria cache)
physical id    : 0  (ID o identificador físico de la CPU en la placa)
siblings    : 4   (coincide con el nºde cores, pero si hubiese tecnología HyperThreading o HT no sería así, contaría núcleos lógicos)
core id        : 0  (ID del core 0, habrá otros 1, 2 y 3)
cpu cores    : 4  (número de cores presentes)
apicid        : 0   (ID del APIC o sistema de gestión de energía)
initial apicid    : 0   (idem)
fpu        : yes   (si contiene o no un coprocesador o FPU (Floating Point Unit))
fpu_exception    : yes  (si soporta excepciones FPU)
cpuid level    : 13  (cantidad de opciones máximas que la instrucción CPUID puede usar para interrogar a la CPU con seguridad)
wp        : yes   (se refiere al bit WP o Write PRotect del registro CR0, que impone páginas de solo lectura de memoria para kernel)

(Por último tenemos los flags o banderas, que son una serie de tecnologías de gestión de energía, virtualización, instrucciones especiales, seguridad, etc. que soporta la CPU):


flags        : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl nonstop_tsc extd_apicid aperfmperf eagerfpu pni pclmulqdq monitor ssse3 cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ibs skinit wdt topoext perfctr_nb bpext perfctr_l2 hw_pstate proc_feedback vmmcall bmi1 xsaveopt arat npt lbrv svm_lock nrip_save tsc_scale flushbyasid decodeassists pausefilter pfthreshold

De hecho, si quieres obtener información concreta de un solo parámetro de la CPU, puedes usar el todopoderoso grep para especificar qué quieres conseguir. Imaginate que deseas saber si tu CPU soporta tecnología de asistencia a la virtualización por hardware, eso que Intel llama VT y que AMD denomina AMD-V, o similares, etc. Pues bien, esto no son más que marcas comerciales que los fabricantes o diseñadores han registrado, pero que se refieren a las técnicas que permiten la llamada virtualización completa. Pero, por decirlo de algún modo, el kernel Linux no entiende demasiado de marcas registradas, así que lo tendremos que buscar de otra forma. En el caso de Intel aparece como vmx y en el caso de AMD como svm:

grep svm /proc/cpuinfo
grep vmx /proc/cpuinfo

Y si no sabes si es AMD o Intel...

cat /proc/cpuinfo | egrep '(vmx|svm)'

En cualquiera de los casos, si te devuelve la línea donde ha localizado el flag vmx o svm, quiere decir que sí está soportada la tecnología. Si no devuelve nada, entonces no tiene soporte para ello.

Más cosas que podemos extraer de este fichero, la cantidad de núcleos con:

egrep -i "processor|physical id" /proc/cpuinfo

Otra opción sería con el comando específico:

nproc

Bien, como puedes imaginar las posibilidades con grep y demás son muchas para obtener información concreta de este fichero. Y ahora seguimos con otros ficheros no menos interesantes. Me refiero a /sys/devices/system/cpu. Allí vas a encontrar una serie de directorios y ficheros muy importantes de los que quizás no vayas a extraer tanta información práctica como con el anterior, pero tal vez te ayude a comprender mejor esa relación CPU/kernel. Si ves el contenido, tienes algo parecido a lo siguiente (en función de tu sistema y tipo de CPU):

total 0
drwxr-xr-x 7 root root    0 jun 15 11:17 cpu0
drwxr-xr-x 7 root root    0 jun 15 11:17 cpu1
drwxr-xr-x 7 root root    0 jun 15 11:17 cpu2
drwxr-xr-x 7 root root    0 jun 15 11:17 cpu3
drwxr-xr-x 7 root root    0 jun 15 11:17 cpufreq
drwxr-xr-x 2 root root    0 jun 15 11:18 cpuidle
-r--r--r-- 1 root root 4096 jun 15 11:17 isolated
-r--r--r-- 1 root root 4096 jun 15 12:04 kernel_max
drwxr-xr-x 2 root root    0 jun 15 12:53 microcode
-r--r--r-- 1 root root 4096 jun 15 12:53 modalias
-r--r--r-- 1 root root 4096 jun 15 12:53 offline
-r--r--r-- 1 root root 4096 jun 15 11:17 online
-r--r--r-- 1 root root 4096 jun 15 12:04 possible
drwxr-xr-x 2 root root    0 jun 15 12:53 power
-r--r--r-- 1 root root 4096 jun 15 11:18 present
-rw-r--r-- 1 root root 4096 jun 15 11:17 uevent

Bien, como se peude apreciar, en mi caso hay directorios CPU0, CPU1, CPU2 y CPU3, puesto que mi APU es quadcore. Si por ejemplo entramos en la CPU0 (en el resto el contenido debe ser similar), tenemos esto:

total 0
drwxr-xr-x 6 root root    0 jun 15 11:19 cache
lrwxrwxrwx 1 root root    0 jun 15 11:17 cpufreq -> ../cpufreq/policy0
drwxr-xr-x 5 root root    0 jun 15 12:53 cpuidle
-r-------- 1 root root 4096 jun 15 12:53 crash_notes
-r-------- 1 root root 4096 jun 15 12:53 crash_notes_size
lrwxrwxrwx 1 root root    0 jun 15 12:53 driver -> ../../../../bus/cpu/drivers/processor
lrwxrwxrwx 1 root root    0 jun 15 12:53 firmware_node -> ../../../LNXSYSTM:00/LNXCPU:00
drwxr-xr-x 2 root root    0 jun 15 12:53 microcode
lrwxrwxrwx 1 root root    0 jun 15 12:53 node0 -> ../../node/node0
drwxr-xr-x 2 root root    0 jun 15 12:53 power
lrwxrwxrwx 1 root root    0 jun 15 11:17 subsystem -> ../../../../bus/cpu
drwxr-xr-x 2 root root    0 jun 15 11:18 topology
-rw-r--r-- 1 root root 4096 jun 15 11:17 uevent

Y ya comenzamos a ver nombres interesantes como Power, Microcode, Firmware, cache, CPUIDLE, CPUFreq, etc., nombres que seguro nos sonarán. De hecho, cpuidle y cpufreq son dos partes bastante importantes del kernel Linux, con la que el núcleo hace la gestión de memoria. El módulo cpufreq se encarga de orquestar la frecuencia del procesador en cada momento. Por eso dije anteriormente que 800Mhz no era la frecuencia nominal, sino la que estaba funcionando en ese momento. Eso permite ahorrar energía y hacer funcionar a la CPU a 500Mhz si estás con un editor de texto (demanda menos recursos) o a 3Ghz cuando estás con un videojuego (más exigente con el rendimiento). Y eso lo consigue gracias a los P-States que la CPU soportará, haciendo uso del controlador o driver adecuado. Para orientarte un poco mejor, me estoy refiriendo a eso que los fabricantes bautizan con nombres muy bonitos y comerciales pero que se refieren a la misma tecnología: Intel SpeedStep, AMD Cool'n'Quiet o Power Now!,... ¿te suenan?  Pues bien, para escalar la frecuencia y modularla se usan los llamados gobernadores o polticas de gobernacion. Por ejemplo, una cpufreq_performance fuerza a la CPU a trabajar al máximo y una cpufreq_powersave lo hará al mínimo para bajar el consumo y aumentar la autonomía si es un portátil, además de otras políticas intermedias...

Bien, para ver más datos de cómo cpufreq está afectando a nuestro sistema, podemos dirigirnos a uno de estos ficheros de los que hablábamos. Por ejemplo, vamos a ver cómo el núcleo CPU0 está trabajando, y para ello nos vamos a /sys/devices/system/cpu/cpu0/cpufreq y allí podemos ver:

  • El núcleo afectado por esta política de energía:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/affected_cpus
  • Ver la frecuencia operativa actual:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq
  • La frecuencia máxima a la que puede escalar (frecuencia nominal, es decir, la que el fabricante te dice que funciona tu microprocesador) y la mínima a la que se puede bajar:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
  • El escalamiento actual de frecuencia que se está aplicando:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
  • Incluso el gobernador actual que se está usando:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
  • La lista de gobernadores disponibles:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
  • Y aquí la lista del controlador o driver para estos gobernadores que en función de si es Intel, VIA, AMD, etc., pues será uno u otro:
sudo cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver

Por cierto, hasta el momento hemos usado el concatenador simplemente para verlos, pero también se pueden modificar...Por cierto, para mayor comodidad, te aconsejo que instales el paquete cpufreq_utils con herramientas para hacer este trabajo de una mejor forma.

Una vez explicado ese módulo, pasamos al otro, cpuidle. Si accedemos al directorio /sys/devices/system/cpu/cpu0/cpuidle, veremos una serie de directorios como state0, state1, etc. Dentro de cada uno de ellos a su vez hay ficheros que te recomiendo que veas, como: desc, disable, latency, name, power, residency, time, usage. Bien, pues resulta que este módulo del kernel se encarga de apagar o encender núcleos o partes de éstos (unidades funcionales, etc.) para ahorrar. Por ejemplo, si en mi APU el núcleo CPU1 no fuese necesario, cpuidle lo apagaría y así no consumiría. Esto se consigue gracias a los C-States, que nuevamente muchos fabricantes se empeñan en bautizar con nombres comerciales. Para más información:

cpupower idle-info

Tanto el módulo cpuidle como el cpufreq estarán a espensas de la carga de trabajo del kernel, es decir, el scheduler o planificador del núcleo será el que marque un poco si se necesita un rendimiento elevado o si se puede recortar un poco para mejorar el consumo...

Y termino con un último apunte, y es que cpuidle y cpuferq funcionan bien en PCs y servidores o supercomputadoras, pero no en dispositivos móviles como smartphones o tablets. Por eso ARM se puso en marcha para crear un subsistema para Linux llamado EAS (Energy-Aware Scheduling) que une cpuidle y cpufreq en un solo subsistema y el control se hace mucho más eficiente, sin que uno compita o moleste al otro. Claro, menudo lío, imagina que cpuidle está apagando y encendiendo núcleos, cpufreq por otro lado modificando las frecuencias y el kernel Linux con el scheduler balanceando la carga de trabajo que tiene para repartirla entre los recursos activos de hardware. Ese follón puede dar lugar a que en ocasiones se molesten unos y otros sistemas y ocurran cosas indeseadas como que el kernel mande una tarea a un núcleo apagado y este se ponga en modo activo cuando ya había otros activos que podrían asumir esa carga... Pues EAS es la solución a eso!! Y con esto me despido de este tutorial y espero que os guste, más información en mi curso C2GL o en mi libro El mundo de Bitman.

¡Atención! Este sitio usa cookies y tecnologías similares.

Si no cambia la configuración de su navegador, usted acepta su uso. Saber más

Acepto

Vea nuestra política de cookies y enlaces de interés aquí