What is interprocess communication

Explanation of interprocess communication in Baidu Encyclopedia

Interprocess communication (IPC) is a set of programming interfaces. Can coordinate different processes so that they can run simultaneously in the same operating system and communicate and exchange information with each other,

Basically, it allows us to interact with data between different processes.

Why interprocess communication

Once you understand what interprocess communication is, there is a question worth thinking about. Why have interprocess communication, but not together? Before we answer that question, imagine that all user processes think of it as a small house, where everyone lives in their own rooms, and without barriers, I can go to your house and take whatever is in your house.

The operating system creates a barrier that prevents you from accessing information about other processes. This barrier has a name in operating systems: process isolation. The purpose of this barrier is to protect processes in the operating system from interfering with each other.

So why is interprocess communication necessary? Imagine that we all live in the same building. Inevitably, we will run out of soy sauce, so we need to borrow some soy sauce from the next door. The operating system will not let you borrow some soy sauce, and then there will be an administrator who is responsible for transporting the soy sauce from the next door to your home.

What are the methods of interprocess communication

  • Use intEnts to communicate between processes

  • Use Messenger to communicate between processes

  • Use AIDL interprocess communication

  • ContentProvider

ContentProvider provides a unified interface for storing and retrieving data. It can share data between different applications and is itself suitable for inter-process communication. The underlying implementation of ContentProvider is also Binder, but is much easier to use than AIDL. Many operations in the system use ContentProvider, such as address book, audio and video, etc. These operations themselves are cross-process communication.

  • File sharing

The two processes share data by reading and writing the same file. The shared file can be text, XML, or JOSN. File sharing is suitable for inter-process communication that does not require high data synchronization.

  • serialization

Serialization refers to Serializable/Parcelable. Serializable is an empty interface provided by Java that provides standard serialization and deserialization operations for objects. Parcelable interface is a serialization method in Android, which is more suitable for use on the Android platform. It is more troublesome to use and very efficient.

  • Bundle

The Bundle implements the Parcelable interface, so it can be easily transferred between different processes. Acitivity, Service, and Receiver all use bundles to transmit data in intents.

The way above. In addition to file sharing, look at several other methods that use binders for interprocess communication.

What is the Binder

Binder was the OpenBinder framework developed by Be Inc. Dianne Hackborn, author of OpenBinder, joined Google and worked on The Android platform, bringing the technology to Android. Binder itself is interprocess communication technology for Android,

The vast majority of cross-processes on the Android platform are done with Binder. And he has a very important role to play.

Why Binder

Before answering the question why Binder is used, there is actually another question: what other options do we have besides Binder?

As we all know,Android is based on the Linux kernel, and Linux itself provides IPC mechanism, including pipe, sinal, Semophore, Message queue and shared memory Memory, Socket, etc.

In the process of Linux IPC communication, process A copies data to the buffer of kernel space through copy_from_USER function, and kernel space copies data to process B through copy_to_user function. There are two data copies in total, as shown in the following figure

👆Linux IPC communication process

Why Binder mechanisms when you can already do interprocess communication with Linux IPC mechanisms? Start with a Binder flow chart that compares with Linux’s traditional IPC communication

👆Binder communication process

performance

Performance wise, binder copies data once to a buffer in kernel space, which then maps data to the data receive cache by mmap. By mapping data from the data receive cache to process B in the same way, performance is improved by having one less copy of the Socket/ pipe/message queue. One more copy than memory sharing

The IPC way Data copy times
The Shared memory 0
Binder 1
Linux sockets/pipes/message queues 2

Second, the Linux way to copy data to the kernel space in the process of PROCESS B, do not know how much space it needs, so you may need to apply for more memory to use. Will cause a certain amount of memory waste.

Binder performs better than memory sharing in terms of performance

The stability of

From the perspective of stability, Binder is based on C/S architecture, with clear architecture and clear responsibilities. Its stability is completely higher than that of relying solely on memory sharing, which is unlayered and difficult to control, and may cause deadlock when accessing critical resources concurrently and synchronously

Binder is superior to memory sharing in terms of stability

security

The traditional IPC receiver cannot obtain the reliable process user ID/ process ID (UID/PID) of the other party to identify the other party. Android assigns its own UID to each installed APP, so the PROCESS UID is an important symbol to identify the process

conclusion

In terms of performance, memory sharing is definitely the best. Binder next and worst is the traditional Linux way. From stability and safety analysis,Binder is the best. Taken together, Binder is the preferred way to communicate across processes on Android.

About why to use Binder Huangshu Liu in his Android Binder principle (I) must understand the knowledge point before learning Binder also has a detailed explanation. The author here is more for the record.

How does Binder communicate

By the two sections above. You know what a Binder is. And why Binder. Binder’s interprocess communication on Android

The architecture analysis

Before analyzing Binder, know that Binder is based on the C/S architecture. The Client/Servcie Service needs to register with the Servicemanager. the Client obtains services from the ServiceManager to complete the connection relationship. The architecture is shown in the figure below

However,ServiceManager belongs to a separate process, and both Client/Service and ServiceManager have process isolation. Therefore,Servcie registration with ServiceManager still needs Binder to complete. So the final interprocess communication should look like this

How to send data

The Binder mechanism is analyzed in the manner of AIDL, using the previous example of using AIDL interprocess communication by passing an int parameter and observing its passing process. For those of you who are not familiar with AIDL, check out “Using AIDL interprocess Communication” as a source of knowledge.

JAVA layer

First, look at the JAVA layer. Execute the proxy class and the data is wrapped as a Parcel object (note that Parcel is a Java Parcel object). BinderProxy (proxy object) is then executed, and finally the transactNative method is executed

// Execute your own interface methods
var info = 100aidl? .getAge(info)Copy the code
// The updateBookInOut method in the agent is executed
private static class Proxy implements com.starot.lib_intent.aidl.ITest{
    private android.os.IBinder mRemote;
    @Override public com.starot.lib_intent.aidl.BookInfo updateBookOut(com.starot.lib_intent.aidl.BookInfo book) throws android.os.RemoteException
    {
        // A Java Parcel object is created
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        
        // the token example is' com.starot.lib_intent.aidl.ITest 'to see the path of the aiDL object written by yourself
        _data.writeInterfaceToken(DESCRIPTOR);
        // Set parameters to Parcel
        _data.writeInt(age);
        // The binderproxy.transact method is executed
        boolean _status = mRemote.transact(Stub.TRANSACTION_updateBookOut, _data, _reply, 0); }}Copy the code
//BinderProxy
public final class BinderProxy implements IBinder {
    /** * Native implementation of transact() for proxies */
    public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;

    public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            // Native methods will eventually be executed
            returntransactNative(code, data, reply, flags); }}Copy the code

Navite layer

The data continues down. At this point you enter the Navite layer, where the original Java layer Parcel becomes a native Parcel. The data is transmitted to IPCThreadState for processing by BpBinder. The processing method is to copy the original native data through memCPy and store it in mOut variable. The mOut is also a Parcel object. Finally, the iocTL method calls the driver layer of the code. Memory map the data.

The source code is available on android_util_binder.cpp

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj, jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    // Object conversion
    Parcel* data = parcelForJavaObject(env, dataObj);
    Parcel* reply = parcelForJavaObject(env, replyObj);
    / / create the IBinder
    IBinder* target = getBPNativeData(env, obj)->mObject.get(a);/ / execution BpBinder. Transact
    status_t err = target->transact(code, *data, reply, flags);
    return JNI_FALSE;
}
Copy the code
status_t BpBinder::transact(
    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
{
    // Status is active
    if (mAlive) {
        // Use the ipcThreadState.transact method to pass the data
        status_t status = IPCThreadState::self() - >transact(
                mHandle, code, data, reply, flags);
        if (status == DEAD_OBJECT) mAlive = 0;
        return status;
    }
    return DEAD_OBJECT;
}
Copy the code
status_t IPCThreadState::transact(int32_t handle,
                                  uint32_t code, const Parcel& data,
                                  Parcel* reply, uint32_t flags)
{
    // Write data to memory without triggering data to binder drivers
    err = writeTransactionData(BC_TRANSACTION_SG, flags, handle, code, data, nullptr);

    // Wait for data to retrun according to different states
    err = waitForResponse(reply);
    err = waitForResponse(&fakeReply);
    err = waitForResponse(nullptr.nullptr);
    return err;
}
Copy the code

First, analyze the logic of writing

status_t IPCThreadState::writeTransactionData(int32_t cmd, uint32_t binderFlags,
    int32_t handle, uint32_t code, const Parcel& data, status_t* statusBuffer)
{
    // binder_transaction_data_sg is declared
    binder_transaction_data_sg tr_sg;
    // Save the data in tr_sg
    tr_sg.transaction_data.data_size = data.ipcDataSize(a); tr_sg.transaction_data.data.ptr.buffer = data.ipcData(a); tr_sg.transaction_data.offsets_size = data.ipcObjectsCount(*)sizeof(binder_size_t);
    tr_sg.transaction_data.data.ptr.offsets = data.ipcObjects(a); tr_sg.buffers_size = data.ipcBufferSize(a);// here mOut is a Parcel object defined.
    // It basically copies the data we're sending to mOut, which is also a class Parcel defined
    mOut.write(&tr_sg, sizeof(tr_sg));

    return NO_ERROR;
}
Copy the code

Will eventually perform � the system/libhwbinder/Parcel. CPP memcpy method

status_t Parcel::write(const void* data, size_t len)
{
    void* const d = writeInplace(len);
    if (d) {
        // Copy the data memory to mOut
        memcpy(d, data, len);
        return NO_ERROR;
    }
    return mError;
}
Copy the code

Once the data is written, we continue with the waitForResponse method, and we’ll continue with the analysis

status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
    while (1) {
        // Execute the talkWithDriver method
        if ((err=talkWithDriver()) < NO_ERROR) break;
        // Get the returned object
        reply->ipcSetDataReference(
              reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer),
              tr.data_size,
              reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets),
              tr.offsets_size/sizeof(binder_size_t),
              freeBuffer, this); }}Copy the code
status_t IPCThreadState::talkWithDriver(bool doReceive)
{
    // Define data
    binder_write_read bwr;

    // Write mOut data to BWR
    bwr.write_size = outAvail;
    bwr.write_buffer = (uintptr_t)mOut.data(a);do {
#if defined(__ANDROID__)
        // Use ioctl to pass data out
        if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)
            err = NO_ERROR;
        else
            err = -errno;
#else
        err = INVALID_OPERATION;
#endif
    } while (err == -EINTR);

}
Copy the code

At this point, the data has been passed from the Java layer to the Binder driver.

The kernel layer

Binder drives memory mapping using MMap. Binder drives memory mapping using MMap. The author knows a probably to this piece only, do not make thorough introduction, lest mistake a person’s children. PS: PERSONALLY, if you are interested, you can have a look. For Android development, it is not necessary to study so deeply. For example, epoll select is implemented by the epoll mechanism.

It’s full of crap. Because the author compares dish, still do not want to admit! ノ (` へ ´ *)

conclusion

A step by step analysis shows that data is encapsulated into data types that the driver can recognize, and then passed to the driver via iocTL methods. In Binder drivers, data is memory-mapped through MMAP to complete the process from the Java layer to the kernel. The following is the sending process.

How to accept data

Java layer

Here we work backwards, start with the Java layer, get the data from the client, and look up. Step by step analysis of Java layer data where is the source

//step1: finally, the data is retrieved in the Service's custom Binder interface
class MyBinder : ITest.Stub(a){
    override fun getAge(age: Int): Int {
        return 10001}}Copy the code
//step2:Binder inherits from Stub to get data in onTransact callbacks
public static abstract class Stub extends android.os.Binder implements com.starot.lib_intent.aidl.ITest
{
    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
        java.lang.String descriptor = DESCRIPTOR;
        switch (code){
                case TRANSACTION_getAge:
                {
                data.enforceInterface(descriptor);
                int _arg0;
                _arg0 = data.readInt();
                int _result = this.getAge(_arg0);
                reply.writeNoException();
                reply.writeInt(_result);
                return true; }}}}Copy the code
//step3: the onTransact method is returned in Binder execTransactInternal
private boolean execTransactInternal(int code, long dataObj, long replyObj, int flags,
        int callingUid) {
    res = onTransact(code, data, reply, flags);
    return res;
}
Copy the code
//step4: the initial method for retrieving the Java layer is Binder's execTransact method
// Entry point from android_util_Binder.cpp's onTransact
@UnsupportedAppUsage
private boolean execTransact(int code, long dataObj, long replyObj,
        int flags) {
        return execTransactInternal(code, dataObj, replyObj, flags, callingUid);
}
Copy the code

Native layer

Java Binder. Java execTransact method is provided by the Entry Point from Android_util_binder.cpp’s onTransact annotation Android_util_binder.cpp is triggered

class JavaBBinder : public BBinder
{
      status_t onTransact(
        uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags = 0) override
    {
        Execute the Java Binder execTransact method
        jboolean res = env->CallBooleanMethod(mObject, gBinderOffsets.mExecTransact,
            code, reinterpret_cast<jlong>(&data), reinterpret_cast<jlong>(reply), flags);
        return res != JNI_FALSE ? NO_ERROR : UNKNOWN_TRANSACTION;
    }
}
Copy the code
static int int_register_android_os_Binder(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, kBinderPathName);

    gBinderOffsets.mClass = MakeGlobalRefOrDie(env, clazz);
    // Notice that the execTransact method is saved in the mExecTransact register
    gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact"."(IJJI)Z");
    gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor"."()Ljava/lang/String;");
    gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject"."J");

    return RegisterMethodsOrDie(
        env, kBinderPathName,
        gBinderMethods, NELEM(gBinderMethods));
}

Copy the code

By working backwards, the Binder virtual machine executes the BBinder onTransact method, then the Java Binder execTransact method and finally the Stub onTransact method.

conclusion

With AIDL we can see how the data goes from Client process to Service process.

The last

Binder is a big thing. A lot of it is not understood yet. I still need to look at it again. Try to understand how each part of the process works. The above analysis is entirely based on the process analysis of data transfer. In fact, there are data verification, MMAP process. Process of interaction with Binder drivers. And so on.

reference

  • Liu Wangshu Binder principle series
  • Binder principle analysis for Android application engineers
  • Android Bander design and Implementation – Design chapter
  • Gityuan Binder series — The Beginning
  • I didn’t cut mine last time. I’ll do it again. Binder, you cut me.
  • Binder you Cut me
  • Binder interprocess Communication with Android
  • Android Binder,