Mapping Memory

Mapping memory refers to the function of making a range of memory from one task available to another. At the lowest level, mapping is handled by the Mach VM subsystem, as discussed in Chapter 2. Memory mapping provides a fast way for tasks to share resources without copying memory, as mapping makes the same memory available between tasks. Writable mappings can be shared until a modification is made, in which case the copy-on-write (COW) optimization is used to copy only the memory that was modified. Memory mappings can occur in a variety of different ways, between multiple tasks, or from the kernel to a user space task or vice versa.

Mapping Memory from a User Space Task into Kernel Space

Mapping memory from a user space task is a common operation performed by a driver. Let's use the example of an audio device driver where an application wants to send us a data buffer containing audio samples for play out on a hardware device. To do this, the user task—that is, the audio player—passes us a memory pointer, which describes where in memory the buffer is located. In user space, the copying of memory is as simple as calling the memcpy() function.

Things are not so simple in the kernel. The address passed by the user task is meaningless to the kernel, as it is valid only within the task's private address space. In order to access the memory in the kernel, we need to create a mapping for the underlying physical memory of the buffer in the kernel's own address space. At the low level, this process happens by manipulating the kernel's VM Map. While it is possible to do this using the Mach low-level interfaces, it is most commonly performed with the help of the I/O Kit IOMemoryDescriptor and IOMemoryMap classes. Listing 6-2 shows the portion of our imaginary audio driver that copies memory from the user space audio player by mapping the memory buffer into the kernel's address space.

Listing 6-2. Mapping a User Space Buffer into the Kernel

void copyBufferFromUserTask(task_t userTask, void* userBuffer,
                            uint32_t userBufferSize, void* dstBuffer)
{
     uint32_t                 bytesWritten = 0;
     bool                     wasPrepared = false;
     IOMemoryDescriptor*      memoryDescriptor = NULL;
     IOMemoryMap*             memoryMap = NULL;

     memoryDescriptor = IOMemoryDescriptor::withAddressRange
                            (userBuffer, userBufferSize,                      
                            kIODirectionOut, userTask);
     if (memoryDescriptor == NULL)
         goto bail;

     if (memoryDescriptor->prepare() != kIOReturnSuccess)
         goto bail;
     wasPrepared = true;

     memoryMap = memoryDescriptor->createMappingInTask
                     (kernel_task, 0, kIOMapAnywhere | kIOMapReadOnly);
     if (memoryMap == NULL)
         goto bail;

    void* srcBufferVirtualAddress = (void*)memoryMap->getVirtualAddress();

    if (srcBufferVirtualAddress != NULL)
        bcopy(srcBufferVirtualAddress, dstBuffer, userBufferSize);
        
    memoryMap->release(); // This will unmap the memory
    memoryMap = NULL;
bail:
    if (memoryDescriptor)
    {
        if (wasPrepared)
            memoryDescriptor->complete();
        memoryDescriptor->release();
        memoryDescriptor = NULL;
    }
}

To map the memory, we first create an IOMemoryDescriptor for the user space buffer. The IOMemoryDescriptor provides an interface to create the memory mapping, but it also allows us to pin the memory down while we copy from the buffer. This prevents the memory from being paged out to secondary storage or disappearing if the audio player should crash or the user exits the application while we are performing the copy.

images Note You may have noticed the use of goto in the preceding method, which language purists often consider a bad practice. However, it is often used in kernel code and provides a convenient way of providing centralized cleanup if an error occurs, in lieu of exceptions that cannot be used in the kernel.

The actual mapping occurs with the invocation of the createMappingInTask() method:

    IOMemoryMap* createMappingInTask(
        task_t                  intoTask,
        mach_vm_address_t       atAddress,
        IOOptionBits            options,
        mach_vm_size_t          offset = 0,
        mach_vm_size_t          length = 0 );

images Tip You can use IOMemoryDescriptor::map() method as a shortcut to create a standard mapping into the kernel's address space. Also beware that the overloaded variant of map() is deprecated in favor of createMappingInTask(), which was introduced in Mac OS X 10.5.

  • The first argument, intoTask, is the task we want to create the mapping in. For our purposes, this is the kernel_task, though it would be possible to provide the task structure of another task, thereby making memory available from one task to another.
  • The second argument, atAddress, is interesting as well. It specifies an optional destination address in the address space of intoTask. This allows the target task to locate the mapping at a fixed address. In our example, we don't really care where in our address space the mapping will be made; we just want one address to access it, so we pass in zero instead of a fixed address and set kIOMapAnywhere in options.
  • The third argument, options, controls how the mapping will be performed using the flags described in the Memory Descriptors section, for example, read-only or read/write. Options also exist to control how the memory should behave in relation to the CPU cache. The following options can be set:
    • kIOMapDefaultCache, which specifies the caching policy for the mapping. It will disable the cache for I/O memory; otherwise, kIOMapCopybackCache is used.
    • kIOMapInhibitCache, which disables caching of this mapping.
    • kIOMapWriteThruCache, which uses write-thru caching.
    • kIOMapCopybackCache, which uses copy-back caching.
    • kIOMapReadOnly, which specifies the mapping will be read-only.
    • kIOMapReference, which is used when mapping an already existing mapping and will fail if the memory is not previously mapped.
    • kIOMapUnique, which ensures no previous mapping exists for the memory.
  • The last two arguments are used to specify an optional offset and length into the buffer, if you want to map up only parts of it. However, note that mappings are a concept of the virtual memory system and operate on pages. You can map memory only in units of the page size (4096 bytes). The rounding happens internally and gives the illusion of working with byte boundaries.

The IOMemoryMap Class

The createMappingInTask() method in Listing 6-2 will return an instance of IOMemoryMap to represent the mapping. In our previous example, we call the IOMemoryMap::getVirtualAddress() method, which returns a value of the IOVirtualAddress type. The exact primitive data type of IOVirtualAddress depends on the architecture, but for 64-bit kernels, a 64-bit unsigned integer (uin64_t) is used and not a pointer type.

When we no longer need the mapping, we simply release the IOMemoryMap object, which takes care of unmapping. You may wonder why we do not call the IOMemoryMap::unmap() function to release the mapping. When you create a mapping, it is possible for another thread or the same thread to map the buffer again. While the mapping will of course only be created once, performing the mapping multiple times will increment an internal reference counter. However, calling unmap() will not simply decrement the reference count and remove the mapping if the count hits zero, it will destroy the mapping regardless of how many times it is referenced. This may lead to the kernel accessing an invalid address; hence, care should be taken when using unmap(). Simply calling release() for the map will decrement or remove the mapping if required. A collection of other interesting IOMemoryMap methods are described in Table 6-2.

Note that in the Listing 6-2 example, we could just as well have copied memory into the mapped buffer with some small modifications to create a writable mapping.

images

images Note It is not necessary to map memory into the kernel unless the kernel needs to actively modify it. If DMA is performed from a user space buffer and the data in the buffer does not have to be modified by the kernel, it is not necessary to map it into the kernel's address space, the buffer can be transferred directly to a hardware device. See Chapter 9 for more information about DMA.

Mapping Memory from the Kernel to a User Space Task

The previous sections showed how we can take memory allocated in user space and map that memory into the kernel's address space so the kernel can access it. While it is possible for the kernel to both read and write from the mapping, it may sometimes be desirable for a user space task to map kernel memory into its address space. It should be noted that Apple recommends against this practice for security and stability reasons and it should be avoided whenever possible. One possible reason for doing it might be the need to map device memory (for example, from a PCI device) to user space so it can access the device's registers.

In I/O Kit this form of memory mapping is usually done through the IOUserClient class. Available memory mappings should be returned via the clientMemoryForType() method. A generic example of how this can be achieved is shown in Listing 6-3.

Listing 6-3. Mapping Kernel Memory to User Space via IOUserClient

#define kTestUserClientDriverBuffer             0
IOReturn com_osxkernel_TestUserClient::
clientMemoryForType(UInt32 type, UInt32 *flags, IOMemoryDescriptor **memory)
{
     IOReturn ret = kIOReturnUnsupported;
     switch (type)
     {
          case kTestUserClientDriverBuffer:
              // Returns a pointer to an IOMemoryDescriptor or
              // if a hardware device, an IODeviceMemory pointer which is a
              // subclass of IOMemoryDescriptor
              *memory = driver->getBufferMemoryDescriptor();
              *memory->retain();
              ret = kIOReturnSuccess;
              break;
          default:
              break;
     }
     return ret;
}

Note that we need to call retain() on the IOMemoryDescriptor before returning it, as it will be released when the user client closes and we do not want the descriptor to be de-allocated as it is a shared resource owned by the driver. In this example, we call a hypothetical driver that, for the sake of the example, has a method called getBufferMemoryDescriptor() that returns an IOMemoryDescriptor for a kernel-allocated buffer (or it could even be device memory mapped into the kernel's address space). The type argument here is simply an integer and can be anything; the important thing is that the user space program that will access the memory knows the value so it can reference the right memory mapping.

In user space code, you can do the following to map the memory from the IOUserClient.

void* addressOfMappedBuffer = NULL;
int sizeOfMappedBuffer;
IOConnectMapMemory(openDeviceHandleHere,
                   kTestUserClientDriverBuffer,
                   mach_task_self(),
                   (vm_address_t *) &addressOfMappedBuffer,
                   &sizeOfMappedBuffer,
                   kIOMapAnywhere);

You may notice the similarity to creating a mapping in the kernel. The kIOMapAnywhere here signifies that we don't care where in our address space the mapping is made; the addressOfMappedBuffer argument will contain the address of the mapping if the call succeeds and can be used to access the mapped memory. If kIOMapAnywhere is not specified, the addressOfMappedBuffer argument is used to specify the preferred address for the mapping. The second last argument will tell us the size of the mapping. The smallest amount that can be mapped is a single page; therefore, if you map buffers smaller than 4096, it would allow a client to see the memory of the entire page the buffer is contained within, which could be a potential security problem.

Mapping Memory to a Specific User Space Task

The preceding example allows any task to map the memory and our driver code does not need to know which task the memory will be mapped to. However, if you know the specific task memory should be mapped to, you can use the approach from Listing 6-2. The difference is simply that the user space task identifier is passed to IOMemoryDescriptor::createMappingInTask() in place of kernel_task.

Apple recommends not mapping memory obtained from functions such as IOMalloc() and IOMallocAligned() (though it is possible using the latter) because they come from the zone allocator, which is intended for private and temporary allocations and not for sharing. The recommended way of mapping memory is to use the IOBufferMemoryDescriptor, a subclass of IOMemoryDescriptor that also allocates memory, as follows.

IOBufferMemoryDescriptor* memoryDescriptor = NULL;  
memoryDescriptor = IOBufferMemoryDescriptor::withOptions(
    kIODirectionOutIn | kIOMemoryKernelUserShared, sizeInBytes, 4096);

An interesting parameter to note is kIOMemoryKernelUserShared, which indicates to the allocator that we wish to share the memory with a user task. We pass 4096 (the page size) to get page-aligned memory, as memory mappings can only be done on page-sized units.

Physical Address Mapping

Virtual memory addresses are only available to the CPU and are meaningless to a hardware device, which requires physical addresses. In order to communicate with hardware outside the CPU, we need to translate virtual memory from the kernel or a user space task into physical addresses the device can use to access information from RAM. This task is not always trivial as virtual memory is often fragmented. Let's look at an example, a 128 KB virtual memory buffer we want to send to a hardware device. The buffer can in the worst case consist of 32 individual 4 KB pages scattered anywhere throughout the system memory. Because of this, we cannot simply translate the address of the first byte of the buffer and tell the device the buffer is 128 KB long; we need to work out how many fragments the buffer consists of and instead send a list/array of addresses and lengths. This is often referred to as a scatter/gather table or list. The IOMemoryDescriptor and classes derived from it provide two methods to help with physical address translation, as follows.

  • getPhysicalAddress(): Translates the address of the first byte to its physical address. This is mainly useful if the buffer is known to be contiguous.
  • getPhysicalSegment(): Translates the address at a specified offset into the buffer and returns the length of the physical segment from that offset. For a contiguous buffer, this will always be the size of the buffer minus the offset.

images Caution This method can cause a kernel panic if used improperly. See the following discussion for correct usage.

Note that there are two versions of getPhysicalSegment() depending on if you are using a 64-bit kernel or 32-bit kernel, as follows:

#ifdef __LP64__
    virtual addr64_t getPhysicalSegment( IOByteCount   offset,
                                         IOByteCount * length,
                                         IOOptionBits  options = 0 ) = 0;
#else /* !__LP64__ */
    virtual addr64_t getPhysicalSegment( IOByteCount   offset,
                                         IOByteCount * length,
                                         IOOptionBits  options );
#endif /* !__LP64__ */

For the 32-bit version (!__LP64__) the options argument must specify: kIOMemoryMapperNone or the method will panic for addresses over the 4 GB mark. A more flexible, safer and easier approach to memory translation is to use IODMACommand class, which works in conjunction with IOMemoryDescriptor. We discuss IODMACommand and this topic in much more detail in Chapter 9.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.117.56