Android development 6: Ashmem anonymous shared memory driver

Anonymous SHared MEMory

The implementation of anonymous shared memory is a set of scheme based on the Ashmem driver. tmpfs is a temporary file system based on linux

The ashmem system is roughly divided into three layers, as follows:

At the bottom is the kernel layer, which is the ashmem driver we will introduce. It will create a device file of / dev/ashmem at startup.

The upper cutils library accesses the driver through file access operation open and IOCTL. In addition, cutils mainly provides three functions: ashmem? Create? Region, ashmem? Pin? Region and ashmem? Unpin? Region.

Finally, there is an application layer encapsulation. What we actually use is the interface provided by this layer.

Android uses a file descriptor to represent a block of marked shared memory, and passes the file descriptor through the binder? Type? FD command for inter process communication.

 

Ashmem driver

The ashmem driver is defined in the kernel, and three files may be involved in the latest 3.18 kernel

    /drivers/staging/android/ashmem.c

    /drivers/staging/android/ashmem.h

    /drivers/staging/android/uapi/ashmem.h

Basic data structure

There are three important data structures in the driver.

struct ashmem_area {
	char name[ASHMEM_FULL_NAME_LEN];
	struct list_head unpinned_list;
	struct file *file;
	size_t size;
	unsigned long prot_mask;
};

The structure ashmem? Area is used to describe an anonymous shared memory

The member variable name represents the name of this anonymous shared memory (Tucao: good anonymous). At the same time, it will be written to the file / proc / < PID > / maps (PID represents the process number PID of creating the anonymous shared memory).

#define ASHMEM_NAME_PREFIX "dev/ashmem/"
#define ASHMEM_NAME_PREFIX_LEN (sizeof(ASHMEM_NAME_PREFIX) - 1)
#define ASHMEM_FULL_NAME_LEN (ASHMEM_NAME_LEN + ASHMEM_NAME_PREFIX_LEN)

Each anonymous shared memory name is prefixed with dev/ashmem. If no name is specified during creation, it will be used by default

#define ASHMEM_NAME_DEF		"dev/ashmem"

A block of anonymous shared memory can be divided into several blocks dynamically. When these blocks are unlocked, they will be added to the unlocked block list, that is, the unpinned list above.

Each anonymous shared memory will correspond to a file in tmpfs, that is, file. The size of the file is the size of this anonymous shared memory.

prot_mask is the access protection bit, and the default is

#define PROT_MASK		(PROT_EXEC | PROT_READ | PROT_WRITE)

Prot? Exec means executable, which in turn is readable and writable.

  

struct ashmem_range {
	struct list_head lru;
	struct list_head unpinned;
	struct ashmem_area *asma;
	size_t pgstart;
	size_t pgend;
	unsigned int purged;
};

Ashmem? Range is just to unlock the memory block.

asma refers to the anonymous shared memory of the host (the anonymous shared memory is divided into small blocks, which is the host of these small blocks), which is sent to the unpinned list of the host through the unpinned chain.

There is a global list ashmem? LRU? List, which is defined as follows

static LIST_HEAD(ashmem_lru_list);

It is a recent minimum use list. When there is insufficient memory, the memory recycling system will recycle the memory in the list according to the recent minimum use principle.

The lru field of the ashmem? Range is used to link other fields such as this ashmem? lru? List.

pgstart and pgend are used to represent the start address and end address of this memory block.

If the memory represented by ashmem? Range has been reclaimed, the value of purged is ashmem? Was? Pruged. Otherwise, it is ashmem? Not? Pruged.

 

struct ashmem_pin {
	__u32 offset;	/* offset into region, in bytes, page-aligned */
	__u32 len;	/* length forward from offset, in bytes, page-aligned */
};

Ashmem pin is the parameter of the driver defined IO control commands ashmem pin and ashmem unpin, which is used to describe that a small piece of memory is about to be locked or unlocked. Offset indicates that there is an offset value in the whole anonymous shared memory in this small block, while len indicates the length of this small block.

Start ashmem drive

The driver of ashmem is initialized in ashmem? Init

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;
    //Create an allocator to allocate ashmem? Area using the slap cache
	ashmem_area_cachep = kmem_cache_create("ashmem_area_cache",
					  sizeof(struct ashmem_area),
					  0, 0, NULL);
	if (unlikely(!ashmem_area_cachep)) {
		pr_err("failed to create slab cache\n");
		return -ENOMEM;
	}
   //Create an allocator to allocate ashmem? Range using the slap cache
	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");
		return -ENOMEM;
	}
    
    //Register anonymous shared memory device
	ret = misc_register(&ashmem_misc);
	if (unlikely(ret)) {
		pr_err("failed to register misc device!\n");
		return ret;
	}
    //Register a memory recovery function with memory management. When the system is low on memory, all functions registered with register ﹣ shrinker will be called.
	register_shrinker(&ashmem_shrinker);

	pr_info("initialized\n");

	return 0;
}

PS: maybe you need some knowledge about slab caching, which is mainly used to manage memory and speed up memory allocation. This is not what we introduced here. It is a mechanism in linux.

From the above point of view, the init method is relatively clear. The anonymous memory device is a misc device type, so it uses a structure structure of miscdevice type, ashmem? Misc, to register. The definition is as follows:

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

We omit the registration process. In a word, this token is called / dev/ashmem. Ashmem? FOPS represents the list of its operation methods.

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
};

 

Anonymous shared memory device opening process

As we know from the above, when we need to use anonymous shared memory, we will first open the device file of anonymous shared memory, and the Ashman? Open function is used to open the device file.

static int ashmem_open(struct inode *inode, struct file *file)
{
	struct ashmem_area *asma;
	int ret;
    //Open the device file / dev/ashmem
	ret = generic_file_open(inode, file);
	if (unlikely(ret))
		return ret;
    //Create a shared memory structure
	asma = kmem_cache_zalloc(ashmem_area_cachep, GFP_KERNEL);
	if (unlikely(!asma))
		return -ENOMEM;
    //Initializes the unpinned list list in the structure
	INIT_LIST_HEAD(&asma->unpinned_list);
    //Name this anonymous shared memory
	memcpy(asma->name, ASHMEM_NAME_PREFIX, ASHMEM_NAME_PREFIX_LEN);
    //Set access protection bit
	asma->prot_mask = PROT_MASK;
    //Put the successfully initialized asma structure into the private data field of the device file.
	file->private_data = asma;

	return 0;
}

We use the default name of dev/ashmem here. We can change the name through ioctl command ashmem ﹣ set ﹣ name.

The ashmem? ioctl method is used in response to ioctl commands.

static long ashmem_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    //Get structure
	struct ashmem_area *asma = file->private_data;
	long ret = -ENOTTY;

	switch (cmd) {
	case ASHMEM_SET_NAME:
        //Finally call the set name method
		ret = set_name(asma, (void __user *) arg);
		break;

    ...
    }

	return ret;
}

static int set_name(struct ashmem_area *asma, void __user *name)
{
	int len;
	int ret = 0;
	char local_name[ASHMEM_NAME_LEN];
    //Extract the name from the parameter and put it in the local name
	len = strncpy_from_user(local_name, name, ASHMEM_NAME_LEN);
	if (len < 0)
		return len;
	if (len == ASHMEM_NAME_LEN)
		local_name[ASHMEM_NAME_LEN - 1] = '\0';
    //
	mutex_lock(&ashmem_mutex);
	/* cannot change an existing mapping's name */
    // Check whether the temporary file has been created in tmpfs (the file name is the name of the anonymous shared memory). If it has been created, modification is not allowed
	if (unlikely(asma->file))
		ret = -EINVAL;
	else
		strcpy(asma->name + ASHMEM_NAME_PREFIX_LEN, local_name);

	mutex_unlock(&ashmem_mutex);
	return ret;
}

 

Anonymous shared memory device file memory mapping process

When the application opens the device, it will call mmap to map the device file to its own process address space. This is the ashmem? mmap function that will be called.

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 */
    //Before using mmap for mapping, the user needs to call the set ﹣ size method to set the size, otherwise it will fail directly
	if (unlikely(!asma->size)) {
		ret = -EINVAL;
		goto out;
	}

	/* requested protection bits must match our allowed protection mask */
     //Check whether the protection permission of the virtual memory vma to be mapped exceeds the protection permission of the anonymous shared memory
     //For example, vma allows writing as well as reading, but asma only allows reading. If this is exceeded, mmap will fail and return directly.
	if (unlikely((vma->vm_flags & ~calc_vm_prot_bits(asma->prot_mask)) &
		     calc_vm_prot_bits(PROT_MASK))) {
		ret = -EPERM;
		goto out;
	}
    
	vma->vm_flags &= ~calc_vm_may_flags(~asma->prot_mask);

    //If you haven't created a temporary file, a temporary file will be created next
	if (!asma->file) {
        //Set temporary file name
		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 */
        //Create a temporary file in tmpfs.
		vmfile = shmem_file_setup(name, asma->size, vma->vm_flags);
		if (unlikely(IS_ERR(vmfile))) {
			ret = PTR_ERR(vmfile);
			goto out;
		}
		asma->file = vmfile;
	}
	get_file(asma->file);
    
    //Determine whether the mapped virtual memory vma needs to be shared among different processes,
	if (vma->vm_flags & VM_SHARED)
        //Set mapping file and memory operation method
		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;
}

Anonymous shared memory lock unlock operation

Anonymous shared memory is locked when it is created. Users can cut it into small pieces according to their needs. When the small pieces are no longer used, they can unlock them and put them into the unlocked memory list. The memory in the unlocking list may be recycled in case of memory shortage. If it has not been recycled, it can be reused by locking.

The ashmem driver controls unlocking and locking through two ioctl commands, namely ashmem? Pin and ashmem? Unpin. They need an ashmem? Pin structure, as described earlier.

In the ashmem? IOCTL method, both commands will eventually call the method ashmem? Pin? Unpin to handle

static int ashmem_pin_unpin(struct ashmem_area *asma, unsigned long cmd,
			    void __user *p)
{
	struct ashmem_pin pin;
	size_t pgstart, pgend;
	int ret = -EINVAL;

	if (unlikely(!asma->file))
		return -EINVAL;
    //Get parameters and save them to ashmem? Pin
	if (unlikely(copy_from_user(&pin, p, sizeof(pin))))
		return -EFAULT;

    //Detect the size of the lock area. If it is 0, it means all spaces from pin.offset to the end of shared memory
	/* per custom, you can pass zero for len to mean "everything onward" */
	if (!pin.len)
		pin.len = PAGE_ALIGN(asma->size) - pin.offset;
    //Offset address and size page alignment detection
	if (unlikely((pin.offset | pin.len) & ~PAGE_MASK))
		return -EINVAL;
    //Does the end address of operation memory exceed the unsigned integer??
	if (unlikely(((__u32) -1) - pin.offset < pin.len))
		return -EINVAL;
    //Whether the operation memory address exceeds the end address of anonymous shared memory
	if (unlikely(PAGE_ALIGN(asma->size) < pin.offset + pin.len))
		return -EINVAL;

    //Page address requiring operation memory
	pgstart = pin.offset / PAGE_SIZE;
	pgend = pgstart + (pin.len / PAGE_SIZE) - 1;

	mutex_lock(&ashmem_mutex);

	switch (cmd) {
	case ASHMEM_PIN:
		ret = ashmem_pin(asma, pgstart, pgend);
		break;
	case ASHMEM_UNPIN:
		ret = ashmem_unpin(asma, pgstart, pgend);
		break;
	case ASHMEM_GET_PIN_STATUS:
		ret = ashmem_get_pin_status(asma, pgstart, pgend);
		break;
	}

	mutex_unlock(&ashmem_mutex);

	return ret;
}

In fact, the main task of this method is to check the validity. After passing the test, the command will be distributed.

Ashmem? Unpin to unlock a block of memory address

static int ashmem_unpin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
	struct ashmem_range *range, *next;
	unsigned int purged = ASHMEM_NOT_PURGED;

restart:
	list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
		/* short circuit: this is our insertion point */
		if (range_before_page(range, pgstart))
			break;

		/*
		 * The user can ask us to unpin pages that are already entirely
		 * or partially pinned. We handle those two cases here.
		 */
		if (page_range_subsumed_by_range(range, pgstart, pgend))
			return 0;
		if (page_range_in_range(range, pgstart, pgend)) {
			pgstart = min_t(size_t, range->pgstart, pgstart),
			pgend = max_t(size_t, range->pgend, pgend);
			purged |= range->purged;
			range_del(range);
			goto restart;
		}
	}

	return range_alloc(asma, range, purged, pgstart, pgend);
}

All unlocking address blocks in an anonymous shared memory are stored in the unpinned_list in the order of address values from large to small. To unlock a block of memory, he needs to first check whether the memory has address intersection with the unlocked block. If there is any intersection, it needs to be aligned and merged, and then stored in the unpinned_list.

Here is a diagram. range indicates the unlocked memory block. pgstart and pgend mark the address of the memory block to be unlocked.

    

In case of a, B, and C, the address needs to be combined. In case of D, the current operation is ignored (because it has been marked). E means to release the memory block marked with the current one with a happy heart.

We use range del and range alloc to add and remove an unlocked block from the target anonymous shared memory asma.

static void range_del(struct ashmem_range *range)
{
    //Remove yourself from the unpinned list of the host
	list_del(&range->unpinned);
    //If you are in the global list ashmem? LRU? List
	if (range_on_lru(range))
		lru_del(range); //Remove the currently unlocked memory block from ashmem? LRU? List
	kmem_cache_free(ashmem_range_cachep, range);//Put the memory occupied by structure range back into slab
}

    

static int range_alloc(struct ashmem_area *asma,
		       struct ashmem_range *prev_range, unsigned int purged,
		       size_t start, size_t end)
{
	struct ashmem_range *range;
    //Create a range structure
	range = kmem_cache_zalloc(ashmem_range_cachep, GFP_KERNEL);
	if (unlikely(!range))
		return -ENOMEM;

	range->asma = asma;
	range->pgstart = start;
	range->pgend = end;
	range->purged = purged;

    //Add to the unpinned list of hosts
	list_add_tail(&range->unpinned, &prev_range->unpinned);

    //If it has not been recycled by the system
	if (range_on_lru(range))
		lru_add(range);//Put it into the global variable ashmem? LRU? List

	return 0;
}

 

Next is the ashmem? Pin method, which is used to lock a block of memory.

static int ashmem_pin(struct ashmem_area *asma, size_t pgstart, size_t pgend)
{
	struct ashmem_range *range, *next;
	int ret = ASHMEM_NOT_PURGED;

	list_for_each_entry_safe(range, next, &asma->unpinned_list, unpinned) {
		/* moved past last applicable page; we can short circuit */
		if (range_before_page(range, pgstart))
			break;

		/*
		 * The user can ask us to pin pages that span multiple ranges,
		 * or to pin pages that aren't even unpinned, so this is messy.
		 *
		 * Four cases:
		 * 1. The requested range subsumes an existing range, so we
		 *    just remove the entire matching range.
		 * 2. The requested range overlaps the start of an existing
		 *    range, so we just update that range.
		 * 3. The requested range overlaps the end of an existing
		 *    range, so we just update that range.
		 * 4. The requested range punches a hole in an existing range,
		 *    so we have to update one side of the range and then
		 *    create a new range for the other side.
		 */
		if (page_range_in_range(range, pgstart, pgend)) {
			ret |= range->purged;

			/* Case #1: Easy. Just nuke the whole thing. */
			if (page_range_subsumes_range(range, pgstart, pgend)) {
				range_del(range);
				continue;
			}

			/* Case #2: We overlap from the start, so adjust it */
			if (range->pgstart >= pgstart) {
				range_shrink(range, pgend + 1, range->pgend);
				continue;
			}

			/* Case #3: We overlap from the rear, so adjust it */
			if (range->pgend <= pgend) {
				range_shrink(range, range->pgstart, pgstart-1);
				continue;
			}

			/*
			 * Case #4: We eat a chunk out of the middle. A bit
			 * more complicated, we allocate a new range for the
			 * second half and adjust the first chunk's endpoint.
			 */
			range_alloc(asma, range, range->purged,
				    pgend + 1, range->pgend);
			range_shrink(range, range->pgstart, pgstart - 1);
			break;
		}
	}

	return ret;
}

An anonymous shared memory must be locked at the beginning. It will be put into the unpinned list after being unlocked. The function of this method is to traverse the unpinned list in anonymous shared memory, find the memory block that intersects or contains the specified pgstart and pgend, and then lock the memory block again.

    

There are four situations.

Case A is relatively simple, which means that the memory to be unlocked contains the current memory block. Just call range del to remove the current memory block.

B and C are similar, but the solution may be different from what you think. Do not delete the range, reassign the range, and add it to the unlock list. Instead, change the range directly (that is, change the size of the range).

Case D is more complex. The memory to be unlocked is included in the current memory block. At this time, the size of the memory block cannot be directly modified like B and C. instead, a memory block from pgend+1 to the end of the range needs to be inserted first, and then the end of the range needs to be changed to pgstart.

The function of the method "range" is to modify the size of the current range.

static inline void range_shrink(struct ashmem_range *range,
				size_t start, size_t end)
{
	size_t pre = range_size(range);

	range->pgstart = start;
	range->pgend = end;

	if (range_on_lru(range))
		lru_count -= pre - range_size(range);
}

 

337 original articles published, praised 134, visited 190000+
His message board follow

Tags: Android Linux

Posted on Thu, 06 Feb 2020 00:58:13 -0500 by ExpertAlmost