Binder File Descriptor Usages

Baiqin Wang
7 min readFeb 4, 2021

--

The RPC mechanism provided by Binder is not a good fit for certain use cases. As we know, Android framework allocates about 1 MB of virtual memory for each process in ProcessState. So you can only transfer at most several hundreds of KB within one transaction. Besides, every bits of data needs to be copied to target process's memory space so transferring bulk data with Binder transaction is less efficient. Another use case where Binder RPC mechanism doesn't fit well is streaming data transfer. Using Binder transactions to implement streaming data transfer will be highly inefficient.‌

But this doesn’t mean Binder cannot help with these use cases. File descriptor is a special kind of Binder object and Binder driver will special handle a file descriptor that goes across process boundaries. The details are discussed in the article “Binder data model” and “Binder transaction”. In short, when a file descriptor is sent to another process, Binder driver will open the same underlying file in the target process and the target process will receive the integer value of the new file descriptor. So both processes will have a file descriptor pointing the same underlying file after the transfer, which basically creates an area of shared memory between the two processes. This kind of shared memory provides a solution for both bulk data transfer and streaming data transfer. (It may be more accurate to call them data sharing though.)

Binder bulk data transfer

In the HamKing sample project [1], the client app can call pickupOrder to get the "burger" when an order is ready, the server app will then send a Bitmap of the "burger" to the client. These Bitmaps can take more than 1 MB in memory, so if the server app indeed tries to copy the bits to client app in a Binder transaction, it will definitely receive a TransactionTooLargeException. However, that doesn't happen in reality. What happens is that the server app passes a file descriptor to client app that points to the memory containing the image data, instead of copying the actual data to client app's memory space. Let's see how this is implemented by the Bitmap class:

The writeToParcel just calls the native method nativeWriteToParcel to do the actual work. Line 11 to 28 writes some metadata of the Bitmap into the Parcel. Line 31 checks whether the data is already in an ashmem region, if so this function just writes the file descriptor pointing the region and return. The ashmem is a custom memory sharing mechanism implemented in Android. An area of shared memory in ashmem is managed by a file descriptor so it can be naturally shared between processes if you send the file descriptor using Binder. If the data is not in an ashmem region, writeBlob method will be called to serialize the data.

Line 8 checks whether the size of the data is less than 16 KB or the Parcel instance doesn't allow file descriptors, if so writeInplace will be called to copy the data into the buffer of this Parcel. The way writeInplace works is similar to how primitive data types are serialized, which is discussed in the article "Binder data model". Otherwise, the method will create an ashmem region to do the serialization. I will not talk about ashmem in this article, but note line 36 writes the file descriptor of this region to the Parcel. After this function returns, Bitmap_writeToParcel method will copy the data from the original memory to the ashmem region. So in short, if the size of a Bitmap is less than 16 KB then the data will be copied to target process's memory space; otherwise an ashmem region will be created to avoid data copying between processes.

Binder streaming data transfer

Perhaps the best usage example of streaming data in Android is the input event stream. The InputManagerService (IMS) in Android is responsible for reading and parsing different kinds of input events from different devices and dispatching the events to different event consumers. Like most other system services, IMS runs in the system_server process. However, the events consumers are usually the applications who run in other processes. So IMS needs some channels to stream the input events to other processes. With the help of unix domain socket and Binder's file descriptor awareness, a pipe can be created between the IMS and an application process.

The input event streaming channel is modeled by the InputChannel class. At the end of day, the windows on the display device is the unit to receive the input events, so WindowManagerService (WMS) needs to manage the InputChannel instances as well. For example, when ViewRootImpl adds a window for a new Activity, it creates a new InputChannel instance and sends it to WMS. The InputChannel instance, which is created using the empty constructor, is just a placeholder and doesn't allocate any sockets inside. If you look at the addToDisplay interface in IWindowSession.aidl you will notice the inputChannel argument is marked out, which means this InputChannel will be initialized with reply data sent back from WMS. What's going to happen is that WMS will allocates a pair of InputChannel instances, send the write side instance to IMS and send the read side instance back to application in the transaction reply, then an input event channel is formed between IMS and the application.

The InputChannel class literally does nothing besides calling the corresponding native methods. The mPtr field points the memory address of its native peer. The empty constructor does nothing, which means the InputChannel instance created by ViewRootImpl is strictly a placeholder. The openInputChannelPair method is used to create a pair of InputChannels with a read side and a write side. The transferTo moves the socket file descriptor from outParameter to itself. The describeContents method returns CONTENTS_FILE_DESCRIPTOR to indicate the presence of a file descriptor in the serialized data; until current Android version this is the only flag for this method other than 0. The getToken returns a Binder token that identifies this InputChannel instance.

The openInputChannel method creates a pair of InputChannels then use the first one as the write side and the second one as the read side. The write side is registered to IMS and the read side is return to the application process later. When this pair of InputChannels are created, a Binder token is created for these them as the identity for this pair. WMS uses this token as the key to manage the mapping between InputChannels and WindowStates.

In essential, an instance of InputChannel is just a file descriptor pointing to a unix domain socket. The openInputChannelPair method uses the socketpair function to open a pair of file descriptors pointing to the same underlying socket. This pair of file descriptors form a duplex communication channel, which means if you write data on any of the two file descriptors, you can read it from the other one. This two file descriptors are identical and the channel is duplex, so how to use each side of socket pair is a choice made by IMS. Line 14 creates a Binder token to uniquely identify the pair of InputChannels. The structure of the native InputChannel class is simple, the core of it is just a file descriptor pointing to a unix domain socket:

After this pair of InputChannels are created, WMS will register the write side to IMS using registerInputChannel which is just a wrapper of the native implementation:

Line 6 uses the Binder token of the InputChannel to search for an existing Connection instance, if an InputChannel was already registered using this token then an error code will return. IMS uses a Connection class to manage event dispatching on an InputChannel. Line 11 creates a new Connection instance and adds it to mConnectionsByFd using the socket file descriptor value as the key. The InputChannel instance is added to mInputChannelsByToken using the Binder token as the key. Later when IMS has an input event to dispatch, it will determine which InputChannel to send the event to with the help of WMS. The event sending is done by writing the input event to the corresponding file descriptor in a specific format. The other side of the channel can then read and process the input event.‌

Now that the write side of the InputChannel is registered to IMS, WMS needs to call the transferTo method to move the file descriptor to the outInputChannel so that the auto-generated Binder service code can send back the read side socket descriptor to application process:

The native implementation of transferTo function sets the mPtr field in target InputChannel Java class to that of the current one. After this call, the outInputChannel owns the native InputChannel instance which contains the read side socket. The auto-generate Binder transaction code on WMS side will then write the outInputChannel to a Parcel and the application side will initialize the placeholder InputChannel with the Parcel. The write and read methods in InputChannel class serialize and deserialize the InputChannel to a Parcel respectively:

Nothing surprise in these two methods. The name, file descriptor and the Binder token are serialized and deserialized respectively. So after the application process reads out the InputChannel, a streaming channel between IMS and the application process is formed. ViewRootImpl can then receive input events from IMS by reading from this socket descriptor.

External links

[1] https://github.com/androidonekb/HamKing

--

--