Взаимодействие форка и памяти пространства пользователя, отображаемой в ядре - PullRequest
3 голосов
/ 28 октября 2010

Рассмотрим драйвер Linux, который использует get_user_pages (или get_page) для сопоставления страниц из вызывающего процесса. Физический адрес страниц затем передается на аппаратное устройство. И процесс, и устройство могут читать и записывать страницы, пока стороны не решат прекратить общение. В частности, сообщение может продолжать использовать страницы после возврата системного вызова, который вызывает get_user_pages. Системный вызов фактически устанавливает зону общей памяти между процессом и аппаратным устройством .

Меня беспокоит, что произойдет, если процесс вызовет fork (это может быть из другого потока и может произойти либо во время выполнения системного вызова, вызывающего get_user_pages, либо позже). В частности, если родитель пишет в область разделяемой памяти после разветвления, что я знаю о базовом физическом адресе (предположительно измененном из-за копирования при записи)? Я хочу понять:

  1. что нужно сделать ядру, чтобы защититься от потенциально некорректного процесса (я не хочу создавать дыру в безопасности !);
  2. какие ограничения должен соблюдать процесс, чтобы функциональность нашего драйвера работала правильно (т.е. физическая память остается сопоставленной по тому же адресу в родительском процессе).

    • В идеале мне бы хотелось, чтобы распространенный случай, когда дочерний процесс вообще не использует наш драйвер (он, вероятно, вызывает exec почти сразу), работает.
    • В идеале родительский процесс не должен предпринимать никаких специальных шагов при выделении памяти, поскольку у нас есть существующий код, который передает выделенный стеку буфер драйверу.
    • Мне известно о madvise с MADV_DONTFORK, и было бы хорошо, если бы память исчезла из пространства дочернего процесса, но это не применимо к выделенному стеку буферу.
    • «Не использовать fork, когда у вас активно соединение с нашим драйвером», это раздражает, но приемлемо в качестве крайней меры, если удовлетворен пункт 1.

Я готов указать на документацию или исходный код. В частности, я рассмотрел Драйверы устройств Linux , но не нашел, что эта проблема решена. RTFS, применяемая даже к соответствующей части исходного кода ядра, немного ошеломляет.

Версия ядра не полностью исправлена, но является последней (скажем, ≥2.6.26). Мы ориентируемся только на платформы Arm (пока что однопроцессорные, но многоядерные не за горами), если это имеет значение.

Ответы [ 2 ]

4 голосов
/ 28 октября 2010

A fork() не будет мешать get_user_pages(): get_user_pages() даст вам struct page.

Вам потребуется kmap(), чтобы получить к нему доступ, и это сопоставление выполняется в пространстве ядра, а не в пространстве пользователя.

РЕДАКТИРОВАТЬ: get_user_pages() дотронуться до таблицы страниц, но вы не должны беспокоиться об этом (просто убедитесь, что страницы отображаются в пространстве пользователя), и возвращает -EFAULT, если у него были какие-либо проблемы при этом.

Если вы выполните fork (), пока копирование при записи не будет выполнено, ребенок сможет увидеть эту страницу. После того, как будет выполнено копирование при записи (поскольку дочерний элемент / драйвер / родительский элемент записал на страницу с помощью сопоставления пользовательского пространства, а не ядра kmap (), имеющегося в драйвере), эта страница больше не будет использоваться совместно. Если вы все еще удерживаете kmap () на странице (в коде драйвера), вы не сможете узнать, держите ли вы родительскую страницу или страницу ребенка.

1) Это не дыра в безопасности, потому что как только вы выполняете (), все это исчезает.

2) Когда вы выполняете fork (), вы хотите, чтобы оба процесса были идентичными (это вилка !!). Я думаю, что ваш дизайн должен позволять как родителю, так и ребенку получить доступ к драйверу. Execve () будет сбрасывать все.

Как насчет добавления некоторых функций в пользовательское пространство, таких как:

 f = open("/dev/your_thing")
 mapping = mmap(f, ...)

Когда на вашем устройстве вызывается mmap (), вы устанавливаете отображение памяти со специальными флагами: http://os1a.cs.columbia.edu/lxr/source/include/linux/mm.h#071

У вас есть несколько интересных вещей, таких как:

#define VM_SHARED       0x00000008
#define VM_LOCKED       0x00002000
#define VM_DONTCOPY     0x00020000      /* Do not copy this vma on fork */

VM_SHARED отключит копирование при записи VM_LOCKED отключит обмен на этой странице VM_DONTCOPY скажет ядру не копировать область vma на fork, хотя я не думаю, что это хорошая идея

3 голосов
/ 29 октября 2010

Краткий ответ - использовать madvise(addr, len, MADV_DONTFORK) в любых буферах пользовательского пространства, которые вы предоставляете своему драйверу.Это говорит ядру, что сопоставление не следует копировать из родительского в дочернее, и поэтому CoW отсутствует.

Недостатком является то, что дочерний объект не наследует сопоставление по этому адресу, поэтому, если вы хотите, чтобы дочерний процесс затем запускалсяс помощью драйвера потребуется переназначить эту память.Но это довольно легко сделать в пользовательском пространстве.

Обновление : буфер в стеке проблематичен, я не уверен, что вы вообще можете сделать его безопасным.

Вы не можете пометить его DONTFORK, потому что ваш ребенок может работать на этой странице стека, когда он разветвляется, или (что еще хуже) может выполнить функцию, возвращающуюся позже, и попадет на страницу неотображенного стека.(Я даже проверял это, вы можете с радостью пометить свой стек DONTFORK, когда вы разветвляетесь, происходят плохие вещи.)

Другой способ избежать CoW - создать общее отображение, но вы не можете отобразить свой стекподелился по очевидным причинам.

Это означает, что вы рискуете CoW, если вы форк.Даже если дочерний процесс «просто» исполняет, он все равно может коснуться страницы стека и вызвать CoW, что приведет к тому, что родитель получит другую страницу, что плохо.

Единственный минус в вашу пользу - это код, использующийбуфер в стеке должен беспокоиться только о коде, который он вызывает, т.е.вы не можете использовать встроенный буфер после того, как функция вернулась.Так что вам нужно только проверять своих вызываемых, и если они никогда не раскошеливаются, вы в безопасности, но это все еще может быть неосуществимо и хрупко, если код когда-либо изменится.

Я думаю, что вы действительно хотите иметь всю памятьэто дается вашему драйверу из пользовательского распределителя в пользовательском пространстве.Это не должно быть так навязчиво.Распределитель может либо mmap напрямую подключаться к вашему устройству, как предлагалось в другом ответе, либо просто использовать анонимные mmap, madvise(DONTFORK) и, вероятно, mlock(), чтобы избежать замены.

...