1. Basic Concepts

Linux process Spaces are isolated from each other.

Linux logically divides memory space into kernel space and user space. Linux operating systems and drivers run in kernel space, and applications run in user space, which are isolated for kernel security. Kernel space has access to all memory space, while user space has no access to kernel space.

User programs can access the kernel space only by dropping into kernel state through system calls. System calls are mainly implemented through copy_to_user() and copy_from_user(). Copy_to_user () is used to copy data from kernel space to user space, and copy_from_user() is used to copy data from user space to kernel space.

Two, Binder analysis

Binder is an interprocess communication mechanism on Android. Binder is implemented in client-server mode and consists of BinderDriver, ServiceManager, Client, and Server modules. Advantages of Binder over Socket and other traditional IPC methods:

Good security: Add UID/PID identity information to the sender. Better performance: Only one data copy is required during transmission, while traditional IPC methods such as sockets and pipes require at least two data copiesCopy the code

Binder four modules:

  • Binder drivers are located in the kernel space and are responsible for the establishment of Binder communication, inter-process delivery, Binder reference count management/packet transport, etc. The cross-process communication between Client and Server is handled by Binder Driver.

  • For clients, all they need to do is know the name of the Binder they want to use, access ServerManager with reference 0 to get a reference to the target Binder, and call the methods of the Binder entity as normal.

  • The Server will bind an alias to a Binder entity and pass the alias to the Binder Driver. If the Binder Driver receives the alias, it will create a corresponding Binder entity node in the kernel space. The Binder Driver then passes the reference to the node to the ServerManager, who then inserts the Binder alias and reference into a data table, similar to the domain-to-IP address mapping stored in DNS.

  • The ServerManager is also a standard Server, and it is stipulated in Android that its unique identifier for Binder communication is always 0, which is the reference 0 mentioned earlier.

During Android startup, SystemServer registers the ServiceManager with BinderDriver, which automatically creates Binder entities for the ServiceManager. All application processes started after this point hold a handle to this Binder, reference 0, which means all user processes’ reference 0 point to this Binder. System services such as ActivityManagerService and PackageManagerService communicate with applications bidirectional through Binder mechanisms.

Traditional IPC:

The kernel server allocates memory in kernel space and copies the data from the sender cache to the kernel cache. The kernel copies the data from the kernel cache into the cache provided by the receiver. * This store-and-forward mechanism has two drawbacks: * The first is inefficient, requiring two copies: user space -> kernel space -> user space. Linux uses copy_from_user() and copy_to_user() to implement these two cross-space copies. If high memory is used in this process, this copy requires the temporary creation/cancellation of page mappings, resulting in a performance penalty. * Secondly, the cache of the received data should be provided by the receiver. The receiver does not know how much cache is enough, so it can only open up as much space as possible or call the API to receive the message header to obtain the message body size, and then open up an appropriate space to receive the message body. Both approaches have their drawbacks. They are a waste of space or time.Copy the code

Obviously, cross-process communication on Linux requires kernel space to support it. Traditional cross-process communication methods such as sockets, semaphores, pipes, and memory sharing are all part of the Linux kernel. Binders on Android are not part of the Linux kernel. Android uses LKM (Loadable Kernel Module) to mount Binder drivers as dynamic kernels. Then the kernel space and the user space of the receiver are memory-mapped by mMAP with Binder Driver, so the data is only copied from the user space of the sender to the kernel space, and the interprocess communication is realized once the data is copied. After mmap is used to map kernel space to user space, the same physical memory can be accessed by virtual address in user space or kernel space. So the essence of MMap is that a virtual address in user space points to the same physical address as a virtual address in kernel space.

Android applications create a singleton ProcessState object at process startup, whose constructor completes binder MMap at the same time, allocating a chunk of memory to the process for binder communication.

Anonymous Binder

Binders registered with ServiceManager are real name binders. After the connection between Client and Server is established with real name Binder, Server can also encapsulate new Binder entities into packets and deliver them to Client through this connection, which is called anonymous Binder. Anonymous Binders still generate physical nodes in Binder drivers, but they are not registered with ServiceManager.

Anonymous Binder establishes a private channel for communication parties. As long as the Server does not send the anonymous Binder to other processes, other processes cannot obtain the reference of the Binder by enumerating or guessing in any way and send requests to the Binder.

Binder thread (The resources)

Binder communication is actually communication between threads located in different processes. If process S is Server and provides Binder entities, thread T1 sends requests to process S from Client process C1 via a reference to Binder. S needs to start thread T2 in order to process the request, while thread T1 is waiting to receive the returned data. After processing the request, T2 will return the processing result to T1, and T1 will wake up to get the processing result. In this process, T2 acts as a proxy for T1 in process S, performing remote tasks on behalf of T1, giving T1 the impression that it has crossed into S to execute a piece of code and returned to C1. To make the crossing more realistic, the driver assigns some attributes of T1 to T2, especially T1’s priority nice, so that T2 takes a similar amount of time to complete the task. Many sources use the misleading term ‘thread migration’ to describe this phenomenon. First, it is impossible for threads to jump from process to process, and second, T2 has nothing in common with T1 except its priority, including identity, open files, stack size, signal handling, private data, etc.

For Server process S, many clients may initiate requests at the same time. In order to improve efficiency, thread pools are often created to process the received requests concurrently. How do you implement concurrent processing using thread pools? This has to do with the specific IPC mechanism. Take the socket as an example. The socket on the Server is set to listening mode. A dedicated thread uses the socket to listen for connection requests from the Client, that is, blocked on accept(). The socket is like a chicken that lays an egg. It will lay an egg as soon as it receives a request from the Client – create a new socket and return it from Accept (). The listener thread starts a worker thread from the thread pool and hands it the egg it just laid. Subsequent business processing is done by the thread and the Client is interacting with the order.

With no listening mode and no laying of eggs, how does Binder manage thread pools? A simple way to do this is to create a bunch of threads, each reading Binder with BINDER_WRITE_READ. These threads block on wait queues set up by drivers for the Binder and wake up a thread from the queue whenever a data driver from the Client arrives. It’s simple and intuitive, eliminating thread pools, but creating a bunch of threads in the first place is a bit wasteful. The Binder protocol introduces special commands or messages to help users manage thread pools, including:

· INDER_SET_MAX_THREADS
· BC_REGISTER_LOOP
· BC_ENTER_LOOP
· BC_EXIT_LOOP
· BR_SPAWN_LOOPER
Copy the code

Managing a thread pool first requires knowing how large the pool is, and the application tells the driver through INDER_SET_MAX_THREADS that the maximum number of threads can be created. BC_REGISTER_LOOP, BC_ENTER_LOOP, and BC_EXIT_LOOP will be used to inform the driver when each thread is created, entered, and exit_loop, respectively, so that the driver can collect and record the current thread pool state. Each time the driver returns to read a Binder thread after receiving a packet, it checks to see if there are no idle threads left. If so, and the total number of threads does not exceed the maximum number of threads in the thread pool, a BR_SPAWN_LOOPER message is appended to the currently read packet to tell the user that the number of threads is running low and to start some more or the next request may not respond in time. Once a new thread is started, the driver is notified of the update status via BC_xxx_LOOP. As long as threads are not running out, there are always idle threads waiting in the queue to process requests in a timely manner.

Binder drivers also make a small optimization for starting worker threads. When thread T1 of process P1 sends a request to process P2, the driver first checks to see if thread T1 is also processing a request from one of the threads of process P2 but has not completed (no reply has been sent). This usually occurs when both processes have Binder entities and make requests to each other. If the driver finds such a thread in process P2, such as T2, it will ask T2 to handle the request from T1. Since T2 has not received a return packet since it sent a request to T1, T2 must (or will) block reading the return packet. It’s better to have T2 do something than just sit there. Moreover, if T2 is not a thread in the thread pool, it can also do some of the work for the thread pool, reducing thread pool utilization.

3. Mmap extension

Linux supports three I/O modes: standard I/O, direct I/O, and MMAP.

The standard IO

The read() and write() used by applications belong to standard IO. After initiating read and write operations, they actually cache read and write data to pages in the kernel space. For write operations, the system defaults to a deferred write mechanism, where the data cached by the page is written to disk by the kernel at the appropriate time.

* The user initiates a write operation * the operating system lookup page cache a. If not, a missing page exception is generated and a page cache is created to write the user's incoming content to page cache B. If a match is made, the user's incoming content is directly written to the page cache * The user's write call is complete * The page is modified to become a dirty page. The operating system has two mechanisms for writing dirty pages back to disk A. The user manually calls fsync() b. The pdflush process periodically writes dirty pages back to diskCopy the code

You can see that there are two copies of data during the write process, the first is written from memory to kernel space, and the second is when the kernel writes the page cache data to disk.

Knowledge expansion:

SSD storage also has a "write magnification" problem compared to mechanical hard drives. The problem has to do with the physical structure of SSD storage. After an SSD has been written, the data to be written cannot be directly updated but can be overwritten. Data must be erased before being overwritten. However, the smallest unit of write is Page, and the smallest unit of erase is Block, and Block is much larger than Page. Therefore, when writing new data, you need to first read the data from the Block and the data to be written together, then erase the Block, and finally write the read data to the storage. As a result, the data actually written may be much larger than the data originally written.Copy the code

Direct I/o

The application reads and writes directly to the disk. Android does not provide a JAVA API for direct IO.

mmap

Mmap is a method of memory mapping in operating systems.

Memory mapping: Mapping a memory region of user space to kernel space. After the mapping relationship is established, the modification of the memory area can be directly reflected in the kernel space. Conversely, changes made to this area in kernel space can be directly reflected in user space.

Mmap is usually used on file systems with physical media. Using Mmap, files can be mapped to the address space of the process, and the corresponding relationship between the disk address and the virtual space address of the process can be realized.

Advantages: * Reduced system calls. With a single system call to mmap(), the mapping is set up as if it were memory. * Reduce the number of data copies. Mmap () requires only one copy of the data. Disadvantages: * Requires more memory.Copy the code

Memory mapping implementation provided in Java: MappedByteBuffer