Контроль доступа к драйверу устройства Linux - PullRequest
0 голосов
/ 30 апреля 2018

Я внедряю драйвер устройства для какого-то нового оборудования и хочу, чтобы только один процесс имел доступ к устройству одновременно. Одновременные операции чтения / записи могли бы привести аппаратное обеспечение в состояние, в котором более чем вероятно потребовалась бы полная перезагрузка. У меня остались следующие вопросы:

  1. В примере кода из Драйверы устройств Linux вызов open() использует блокировку, но close() - нет. Здесь все еще нет условия гонки, или гарантированно, что декремент scull_s_count будет атомарным? По сути, в этом примере мне интересно, что произойдет, если один процесс попытается открыть устройство в тот момент, когда другой процесс завершит работу и закроет его.

  2. Я предполагаю, что мне не нужно проверять состояние моего флага открытия (я делаю что-то похожее на scull_s_count в примере) в моих вызовах read() и write(), так как Единственный способ войти в эти вызовы - это если пользовательское приложение уже получило fd через успешный вызов open(). Это предположение верно?

Благодаря комментариям tadman, я сделал самый краткий поиск механизмов ядра atomic_t. Вот некоторый псевдокод того, что у меня сейчас есть:

int open(struct inode *inode, struct file *filp) {
  spin_lock(&lock);
  if (atomic_read(&open_flag)) {
    spin_unlock(&lock);
    return -EBUSY;
  }
  atomic_set(&open_flag, 1);
  /* do other open() related stuff */
  spin_unlock(&lock);
  return 0;
}

int close(struct inode *inode, struct file *filp) {
  int rc;
  /* do close() stuff */
  atomic_set(&open_flag, 0);
  return rc;
}

open_flag - это atomic_t, который является частью большей структуры, которая выделяется с помощью kzalloc(). В результате он инициализируется до нуля.

В результате код здесь показывает, что целью блокировки является предотвращение одновременного открытия устройства несколькими процессами / потоками, а тот факт, что open_flag является atomic_t, предотвращает состояние гонки, которым я был обеспокоен в вопросе 1 выше. Достаточно ли этой реализации? Кроме того, я все еще ищу ответ на вопрос 2.

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

Ответы [ 5 ]

0 голосов
/ 22 мая 2018

хочет, чтобы только один процесс имел доступ к устройству одновременно

Вы не можете: fork() сделать невозможным запрет на открытие одного устройства одним процессом, потому что после fork () дочерний объект наследует файловые дескрипторы от своего родительского элемента, что позволяет как родительскому, так и дочернему элементам выполнять чтение / запись. / ioctl устройство. Также exec() перенесет дескриптор файла для вашего устройства в другую программу.

И не забудьте, что сокет Unix может использоваться для передачи файлового дескриптора на ваше устройство другому процессу (см. SCM_RIGHTS).

0 голосов
/ 22 мая 2018

Вы и все остальные, кто предоставил ответы, все правы в том, что пример ошибочен, а TrentP верен в том, что вам вообще не нужна блокировка, если вы используете атомарные битовые операции, такие как test_and_set_bit() (или вы можете использовать atomic_add_unless() и т. Д.).

Однако его ответ также не совсем корректен, поскольку он не учитывает порядок перестановки команд в close() - clear_bit(), не включающий барьер памяти. Таким образом, установка переменной в ноль может произойти до того, как «сделать закрытие», что, вероятно, полностью испортит драйвер, если кто-то еще откроет его одновременно. Фиксированное решение добавляет барьер перед вызовом clear_bit:

static unsigned long mydriver_flags;
#define IN_USE_BIT 0

static int mydriver_open(struct inode *inode, struct file *filp)
{
        if (test_and_set_bit(IN_USE_BIT, &mydriver_flags))
                return -EBUSY;
        /* continue with open */
        return 0;
}

static int mydriver_close(struct inode *inode, struct file *filp)
{
        /* do close stuff first */
        smp_mb_before_atomic();
        clear_bit(IN_USE_BIT, &mydriver_flags);
        return 0;
}

Обратите внимание, что test_and_set_bit() полностью упорядочен и уже подразумевает барьеры памяти, поэтому функция open() не требует изменений.

Теперь, чтобы ответить на ваши актуальные вопросы:

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

  2. Да, ваше предположение верно. Вам не нужны никакие проверки в ваших методах чтения / записи.

Дополнительные примечания:

  • Код, указанный в вопросе, недостаточен, поскольку в close() нет порядка; вам нужно smp_mb_before_atomic() перед atomic_set() in close(), аналогично приведенному выше коду. Но блокировка не требуется, если вы просто комбинируете атомарное чтение и устанавливаете, используя test_and_set_bit(), как показано выше, и TrentP.

  • spinlock vs mutex: решение очень просто зависит от того, нужно ли вам спать под блокировкой, то есть, если ваш код инициализации содержит что-то, что может привести к сну процесса, вам следует использовать мьютекс, пока если ваша инициализация просто устанавливает некоторые переменные, а затем снова снимает блокировку, тогда спин-блокировка является совершенно разумной, поскольку она более легкая, чем мьютекс. Учитывая, что это ваше открытие / закрытие, то есть ничего не критично для производительности, было бы прекрасно просто использовать мьютекс и не беспокоиться о том, может ли код спать или нет. Однако код будет намного лучше, если вы просто сбросите блокировку и просто используете test_and_set_bit(), как показано.

0 голосов
/ 16 мая 2018

Вы смотрите на проблему неправильно. Если аппаратное обеспечение не может обрабатывать одновременные операции чтения / записи, то это зависит от драйвера. Драйвер - это единственный процесс, который имеет доступ к оборудованию. Драйвер разрешает доступ к себе потокобезопасным способом. Чтение / запись из пользовательского пространства не должны идти непосредственно на аппаратное обеспечение, они должны обрабатываться драйвером, а драйвер работает с аппаратным обеспечением в соответствии с аппаратным обеспечением. Например, обработчик write () может просто сбросить данные в очередь и установить флаг, чтобы ваш бесконечный цикл write_hardware мог взять его и фактически записать на аппаратное обеспечение, когда это можно сделать.

0 голосов
/ 22 мая 2018

Я немного зациклен на программировании ядра Linux, но использование и атомарного, и спин-блокировки кажется мне непосильным.

хочет, чтобы только один процесс имел доступ к устройству одновременно

И если это то, что вам нужно, реализация scull работает просто отлично, спин-блокировка в открытой реализации драйвера scull гарантирует, что только один процесс получит действительный дескриптор файла одновременно.

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

В примере кода используется спин-блокировка, но будет ли мьютекс более подходящим?

Реализация спин-блокировки быстрее и достаточна для этой задачи.

int scull_s_release(struct inode *inode, struct file *filp)
{
  scull_s_count--; /* release the device */

  /* from there until the function return is the only place where a race can occur
  *  so I wouldn't define the scull implementation "flawed" */

  MOD_DEC_USE_COUNT;
  return 0;
}
0 голосов
/ 01 мая 2018

Пример, на который вы указываете, действительно ошибочен. Декремент абсолютно не гарантированно будет атомарным и почти наверняка не будет.

Но на самом деле, я не думаю, что есть комбинация компилятор / ЦП, которая когда-либо давала бы код, который мог дать сбой. Худшее, что может случиться, - это то, что одно ядро ​​ЦП может завершить закрытие, а затем другое ядро ​​может вызвать открытие и вернуться к работе, поскольку оно имеет устаревшее кэшированное значение флага.

Linux предоставляет для этого atomic_* функции, а также *_bit операции с атомными битовыми флагами. Смотрите core_api / atomic_ops.rst в документации по ядру.

Пример правильного и простого шаблона для выполнения его будет выглядеть примерно так:

unsigned long mydriver_flags;
#define IN_USE_BIT 0

static int mydriver_open(struct inode *inode, struct file *filp)
{
        if (test_and_set_bit(IN_USE_BIT, &mydriver_flags))
                return -EBUSY;
        /* continue with open */
        return 0;
}

static int mydriver_close(struct inode *inode, struct file *filp)
{
        /* do close stuff first */
        smp_mb__before_atomic();
        clear_bit(IN_USE_BIT, &mydriver_flags);
        return 0;
}

Реальный драйвер должен иметь структуру состояния устройства для каждого устройства с mydriver_flags. Вместо использования одного глобального для всего драйвера, как показано в примере.

Тем не менее, то, что вы пытаетесь сделать, вероятно, не очень хорошая идея. Даже если только один процесс может открыть устройство одновременно, дескрипторы открытого файла процесса совместно используются всеми потоками в процессе. Несколько потоков могут одновременно выполнять read() и write() вызовы одного и того же файлового дескриптора.

Если процесс имеет открытый дескриптор файла и вызывает fork(), этот дескриптор будет унаследован в новом процессе. Это способ, которым несколько процессов могут одновременно открывать устройство, несмотря на вышеуказанное ограничение "одного открытия".

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

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

...