Гостевой Linux с проброшенной NVIDIA-картой обычно требует, чтобы карта была отдана целиком через PCI passthrough — тогда гостевой драйвер nvidia.ko общается с железом напрямую. virtio-nvgpuпроксирующий слой, который выставляет в госте те же узлы устройств NVIDIA, что и nvidia.ko, но перенаправляет каждый запрос на хост, где его выполняет настоящий драйвер NVIDIA идёт другим путём: гостевой драйвер выставляет те же узлы устройств, что и nvidia.ko, но каждый ioctlсистемный вызов, которым программа передаёт драйверу команду управления устройством к ним уходит на хост, где его выполняет настоящий NVIDIA-драйвер. Разбираем, как устроен этот проксирующий слой: какие подсистемы NVIDIA он подменяет, как устроен транспорт virtioстандартный механизм виртуализации ввода-вывода в KVM, при котором гость общается с хостом через общие очереди в памяти , как транслируются дескрипторы файлов и регистрации памяти, и где проходят границы возможностей.
Какие подсистемы NVIDIA проксируются
Гостю выставляются пять подсистем NVIDIA, и каждая открывается через свой узел устройства.
RM (Resource Manager — ядро NVIDIA-драйвера, обслуживающее объекты GPU) доступен через /dev/nvidiaN. Открытие идёт через nvgpu_gpu_open, который вызывает nvgpu_open_common с типом устройства, равным номеру минора: так каждый узел /dev/nvidia0, /dev/nvidia1 и так далее получает свой тип. Через этот узел проходят ioctl — они проксируются на хост, потому что объекты GPU живут там.
UVM (Unified Virtual Memory — подсистема управления единым адресным пространством CPU и GPU) открывается через nvgpu_uvm_open с типом NVGPU_DEV_UVM. nvidia-modeset (подсистема управления режимами дисплея) — через nvgpu_modeset_open с типом NVGPU_DEV_MODESET.
DRM/GEM (подсистема Direct Rendering Manager и управления графическими буферами) выставляется как /dev/dri/renderD128 с операциями файла nvgpu_drm_fops, где .open = drm_open, а .unlocked_ioctl = nvgpu_drm_unlocked_ioctl.
nvidia-caps (подсистема запроса возможностей GPU) — через nvgpu_caps_fops с .open = nvgpu_caps_open.
Узлы создаются devnode-колбэками классов. Класс "nvidia" использует nvgpu_devnode, который выставляет права 0666 и не переопределяет имя узла. Класс "nvgpu_dri" использует nvgpu_dri_devnode — права 0666, имя вида dri/%s. Класс "nvgpu_caps" использует nvgpu_caps_devnode — права 0444, имя вида nvidia-caps/%s.
Маршрутизация ioctl различается по узлам. На DRM-узле ioctl типа 'F' — это NVIDIA RM, и он проксируется на хост так же, как на любом другом узле; core DRM (VERSION, GET_UNIQUE и подобные) обслуживается локально через drm_ioctl(). UVM-ioctl ищется в таблице хоста через nvgpu_find_uvm_cmd, и если команда не найдена — возвращается -ENOTTY. nvidia-modeset-ioctl с типом 0x6d уходит в nvgpu_ioctl_modeset, остальные — в общий nvgpu_ioctl. nvgpu_caps_ioctl просто возвращает 0, сообщая userspace об отсутствии специальных возможностей.
Три компонента и их контракт
Система состоит из трёх частей: гостевого драйвера virtio_gpu_nv.c, VMMпрограмма, управляющая виртуальными машинами: создаёт их, выделяет память и подключает устройства-части virtio.rs и vhost-user backendпроцесс, который обслуживает устройство virtio от имени виртуальной машины и обменивается с ней через общую память vhost-user-nvgpu.rs. VMM-часть и vhost-user backend — это две серверные части, которые обязаны соблюдать общий контракт. Контракт между ними задаётся общими константами и структурами в virtio.rs, которые гостевой драйвер проверяет своими static_assert.
Гостевой драйвер привязывается к virtio device IDчисловой идентификатор типа устройства в шине virtio, по которому драйвер находит своё устройство 45 и вызывает virtio_find_vqs(vdev, 2, ...) для двух очередей — control и event. Поэтому backend объявляет num_queues() == QUEUE_COUNT == NUM_QUEUES == 2 и max_queue_size() == QUEUE_SIZE == 256.
Конфигурационное пространство описывает VirtioGpuNvConfig: backend строит его через VirtioGpuNvConfig::new(&version, &gpus, caps, nvidia_vram_mib), где gpus — это GpuSlot. Гостевой драйвер читает его как struct virtio_gpu_nv_config размером 4024 байта, с num_fd_translations по смещению 3880 и vram_limit_mib по смещению 4016.
NvidiaBackend — внутренний бэкенд устройства, который backend оборачивает в Arc<Mutex<...>> и вызывает dispatch(&self.req, resp) для обработки каждого запроса из virtqueue, а также set_window, set_guest_ram, set_caps, set_vram_limit_mib и set_host_driver_version. Контракт описывают именно VirtioGpuNvConfig и GpuSlot, а также FdTranslation и FEATURE_RMCTRL_SEGMENTS.
Транспорт virtio: конфигурационное пространство и очереди
Гость отображает конфигурацию устройства с PAGE_SIZE как максимумом и молча обрезает всё, что за ним. Поэтому config_fits_in_one_page требует, чтобы размер VirtioGpuNvConfig не превышал 4096 байт: всё за этой границей не «медленно» и не «расточительно», а нечитаемо, и чтение за ней роняет гостевой драйвер.
Два других теста фиксируют раскладку. field_offsets_are_where_the_driver_reads_them проверяет, что каждое поле, читаемое драйвером по фиксированному смещению, лежит именно там, где драйвер его ждёт: driver_version по 0, num_gpus по 32, caps по 36, gpu_device_ids по 40, gpus по 72, а внутри GpuSlot — minor по 16, info_len по 20, info_text по 28. layout_matches_the_guest_driver дополнительно фиксирует размеры: GpuSlot — 476 байт, VirtioGpuNvConfig — 4024 байта, vram_limit_mib по 4016, num_fd_translations по 3880.
Вместе эти проверки гарантируют согласованность раскладки между VMM и гостевым драйвером: если любая сторона сдвинется, гость перестанет проходить probe, и ничто больше не объяснит причину.
Число очередей задаёт num_queues, возвращающая QUEUE_COUNT, а максимальный размер очереди — max_queue_size, возвращающая QUEUE_SIZE. Набор возможностей устройства объявляется в features: биты VIRTIO_F_VERSION_1, VIRTIO_F_NOTIFY_ON_EMPTY, VIRTIO_RING_F_EVENT_IDX и PROTOCOL_FEATURES. Последний включает механизм protocol_features, где перечислены MQвозможность заявить несколько очередей virtio, CONFIGвозможность читать и писать конфигурационное пространство устройства, BACKEND_REQвозможность открыть отдельный канал запросов к backend и SHMEMвозможность разделяемой памяти устройства, отображаемой через VMM.
Возможность EVENT_IDX активируется через set_event_idx, который сохраняет флаг в self.event_idx. При включённом флаге handle_event в цикле вызывает disable_notification, process и enable_notification, чтобы не ждать пропущенного уведомления. Возможность BACKEND_REQ реализуется через set_backend_req_fd, который открывает канал запросов и делает память устройства отображаемой.
Если гость присылает событие с номером очереди не меньше QUEUE_COUNT, handle_event возвращает ошибку "event for unknown queue". Для очереди EVENT_QUEUE событие игнорируется, так как этой очередью владеет поток-насос. Поле features в конфигурации заполняется значением FEATURE_RMCTRL_SEGMENTS и описывает, что бэкенд умеет сверх версии v0.1.
Control-queue: одна операция в полёте
Инвариант «одна операция в полёте» на control-queueочереди управляющих запросов, через которую гость отправляет хосту команды и получает на них ответы реализован тем, что каждый вызов nvgpu_send_recv создаёт собственную структуру nvgpu_req с отдельным completion done. Два потока с незавершёнными вызовами пробуждаются каждый своим ответом.
Запрос и ответ размещаются в этой же структуре (data[]), а не в буферах вызывающего. Это нужно, чтобы вызывающий мог уйти по таймауту или быть убитым, не освобождая память, которую ещё держит устройство. Запрос помещается в очередь, и устройство уведомляется о новом элементе; после этого вызывающий ждёт через wait_for_completion_killable_timeout(&r->done, 10 * HZ). Если время вышло и completion не завершён, под vq_lock ставится r->abandoned = true и возвращается -ETIMEDOUT, а освобождение откладывается на callback.
nvgpu_ctrl_drain под vq_lock забирает каждый готовый буфер через virtqueue_get_buf. Если r->abandoned — просто kfree(r), иначе записывает r->written = len и вызывает complete(&r->done), пробуждая ожидающего. nvgpu_ctrl_vq_cb — это callback virtqueue: он берёт vq_lock, вызывает nvgpu_ctrl_drain и отпускает блокировку, то есть пробуждение происходит из контекста прерывания.
В маршрутизации участвует поле device_type. DeviceKind::from_device_type декодирует его как диапазон: 0..=MAX_GPU_INDEX даёт Gpu(v), константы DEV_CTL/DEV_UVM/DEV_UVM_TOOLS/DEV_MODESET дают соответствующие singleton-устройства, а v >= DEV_DRI_BASE даёт Dri(v - DEV_DRI_BASE). msg_type декодируется MsgType::from_u32 по значениям 1..=8, а status — знаковый результат, который драйвер проверяет как (s32)status < 0.
Протокол сообщений и знаковый статус
MsgType::from_u32 превращает числовой код сообщения в вариант перечисления: значения 1..8 соответствуют Open, Close, Ioctl, Mmap, Munmap, GetProcFiles, GetSysFiles, EventReady, а любое другое значение даёт None.
DeviceKind::from_device_type декодирует поле device_type запроса Open: значения 0..=MAX_GPU_INDEX дают Gpu(v), константы устройств — соответствующие варианты, значения не меньше DEV_DRI_BASE дают Dri(v - DEV_DRI_BASE), остальные — None.
Поле status в MsgHeader объявлено как i32 и описано как знаковое: ноль при успехе, отрицательный errno при ошибке. Причина в том, что драйвер проверяет именно знак: (s32)status < 0. Конструктор MsgHeader::err принимает errno положительным и сохраняет его отрицательным через -errno.abs(). Тест an_error_status_is_negative подтверждает: err(Open, 2) даёт status == -2, err(Open, -2) тоже даёт -2, а ok(Open, 7) даёт status == 0 и handle == 7.
Если backend вернёт положительный код, драйвер прочитает его как успех, и гость продолжит работу после неудачного вызова.
Event-queue и poll()
Гость заранее выкладывает буферы на event-очередь через nvgpu_event_post, который вызывает virtqueue_add_inbuf. Когда хост заполняет дескриптор, срабатывает nvgpu_event_vq_cb: он забирает буферы через virtqueue_get_buf и, если длина не меньше заголовка и msg_type равен NVGPU_MSG_EVENT_READY, вызывает nvgpu_event_deliver.
nvgpu_event_deliver под fds_lock ищет nfd с совпадающим handle, ставит atomic_set(&nfd->pending, 1) и будит ожидающего через wake_up_interruptible(&nfd->wq). Затем буфер возвращается в очередь через nvgpu_event_post и делается virtqueue_kick.
nvgpu_poll возвращает EPOLLERR при отсутствии nfd. При выключенном nvgpu_poll_events — EPOLLIN|EPOLLOUT|EPOLLRDNORM|EPOLLWRNORM. Иначе при наличии poll_table и nvgpu_poll_spin_us > 0 крутится до deadline, затем вызывает poll_wait, и если atomic_xchg(&nfd->pending, 0) был ненулевым — возвращает EPOLLIN|EPOLLRDNORM, иначе 0.
В event_pump дескрипторы ставятся на epoll с EPOLLIN|EPOLLET. Раз в SWEEP = 1 мс выполняется safety net: для каждого watched дескриптора делается libc::poll с нулевым таймаутом, и если POLLIN виден, вызывается push_event. Так пропущенный edge переотправляется, и гость не зависает в poll_wait().
Трансляция файловых дескрипторов
Таблица трансляции fdДескриптор файла — целое число, которым процесс ссылается на открытый файл или устройство. — это не отдельный массив, а связь между открытым файлом /dev/nvidia* и handle, выданным backend'ом. Каждый open создаёт структуру nvgpu_fd, в которой хранится handle, device_type, очередь ожидания wq, счётчик pending, узел node для списка dev->fds, drm_unique_id и список pins.
При открытии nvgpu_open_common выделяет nfd, шлёт NVGPU_MSG_OPEN и записывает ответный handle в nfd->handle. После этого nvgpu_fd_register добавляет узел в dev->fds под fds_lock и инициализирует wq/pending, чтобы файл можно было найти по handle из события и разбудить ожидающего. Обратная операция nvgpu_fd_unregister удаляет узел из списка и будит всех в poll_wait, чтобы они увидели закрытие файла.
nvgpu_handle_for_fd берёт guest_fd, через fget получает struct file, извлекает private_data как nvgpu_fd и возвращает other->handle — то есть переводит дескриптор гостя в handle backend'а.
nvgpu_ioctl_translate_fd делает то же самое, но внутри ioctl: копирует весь payload из userspace, читает guest_fd по payload_offset, и если guest_fd >= 0, через fget находит nvgpu_fd и заменяет в буфере запроса guest_fd на host_handle. Значение -1 означает отсутствие дескриптора и передаётся без изменений.
Единственная тонкость — на обратном пути copy_to_user записывает в буфер гостя именно VMM handle, а не host fd. Это допустимо, потому что nvidia-smi после REGISTER_FD не читает payload, а только проверяет код возврата. Если в будущем какой-то ioctl с fd потребует ответа, VMM должен будет транслировать host fd → handle перед возвратом.
RM ioctl: NV_ESC_RM_CONTROL и вложенные параметры
nvgpu_ioctl_rm_control сначала копирует внешнюю структуру NVOS54_PARAMETERS из пользователя и извлекает из неё вложенный буфер (user_nested, nested_size) и команду ctl_cmd. Если размер вложенного буфера превышает 1 МиБ, возвращается -EINVAL. Затем вызывается nvgpu_try_intercept_rm_control, который перехватывает многоуказательные команды, которые нельзя форвардить напрямую.
Далее, если у устройства включён NVGPU_FEATURE_RMCTRL_SEGMENTS и найдена запись nvgpu_rmctrl_find с ptr_count в допустимых пределах, код копирует вложенный буфер в pbuf и для каждого указателя из таблицы читает его значение по смещению ptr_offset. Нулевой указатель пропускается как «RM's own "nothing to copy"», а для ненулевого вычисляется длина через nvgpu_rmctrl_len и заполняется массив seg.
Если хотя бы один указатель не удалось описать (i < ent->ptr_count), то nseg обнуляется, чтобы не отправлять часть сегментов: отправка части заставила бы backend выделить буфер под один указатель и не под другой, поэтому не отправляется ничего.
Если сегментов нет, но есть вложенный буфер, вызывается nvgpu_find_deep_rewrite, который сначала ищет запись через nvgpu_find_v1v2_rewrite, а при отсутствии — перебирает nvgpu_deep_only_table по полю v1_cmd. Для найденной записи читается второй указатель по смещению v1_userptr_offset и ведущее поле count, из которого deep_len вычисляется как count * 8 для info_style или как count иначе. Если deep_user_ptr нулевой, deep_len нулевой или превышает NVGPU_DEEP_MAX, все три поля deep_user_ptr, deep_ptr_offset и deep_len сбрасываются в ноль.
Направление копирования и paramsSize == 0
Разница между двумя подходами — в направлении копирования. В случае сегментов копируется только то, что RM читает, а буфер, который RM только пишет, отправляется нулями и возвращается с ответом внутри.
В nvgpu_ioctl_rm_alloc при paramsSize == 0 и непустом pAllocParms размер определяется через nvgpu_host_alloc_param_size по hClass, чтобы знать, сколько байт копировать из userspace. nvgpu_host_alloc_param_size сначала ищет class_id в dev->alloc_sizes и возвращает params_size, иначе вызывает nvgpu_rmalloc_class_param_size. Затем, если user_alloc && nested_size == 0, размер вычисляется и логируется, после чего проверяется, что nested_size не превышает 1 МиБ. Далее выделяются req_buf и resp_buf, заполняется заголовок запроса, и если user_alloc && nested_size > 0, данные копируются из user_alloc в nested.
Регистрация памяти и пины
nvgpu_pin_region сначала проверяет, что диапазон [uaddr, uaddr+len) состоит только из целых страниц: если len равен нулю или адрес/длина не выровнены на страницу, возвращается -EINVAL, а если число страниц превышает NVGPU_MAX_PIN_PAGES — -E2BIG. Затем она выделяет структуру pin, сохраняет uaddr и npages, выделяет массив pages и вызывает pin_user_pages_fast с флагами FOLL_WRITE | FOLL_LONGTERM, чтобы закрепить страницы за адресом пользователя на всё время жизни объекта.
Если получено меньше страниц, чем запрошено, это не частичный успех: регистрация покрывает весь диапазон или не происходит вовсе. Уже закреплённые страницы освобождаются через unpin_user_pages, а сама pin — через kvfree/kfree.
nvgpu_emit_page_runs обходит закреплённые страницы и объединяет соседние, у которых физический адрес следующей страницы равен run_gpa + run_len, в один run, увеличивая run_len на PAGE_SIZE. Разрозненные страницы дают по run на страницу, а при превышении границы регистрация отклоняется, а не урезается.
В nvgpu_ioctl_maybe_register решение о сохранении пина принимает ответ RM: status читается из тела ответа, и только если status == 0, пин сохраняется — заполняются hclient и hmemory, он добавляется в список nfd->pins под pins_lock, а локальная переменная pin обнуляется. Если status не равен нулю, пин не сохраняется, и в out-блоке вызывается nvgpu_pin_free(pin), потому что несохранённый пин означает незарегистрированную память и страницы возвращаются — иначе они оставались бы закреплёнными до закрытия файла.
Освобождение регистрации и смещение limit
Регистрация запоминается в списке nfd->pins: после успешного ответа RM, если status == 0, в pin записываются hclient и hmemory (либо из hObjectNew по смещению 8, либо из hmemory_at), и элемент добавляется в список.
nvgpu_pin_release при NV_ESC_RM_FREE берёт из параметров освобождения hRoot (смещение 0) и hObjectOld (смещение 8), затем ищет в списке элемент с совпадающими hclient и hmemory, удаляет его из списка и вызывает nvgpu_pin_free, который откалывает страницы через unpin_user_pages и освобождает память.
nvgpu_pins_drain при закрытии файла переносит весь список в локальный dead через list_splice_init, а затем для каждого элемента логирует, удаляет из списка и освобождает через nvgpu_pin_free.
Про limit: в коде прямо сказано, что это смещение последнего байта, поэтому length = limit + 1; значение U64_MAX отвергается как -EINVAL. Одностраничная регистрация несёт 4095 и означает 4096.
GEM, dma-buf и mmap
Прокси-объект nvgpu_gem_object описывает память хоста, размещённую в общем окне: поля window_off (смещение в окне), mapping_id (идентификатор для MUNMAP) и window_valid (флаг готовности) заполняются один раз при первом отображении.
nvgpu_gem_place_in_window сначала проверяет READ_ONCE(ng->window_valid) и ng->dev->window.len, затем под mutex_lock(&ng->map_lock) через NVGPU_IOCTL_GEM_MAP_OFFSET у owner_handle получает mmap-смещение хостового GEM-объекта, шлёт NVGPU_MSG_MMAP с size, offset и prot=3 и сохраняет guest_phys_addr в ng->window_off, mapping_id и window_valid=true.
nvgpu_gem_phys возвращает ng->dev->window.addr + ng->window_off — физический адрес памяти объекта в окне.
nvgpu_gem_object_mmap берёт node_start = drm_vma_node_start(&obj->vma_node), проверяет vma->vm_pgoff >= node_start, вычисляет within = (vm_pgoff - node_start) << PAGE_SHIFT, проверяет within + size <= obj->size, вызывает nvgpu_gem_place_in_window, ставит VM_IO|VM_PFNMAP|VM_DONTEXPAND|VM_DONTDUMP и отображает память объекта в адресное пространство процесса.
vm_pgoff приходит абсолютным, потому что ни путь через node's mmap, ни путь через dma-bufобщий буфер, которым один драйвер передаёт память другому без копирования не вычитают fake offset, и это учитывается именно вычитанием node_start внутри nvgpu_gem_object_mmap.
nvgpu_gem_vmap вызывает nvgpu_gem_place_in_window, затем ioremap_wc(nvgpu_gem_phys(ng), obj->size) и iosys_map_set_vaddr_iomem; nvgpu_gem_vunmap делает iounmap(map->vaddr_iomem) при map->is_iomem и iosys_map_clear(map).
Общий регион памяти, куда размещается память устройств, имеет id 1.
Парность ссылок на объект для vma
nvgpu_gem_export создаёт dma-buf с операциями nvgpu_dmabuf_ops, а nvgpu_gem_prime_import принимает только dma-buf с этими же операциями и, если объект принадлежит тому же устройству, берёт на него дополнительную ссылку через drm_gem_object_get(obj) и возвращает его.
nvgpu_dmabuf_map при импорте получает DMA-адрес окна через dma_map_resource и заполняет sg_table, а nvgpu_dmabuf_unmap освобождает этот адрес через dma_unmap_resource и затем освобождает sg_table и его структуру.
Парность ссылок на объект для vma обеспечивается тем, что оба пути mmap берут ссылку на объект для vma и оставляют её vma, чтобы та вернула её через vm_ops, скопированные из funcs объекта: nvgpu_gem_vm_ops содержит .open = drm_gem_vm_open и .close = drm_gem_vm_close.
Если unmap не будет вызван, ссылка никогда не будет снята: объект переживёт свой последний handle, его free никогда не выполнится, и удерживаемое им размещение в окне никогда не вернётся. Это проявлялось как исчерпание гостем гигабайта окна на 191 буфере, которые он уже закрыл.
nvidia-modeset и nvidia-caps
nvgpu_modeset_ioctl — обработчик unlocked_ioctl для файла modeset. Он читает тип ioctl через _IOC_TYPE(cmd) и размер через _IOC_SIZE(cmd), отвергает размер больше 65536 и, если тип равен 0x6d ('m'), передаёт управление в nvgpu_ioctl_modeset, иначе — в общий nvgpu_ioctl.
nvgpu_ioctl_modeset копирует из пользователя внешнюю структуру nvidia_modeset_outer, извлекает из неё вложенный указатель pData и размер dataSize, ограничивает вложенный размер 1 МиБ, выделяет буферы под запрос и ответ и заполняет заголовок nvgpu_ioctl_req (msg_type = NVGPU_MSG_IOCTL, handle = nfd->handle, cmd, data_len = sizeof(outer), nested_offset = sizeof(outer), nested_len = nested_size). После этого копирует внешнюю структуру и вложенные данные в один буфер и отправляет всё через nvgpu_send_recv. Ответ возвращается обратно: статус берётся из resp->hdr.status, внешняя структура копируется в uarg, а вложенные данные — во вложенный указатель пользователя с ограничением copy_back = min(nested_size, resp->nested_len).
Для NVKMS_IOCTL_REGISTER_SURFACE, когда useFd установлен, дескриптор файла в гостевом процессе ничего не значит в процессе backend'а, поэтому его нельзя пересылать как есть. Код проверяет le32_to_cpu(outer.cmd) == NVGPU_NVKMS_REGISTER_SURFACE и достаточный nested_size, читает use_fd по смещению 4 и, если он ненулевой, читает guest_fd по смещению NVGPU_NVKMS_SURFACE_FD_OFFSET, вызывает nvgpu_handle_for_fd(guest_fd, &handle) и, при успехе, записывает handle (как u64) обратно на место дескриптора. nvgpu_handle_for_fd получает struct file по guest_fd через fget, берёт из private_data структуру nvgpu_fd и возвращает её поле handle — это и есть handle, выданный backend'ом при открытии файла, которым backend может однозначно назвать нужный файл. При неудаче выводится предупреждение и дескриптор пересылается без изменений.
Устройства /dev/nvidia-caps/nvidia-cap1 и nvidia-cap2 создаются в nvgpu_probe: регистрируется диапазон из двух номеров через register_chrdev_region(MKDEV(NV_CAPS_MAJOR, 1), 2, "nvidia-caps"), затем cdev_init(&dev->cdev_caps, &nvgpu_caps_fops) и cdev_add на два минора, после чего device_create создаёт узлы "nvidia-cap1" и "nvidia-cap2". Класс nvidia-caps получает devnode = nvgpu_caps_devnode, который выставляет права 0444 и возвращает имя вида "nvidia-caps/<dev_name>".
При открытии nvgpu_caps_open просто устанавливает filp->private_data = NULL и возвращает 0, потому что это read-only проверки возможностей; nvgpu_caps_release тоже возвращает 0. Любой ioctl обрабатывается nvgpu_caps_ioctl, который всегда возвращает 0 — это сигнал userspace «нет особых возможностей», что корректно для одиночного GPU без MIG. При удалении nvgpu_remove уничтожает оба узла через device_destroy, удаляет cdev_caps, освобождает диапазон и уничтожает класс.
В caps.rs структура Caps(u32) хранит набор битов. parse разбирает список через split(','), ищет имя в NAMES без учёта регистра и складывает биты через |=, отказывая при неизвестном или пустом имени и при нулевом результате. bits() возвращает внутреннее u32, has(bit) проверяет (self.0 & bit) == bit. for_class(class) возвращает Some(GRAPHICS), если класс в THREE_D_CLASSES, Some(VIDEO), если в VIDEO_CLASSES, иначе None — то есть None означает класс, нужный любой нагрузке (клиенты, устройства, память, каналы, compute-класс) или неизвестный, который решает RM-allowlist.
Жизненный цикл: open/release и очистка
Открытие всех узлов идёт через nvgpu_open_common: он по типу устройства выбирает nvgpu_device из соответствующего cdev, выделяет nvgpu_fd, nvgpu_open_req и nvgpu_open_resp, заполняет запрос типом NVGPU_MSG_OPEN и флагами файла, шлёт его через nvgpu_send_recv и только после успешного ответа берёт handle, регистрирует дескриптор через nvgpu_fd_register и кладёт его в filp->private_data.
nvgpu_gpu_open, nvgpu_ctl_open и nvgpu_uvm_open — тонкие обёртки, передающие в nvgpu_open_common соответственно iminor(inode), NVGPU_DEV_CTL и NVGPU_DEV_UVM.
Закрытие nvgpu_release сначала вызывает nvgpu_pins_drain, потому что закрытие файла — это то, что заставляет RM снять объекты, державшие эти страницы: процесс, освободивший регистрации, приходит сюда уже ни с чем, а тот, кто завершился без освобождения, получает их освобождение здесь. Затем формируется запрос NVGPU_MSG_CLOSE с handle и отправляется через nvgpu_send_recv. После этого nvgpu_fd_unregister удаляет дескриптор из списка dev->fds и будит всех в poll_wait через wake_up_interruptible_all, и только потом nfd освобождается.
`nvgpu_drm
Где смотреть в коде
- vhost-user-nvgpu.rs: num_queues
- virtio_gpu_nv.c: nvgpu_fd_unregister
- virtio_gpu_nv.c: nvgpu_emit_page_runs
- caps.rs: parse
- virtio.rs: as_bytes
- virtio_gpu_nv.c: nvgpu_caps_ioctl
- virtio_gpu_nv.c: nvgpu_drm_postclose
- virtio_gpu_nv.c: nvgpu_gem_proxy_create