Android匿名共享內存(Anonymous Shared Memory) --- 瞎折騰記錄 (驅動程序篇)

#PS:要轉載請註明出處,本人版權所有

#PS:這個只是 《 我自己 》理解,如果和你的

#原則相沖突,請諒解,勿噴

背景

沒有買賣,就沒有傷害。 ------ (佚名)
作爲一個打工仔,要積極完成領導分配的任務。so,我分配到一個關於android進程間高效傳輸大量數據的的任務。不用我說,只要提及“大量”“高效”“進程間”這幾個詞,首先就得想到共享內存。雖然共享內存有這樣那樣的缺點,但是有一個優點,那就是快。
可是這裏有個問題,爲什麼我需要去讀這些驅動或者亂七八糟的東西?因爲Android提供了一個java類叫做:MemoryFile就可以實現共享內存,MemoryFile是android基於其共享內存機制實現的java層到java層之間的數據傳輸。但是,在我們的工作內容中,需要在android系統中用前向框架跑深度學習算法,這裏有個問題,我們跑算法的時候是在C or C++層跑的,但是我們有一些圖像數據需要在apk裏面採集或者使用。在Android中,怎麼讓圖像數據在c or c++層和java層快速傳輸,c or c++ 層之間快速傳輸,這就需要我們瞭解android 共享內存機制,做出合適的抉擇。

Android Anonymous Shared Memory 簡介

我們都知道Android是基於Linux內核搭建的一個分層系統,其中有kernel層,RunTimeLib和其他lib層,Framework層,Application層。雖然Linux os帶了很多IPC的機制,其中Sharedmemory也有兩種,但是在android系統裏面,是沒有這些內容的,android的linuxkernel你可以理解爲是一種深度定製版,很多東西都與普通的linux kenel不一致。
由於Androidkernel不帶普通的linuxkernel的常用共享內存方式,所以android 系統提供了另外一種替代方式,當然不是全部重複造輪子,只是通過驅動程序的方式,增加了自己想要的新特性,並封裝了一個新的共享內存接口出來。
更詳細和基本的介紹:請大家去參考百度的很多對Android共享內存的介紹。現在網上有很多關於android共享內存的介紹,有部分是精品,讓我受益匪淺,是可以參考的。這裏向這些無私奉獻精品的前輩致敬。

Android Anonymous Shared Memory 驅動源碼分析

在我記憶中,有一個不知道誰說的,學習的好方法,那就是:從源碼來,到源碼去。我一直抱着這種心態來學習新的事物,畢竟許多理論是需要我們去了解,雖然不需要去重複造輪子。

Android kernel的驅動編寫和linuxkernel的驅動編寫類似,都是有一個入口函數。我們首先從這兩個函數開始分析,然後分析幾個比較重要的接口就行。

源碼版本:android-p release kernel 4.9
目錄:\drivers\staging\android\ashmem.c

驅動入口

/**
 * struct ashmem_area - The anonymous shared memory area
 * @name:		The optional name in /proc/pid/maps
 * @unpinned_list:	The list of all ashmem areas
 * @file:		The shmem-based backing file
 * @size:		The size of the mapping, in bytes
 * @prot_mask:		The allowed protection bits, as vm_flags
 *
 * The lifecycle of this structure is from our parent file's open() until
 * its release(). It is also protected by 'ashmem_mutex'
 *
 * Warning: Mappings do NOT pin this structure; It dies on close()
 */
struct ashmem_area {
	char name[ASHMEM_FULL_NAME_LEN];
	struct list_head unpinned_list;
	struct file *file;
	size_t size;
	unsigned long prot_mask;
};

/**
 * struct ashmem_range - A range of unpinned/evictable pages
 * @lru:	         The entry in the LRU list
 * @unpinned:	         The entry in its area's unpinned list
 * @asma:	         The associated anonymous shared memory area.
 * @pgstart:	         The starting page (inclusive)
 * @pgend:	         The ending page (inclusive)
 * @purged:	         The purge status (ASHMEM_NOT or ASHMEM_WAS_PURGED)
 *
 * The lifecycle of this structure is from unpin to pin.
 * It is protected by 'ashmem_mutex'
 */
struct ashmem_range {
	struct list_head lru;
	struct list_head unpinned;
	struct ashmem_area *asma;
	size_t pgstart;
	size_t pgend;
	unsigned int purged;
};

static const struct file_operations ashmem_fops = {
	.owner = THIS_MODULE,
	.open = ashmem_open,
	.release = ashmem_release,
	.read = ashmem_read,
	.llseek = ashmem_llseek,
	.mmap = ashmem_mmap,
	.unlocked_ioctl = ashmem_ioctl,
#ifdef CONFIG_COMPAT
	.compat_ioctl = compat_ashmem_ioctl,
#endif
};


static struct miscdevice ashmem_misc = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "ashmem",
	.fops = &ashmem_fops,
};


static struct kmem_cache *ashmem_area_cachep __read_mostly;
static struct kmem_cache *ashmem_range_cachep __read_mostly;

static int __init ashmem_init(void)
{
	int ret = -ENOMEM;
	//slab 緩存 中 創建struct ashmem_area內存區域結構,這個創建後,下一次分配這個結構體的時候可以更快。
	ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
					       sizeof(struct ashmem_area),
					       0, 0, NULL);
	////unlikely()--執行else後面的語句概率較高,增加cache命中率,likely()與此功能相反
	//
	if (unlikely(!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, 0, NULL);
	if (unlikely(!ashmem_range_cachep)) {
		pr_err("failed to create slab cache\n");
		goto out_free1;
	}
	//雜項設備註冊
	ret = misc_register(&ashmem_misc);
	if (unlikely(ret)) {
		pr_err("failed to register misc device!\n");
		goto out_free2;
	}
	//這個好像和內存回收有關。我這裏不關心。
	register_shrinker(&ashmem_shrinker);

	pr_info("initialized\n");

	return 0;

out_free2:
	kmem_cache_destroy(ashmem_range_cachep);
out_free1:
	kmem_cache_destroy(ashmem_area_cachep);
out:
	return ret;
}

device_initcall(ashmem_init);

當這個設備創建以後,就會在/dev/下生成一個ashmem字符設備。

在struct file_operations ashmem_fops中,我們註冊了很多接口,如果大家對linux 驅動編程沒有一點了解的話,你可以直接理解爲我們在用戶態調用open就會調用這裏的ashmem_open,其他的類似。
下面我們重點介紹幾個我們常用的接口:ashmem_open,ashmem_ioctl,ashmem_mmap。

ashmem_open
我們調用open的時候,打開一個內存共享。

/**
 * ashmem_open() - Opens an Anonymous Shared Memory structure
 * @inode:	   The backing file's index node(?)
 * @file:	   The backing file
 *
 * Please note that the ashmem_area is not returned by this function - It is
 * instead written to "file->private_data".
 *
 * Return: 0 if successful, or another code if unsuccessful.
 */
static int ashmem_open(struct inode *inode, struct file *file)
{
	struct ashmem_area *asma;
	int ret;
	
	//檢查vfs打開的文件,在32爲系統下打開大文件導致overflow的問題
	ret = generic_file_open(inode, file);
	////unlikely()--ret爲0的概率較大,增加cache命中率,方便編譯器優化分支語句。
	//(把else後的語句緊貼if之後,把return 放到jmp之後。)likely()與此功能相反
	//
	if (unlikely(ret))
		return ret;

	/*

	GFP_KERNEL —— 正常分配內存,可以被中斷,還有其他內存分配標誌。GFP_KERNEL,GFP_DMA

	*/
	//快速分配一個struct ashmem_area結構體,相當於這段新開闢的共享內存的句柄
	asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
	if (unlikely(!asma))
		return -ENOMEM;
	//這個和內存回收有關,不管
	INIT_LIST_HEAD(&asma->unpinned_list);
	//給共享內存名字賦初值
	memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);
	//設置保護位
	asma->prot_mask = PROT_MASK;
	//利用struct file結構體中的private_data 來保存我們剛剛分配的句柄的指針。private_data 可以用來攜帶個人定製的數據,這裏用來攜帶我們定義的共享內存句柄。注意這個地方很重要,爲啥重要後面獨立解釋。
	file->private_data = asma;

	return 0;
}

ashmem_ioctl
這裏我們關注一下,給struct ashmem_area 的name和size賦值即可,這裏注意,還沒有分配實際的內存,僅僅是句柄的相關信息填充完畢了。

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	//這裏就是獲取之前ashmem_open的過程中,我們保存的共享內存句柄
	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_GET_NAME:
		ret = get_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;
	case ASHMEM_GET_SIZE:
		ret = asma->size;
		break;
	case ASHMEM_SET_PROT_MASK:
		ret = set_prot_mask(asma, arg);
		break;
	case ASHMEM_GET_PROT_MASK:
		ret = asma->prot_mask;
		break;
	case ASHMEM_PIN:
	case ASHMEM_UNPIN:
	case ASHMEM_GET_PIN_STATUS:
		ret = ashmem_pin_unpin(asma, cmd, (void __user *)arg);
		break;
	case ASHMEM_PURGE_ALL_CACHES:
		ret = -EPERM;
		if (capable(CAP_SYS_ADMIN)) {
			struct shrink_control sc = {
				.gfp_mask = GFP_KERNEL,
				.nr_to_scan = LONG_MAX,
			};
			ret = ashmem_shrink_count(&ashmem_shrinker, &sc);
			ashmem_shrink_scan(&ashmem_shrinker, &sc);
		}
		break;
	}

	return ret;
}

ashmem_mmap

static int ashmem_mmap(struct file *file, struct vm_area_struct *vma)
{
	//同上
	struct ashmem_area *asma = file->private_data;
	int ret = 0;

	mutex_lock(&ashmem_mutex);

	/* user needs to SET_SIZE before mapping */
	if (unlikely(!asma->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 (unlikely((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);

	//第一次mmap時,正式申請內存
	if (!asma->file) {
		char *name = ASHMEM_NAME_DEF;
		struct file *vmfile;

		if (asma->name[ASHMEM_NAME_PREFIX_LEN] != '\0')
			name = asma->name;

		/* ... and allocate the backing shmem file */

		/**
		 * shmem_kernel_file_setup - get an unlinked file living in tmpfs which must be
		 *	kernel internal.  There will be NO LSM permission checks against the
		 *	underlying inode.  So users of this interface must do LSM checks at a
		 *	higher layer.  The users are the big_key and shm implementations.  LSM
		 *	checks are provided at the key or shm level rather than the inode.
		 * @name: name for dentry (to be seen in /proc/<pid>/maps
		 * @size: size to be set for the file
		 * @flags: VM_NORESERVE suppresses pre-accounting of the entire object size
		 */
		//在tmpfs中創建一個文件,並創建一個inode指向這個文件,並把inode和struct file的返回值關聯起來。這個文件就是我們實際的共享的內存文件。這裏我們看到其實android 匿名共享內存也是基於linux 普通的共享內存底層來實現的,不重複造輪子。
		vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
		if (IS_ERR(vmfile)) {
			ret = PTR_ERR(vmfile);
			goto out;
		}
		vmfile->f_mode |= FMODE_LSEEK;
		//用file域保存我們在tmpfs中創建的共享內存文件的file結構指針。也就是說現在開始,asma->file指向了我們的共享內存文件。
		asma->file = vmfile;
	}
	
	get_file(asma->file);

	//把asma->file和我們mmap 的內存區域中的vma->vm_file關聯起來。這樣訪問mmap的這段內存區域就等於訪問我們創建的這個共享內存文件。
	if (vma->vm_flags & VM_SHARED)
		shmem_set_file(vma, asma->file);
	else {
		if (vma->vm_file)
			fput(vma->vm_file);
		vma->vm_file = asma->file;
	}

out:
	mutex_unlock(&ashmem_mutex);
	return ret;
}

Android Anonymous Shared Memory 驅動使用

Android 官方的一個lib用例庫中,封裝了一部分共享內存的用法,這部分內容就是給MemoryFile的Native層的庫使用的。下面我們一起來分析分析。
Android O r4
system/core/libcutils/ashmem-dev.c

/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * Implementation of the user-space ashmem API for devices, which have our
 * ashmem-enabled kernel. See ashmem-sim.c for the "fake" tmp-based version,
 * used by the simulator.
 */
#define LOG_TAG "ashmem"

#include <errno.h>
#include <fcntl.h>
#include <linux/ashmem.h>
#include <pthread.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <cutils/ashmem.h>
#include <log/log.h>

#define ASHMEM_DEVICE "/dev/ashmem"

/* ashmem identity */
static dev_t __ashmem_rdev;
/*
 * If we trigger a signal handler in the middle of locked activity and the
 * signal handler calls ashmem, we could get into a deadlock state.
 */
static pthread_mutex_t __ashmem_lock = PTHREAD_MUTEX_INITIALIZER;

/* logistics of getting file descriptor for ashmem */
static int __ashmem_open_locked()
{
    int ret;
    struct stat st;
	//這裏打開了"/dev/ashmem"驅動設備,創建了一個共享內存文件,返回了這個共享文件的文件描述符
    int fd = TEMP_FAILURE_RETRY(open(ASHMEM_DEVICE, O_RDWR));
    if (fd < 0) {
        return fd;
    }

    ret = TEMP_FAILURE_RETRY(fstat(fd, &st));
    if (ret < 0) {
        int save_errno = errno;
        close(fd);
        errno = save_errno;
        return ret;
    }
    if (!S_ISCHR(st.st_mode) || !st.st_rdev) {
        close(fd);
        errno = ENOTTY;
        return -1;
    }

    __ashmem_rdev = st.st_rdev;
    return fd;
}

static int __ashmem_open()
{
    int fd;

    pthread_mutex_lock(&__ashmem_lock);
    fd = __ashmem_open_locked();
    pthread_mutex_unlock(&__ashmem_lock);

    return fd;
}

/* Make sure file descriptor references ashmem, negative number means false */
static int __ashmem_is_ashmem(int fd, int fatal)
{
    dev_t rdev;
    struct stat st;

    if (TEMP_FAILURE_RETRY(fstat(fd, &st)) < 0) {
        return -1;
    }

    rdev = 0; /* Too much complexity to sniff __ashmem_rdev */
    if (S_ISCHR(st.st_mode) && st.st_rdev) {
        pthread_mutex_lock(&__ashmem_lock);
        rdev = __ashmem_rdev;
        if (rdev) {
            pthread_mutex_unlock(&__ashmem_lock);
        } else {
            int fd = __ashmem_open_locked();
            if (fd < 0) {
                pthread_mutex_unlock(&__ashmem_lock);
                return -1;
            }
            rdev = __ashmem_rdev;
            pthread_mutex_unlock(&__ashmem_lock);

            close(fd);
        }

        if (st.st_rdev == rdev) {
            return 0;
        }
    }

    if (fatal) {
        if (rdev) {
            LOG_ALWAYS_FATAL("illegal fd=%d mode=0%o rdev=%d:%d expected 0%o %d:%d",
              fd, st.st_mode, major(st.st_rdev), minor(st.st_rdev),
              S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IRGRP,
              major(rdev), minor(rdev));
        } else {
            LOG_ALWAYS_FATAL("illegal fd=%d mode=0%o rdev=%d:%d expected 0%o",
              fd, st.st_mode, major(st.st_rdev), minor(st.st_rdev),
              S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IRGRP);
        }
        /* NOTREACHED */
    }

    errno = ENOTTY;
    return -1;
}

int ashmem_valid(int fd)
{
    return __ashmem_is_ashmem(fd, 0) >= 0;
}

/*
 * ashmem_create_region - creates a new ashmem region and returns the file
 * descriptor, or <0 on error
 *
 * `name' is an optional label to give the region (visible in /proc/pid/maps)
 * `size' is the size of the region, in page-aligned bytes
 */
 //實際我們要創建的共享內存方法就是這個,其實就是打開設備,然後設置name和size,最終通過mmap把這個fd映射到我們的進程空間,然後我們的程序就可以訪問了。
int ashmem_create_region(const char *name, size_t size)
{
    int ret, save_errno;

    int fd = __ashmem_open();
    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));
        if (ret < 0) {
            goto error;
        }
    }

    ret = TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_SIZE, size));
    if (ret < 0) {
        goto error;
    }

    return fd;

error:
    save_errno = errno;
    close(fd);
    errno = save_errno;
    return ret;
}

int ashmem_set_prot_region(int fd, int prot)
{
    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_SET_PROT_MASK, prot));
}

int ashmem_pin_region(int fd, size_t offset, size_t len)
{
    struct ashmem_pin pin = { offset, len };

    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_PIN, &pin));
}

int ashmem_unpin_region(int fd, size_t offset, size_t len)
{
    struct ashmem_pin pin = { offset, len };

    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_UNPIN, &pin));
}

int ashmem_get_size_region(int fd)
{
    int ret = __ashmem_is_ashmem(fd, 1);
    if (ret < 0) {
        return ret;
    }

    return TEMP_FAILURE_RETRY(ioctl(fd, ASHMEM_GET_SIZE, NULL));
}

同時,在MemoryFile的jni接口中,我們可以發現直接調用ashmem_create_region,創建了一塊共享內存區域。

static jobject SharedMemory_create(JNIEnv* env, jobject, jstring jname, jint size) {

    // Name is optional so we can't use ScopedUtfChars for this as it throws NPE on null
    const char* name = jname ? env->GetStringUTFChars(jname, nullptr) : nullptr;

    int fd = ashmem_create_region(name, size);

    // Capture the error, if there is one, before calling ReleaseStringUTFChars
    int err = fd < 0 ? errno : 0;

    if (name) {
        env->ReleaseStringUTFChars(jname, name);
    }

    if (fd < 0) {
        throwErrnoException(env, "SharedMemory_create", err);
        return nullptr;
    }

    return jniCreateFileDescriptor(env, fd);
}

總結

這裏我們看到,共享內存的創建原理。但是這裏有個問題沒有解釋清楚,那就是內存是怎麼共享的?
在本文中,我們可以知道我們的創建共享內存的進程可以得到一個共享內存文件的文件描述符,其他的進程怎麼知道這塊內存在那裏呢?這裏我明確說明,android還要靠共享共享內存文件文件描述符來實現共享內存,但是一個文件描述符只對當前進程有效,其他進程的同一個值的文件描述符可能指向不同的文件,所以得有一種可靠的方式來實現文件描述符的傳遞即可。
預知後事如何,請聽下回分解!!

2019/3/18更新
本來是一系列的文章,但是後續篇整理後,發現不能成爲一個系列了。所以這裏留個傳送門:
《linux kernel 中進程間描述符的傳遞方法及原理》:https://blog.csdn.net/u011728480/article/details/88553602
#PS:請尊重原創,不喜勿噴

#PS:要轉載請註明出處,本人版權所有.

有問題請留言,看到後我會第一時間回覆

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章