Eingehende Analyse des anonymen Android-Speichers

Anonymes Android-Speicher-Parsing

Warum benötigen wir beim Binder-Mechanismus anonymen Speicher, um IPC zu implementieren? Ich denke, der Hauptgrund ist, dass es bei der Bindemittelübertragung Größenbeschränkungen gibt, ganz zu schweigen von den Beschränkungen der Anwendungsschicht. Die Größe der Ordnerübertragung im Treiber ist auf 4 MB begrenzt, und die Freigabe eines Bildes kann diese Grenze überschreiten. Die Hauptlösung für anonymen Speicher besteht darin, Dateideskriptoren über Binder zu übertragen, sodass beide Prozesse zur gemeinsamen Nutzung auf dieselbe Adresse zugreifen können.

MemoryFile-Nutzung

In der normalen Entwicklung stellt Android MemoryFile bereit, um anonymen Speicher zu implementieren. Schauen wir uns die einfachste Implementierung an.

Serviceseite


const val GET_ASH_MEMORY = 1000
class MyService : Service() {
    val ashData = "AshDemo".toByteArray()
    override fun onBind(intent: Intent): IBinder {
        return object : Binder() {
            override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
                when(code){
                    GET_ASH_MEMORY->{//收到客户端请求的时候会烦
                        val descriptor = createMemoryFile()
                        reply?.writeParcelable(descriptor, 0)
                        reply?.writeInt(ashData.size)
                        return true
                    }
                    else->{
                        return super.onTransact(code, data, reply, flags)
                    }
                }
            }
        }
    }
    private fun createMemoryFile(): ParcelFileDescriptor? {
        val file = MemoryFile("AshFile", 1024)//创建MemoryFile
        val descriptorMethod = file.javaClass.getDeclaredMethod("getFileDescriptor")
        val fd=descriptorMethod.invoke(file)//反射拿到fd
        file.writeBytes(ashData, 0, 0,ashData.size)//写入字符串
        return ParcelFileDescriptor.dup(fd as FileDescriptor?)//返回一个封装的fd
    }
}

 Die Funktion des Servers ist sehr einfach: Beim Empfang der GET_ASH_MEMORY-Anfrage erstellt er eine MemoryFile, schreibt ein Byte-Array mit Zeichenfolgen hinein, schreibt dann den fd und die Zeichenlänge in die Antwort und gibt sie an den Client zurück.

 

Klient


class MainActivity : AppCompatActivity() {
    val connect = object :ServiceConnection{
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            val reply = Parcel.obtain()
            val sendData = Parcel.obtain()
            service?.transact(GET_ASH_MEMORY, sendData, reply, 0)//传输信号GET_ASH_MEMORY
            val pfd = reply.readParcelable<ParcelFileDescriptor>(javaClass.classLoader)
            val descriptor = pfd?.fileDescriptor//拿到fd
            val size = reply.readInt()//拿到长度
            val input = FileInputStream(descriptor)
            val bytes = input.readBytes()
            val message = String(bytes, 0, size, Charsets.UTF_8)//生成string
            Toast.makeText(this@MainActivity,message,Toast.LENGTH_SHORT).show()
        }
        override fun onServiceDisconnected(name: ComponentName?) {
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.intent).setOnClickListener {
          //启动服务
            bindService(Intent(this,MyService::class.java),connect, Context.BIND_AUTO_CREATE)
        }
    }
}

Der Client ist ebenfalls sehr einfach: Er startet den Dienst, sendet eine Anforderung zum Abrufen von MemoryFile, ruft dann FD und Länge durch Antwort ab, verwendet FileInputStream, um den Inhalt von FD zu lesen, und verwendet schließlich Toast, um zu überprüfen, ob die Nachricht empfangen wurde.

Prinzip der AshMemory-Erstellung

    public MemoryFile(String name, int length) throws IOException {
        try {
            mSharedMemory = SharedMemory.create(name, length);
            mMapping = mSharedMemory.mapReadWrite();
        } catch (ErrnoException ex) {
            ex.rethrowAsIOException();
        }
    }

MemoryFile ist eine Kapselungsschicht von SharedMemory, und die spezifischen Funktionen werden von SharedMemory implementiert. Schauen Sie sich die Implementierung von SharedMemory an.

    public static @NonNull SharedMemory create(@Nullable String name, int size)
            throws ErrnoException {
        if (size <= 0) {
            throw new IllegalArgumentException("Size must be greater than zero");
        }
        return new SharedMemory(nCreate(name, size));
    }
  private static native FileDescriptor nCreate(String name, int size) throws ErrnoException;

Wenn Sie fd über JNI erhalten, können Sie hieraus schließen, dass die Java-Ebene nur ein Paket ist und Sie den erstellten fd erhalten.

//frameworks/base/core/jni/android_os_SharedMemory.cpp
jobject SharedMemory_nCreate(JNIEnv* env, jobject, jstring jname, jint size) {
    const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;
    int fd = ashmem_create_region(name, size);//创建匿名内存块
    int err = fd < 0 ? errno : 0;
    if (name) {
        env->ReleaseStringUTFChars(jname, name);
    }
    if (fd < 0) {
        jniThrowErrnoException(env, "SharedMemory_create", err);
        return nullptr;
    }
    jobject jifd = jniCreateFileDescriptor(env, fd);//创建java fd返回
    if (jifd == nullptr) {
        close(fd);
    }
    return jifd;
}

Erstellung durch die Funktion ashmem_create_region in Cutils

//system/core/libcutils/ashmem-dev.cpp
int ashmem_create_region(const char *name, size_t size)
{
    int ret, save_errno;
    if (has_memfd_support()) {//老版本兼容用
        return memfd_create_region(name ? name : "none", size);
    }
    int fd = __ashmem_open();//打开Ashmem驱动
    if (fd < 0) {
        return fd;
    }
    if (name) {
        char buf[ASHMEM_NAME_LEN] = {0};
        strlcpy(buf, name, sizeof(buf));
        ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_NAME, buf));//通过ioctl设置名字
        if (ret < 0) {
            goto error;
        }
    }
    ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));//通过ioctl设置大小
    if (ret < 0) {
        goto error;
    }
    return fd;
error:
    save_errno = errno;
    close(fd);
    errno = save_errno;
    return ret;
}

Standard-Fahrerinteraktion

1. Öffnen Sie den Treiber

2. Interagieren Sie mit dem Treiber über ioctl

Werfen wir einen Blick auf den Öffnungsprozess

static int __ashmem_open()
{
    int fd;
    pthread_mutex_lock(&__ashmem_lock);
    fd = __ashmem_open_locked();
    pthread_mutex_unlock(&__ashmem_lock);
    return fd;
}
/* logistics of getting file descriptor for ashmem */
static int __ashmem_open_locked()
{
    static const std::string ashmem_device_path = get_ashmem_device_path();//拿到Ashmem驱动路径
    if (ashmem_device_path.empty()) {
        return -1;
    }
    int fd = TEMP_FAILURE_RETRY(open(ashmem_device_path.c_str(), O_RDWR | O_CLOEXEC));
    return fd;
}

Gehen Sie zurück zum Konstruktor von MemoryFile, holen Sie sich den fd des Treibers und rufen Sie mapReadWrite auf

    public @NonNull ByteBuffer mapReadWrite() throws ErrnoException {
        return map(OsConstants.PROT_READ | OsConstants.PROT_WRITE, 0, mSize);
    }
 public @NonNull ByteBuffer map(int prot, int offset, int length) throws ErrnoException {
        checkOpen();
        validateProt(prot);
        if (offset < 0) {
            throw new IllegalArgumentException("Offset must be >= 0");
        }
        if (length <= 0) {
            throw new IllegalArgumentException("Length must be > 0");
        }
        if (offset + length > mSize) {
            throw new IllegalArgumentException("offset + length must not exceed getSize()");
        }
        long address = Os.mmap(0, length, prot, OsConstants.MAP_SHARED, mFileDescriptor, offset);//调用了系统的mmap
        boolean readOnly = (prot & OsConstants.PROT_WRITE) == 0;
        Runnable unmapper = new Unmapper(address, length, mMemoryRegistration.acquire());
        return new DirectByteBuffer(length, address, mFileDescriptor, unmapper, readOnly);
    }

An dieser Stelle stellt sich die Frage: Linux verfügt über einen gemeinsamen Speicher. Warum muss Android einen eigenen Speicher erstellen? Wir können uns nur die Implementierung des Ashmemory-Treibers ansehen.

Der erste Schritt beim Fahren besteht darin, sich init und file_operations anzusehen

static int __init ashmem_init(void)
{
    int ret = -ENOMEM;
    ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
                           sizeof(struct ashmem_area),
                           0, 0, NULL);//创建
    if (!ashmem_area_cachep) {
        pr_err("failed to create slab cache\n");
        goto out;
    }
    ashmem_range_cachep = kmem_cache_create("ashmem_range_cache",
                        sizeof(struct ashmem_range),
                        0, SLAB_RECLAIM_ACCOUNT, NULL);//创建
    if (!ashmem_range_cachep) {
        pr_err("failed to create slab cache\n");
        goto out_free1;
    }
    ret = misc_register(&ashmem_misc);//注册为了一个misc设备
    ........
    return ret;
}

Für die Zuweisung von ashmem_area und ashmem_range werden zwei Speicherzuweiser erstellt: ashmem_area_cachep und ashmem_range_cachep

//common/drivers/staging/android/ashmem.c
static const struct file_operations ashmem_fops = {
    .owner = THIS_MODULE,
    .open = ashmem_open,
    .release = ashmem_release,
    .read_iter = ashmem_read_iter,
    .llseek = ashmem_llseek,
    .mmap = ashmem_mmap,
    .unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT
    .compat_ioctl = compat_ashmem_ioctl,
#endif
#ifdef CONFIG_PROC_FS
    .show_fdinfo = ashmem_show_fdinfo,
#endif
};

open ruft ashmem_open auf

static int ashmem_open(struct inode *inode, struct file *file)
{
    struct ashmem_area *asma;
    int ret;
    ret = generic_file_open(inode, file);
    if (ret)
        return ret;
    asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);//分配一个ashmem_area
    if (!asma)
        return -ENOMEM;
    INIT_LIST_HEAD(&asma->unpinned_list);//初始化unpinned_list
    memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);//初始化一个名字
    asma->prot_mask = PROT_MASK;
    file->private_data = asma;
    return 0;
}

Das ioctl zum Festlegen des Namens und der Länge heißt ashmem_ioctl

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct ashmem_area *asma = file->private_data;
    long ret = -ENOTTY;
    switch (cmd) {
    case ASHMEM_SET_NAME:
        ret = set_name(asma, (void __user *)arg);
        break;
    case ASHMEM_SET_SIZE:
        ret = -EINVAL;
        mutex_lock(&ashmem_mutex);
        if (!asma->file) {
            ret = 0;
            asma->size = (size_t)arg;
        }
        mutex_unlock(&ashmem_mutex);
        break;
    }
  ........
  }

Die Implementierung ist ebenfalls sehr einfach. Ändern Sie einfach den Wert in asma. Als nächstes liegt der Schwerpunkt von mmap, insbesondere auf der Zuweisung von Speicher.

​static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
    static struct file_operations vmfile_fops;
    struct ashmem_area *asma = file->private_data;
    int ret = 0;
    mutex_lock(&ashmem_mutex);
    /* user needs to SET_SIZE before mapping */
    if (!asma->size) {//判断设置了size
        ret = -EINVAL;
        goto out;
    }
    /* requested mapping size larger than object size */
    if (vma->vm_end - vma->vm_start > PAGE_ALIGN(asma->size)) {//判断大小是否超过了虚拟内存
        ret = -EINVAL;
        goto out;
    }
    /* requested protection bits must match our allowed protection mask */
    if ((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask, 0)) &
        calc_vm_prot_bits(PROT_MASK, 0)) {//权限判断
        ret = -EPERM;
        goto out;
    }
    vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);
    if (!asma->file) {//是否创建过临时文件,没创建过进入
        char *name = ASHMEM_NAME_DEF;
        struct file *vmfile;
        struct inode *inode;
        if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')
            name = asma->name;
        /* ... and allocate the backing shmem file */
        vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);//调用linux函数在tmpfs中创建临时文件
        if (IS_ERR(vmfile)) {
            ret = PTR_ERR(vmfile);
            goto out;
        }
        vmfile->f_mode |= FMODE_LSEEK;
        inode = file_inode(vmfile);
        lockdep_set_class(&inode->i_rwsem, &backing_shmem_inode_class);
        asma->file = vmfile;
        /*
         * override mmap operation of the vmfile so that it can't be
         * remapped which would lead to creation of a new vma with no
         * asma permission checks. Have to override get_unmapped_area
         * as well to prevent VM_BUG_ON check for f_ops modification.
         */
        if (!vmfile_fops.mmap) {//设置了临时文件的文件操作,防止有其他程序mmap这个临时文件
            vmfile_fops = *vmfile->f_op;
            vmfile_fops.mmap = ashmem_vmfile_mmap;
            vmfile_fops.get_unmapped_area =
                    ashmem_vmfile_get_unmapped_area;
        }
        vmfile->f_op = &vmfile_fops;
    }
    get_file(asma->file);
    /*
     * XXX - Reworked to use shmem_zero_setup() instead of
     * shmem_set_file while we're in staging. -jstultz
     */
    if (vma->vm_flags & VM_SHARED) {//这块内存是不是需要跨进程
        ret = shmem_zero_setup(vma);//设置文件
        if (ret) {
            fput(asma->file);
            goto out;
        }
    } else {
    /**
    实现就是把vm_ops设置为NULL
    static inline void vma_set_anonymous(struct vm_area_struct *vma)
        {
            vma->vm_ops = NULL;
        }
    */
        vma_set_anonymous(vma);
    }
    vma_set_file(vma, asma->file);
    /* XXX: merge this with the get_file() above if possible */
    fput(asma->file);
out:
    mutex_unlock(&ashmem_mutex);
    return ret;
}

Die Funktion ist sehr lang, aber die Idee ist immer noch sehr klar. Erstellen Sie temporäre Dateien und richten Sie Dateivorgänge ein. Alle aufgerufenen Systemfunktionen sind Linux-Systemfunktionen. Siehe die eigentliche Funktion shmem_zero_setup.

int shmem_zero_setup(struct vm_area_struct *vma)
{
    struct file *file;
    loff_t size = vma->vm_end - vma->vm_start;
    /*
     * Cloning a new file under mmap_lock leads to a lock ordering conflict
     * between XFS directory reading and selinux: since this file is only
     * accessible to the user through its mapping, use S_PRIVATE flag to
     * bypass file security, in the same way as shmem_kernel_file_setup().
     */
    file = shmem_kernel_file_setup("dev/zero", size, vma->vm_flags);
    if (IS_ERR(file))
        return PTR_ERR(file);
    if (vma->vm_file)
        fput(vma->vm_file);
    vma->vm_file = file;
    vma->vm_ops = &shmem_vm_ops;//很重要的操作将这块虚拟内存的vm_ops设置为shmem_vm_ops
    if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE) &&
            ((vma->vm_start + ~HPAGE_PMD_MASK) & HPAGE_PMD_MASK) <
            (vma->vm_end & HPAGE_PMD_MASK)) {
        khugepaged_enter(vma, vma->vm_flags);
    }
    return 0;
}
static const struct vm_operations_struct shmem_vm_ops = {
    .fault      = shmem_fault,//Linux的共享内存实现的基础
    .map_pages  = filemap_map_pages,
#ifdef CONFIG_NUMA
    .set_policy     = shmem_set_policy,
    .get_policy     = shmem_get_policy,
#endif
};

Die Initialisierung des Shared Memory endet hier.

AshMemory lesen und schreiben

​//frameworks/base/core/java/android/os/MemoryFile.java
public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)
            throws IOException {
        beginAccess();
        try {
            mMapping.position(destOffset);
            mMapping.put(buffer, srcOffset, count);
        } finally {
            endAccess();
        }
    }
    private void beginAccess() throws IOException {
        checkActive();
        if (mAllowPurging) {
            if (native_pin(mSharedMemory.getFileDescriptor(), true)) {
                throw new IOException("MemoryFile has been purged");
            }
        }
    }
    private void endAccess() throws IOException {
        if (mAllowPurging) {
            native_pin(mSharedMemory.getFileDescriptor(), false);
        }
    }

Unter diesen entsprechen beginAccess und endAccess. Was aufgerufen wird, ist native_pin, eine native Funktion. Ein Parameter ist wahr und der andere ist falsch. Die Funktion des Pins besteht darin, diesen Speicher vor der Wiederverwendung durch das System zu sperren und ihn zu entsperren, wenn er nicht verwendet wird.

static jboolean android_os_MemoryFile_pin(JNIEnv* env, jobject clazz, jobject fileDescriptor,
        jboolean pin) {
    int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);
    int result = (pin ? ashmem_pin_region(fd, 0, 0) : ashmem_unpin_region(fd, 0, 0));
    if (result < 0) {
        jniThrowException(env, "java/io/IOException", NULL);
    }
    return result == ASHMEM_WAS_PURGED;
}

Rufen Sie ashmem_pin_region und ashmem_unpin_region auf, um das Entsperren und Entsperren zu erreichen. Die Implementierung befindet sich noch in ashmem-dev.cpp

//system/core/libcutils/ashmem-dev.cpp
int ashmem_pin_region(int fd, size_t offset, size_t len)
{
    .......
    ashmem_pin pin = { static_cast<uint32_t>(offset), static_cast<uint32_t>(len) };
    return __ashmem_check_failure(fd, TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin)));
}

Der übergebene Treiber ist auch die IOCLT-Benachrichtigung. Auf die Einzelheiten der Sperrung wird nicht näher eingegangen. Das spezifische Schreiben erfolgt über den Shared-Memory-Mechanismus von Linux.

Einführung in den Linux-Freigabemechanismus

Der einfache Weg, die gemeinsame Nutzung zu erreichen, besteht darin, dieselbe Datei zuzuordnen. Da die Lese- und Schreibgeschwindigkeit realer Dateien jedoch zu langsam ist, verwenden wir tmpfs, ein virtuelles Dateisystem, um eine virtuelle Datei zum Lesen und Schreiben zu erstellen. Gleichzeitig wird auch dieser virtuelle Speicher darauf geschrieben und vm_ops neu geschrieben. Wenn ein Prozess diesen virtuellen Speicher betreibt, wird ein Seitenfehler ausgelöst und dann der Seitencache durchsucht. Da es zum ersten Mal keinen Cache gibt, wird der physische Speicher gelesen und der Seitencache hinzugefügt Zur gleichen Zeit. Wenn der zweite Prozess eintrifft, wird auch der Seitencache gefunden, wenn ein Seitenfehler ausgelöst wird, sodass sie denselben physischen Speicher betreiben.

Zusammenfassen

Nachdem ich es gelesen hatte, stellte ich fest, dass AshMemory auf dem gemeinsam genutzten Linux-Speicher basiert. Einige Änderungen vorgenommen

  • Zunächst wird ein ganzer Speicherblock in Regionen umgewandelt, sodass diese bei Nichtgebrauch vom System entsperrt und recycelt werden können.
  • Die Ganzzahl des gemeinsam genutzten Linux-Speichers wird als gemeinsam genutzter Speicher markiert, und AshMemory verwendet fd, sodass die fd-Übertragung des Bindermechanismus genutzt werden kann.
  • Die Lese- und Schreibeinstellungen wurden gesperrt, was die Schwierigkeit für Benutzer verringert.

Supongo que te gusta

Origin blog.csdn.net/jdsjlzx/article/details/134587730
Recomendado
Clasificación