El modelo de subprocesos de Binder está diseñado para facilitar las llamadas a funciones locales, incluso si esas llamadas son a un proceso remoto. Específicamente, cualquier proceso que aloje un nodo debe tener un grupo de uno o más subprocesos de Binder para controlar las transacciones a los nodos alojados en ese proceso.
Transacciones síncronas y asíncronas
Binder admite transacciones síncronas y asíncronas. En las siguientes secciones, se explica cómo se ejecuta cada tipo de transacción.
Transacciones síncronas
Las transacciones síncronas se bloquean hasta que se ejecutan en el nodo y el llamador recibe una respuesta para esa transacción. En la siguiente figura, se muestra cómo se ejecuta una transacción síncrona:
Figura 1: Transacción síncrona.
Para ejecutar una transacción síncrona, el binder hace lo siguiente:
- Los subprocesos en el grupo de subprocesos de destino (T2 y T3) llaman al controlador del kernel para esperar el trabajo entrante.
- El kernel recibe una nueva transacción y activa un subproceso (T2) en el proceso de destino para controlar la transacción.
- El subproceso de llamada (T1) se bloquea y espera una respuesta.
- El proceso de destino ejecuta la transacción y devuelve una respuesta.
- El subproceso del proceso de destino (T2) vuelve a llamar al controlador del kernel para esperar un nuevo trabajo.
Transacciones asíncronas
Las transacciones asíncronas no se bloquean hasta que se completan; el subproceso de llamada se desbloquea en cuanto la transacción se pasa al kernel. En la siguiente figura, se muestra cómo se ejecuta una transacción asíncrona:
Figura 2: Transacción asíncrona.
- Los subprocesos en el grupo de subprocesos de destino (T2 y T3) llaman al controlador del kernel para esperar el trabajo entrante.
- El kernel recibe una nueva transacción y activa un subproceso (T2) en el proceso de destino para controlar la transacción.
- El subproceso de llamada (T1) continúa la ejecución.
- El proceso de destino ejecuta la transacción y devuelve una respuesta.
- El subproceso del proceso de destino (T2) vuelve a llamar al controlador del kernel para esperar un nuevo trabajo.
Cómo identificar una función síncrona o asíncrona
Las funciones marcadas como oneway
en el archivo AIDL son asíncronas. Por ejemplo:
oneway void someCall();
Si una función no está marcada como oneway
, es una función síncrona, incluso si devuelve void
.
Serialización de transacciones asíncronas
Binder serializa las transacciones asíncronas desde cualquier nodo único. En la siguiente figura, se muestra cómo Binder serializa las transacciones asíncronas:
Figura 3: Serialización de transacciones asíncronas
- Los subprocesos en el grupo de subprocesos de destino (B1 y B2) llaman al controlador del kernel para esperar el trabajo entrante.
- Se envían dos transacciones (T1 y T2) en el mismo nodo (N1) al kernel.
- El kernel recibe transacciones nuevas y, como provienen del mismo nodo (N1), las serializa.
- Se envía otra transacción en un nodo diferente (N2) al kernel.
- El kernel recibe la tercera transacción y activa un subproceso (B2) en el proceso de destino para controlar la transacción.
- Los procesos de destino ejecutan cada transacción y devuelven una respuesta.
Transacciones anidadas
Las transacciones síncronas se pueden anidar; un subproceso que controla una transacción puede emitir una transacción nueva. La transacción anidada puede ser para un proceso diferente o para el mismo proceso del que recibiste la transacción actual. Este comportamiento imita las llamadas a funciones locales. Por ejemplo, supongamos que tienes una función con funciones anidadas:
def outer_function(x):
def inner_function(y):
def inner_inner_function(z):
Si se trata de llamadas locales, se ejecutan en el mismo subproceso.
Específicamente, si la entidad que llama a inner_function
también resulta ser el proceso que aloja el nodo que implementa inner_inner_function
, la llamada a inner_inner_function
se ejecuta en el mismo subproceso.
En la siguiente figura, se muestra cómo los identificadores de Binder controlan las transacciones anidadas:
Figura 4: Transacciones anidadas
- El subproceso A1 solicita la ejecución de
foo()
. - Como parte de esta solicitud, el subproceso B1 ejecuta
bar()
, que A ejecuta en el mismo subproceso A1.
En la siguiente figura, se muestra la ejecución de subprocesos si el nodo que implementa bar()
se encuentra en un proceso diferente:
Figura 5: Transacciones anidadas en diferentes procesos.
- El subproceso A1 solicita la ejecución de
foo()
. - Como parte de esta solicitud, el subproceso B1 ejecuta
bar()
, que se ejecuta en otro subproceso C1.
En la siguiente figura, se muestra cómo el subproceso reutiliza el mismo proceso en cualquier parte de la cadena de transacciones:
Figura 6: Transacciones anidadas que reutilizan un subproceso.
- El proceso A llama al proceso B.
- El proceso B llama al proceso C.
- Luego, el proceso C realiza una devolución de llamada al proceso A, y el kernel reutiliza el subproceso A1 en el proceso A que forma parte de la cadena de transacciones.
En el caso de las transacciones asíncronas, el anidamiento no desempeña un papel, ya que el cliente no espera el resultado de una transacción asíncrona, por lo que no hay anidamiento. Si el controlador de una transacción asíncrona realiza una llamada al proceso que emitió esa transacción asíncrona, la transacción se puede controlar en cualquier subproceso libre de ese proceso.
Evita los bloqueos
En la siguiente imagen, se muestra un bloqueo común:
Figura 7: Interbloqueo común.
- El proceso A toma el mutex MA y realiza una llamada de Binder (T1) al proceso B, que también intenta tomar el mutex MB.
- Al mismo tiempo, el proceso B toma el mutex MB y realiza una llamada de Binder (T2) al proceso A, que intenta tomar el mutex MA.
Si estas transacciones se superponen, cada transacción podría tomar un mutex en su proceso mientras espera que el otro proceso libere un mutex, lo que provocaría un interbloqueo.
Para evitar interbloqueos mientras usas Binder, no mantengas ningún bloqueo cuando realices una llamada de Binder.
Reglas de ordenamiento de bloqueos y bloqueos mutuos
En un solo entorno de ejecución, a menudo se evita el bloqueo con una regla de ordenamiento de bloqueos. Sin embargo, cuando se realizan llamadas entre procesos y entre bases de código, en especial a medida que se actualiza el código, es imposible mantener y coordinar una regla de ordenamiento.
Interbloqueos y mutex único
Con las transacciones anidadas, el proceso B puede volver a llamar directamente al mismo subproceso en el proceso A que contiene un mutex. Por lo tanto, debido a una recursión inesperada, es posible que se produzca un interbloqueo con un solo mutex.
Llamadas síncronas y bloqueos
Si bien las llamadas asíncronas del vinculador no se bloquean hasta que se completan, también debes evitar mantener un bloqueo para las llamadas asíncronas. Si mantienes un bloqueo, es posible que experimentes problemas de bloqueo si una llamada unidireccional se cambia accidentalmente a una llamada síncrona.
Un solo subproceso de Binder y bloqueos
El modelo de transacción de Binder permite la reentrada, por lo que, incluso si un proceso tiene un solo subproceso de Binder, aún necesitas bloqueo. Por ejemplo, supongamos que iteras sobre una lista en un proceso de un solo subproceso A. Para cada elemento de la lista, realizas una transacción de Binder saliente. Si la implementación de la función a la que llamas realiza una nueva transacción de Binder en un nodo alojado en el proceso A, esa transacción se controla en el mismo subproceso que estaba iterando la lista. Si la implementación de esa transacción modifica la misma lista, es posible que tengas problemas cuando sigas iterando sobre la lista más adelante.
Configura el tamaño del grupo de subprocesos
Cuando un servicio tiene varios clientes, agregar más subprocesos al grupo de subprocesos puede reducir la contención y atender más llamadas en paralelo. Después de controlar la simultaneidad correctamente, puedes agregar más subprocesos. Un problema que puede deberse a la adición de más subprocesos que algunos subprocesos podrían no usarse durante cargas de trabajo inactivas.
Los subprocesos se generan a pedido hasta alcanzar un máximo configurado. Después de que se genera un subproceso de Binder, permanece activo hasta que finaliza el proceso que lo aloja.
La biblioteca libbinder tiene un valor predeterminado de 15 subprocesos. Usa setThreadPoolMaxThreadCount
para cambiar este valor:
using ::android::ProcessState;
ProcessState::self()->setThreadPoolMaxThreadCount(size_t maxThreads);