Android's underlying inter-process synchronization mechanism

Author: Android interviewer

Classical realization of inter-process communication

Inter-process communication (IPC) refers to the data exchange between several threads running in different processes, which can occur on one machine or across machines through a network.

Shared memory, pipes, UNIX Domain Socket and RPC are used in almost all operating systems due to their high efficiency and stability.


Shared memory

Shared memory is a commonly used inter-process communication mechanism. Different processes can directly share and access the same memory area, which avoids data copy and is faster. The implementation steps are as follows:

1. Create a shared memory area

Linux creates a shared memory block associated with a specific key through the shmget method:

//返回共享内存块的唯一 Id 标识
int shmget(key_t key, size_t size, int shmflg);

2. Map memory shared area

Linux uses the shmat method to map a memory block to a memory address in the current process:

//成功返回指向共享存储段的指针 
void *shmat(int shm_id, const void *shm_addr, int shmflg);

3. Access the shared memory area

If other processes want to access an existing shared memory area, they can call shmget through the key to obtain the shared memory block Id, and then call the shmat method to map.

4. Inter-process communication

After the two processes have realized the mapping of the same memory shared area, they can use this memory shared area for data exchange, but they must implement the synchronization mechanism themselves.

5. Undo the memory map

After the inter-process communication ends, each process needs to cancel the previous mapping. Linux can call the shmdt method to cancel the mapping:

//成功则返回 0,否则出错
int shmdt(const void *shmaddr);

6. Delete the shared memory area

Finally, you need to delete the memory shared area to reclaim the memory. Linux can call shctl to delete:

//成功则返回 0,否则出错,删除操作 cmd 需传 IPC_RMID
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

The shmget method is concise and concise, share memory get! Among them, get has another meaning, why not call it create? If a shared memory block of a key has been created before, the memory block will be returned directly after another call, and the creation operation will not occur.


pipeline

Pipe is a common method of inter-process communication in operating systems. A pipe has two ends of "read" and "write". Read and write operations are similar to ordinary file operations and are one-way.

The pipeline has a capacity limit. When it is full, write operations will be blocked; when it is empty, read operations will be blocked.

Linux opens a pipe through the pipe method:

//pipe_fd[0] 代表读端,pipe_fd[1] 代表写端,
int pipe(int pipe_fd[2], int flags);

The above method can only be used for parent and child processes, because the pipe_fd file descriptor defined in only one process can only be passed to another process through fork and inherited. It is precisely because of this limitation that Named Pipe has developed and changed the former’s anonymity. The pipeline method can be used between two processes that have no relationship.

UNIX Domain Socket

UNIX Domain Socket (UDS) is specifically for inter-process communication within a single machine, also known as IPC Socket, which is basically the same as Network Socket, but the implementation principle is very different:

  • Network Socket is based on the TCP/IP protocol, through the IP address or port number for cross-process communication
  • UDS is based on the native socket file, does not need to go through the network protocol stack, does not need to pack or unpack, calculate and verify, etc.

The most used IPC in Android is Binder, followed by UDS.

Remote Procedure Calls

RPC stands for Remote Procedure Call. RPC refers to a process on computer A that calls a process on another computer B. The calling process on A is suspended and the called process on B starts to execute. When the value is returned to A, the A process continues.

The caller can pass information to the callee by using parameters, and then get the information through the returned result.

Java RMI is a kind of RPC framework, which refers to remote method invocation (Remote Method Invocation), which allows an object on a Java virtual machine to call a method of an object in another Java virtual machine.

RPC can be understood as a programming model, just like IPC, we often say that Android AIDL is an IPC implementation method, and can also be called an RPC method.


Classical implementation of synchronization mechanism

signal

Semaphore and PV primitive operations are a widely used effective method to achieve process/thread mutual exclusion and synchronization. Semaphore S semaphore is used to indicate the available amount of shared resources.

P operation:

  1. S = S - 1
  2. Then judge if S is greater than or equal to 0, it means that the shared resource is allowed to be accessed, and the process continues to execute
  3. If S is less than 0, it means that the shared resource is occupied, and you need to wait for others to actively release the resource. The process is blocked and placed in the queue waiting for the semaphore, waiting to be awaken

V operation:

  1. S = S + 1
  2. Then judge that if S is greater than 0, it means that there is no process waiting to access the resource and no need to deal with
  3. If S is less than or equal to 0, wake up a process from the signal waiting queue

The implementation class of semaphore in Java is Semaphore, and P and V operations correspond to acquire and release methods respectively.

Mutex

Mutex is a mutual exclusion lock, which can be understood by comparing it with a semaphore. A semaphore can enable resources to be accessed by multiple threads at the same time, while a mutex can only be accessed by one thread at the same time. In other words, the mutex is equivalent to a semaphore that only allows the value 0 or 1.

ReentrantLock in Java is an implementation of mutual exclusion lock.


Tube

In the program using the Semaphore mechanism, a large number of P and V operations are scattered in the program, and the code is not easy to read, difficult to manage, and prone to deadlock, so the monitor monitor is introduced.

The management process centralizes the critical areas scattered in each process for management, prevents intentional or unintentional illegal synchronization operations of the process, facilitates writing programs in high-level languages, and facilitates verification of program correctness.

The monitor encapsulates the synchronization operation, conceals the synchronization details from the process, and simplifies the call interface of the synchronization function. The user writes a concurrent program like a sequential (serial) program.

The synchronized synchronization code block in Java is an implementation of Monitor.

Linux Futex

The full name of Futex is Fast Userspace muTexes, which is literally translated as fast user space mutex . How is it faster than ordinary Mutex?

Traditional synchronization mechanisms such as Semaphore need to enter the kernel state from user state, and complete synchronization through a kernel object that provides shared state information and atomic operations. However, in most scenes, synchronization is non-competitive, and the lock can be directly acquired without entering the mutual exclusion zone and waiting, but the kernel mode switching operation is still performed, which causes a lot of performance overhead.

Futex uses mmap to share a section of memory between processes. When a process tries to enter or exit the mutex area, first check the Futex variable in the shared memory. If no competition occurs, only modify the Futex variable without executing the system call switch Kernel mode.

Futex's Fast is reflected in most situations where there is no competition, the lock can be acquired in the user mode without entering the kernel mode, thereby improving efficiency.

If traditional synchronization mechanisms such as Semaphore are a kernel-mode synchronization mechanism, then Futex is a synchronization mechanism that combines user mode and kernel mode.

An important application scenario of Futex in Android is the ART virtual machine. If the ART_USE_FUTEXES macro is enabled in the Android version, the synchronization mechanism in the ART virtual machine will be implemented with Futex as the cornerstone. The key code is as follows:

// art/runtime/base/mutex.cc
void Mutex::ExclusiveLock(Thread* self){
    #if ART_USE_FUTEXES
        //若开启 Futex 宏就通过 Futex 实现互斥加锁
        futex(...)
    #else
        //否则通过传统 pthread 实现
        CHECK_MUTEX_CALL(pthread_mutex_lock,(&mutex_));
}

For the source code, see http://androidxref.com/7.0.0_r1/xref/art/runtime/base/mutex.cc


Inter-process synchronization mechanism in Android

After understanding the classic synchronization mechanism of the operating system, let's see how it is implemented in Android.


Inter-process synchronization Mutex

Mutex implementation class source code is very short, see http://androidxref.com/7.0.0_r1/xref/system/core/include/utils/Mutex.h

Note that the Mutex mentioned here and the mutex.cc above are two things. Mutex.cc is the implementation class in ART and supports the Futex method; and Mutex.h just simply encapsulates the pthread API, and the function declaration and implementation are in Mutex.h in a file.

An enumeration type definition can be seen in the source code:

class Mutex {
public:
    enum {
        PRIVATE = 0,
        SHARED = 1
    };

Where PRIVATE stands for intra-process synchronization, and SHARED stands for inter-process synchronization. Mutex is simpler than Semaphore, with only two states of 0 and 1. The key methods are:

inline status_t Mutex::lock() {//获取资源锁,可能阻塞等待
    return -pthread_mutex_lock(&mMutex);
}
inline void Mutex::unlock() {//释放资源锁
    pthread_mutex_unlock(&mMutex);
}
inline status_t Mutex::tryLock() {//获取资源锁,不论成功与否都立即返回
    return -pthread_mutex_trylock(&mMutex);
}

When you want to access critical resources, you need to obtain the resource lock through lock() first. If the resource is available, this function will return immediately, otherwise it will block and wait until other processes (threads) call unlock() to release the resource lock and be awakened.

What is the significance of the tryLock() function? When the resource is occupied, it will not wait like lock(), but return immediately, so it can be used to tentatively query whether the resource lock is occupied.


Automatic operation Autolock

Autolock is a nested class in Mutex.h, implemented as follows:

// Manages the mutex automatically. It'll be locked when Autolock is
// constructed and released when Autolock goes out of scope.
class Autolock {
public:
    inline Autolock(Mutex& mutex) : mLock(mutex)  { mLock.lock(); }
    inline Autolock(Mutex* mutex) : mLock(*mutex) { mLock.lock(); }
    inline ~Autolock() { mLock.unlock(); }
private:
    Mutex& mLock;
};

As the comments indicate, Autolock will actively acquire the lock during construction, and will automatically release the lock during destruction, that is to say, the resource lock will be automatically released at the end of the life cycle.

This allows you to construct an Autolock for a Mutex at the beginning of a method. When the method is executed, the lock will be automatically released. There is no need to actively call unlock. This makes the use of lock/unlock more convenient and less error-prone.


Condition

The core idea of ​​condition judgment is to judge whether a certain "condition" is met, and if it is met, it will return immediately, otherwise it will block and wait until it is awakened when the condition is met.

You may be wondering, can Mutex be implemented? Why do you have another Condition? What is so special about it?

Mutex can indeed achieve synchronization based on conditional judgment. If the condition is that a is 0, the implementation code will be like this:

while(1){
  acquire_mutex_lock(a); //获取 a 的互斥锁
  if(a==0){
    release_mutex_lock(a); //释放锁
    break; //条件满足,退出死循环
  }else{
    release_mutex_lock(a); //释放锁
    sleep();//休眠一段时间后继续循环
  }
}

When a==0 is met is unknown, it may be a long time later, but the above method infinitely loops to determine the condition, which greatly wastes CPU.

The condition judgment does not require an endless loop, and the waiter can be notified to wake up when the condition is met.

Condition source code can be found at http://androidxref.com/7.0.0_r1/xref/system/core/include/utils/Condition.h , it also has PRIVATE and SHARED types like Mutex, PRIVATE means intra-process synchronization, SHARED means inter-process synchronization . The key methods are:

//在某个条件上等待
status_t wait(Mutex& mutex)
//在某个条件上等待,增加超时机制
status_t waitRelative(Mutex& mutex, nsecs_t reltime)
//条件满足时通知相应等待者
void signal()
//条件满足时通知所有等待者
void broadcast()

Mutex+Autolock+Condition example

We are familiar with LinkedBlockingQueue, the source code can be found at http://androidxref.com/7.0.0_r1/xref/frameworks/av/media/libstagefright/webm/LinkedBlockingQueue.h

class LinkedBlockingQueue {
    List<T> mList;
    Mutex mLock;
    Condition mContentAvailableCondition;

    T front(bool remove) {
        Mutex::Autolock autolock(mLock);
        while (mList.empty()) {
            mContentAvailableCondition.wait(mLock);
        }
        T e = *(mList.begin());
        if (remove) {
            mList.erase(mList.begin());
        }
        return e;
    }
    //省略...

    void push(T e) {
        Mutex::Autolock autolock(mLock);
        mList.push_back(e);
        mContentAvailableCondition.signal();
    }
}

When calling the front method to dequeue an element, first acquire the mLock lock, and then judge if the list is empty, call the wait method to enter the waiting state, and wake up the element after the push method enters the queue.

The front method occupies the mLock lock. Shouldn't the push method block the execution of the first line of code?

Very simple, the mLock lock is released in the wait method, see pthread_cond.cpp: http://androidxref.com/7.0.0_r1/xref/bionic/libc/bionic/pthread_cond.cpp#173

Can it be achieved without relying on Mutex only through the wait/signal of Condition?

No, the access to mList requires a mutex lock, otherwise the signal may become invalid. For example, process A calls front and judges that mList is empty. When the wait method is about to be executed, process B calls the push method and finishes executing, then process A will not be awakened even though there are elements in the queue.


At last

No matter what kind of operating system, its technical essence is similar, and more is to apply these core theories to scenarios that meet your own needs.

Here I have an Android learning PDF+architecture video+source notes collected and compiled by a big cow in the Android industry , as well as advanced architecture technology advanced brain maps, Android development interview special materials, advanced advanced architecture materials to help everyone learn and advance. It also saves everyone's time to search for information on the Internet to learn, and you can also share with friends around you to learn together.

If you want, you can click on to obtain.

Don't be satisfied with your existing knowledge, leave your comfort zone and learn endlessly. I wish you success.

Guess you like

Origin blog.csdn.net/ajsliu1233/article/details/111274332