Sitema Operativo Android: SELinux y el Ciclo de Vida

El sistema operativo Android, al estar destinado principalmente a dispositivos móviles, que generalmente presentan recursos reducidos, debe poner especial énfasis en aspectos como la gestión de memoria. Tanto la rutina Android Runtime como la máquina virtual Dalvik, como la mayoría de sistemas operativos, utilizan la paginación de memoria y el mapeado de memoria para gestionarla.

Antes de explicar cómo se gestiona la memoria virtual en Android, se deben entender los conceptos de paginación y mapeado de memoria: La paginación de memoria es una estrategia de organización de la memoria física que consiste en dividir dicha memoria en porciones de igual tamaño, conocidas como páginas físicas o marco. De igual manera, se dispone de un espacio de páginas virtuales asociadas a cada proceso. Cada página virtual se mapea en un marco de página, o página física. Este mapeo entre páginas físicas y lógicas se mantiene registrado en la tabla de páginas, que posteriormente utilizará el sistema traductor de direcciones para obtener la dirección física asociada a la dirección virtual recibida.

Por otro lado, el mapeado de memoria consiste en la generación de un mapa de memoria, o estructura de datos que indica cómo está distribuida la memoria, además de información sobre el tamaño real de la misma y las relaciones existentes entre direcciones físicas y virtuales.

En Android, cuando una aplicación genera algún cambio en memoria, generando nuevos objetos o modificando páginas, provoca que la página asociada no pueda ser eliminada de la memoria principal. Por el contrario, cualquier archivo mapeado en memoria que no haya sido modificado, como puede ser el caso del código, puede ser extraído de la memoria RAM si el sistema requiere dicho espacio de memoria para otro propósito.

El recolector de basura es el encargado de liberar espacios de memoria que no van a ser utilizados. El recolector realiza un seguimiento de cada alojamiento en memoria, y cuando determina que un fragmento de dicha memoria no va a ser utilizado de nuevo por el programa, lo libera automáticamente, enviándolo de vuelta al heap.

El sistema tiene un conjunto de criterios para determinar cuándo debe ser ejecutado el recolector de basura, dado que, a pesar de su rapidez de ejecución, puede afectar al rendimiento de las aplicaciones. Si, por ejemplo, la recolección de basura ocurriese durante la ejecución de un proceso de coste elevado, el tiempo de procesamiento se elevaría aún más. Este es uno de los principales problemas que ocurren con los recolectores de basura, como es común en la plataforma Java. Esto obliga a que las aplicaciones deban tener cuidado con sus ejecuciones, dado que una mala optimización de sus recursos de memoria podría forzar al recolector de basura a realizar múltiples recolecciones, perdiendo velocidad de procesamiento.

Además de la recolección de basura, otra de las iniciativas de Android para ahorrar recursos de memoria consiste en la memoria compartida. Para poder encajar todo lo necesario en memoria principal, se intenta realizar una compartición de páginas de memoria entre procesos. Esto puede realizarse de diversas maneras:

  • Dado que cada aplicación se genera a partir de un fork del proceso Zygote, las páginas de memoria asociadas al framework y recursos pueden ser compartidos entre procesos.
  • La mayoría de datos estáticos son mapeados en memoria en un proceso, con lo que se permite compartir dichos datos entre procesos y eliminar dicha página de memoria cuando sea necesario.
  • Por ejemplo, la superficie de una ventana utiliza memoria compartida entre la aplicación y la pantalla.

Cada instancia de la máquina Dalvik, asociada a la ejecución de una determinada aplicación, tiene un tamaño de heap en memoria virtual restringido (espacio lógico), que puede crecer según las necesidades de dicha aplicación, pero sin superar un tamaño máximo determinado por el sistema. Estas restricciones van asociadas principalmente a la cantidad de memoria RAM de la que dispone el dispositivo en cuestión

Android es incapaz de desfragmentar el tamaño del heap de una máquina Dalvik, por lo que solamente podrá utilizar la memoria restante si esta se encuentra al final de dicho apartado. Sin embargo, el sistema sí que puede reducir el uso de memoria física usado por sí mismo.

La cantidad de memoria principal disponible también afecta al funcionamiento de las aplicaciones. Si bien Android nunca eliminará una aplicación que se encuentre en primer plano, si el sistema requiere recursos de memoria, podrá ir eliminando ciertas aplicaciones, procesos y servicios en función de su relevancia, el tiempo que han permanecido sin utilizarse (segundo plano) y los recursos que consume dicha aplicación. Aunque generalmente se crea una lista LRU de procesos en segundo plano, el sistema puede saltarse dicha ordenación si finalizar la ejecución de una determinada aplicación supone una ganancia mayor de recursos. Por tanto, los recursos utilizados juegan un papel muy importante a la hora de determinan qué aplicación finalizar.

Nivel y el modelo de protección y seguridad de Android, mecanismos SELinux

El modelo de seguridad de Android se basa parcialmente en la utilización de “cajas de arena” para cada aplicación y se fundamenta principalmente en 3 pilares:

  1. Dado que Android es un sistema basado en el kernel de Linux, es posible impedir a las aplicaciones del sistema móvil que accedan directamente al hardware del dispositivo, o que interfieran con los recursos de otras aplicaciones.
  2. Toda aplicación del sistema debe ser firmada con un certificado que identifique a sus autores. Mediante esta firma también se puede verificar que dicha aplicación no ha sido modificada externamente
  3. Si una aplicación necesita acceder a recursos del sistema que puedan comprometer la seguridad del mismo, deberá solicitarlo mediante un modelo de permisos, de forma que el usuario los conozca y acepte antes de instalar la aplicación. Esto se ve ligeramente modificado en versiones actuales Android.

En versiones anteriores a Android 4.3, se utilizaba una seguridad a nivel de kernel con la ejecución de cada aplicación como si de un usuario se tratase. Así pues, a cada aplicación se le asignaba un identificador de usuario y grupo propio. Sin embargo, dos aplicaciones distintas podían compartir dichos identificadores en caso de haber sido firmados por la misma clave (al generar el fichero apk). Sin embargo, en versiones posteriores, se comenzó a utilizar adicionalmente SELinux para restringir los limites asociados a una aplicación y su caja de arena.

Cada aplicación define que permisos necesita para realizar sus servicios y actividades y se le comunican al usuario. En versiones anteriores de Android, dichos permisos debían ser aceptados antes de instalar la aplicación. Sin embargo, las últimas versiones realizan la petición de permisos una vez instalada, la primera vez que dicho recurso sea requerido, y, en caso de no aceptarse, la aplicación puede seguir siendo utilizada sin dicho recurso.

Como parte del modelo de seguridad de Android, el sistema operativo utiliza Security-Enhanced Linux (SELinux) para hacer cumplir el acceso de control mandatorio (MAC), frente al modelo de seguridad estándar y tradicional de Linux (DAC); en todos los procesos, incluso procesos que estén ejecutándose con permisos especiales root. Sin embargo, a qué procesos afecta dependerá de la implementación concreta de la versión de Android.

SELinux es un módulo de seguridad que se integra con el kernel de las distribuciones Linux desde la 2.6 y que, mediante un conjunto de políticas de seguridad, asegura el sistema bloqueando un conjunto de accesos. Muchas compañías y organizaciones han apoyado la implementación de SELinux, dado que, con ello, Android puede proteger y limitar mejor los recursos y servicios del sistema, controlar el acceso a los datos y registros del sistema por parte de cada aplicación, reducir las consecuencias de un software malicioso y proteger a los usuarios de posibles errores de código en dispositivos móviles.

SELinux trabaja con el principio de denegación por defecto, es decir, todo lo que no se permita explícitamente está denegado. En base a esto, SELinux puede operar en 2 modos:

  • Modo permisivo, en el que las negaciones de permisos se registran, pero no se aplican
  • Modo obligatorio, en el que las negaciones de permisos se registran y aplican.

El modo utilizado en Android depende de la versión concreta a la que nos estemos refiriendo, ya que la implementación ha ido variando a lo largo de las diferentes actualizaciones:

  • En Android 4.3 se hace uso de SELinux en modo permisivo (Las acciones no permitidas son registradas, pero no bloqueadas)
  • En Android 4.4, se hace un uso parcial del modo obligatorio. Es decir, solo se hace uso de este modo en un conjunto crítico de procesos, como el proceso Zygote. Para el resto de procesos se continúa utilizando el modo permisivo.
  • A partir de Android 5.0, se hace uso de un modo obligatorio completo, es decir, aplicable a todo tipo de procesos.

A partir de Android Lollipop se introduce Seccomp, que permite la implementación de sandboxes, es decir, entornos de ejecución restringidos donde un proceso sólo dispone de un número limitado de llamadas al sistema, y únicamente sobre los ficheros que tenía previamente abiertos. Si el proceso intenta realizar una llamada al sistema fuera de su rango de actuación, automáticamente se finaliza su ejecución.

Android Nougat introduce actualizaciones en la configuración de SELinux, incrementado la cobertura asociada a Seccomp. Además, en esta versión se introducen protecciones de memoria adicionales para las nuevas versiones del kernel de Linux, marcando nuevas zonas de memoria del kernel de únicamente lectura, y restringiendo el acceso del kernel a direcciones de memoria del espacio de usuario.

SELinux implementa el modelo de permisos mediante la definición de etiquetas asociadas a recursos y que marcan qué acciones están permitidas. Las políticas de acceso a un determinado objeto establecen qué se permite para una determinada etiqueta.

Además, SELinux en Android incorpora elementos de protección del espacio de kernel, mediante la división de los segmentos de memoria en secciones lógicas y el establecimiento de restricciones de acceso a su espacio de direcciones.

En último lugar, otra de las características de SELinux es la restricción de acceso por parte de las aplicaciones a los comandos ioctl, es decir, las llamadas al sistema asociadas al control y comunicación con controladores. De esta forma, sólo una pequeña lista de sockets ioctl está disponible para las aplicaciones, limitando el acceso a identificadores de dispositivos, y evitando vulnerabilidades de escalada de privilegios y ejecución de código en el contexto del kernel.

Gestionamiento de Android de los procesos y los hilos

En Android, cada aplicación siempre se ejecuta dentro de un mismo proceso, incluyendo en dicho proceso todos los componentes de la misma, servicios y actividades. Es decir, cuando se inicia una aplicación, se ejecutará el primer componente de la misma y el sistema Android creará un nuevo proceso de Linux con un único subproceso asociado para dicha aplicación. A este subproceso generado por defecto se le conoce como hilo principal de ejecución. Cuando posteriormente se inicien nuevos componentes, estos se acoplarán al mismo proceso de aplicación y se ejecutarán en el mismo subproceso descrito anteriormente. Sin embargo, este comportamiento por defecto es configurable, de manera que se puedan generar varios procesos y subprocesos, o hilos, asociados a la ejecución de una aplicación.

El hilo o subproceso principal de una aplicación es muy importante, dado que es el encargado de manejar los eventos de interacción con el usuario y la interfaz gráfica de la aplicación. Además, este hilo principal también es generalmente el proceso encargado de manejar el kit de interfaz de usuario de Android y, por ello, suele recibir el nombre de subproceso de interfaz de usuario. Sin embargo, no es de obligado cumplimiento que el hilo principal maneje la interfaz de usuario.

Por defecto, todos los nuevos componentes de la aplicación que sean necesitados serán ejecutados en el hilo principal. Esto puede suponer un problema cuando se ejecute una operación de uso intensivo y larga duración, ya que podría bloquear la interacción con el usuario. En este caso, Android mostraría el típico mensaje: “La aplicación no responde”.

Es debido a esto por lo que se recomiendan dos reglas respecto al hilo principal de un proceso:

  • No se debe bloquear el hilo principal.
  • Todas las operaciones de interfaz gráfica se realizan en el hilo principal.

Para solucionar los problemas anteriores, Android permite crear y administrar nuevos hilos para operaciones diferentes. De esta manera, se introduce el multithreading, o concurrencia en la ejecución de hilos. En Android, se pueden diferenciar dos tipos de hilos o subprocesos:

  • Subprocesos asociados a la vida de una actividad o servicio: se vinculan al ciclo de vida de la actividad que están ejecutando, y finalizan tan pronto como dicha actividad es destruida. Generalmente, un uso
  • Subprocesos no asociados a la vida de una actividad o servicio: pueden continuar su ejecución más allá del ciclo de vida de una actividad.

Sin embargo, la inclusión de nuevos hilos puede suponer un aumento en la complejidad de la ejecución de una aplicación. Para ello, Android incorpora un mecanismo de intercomunicación entre procesos.

Por último, cuando el sistema Android detecta que los recursos disponibles son bajos y se requieren para la ejecución de otro proceso de más importancia, puede finalizar la ejecución de un proceso para liberar dichos recursos. Como se vio anteriormente, dicha finalización de procesos para liberar recursos se realiza en función de una jerarquía de prioridades en la que se sitúa cada proceso. Adicionalmente, Android nunca finalizará un proceso que se encuentra ejecutándose en primer plano en el instante de tiempo actual.

Ciclo de vida de un proceso en Andorid

El ciclo de vida de un proceso se define como el espacio de tiempo que transcurre desde que dicho proceso es creado hasta que finalmente es destruido.

En el sistema operativo Android, el ciclo de vida de un proceso se define por su transición entre una serie de estados:

  • Estado Starting: Este estado se inicia el proceso. Ocurre cuando se hace uso de la llamada de sistema “onCreate()”.
  • Estado Running: En este estado, el proceso tiene el foco principal de ejecución, es decir, mientras un proceso se encuentra en este estado, se trata de un proceso en primer plano. Este estado se inicia cuando se realiza la llamada “onStart()”, “onResume()” u “onRestart()”.
  • Estado Paused: Una vez se utiliza la llamada “onPause()”, un proceso pasa al estado de pausa. En este estado, el proceso continúa siendo visible para el usuario, pero deja de abarcar el foco de atención.
  • Estado Stopped: Una vez un proceso alcanza este estado, deja de ser visible para el usuario, y, por tanto, queda catalogado como proceso en segundo plano. Un proceso se considera en este estado cuando se ha invocado la llamada “onStop()”.
  • Estado Destroyed: Este estado denota el final del ciclo de vida de un proceso. Cuando el proceso es destruido, se liberan todos los recursos asociados al mismo. Para destruir un proceso, se utiliza la llamada “onDestroy()”.

Podemos denotar, de igual manera, el ciclo de vida de un proceso como el espacio de tiempo que transcurre desde que se invoca la llamada “onCreate()” hasta que se hace lo propio con “onDestroy()”. Además, el espacio de tiempo que un proceso permanece visible será el correspondiente entre las llamadas “onStart()” y “onStop()”; y el tiempo entre el primero y “onPause()” denotará el tiempo que permanece catalogado como proceso en primer plano.

Es importante destacar que un proceso únicamente puede acceder al estado Destroyed desde el estado Paused o Stopped. Esto quiere decir, que el sistema Android nunca finalizará un proceso, si se produjese una situación de escasez de recursos, que se encuentre ejecutándose en primer plano. 

07 July 2022
close
Tu email

Haciendo clic en “Enviar”, estás de acuerdo con nuestros Términos de Servicio y  Estatutos de Privacidad. Te enviaremos ocasionalmente emails relacionados con tu cuenta.

close thanks-icon
¡Gracias!

Su muestra de ensayo ha sido enviada.

Ordenar ahora

Utilizamos cookies para brindarte la mejor experiencia posible. Al continuar, asumiremos que estás de acuerdo con nuestra política de cookies.