Сопоставить большой буфер пользовательского режима с пространством ядра с помощью get_user_pages и vmap - PullRequest
0 голосов
/ 25 апреля 2019

У нас есть потоковая видеокамера. API реализован как библиотека пользовательского режима, он передает ioctl s для получения информации о камере, захвата видеокадров и т. Д.

Также у нас есть два потока - каждый имеет отдельный указатель файла. Один поток передает команды, другой захватывает видеокадры. С точки зрения драйвера некоторые запросы могут выполняться параллельно, тогда как другие должны быть сериализованы.

В драйвере мы хотим отобразить входные и выходные пользовательские буферы в пространство ядра, чтобы избежать использования функций copy_to_user, copy_from_user.

Выходной буфер, содержащий видеокадр, может быть довольно большим: более 5 МБ, он будет занимать почти 2462 страницы.

Идея состоит в том, чтобы выделить хранилище для пользовательских страниц для каждого имеющегося у нас указателя файла и отобразить пользовательские буферы внутри ioctl. Вот код:

#define PAGES_REQUIRED(size)            (PAGE_ALIGN(size) / PAGE_SIZE)
#define PAGES_ADDR(ptr)                 ((PAGE_ALIGN(ptr) > ptr) ? (PAGE_ALIGN(ptr) - PAGE_SIZE) : ptr)

#define BUFFER_IN_MAX                   10000
#define BUFFER_OUT_MAX                  (FRAMEINFO_SIZEF + MAX_FRAME_SIZE_16)

typedef struct _FILE_CONTEXT
{
    PDEVICE_CONTEXT context;
    struct page *in_pages[(PAGES_REQUIRED(BUFFER_IN_MAX) + 1)];
    struct page *out_pages[(PAGES_REQUIRED(BUFFER_OUT_MAX) + 1) + (PAGES_REQUIRED(BUFFER_IN_MAX) + 1)];
} FILE_CONTEXT, *PFILE_CONTEXT;

struct user_packet
{
    int in_size;
    unsigned char *in_buffer;
    int out_size;
    unsigned char *out_buffer;
};

bool device_user_buffers_share_pages(uint8_t *user_addr_in, size_t user_size_in,
                                    uint8_t *user_addr_out, size_t user_size_out)
{
    uint8_t *user_page_addr_in = PAGES_ADDR((size_t)user_addr_in);
    size_t user_aligned_size_in = user_size_in + (user_addr_in - user_page_addr_in);
    uint8_t *user_page_addr_out = PAGES_ADDR((size_t)user_addr_out);
    size_t user_aligned_size_out = user_size_out + (user_addr_out - user_page_addr_out);
    return ((user_page_addr_in < (user_page_addr_out + user_aligned_size_out)) &&
            (user_page_addr_out < (user_page_addr_in + user_aligned_size_in)));
}

status_t device_map_user_buffer(/*in*/uint8_t *user_addr, /*in*/size_t user_size, /*in*/bool read_only,
                           /*out*/struct page **out_user_pages, /*out*/size_t *out_user_pages_maped,
                           /*out*/uint8_t **out_kernel_base, /*out*/uint8_t **out_kernel_addr)
{
    LogDebug("user_addr: %llu", user_addr);
    LogDebug("user_size: %llu", user_size);
    uint8_t *user_page_addr = PAGES_ADDR((size_t)user_addr);
    LogDebug("user_page_addr: %llu", user_page_addr);
    size_t user_page_offset = user_addr - user_page_addr;
    LogDebug("user_page_offset: %llu", user_page_offset);
    size_t user_pages_required = PAGES_REQUIRED(user_size + user_page_offset);
    LogDebug("user_pages_required: %llu", user_pages_required);
    size_t user_pages_pined = get_user_pages_fast(user_page_addr, user_pages_required, read_only ? 0 : 1, out_user_pages);
    LogDebug("user_pages_pined: %llu", user_pages_pined);
    if (user_pages_pined < user_pages_required) goto error;
    uint8_t *kernel_base = vmap(out_user_pages, user_pages_pined, VM_MAP, PAGE_KERNEL);
    LogDebug("kernel_base: %llu", kernel_base);
    if (!kernel_base) goto error;
    *out_user_pages_maped = user_pages_pined;
    *out_kernel_base = kernel_base;
    *out_kernel_addr = kernel_base + user_page_offset;
    return s_success;
error:
    for (size_t i = 0; i < user_pages_pined; ++i) put_page(out_user_pages[i]);
    return s_fail;
}

void device_unmap_user_buffer(/*in*/struct page **user_pages,
                                 /*in*/size_t user_pages_maped,
                                 /*in*/uint8_t *kernel_base)
{
    vunmap(kernel_base);
    for (size_t i = 0; i < user_pages_maped; ++i) put_page(user_pages[i]);
}

status_t device_open(struct inode *pinode, struct file *pfile)
{
    int minor;
    struct usb_interface *interface;
    PDEVICE_CONTEXT DeviceContext;
    PFILE_CONTEXT FileContext;

    minor = iminor(pinode);
    interface = usb_find_interface(&g_driver, minor);
    if (!interface) return s_dev;
    DeviceContext = usb_get_intfdata(interface);
    if (!DeviceContext) return s_dev;
    FileContext = heap_alloc(sizeof(*FileContext));
    FileContext->context = DeviceContext;
    pfile->private_data = FileContext;
    return s_success;
}

status_t device_close(struct inode *pinode, struct file *pfile)
{
    heap_free(pfile->private_data);
    pfile->private_data = NULL;
    return s_success;
}

long device_ioctl(struct file *pfile, unsigned int IoControlCode, unsigned long arg)
{
    status_t status;
    bool sequential;
    ERROR_CODE errCode;
    size_t szBytestoReturn = 0;
    struct user_packet packet;
    PFILE_CONTEXT FileContext;
    PDEVICE_CONTEXT DeviceContext;
    size_t inSize = 0, outSize = 0;
    uint8_t *inBuf = NULL, *outBuf = NULL;
    size_t inPages = 0, outPages = 0;
    uint8_t *inBase = NULL, *outBase = NULL;

    FileContext = pfile->private_data;
    if (!FileContext) return s_dev;
    DeviceContext = FileContext->context;
    if (!DeviceContext) return s_dev;

    copy_from_user(&packet, (unsigned char*)arg, sizeof(packet));

    // for now we don't test if user buffers actually overlap

    if (packet.in_size > BUFFER_IN_MAX) packet.in_size = BUFFER_IN_MAX;
    if (packet.out_size > BUFFER_OUT_MAX) packet.out_size = BUFFER_OUT_MAX;

    bool in_ok = packet.in_buffer && packet.in_size && access_ok(VERIFY_READ, packet.in_buffer, packet.in_size);
    bool out_ok = packet.out_buffer && packet.out_size && access_ok(VERIFY_WRITE, packet.out_buffer, packet.out_size);
    bool share = in_ok && out_ok && device_user_buffers_share_pages(packet.in_buffer, packet.in_size,
                                                                   packet.out_buffer, packet.out_size);
    if (!share)
    {
        LogDebug("Not shared region {");
        if (in_ok)
        {
            inSize = packet.in_size;
            status = device_map_user_buffer(packet.in_buffer, packet.in_size, true,
                                          FileContext->in_pages, &inPages, &inBase, &inBuf);
            if (!status_success(status))
            {
                LogDebug("device_map_user_buffer failed");
                return status;
            }
        }
        if (out_ok)
        {
            outSize = packet.out_size;
            status = device_map_user_buffer(packet.out_buffer, packet.out_size, false,
                                  FileContext->out_pages, &outPages, &outBase, &outBuf);
            if (!status_success(status))
            {
                LogDebug("device_map_user_buffer failed");
                return status;
            }
        }
        LogDebug("}");
    }
    else
    {
        LogDebug("Shared region {");
        uint8_t *begin = (packet.in_buffer < packet.out_buffer) ? (packet.in_buffer) : (packet.out_buffer);
        uint8_t *end = ((packet.in_buffer + packet.in_size) < (packet.out_buffer + packet.out_size)) ? (packet.out_buffer + packet.out_size) : (packet.in_buffer + packet.in_size);

        inSize = packet.in_size;
        outSize = packet.out_size;
        status = device_map_user_buffer(begin, end - begin, false,
                                       FileContext->out_pages, &outPages, &outBase, &outBuf);
        if (!status_success(status))
        {
            LogDebug("device_map_user_buffer failed");
            return status;
        }

        if (packet.in_buffer < packet.out_buffer)
        {
            inBuf = outBuf;
            outBuf = inBuf + (packet.out_buffer - packet.in_buffer);
        }
        else
        {
            inBuf = outBuf + (packet.in_buffer - packet.out_buffer);
        }
        LogDebug("}");
    }

    // do ioctl like this:
    // switch (IoControlCode)
    // {
    // case A:
    //     errCode = ioctlA(DeviceContext, inBuf, inSize, outBuf, outSize, &szBytestoReturn);
    //     break;
    // }
    // if (errCode == ERR_NO_ERROR) status = szBytestoReturn;
    // else status = GetSystemStatusByErrCode(errCode);

    if (inBase) device_unmap_user_buffer(FileContext->in_pages, inPages, inBase);
    if (outBase) device_unmap_user_buffer(FileContext->out_pages, outPages, outBase);

    return status;
}

Когда мы выдаем ioctl с одной пользовательской страницей, все работает нормально. Однако, когда мы пытаемся захватить несколько видеокадров, все становится странным, и в итоге мы сталкиваемся с ошибкой сегментации.

Причины, по которым мы не используем функции copy_to_user, copy_from_user:

  1. Нам нужен код, который также работает в Windows.
  2. 16-битный кадр должен быть преобразован в 8-битный кадр на месте, поэтому этот подход приведет нас к ситуации, когда нам нужно вызывать функцию put_user для каждого слова кадра, что будет довольно медленным.

Можете ли вы помочь мне понять, что не так с приведенным выше кодом?

...