Principle and use of Android binder

Time:2021-12-7
catalogue
  • preface
  • Use of binder
    • Fuzzy interprocess call
  • Binder principle
    • ioctl
    • Binder initialization
  • summary

    preface

    Binder is a common means to realize IPC (inter process communication) in Android. The cross process communication between the four components is also realized by binder. Binder is an important basis for learning the working principle of the four components. Many articles will go deep into C code to introduce binder’s workflow. It’s really difficult to understand without a certain level. This article will not go deep into the bottom to analyze the principle, and let everyone know how binder works as simple as possible.

    Use of binder

    Before introducing binder’s principle, let’s take a look at how binder is used in Android to communicate between processes. Before using binder, let’s introduce several methods of binder:

    
    public final boolean transact(int code, Parcel data, Parcel reply, int flags)
    
    
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
    

    These two methods represent the client and server respectively. Transact is used to send messages, and ontransact is responsible for receiving messages from transact, which is easy to understand.

    • Code method identifier. In the same process, we can easily execute our target method through method call, but between different processes, the method call method can no longer be used, so we use code to represent the identification of remote calling function. This ID must be between first_ CALL_ Transaction (0x00000001) and last_ CALL_ Between transaction (0x00ffffff).
    • The data packet of data parcel type is the request parameter to be passed to the client.
    • Reply if the client needs to return a value, reply is the data returned by the server.
    • Flags is used to distinguish whether the call is a normal call or a one-way call. During a normal call, the client thread will block until the return value is received from the server. If flag = = ibinder.flag_ Oneway, then this call is a one-way call, and the client will execute the next section of code immediately after the data is transferred out. At this time, both ends execute asynchronously. During one-way call, the function return value must be void (that is, the return value must be discarded for one-way call, and the return value must be blocked to wait)

    Using these two methods, we can realize the communication between client and server. Next, let’s see how to use it. When the server receives the message (data) from the client, it will verify the data. Data.enforceinterface (descriptor). The descriptor is a string descriptor. It can pass the verification only when the descriptor of data is the same as the descriptor.

    public class Stub extends Binder {
        //Descriptor
        public static final java.lang.String DESCRIPTOR = "MyTestBinder";
        //Code method descriptor
        public static final int TRANSACTION_test0 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        public static final int TRANSACTION_test1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    
        @Override
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            switch (code) {
                case TRANSACTION_test0:
                    //Validation descriptor
                    data.enforceInterface(DESCRIPTOR);
                    //Execution method
                    test0();
                    return true;
                case TRANSACTION_test1:
                    //Validation descriptor
                    data.enforceInterface(DESCRIPTOR);
                    //Execution method
                    test1(data, reply);
                    return true;
            }
            return super.onTransact(code, data, reply, flags);
        }
    
        //No return value
        private void test0() {
            Log.d("MyBinderServer", "test0");
        }
    
        //There is a return value
        private void test1(Parcel data, Parcel reply) {
            Log.d("MyBinderServer", "test1");
            reply.writeInt(data.readInt() + 1);
        }
    }

    We know that in order to realize the communication connection between the client and the server, we must let the client know the address of the server, just like the HTTP request. We need to know the IP and port of the server. Binder communication is actually the same, so how can we let the client get the server address? One is the same as the HTTP request. We know that the HTTP request needs to convert the domain name into IP and port. This is DNS. We also need a binder DNS. Android also provides us with binder’s “DNS”, that is, servicemanager. All system services (such as MediaServer) are registered in servicemanager. We can use servicemanager to get the remote binder address. This method is called named binder lookup (system services such as binder, MediaServer, etc. have names when they are registered. For example, we can get WindowManager through the name of window_service) However, the problem is that the process of registering a service with the servicemanager is implemented by the system process, and our application process cannot register its own binder. The other is to use the well-known binder to assist in passing anonymous binder, that is, if there is a well-known binder service that provides a method of passing binder, we can pass our binder through this binder service Anonymous binder, we can find out if this famous binder can get our anonymous binder. AMS actually provides such a function. It encapsulates the anonymous binder in the service through service.onbind for us to call.

    
     public class MyService extends Service {
        
        @Override
        public IBinder onBind(Intent intent) {
            return new Stub();
        }
    }
    

    We use binderservice () to get the remote binder.

    Intent serviceIntent = new Intent(this, MyService.class);
            bindService(serviceIntent, new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    //Service can be understood as the address of the remote binder. We use it to communicate with the binder remotely. The C + + layer will convert this ibinder to communicate with the binder
                }
    
                @Override
                public void onServiceDisconnected(ComponentName name) {
    
                }
            }, BIND_AUTO_CREATE);

    After obtaining binder, we will supplement the communication code:

    bindService(serviceIntent, new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    Parcel data0 = Parcel.obtain();// Request parameters
                    Parcel reply0 = Parcel.obtain();// Response parameters
                    Parcel data1 = Parcel.obtain();
                    Parcel reply1 = Parcel.obtain();
    
                    //Call the first method
                    try {
                        //Add descriptor
                        data0.writeInterfaceToken(Stub.DESCRIPTOR);
                        /*
                         *Write parameters. To pass multiple int parameters, call writeint sequentially
                         * data0.writeInt(10);
                         * data0.writeInt(20);
                         *Get
                         * int num10 = data0.readInt();
                         * int num20 = data0.readInt();
                         */
                        data0.writeInt(10);
                        //Initiate remote call
                        service.transact(Stub.TRANSACTION_test0, data0, reply0, 0);
                        reply0.readException();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    } finally {
                        data0.recycle();
                        reply0.recycle();
                    }
    
                    //Call the second method
                    try {
                        //Add descriptor
                        data1.writeInterfaceToken(Stub.DESCRIPTOR);
                        data1.writeInt(10);
                        //Initiate remote call
                        service.transact(Stub.TRANSACTION_test1, data1, reply1, 0);
                        reply1.readException();
                        //Read return value
                        int num = reply1.readInt();
                        Log.d(TAG, "reply value: " + num);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    } finally {
                        data1.recycle();
                        reply1.recycle();
                    }
                }
    
                @Override
                public void onServiceDisconnected(ComponentName name) {
    
                }
            }, BIND_AUTO_CREATE);

    To facilitate the call, we write a proxy class to encapsulate the communication process

    public class Proxy {
        //Descriptor
        public static final java.lang.String DESCRIPTOR = "MyTestBinder";
        //Code method descriptor
        public static final int TRANSACTION_test0 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        public static final int TRANSACTION_test1 = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
        private IBinder mRemote;
    
        public Proxy(IBinder remote) {
            this.mRemote = remote;
        }
    
        public void test1() {
            Parcel data0 = Parcel.obtain();// Request parameters
            Parcel reply0 = Parcel.obtain();// Response parameters
            //Call the first method
            try {
                //Add descriptor
                data0.writeInterfaceToken(DESCRIPTOR);
                /*
                 *Write parameters. To pass multiple int parameters, call writeint sequentially
                 * data0.writeInt(10);
                 * data0.writeInt(20);
                 *Get
                 * int num10 = data0.readInt();
                 * int num20 = data0.readInt();
                 */
                data0.writeInt(10);
                //Initiate remote call
                mRemote.transact(TRANSACTION_test0, data0, reply0, 0);
                reply0.readException();
            } catch (RemoteException e) {
                e.printStackTrace();
            } finally {
                data0.recycle();
                reply0.recycle();
            }
        }
    
        public int test2() {
            Parcel data1 = Parcel.obtain();
            Parcel reply1 = Parcel.obtain();
            //Call the second method
            int num = 0;
            try {
                //Add descriptor
                data1.writeInterfaceToken(DESCRIPTOR);
                data1.writeInt(10);
                //Initiate remote call
                mRemote.transact(TRANSACTION_test1, data1, reply1, 0);
                reply1.readException();
                //Read return value
                num = reply1.readInt();
            } catch (RemoteException e) {
                e.printStackTrace();
            } finally {
                data1.recycle();
                reply1.recycle();
            }
            return num;
        }
    }
    
     bindService(serviceIntent, new ServiceConnection() {
                @Override
                public void onServiceConnected(ComponentName name, IBinder service) {
                    Proxy proxy = new Proxy(service);
                    proxy.test1();
                    int i = proxy.test2();
                }
    
                @Override
                public void onServiceDisconnected(ComponentName name) {
    
                }
            }, BIND_AUTO_CREATE);
    

    Fuzzy interprocess call

    The first is the use of binder, but there is still a problem left. Our service can only be called remotely after specifying a new process name. If the ibinder object passed through bindservice is the same process, we don’t need to use ibinder.transact for kernel communication. We know that communication can be achieved between the same process by means of method call. We print the ibinder type in onserviceconnected. If it is found that it is a remote call, the ibinder passed to us is the binderproxy type. Binderproxy is the internal class of binder, which implements the ibinder interface as well. It will correspond to a C + + bpbinder in the native layer, and the bpbinder will eventually communicate with the server through the binder driver. If it is a local call, the printed type is stub, which means that during the local call, what onserviceconnected passes is the stub object itself returned in the onbind method of the service. Based on this principle, we can design a conversion method.

    First of all, how do we judge whether it is a remote call or the same process call? We use the querylocalinterface (descriptor) method. Querylocalinterface in binder does not return null, but in the implementation of binderproxy, querylocalinterface returns null. Binder:

    
    public IInterface queryLocalInterface(String descriptor) {
            if (mDescriptor != null && mDescriptor.equals(descriptor)) {
                return mOwner;
            }
            return null;
        }
    

    Mowner is the interface itself passed in by attachinterface method, which will appear later. BinderProxy:

    
    public IInterface queryLocalInterface(String descriptor) {
            return null;
        }
    

    When it is found that it is a remote call, we create the upper proxy to proxy the cross process communication process. If we call locally, we return the local stub object directly.

    public static IMyInterface asInterface(IBinder iBinder){
            if ((iBinder == null)) {
                return null;
            }
            //Get local interface
            IInterface iin = iBinder.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof IMyInterface))) {
                //If it is not empty, it indicates that it is a local call and returned after direct forced conversion.
                //IMyInterface encapsulates two methods, Test0 () and test1 (). The local object and proxy inherit from the interface
                return ((IMyInterface)iin );
            }
            //Null, remote call, new proxy
            return new Proxy(iBinder);
        }

    After completing the relevant code above

    public interface IBinderTest extends android.os.IInterface {
        /**
         *Local stub object
         */
        public static abstract class Stub extends android.os.Binder implements IBinderTest {
            private static final java.lang.String DESCRIPTOR = "com.XXX.XXXX.IBinderTest";
    
            public Stub() {
                //Bind the descriptor and set the mowner returned by the querylocalinterface method
                this.attachInterface(this, DESCRIPTOR);
            }
    
            /**
             *Local remote conversion
             */
            public static IBinderTest asInterface(android.os.IBinder obj) {
                if ((obj == null)) {
                    return null;
                }
                android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
                if (((iin != null) && (iin instanceof IBinderTest))) {
                    return ((IBinderTest) iin);
                }
                return new IBinderTest.Stub.Proxy(obj);
            }
    
            @Override
            public android.os.IBinder asBinder() {
                return this;
            }
    
            /**
             *Processing client call requests
             */
            @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 INTERFACE_TRANSACTION: {
                        reply.writeString(descriptor);
                        return true;
                    }
                    case TRANSACTION_testVoidAidl: {
                        data.enforceInterface(descriptor);
                        this.testVoidAidl();
                        reply.writeNoException();
                        return true;
                    }
                    case TRANSACTION_testStringAidl: {
                        data.enforceInterface(descriptor);
                        java.lang.String _result = this.testStringAidl();
                        reply.writeNoException();
                        reply.writeString(_result);
                        return true;
                    }
                    default: {
                        return super.onTransact(code, data, reply, flags);
                    }
                }
            }
    
            /**
             *Remote call proxy class
             */
            private static class Proxy implements IBinderTest {
                private android.os.IBinder mRemote;
    
                Proxy(android.os.IBinder remote) {
                    mRemote = remote;
                }
    
                @Override
                public android.os.IBinder asBinder() {
                    return mRemote;
                }
    
                public java.lang.String getInterfaceDescriptor() {
                    return DESCRIPTOR;
                }
    
                @Override
                public void testVoidAidl() throws android.os.RemoteException {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    try {
                        _data.writeInterfaceToken(DESCRIPTOR);
                        mRemote.transact(Stub.TRANSACTION_testVoidAidl, _data, _reply, 0);
                        _reply.readException();
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                }
    
                @Override
                public java.lang.String testStringAidl() throws android.os.RemoteException {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    java.lang.String _result;
                    try {
                        _data.writeInterfaceToken(DESCRIPTOR);
                        mRemote.transact(Stub.TRANSACTION_testStringAidl, _data, _reply, 0);
                        _reply.readException();
                        _result = _reply.readString();
                    } finally {
                        _reply.recycle();
                        _data.recycle();
                    }
                    return _result;
                }
            }
    
            /**
             *Calling function code
             */
            static final int TRANSACTION_testVoidAidl = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
            static final int TRANSACTION_testStringAidl = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
        }
    
        public void testVoidAidl() throws android.os.RemoteException;
    
        public java.lang.String testStringAidl() throws android.os.RemoteException;
    }

    If you have used Aidl and have seen the code generated by Aidl, you will find that the above code is generated by Aidl. Replace the service call

    
    public class MyService extends Service {
    
        private String TAG = "MyService";
    
        @Override
        public IBinder onBind(Intent intent) {
            return new MyBinder();
        }
    
        class MyBinder extends IBinderTest.Stub{
    
            @Override
            public void testVoidAidl() throws RemoteException {
                Log.d(TAG, "testVoidAidl");
            }
    
            @Override
            public String testStringAidl() throws RemoteException {
                Log.d(TAG, "testStringAidl");
                return "hello";
            }
        }
    }
    

    The above is a brief introduction to binder’s use method and principle.

    Binder principle

    Before we introduced the basic use of binder, let’s take a look at the underlying principle of binder.

    (photo source: gityuan. COM / 2015 / 10 / 31 /…), Android’s application memory is isolated, but the kernel space is shared. To realize IPC, we need to exchange data in the shared kernel space.

    Binder communication model:

    ioctl

    Binder’s communication principle:

    Due to the isolation mechanism of user space (sandbox mode), we need to use kernel space for IPC operation. The interaction between user space and kernel space uses the IOCTL function of Linux kernel. Next, let’s briefly understand the use of IOCTL. IOCTL can control I / O devices (driving devices), and provides a means to obtain device information and send control parameters to devices. Simply put, IOCTL can be used to operate the driving device. In the process of IPC, the kernel space uses the virtual driver / dev / binder to control the communication process. If we want to interact with the binder driver, we need to use the IOCTL command. In brief, Linux needs three roles to realize the use of driver devices

    • The application (caller) uses IOCTL to send operation instructions.
    • The driver is used to process the instructions from IOCTL and complete the operation of the driving device. After the driver is registered into the kernel, it will wait for the application to call.
    • In the binder mechanism, the driver device is / dev / binder. This file is mapped to the virtual memory of each system service (described later). The driver can operate this file for inter process data interaction.

    The following figure shows how applications in Linux operate hardware devices through drivers:

    Take a diagram to illustrate the relationship between the application layer and the IOCTL of the driver function:

    Briefly introduce the function:

    
    int (*ioctl) (struct inode * node, struct file *filp, unsigned int cmd, unsigned long arg);
    

    Parameters:

    • Inode and file: IOCTL operations may be to modify file properties or access hardware. These two structures are needed to modify the file properties, so their pointers are passed here.
    • CMD: command. I’ll talk about it next
    • Arg: parameter, which will be described next

    Return value: if an illegal command is passed in, IOCTL returns the error number – einval. The return value of the driver function in the kernel has a default method. As long as it is a positive number, the kernel will foolishly think it is the correct return and pass it to the application layer. If it is a negative value, the kernel will think it is an error.

    The CMD CMD of IOCTL is a number. If the value transmitted from the application layer has a corresponding operation in the driver, it will be executed. It is the same as the function identification in the transact method of ibinder. To define a command first, use a simple 0 to the header file of a command. Both the driver and application functions should contain this header file:

    /*test_cmd.h*/
    #ifndef _TEST_CMD_H
    #define _TEST_CMD_H
    
    #define TEST_ Clear 0 / * defined CMD*/
    
    #endif /*_TEST_CMD_H*/

    Driver implementation IOCTL: Command test_ The clear operation is to clear the kbuf in the driver.

    int test_ioctl (struct inode *node, struct file *filp, unsigned int cmd, uns igned long arg)
    {
        int ret = 0;
        struct _test_t *dev = filp->private_data;
        switch(cmd){
            case TEST_CLEAR:
            memset(dev->kbuf, 0, DEV_SIZE);
            dev->cur_size = 0;
            filp->f_pos = 0;
            ret = 0;
            break;
            Default: / * handling of command errors*/
            P_DEBUG("error cmd!\n");
            ret = - EINVAL;
            break;
        }
        return ret;
    }

    Then IOCTL (FD, TEST_CLEAR) is called in the application. You can execute the method of clearing kbuf in the driver.

    The Arg IOCTL command of IOCTL can also pass parameters. The “…” after IOCTL (FD, CMD,…) of the application layer refers to a parameter of any type. Note that one parameter is not any more, but the type is not checked.

    Binder initialization

    After we understand IOCTL, let’s take a look at how the binder device is initialized. Here we introduce the binder device, not the binder device driver. The binder driver is a misc device driver. To understand the content of the binder driver, please click the link below.

    gityuan.com/2015/11/01/…

    During the creation of our system service, we need to create and open binder devices. The following is the specific process. Let’s first introduce frameworks / native / LIBS / binder / processstate.cpp. Processstate is used to store various information of the current process. When the system service is started, the processstate singleton object of the current process will be created.

    ProcessState::ProcessState()
        //Open binder 
        : mDriverFD(open_driver()) //
          //Start address of mapped memory
        , mVMStart(MAP_FAILED)
        , mThreadCountLock(PTHREAD_MUTEX_INITIALIZER)
        , mThreadCountDecrement(PTHREAD_COND_INITIALIZER)
        , mExecutingThreadsCount(0)
        , mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
        , mStarvationStartTimeMs(0)
        , mManagesContexts(false)
        , mBinderContextCheckFunc(NULL)
        , mBinderContextUserData(NULL)
        , mThreadPoolStarted(false)
        , mThreadPoolSeq(1)
    {
        if (mDriverFD >= 0) {
            //Allocating virtual address space, completing data wirte / read, memory memcpy and other operations are equivalent to write / read (mdriverfd)
            mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);
            if (mVMStart == MAP_FAILED) {
                close(mDriverFD);
                mDriverFD = -1;
            }
        }
    }

    For a person who doesn’t understand C + +, it seems very uncomfortable, but this code is very important and should be understood. In fact, we only need to focus on these important lines of code_ Driver () will talk about mvmstart = MMAP (0, binder_vm_size, prot_read, map_private | map_noreserve, mdriverfd, 0) allocating virtual memory mapping. Let’s look at open first_ Driver function

    static int open_driver()
    {
        int fd = open("/dev/binder", O_RDWR | O_CLOEXEC); // Open / dev / binder
        if (fd >= 0) {
            int vers = 0;
            //Notify the binder driver of the binder version through IOCTL
            status_t result = ioctl(fd, BINDER_VERSION, &vers);
            if (result == -1) {
                ALOGE("Binder ioctl to obtain version failed: %s", strerror(errno));
                close(fd);
                fd = -1;
            }
            if (result != 0 || vers != BINDER_CURRENT_PROTOCOL_VERSION) {
                ALOGE("Binder driver protocol does not match user space protocol!");
                close(fd);
                fd = -1;
            }
            //Sets the current FD maximum support for default_ MAX_ BINDER_ Threads number
            size_t maxThreads = DEFAULT_MAX_BINDER_THREADS;
            result = ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);
            if (result == -1) {
                ALOGE("Binder ioctl to set max threads failed: %s", strerror(errno));
            }
        } else {
            ALOGW("Opening '/dev/binder' failed: %s\n", strerror(errno));
        }
        return fd;
    }

    First, execute int FD = open (“/ dev / binder”, o_rdwr | o_cloxec); Got the file descriptor of the driver file. After the file is opened successfully, IOCTL is used to query the version number and set the maximum number of connection threads. Then we call the mVMStart = MMAP (0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE MAP_NORESERVE MAP_NORESERVE, mDriverFD, 0) to map /dev/binder files into the process virtual memory space. Here we also need to know the MMAP function of Linux.

    MMAP reference from: blog. Itpub. Net / 7728585 / vie

    In Linux, we can use MMAP to allocate and create a virtual memory address map in the process virtual address space

    We can get a mapping area in the virtual memory of the current process. By directly operating the mapping area, we can indirectly operate the files in the kernel. We use MMAP to create shared file mappings

    Each process has a file map and points to the same file, which realizes shared memory. Binder uses this shared memory method to interact with data. Each process will retain a mapping area of the dev / binder device, so that we can use binder to realize cross process data after one copy, while the pipeline mechanism of Linux needs four copies

    summary

    1. This paper introduces the use of binder in Android. 2. Binder mechanism principle, user process isolation, IPC with the help of kernel space. 3. Use IOCTL system call function to call binder device driver to complete IPC call. 4. Dev / binder is a virtual device in binder mechanism, and the binder driver can operate the device (data interaction) 5 MMAP instruction can create process virtual memory mapping space, map dev / binder files, and realize shared memory. Binder’s one-time copy principle

    The above is the detailed content of the principle and use of Android binder. For more information about Android binder, please pay attention to other relevant articles of developeppaer!