[Перевод] Пособие по программированию модулей ядра Linux. Ч.5
Пятая часть последней версии руководства по написанию модулей ядра от 2 июля 2022 года. В ней мы разберемся, как в ядре реализована совместная работа процессов и потоков, узнаем, какую роль в этом играет режим ожидания (sleep), рассмотрим возможные способы избежания коллизий и взаимных блокировок, а также познакомимся с назначением и использованием атомарных операций.
▍ Готовые части руководства:
11. Блокировка процессов и потоков
▍ 11.1 Ожидание
Что вы делаете, когда вас просят сделать что-то, чем пока вы заняться не можете? Как обычный человек, которого просит другой такой же человек, вы на это можете сказать лишь: «Я пока занят. Не мешай». Но если вы являетесь ядром, а обратился к вам процесс, то у вас есть другой вариант. Вы можете поставить этот процесс в режим ожидания (sleep), пока не появится возможность его обслужить. По факту ядро постоянно отправляет процессы в ожидание и пробуждает их. Именно так реализовано одновременное выполнение множества процессов на одном ЦПУ.
И текущий модуль ядра является примером этого. Файл (с именем /proc/sleep
) одновременно может быть открыт лишь одним процессом. Если он уже открыт, модуль вызывает wait_event_interruptible
. Самый простой способ сохранять файл открытым — это использовать команду:
tail -f
Эта функция изменяет статус задачи (задача — это структура данных ядра, содержащая информацию о процессе и системном вызове, в котором он находится, если таковой присутствует) на TASK_INTERRUPTIBLE
. Это означает, что выполнение задачи будет отложено до момента ее пробуждения, а пока она добавляется в WaitQ
, то есть очередь задач, ожидающих возможности получить доступ к файлу. Затем эта функция вызывает планировщик для переключения контекста на другой процесс, которому нужен ЦПУ.
Когда процесс закончил работу с файлом, он его закрывает, и вызывается module_close
. Эта функция пробуждает все процессы в очереди (не существует механизма для пробуждения их по одиночке), после чего делает возврат, и процесс, закрывший файл, может продолжать свое выполнение. Далее в свое время планировщик решает, что этот процесс уже достаточно поработал, и передает управление ЦПУ другому процессу из очереди. Свое выполнение этот процесс начинает с момента, следующего сразу за вызовом module_interruptible_sleep_on
.
Это означает, что процесс все еще находится в режиме ядра — по имеющейся у него информации, он отправил системный вызов open()
, который возврат еще не сделал. Процессу не известно, что большую часть времени между моментом отправки этого вызова и его возвратом ЦПУ использовался кем-то еще.
После этого он может установить глобальную переменную, указывающую всем другим процессам, что файл пока открыт, и продолжить выполнение. Когда другие процессы будут получать долю внимания ЦПУ, они будут видеть эту установленную переменную и возвращаться в режим ожидания.
Итак, мы используем tail -f
, чтобы фоново удерживать файл в открытом состоянии при попытке получить к нему доступ другим процессом (также в фоновом режиме, чтобы не пришлось переключаться на другой VT). Как только первый фоновый процесс завершится командой kill %1
, пробудится второй, который получит доступ к файлу, а затем также завершится.
При этом module_close
не единственный, кто имеет право на пробуждение процессов, ожидающих доступа к файлу. Помимо этого, они могут пробуждаться сигналом Ctrl+C (SIGINT
). Причина тому в использованной нами функции module_interruptible_sleep_on
. Можно было задействовать module_sleep_on
, но это бы сильно разозлило пользователей, чьи нажатия Ctrl+С тогда бы игнорировались.
В этом случае нам нужно сразу же возвращать -EINTR
. Это необходимо, чтобы пользователи могли, например, завершить процесс до получения им доступа к файлу.
Нужно помнить и еще кое-что. Иногда процессы не хотят спать, они хотят незамедлительно получить либо желаемое, либо ответ, что это действие выполнить нельзя. Подобные процессы используют при открытии файла флаг O_NONBLOCK
. На это ядро должно возвращать код ошибки -EAGAIN
от операций, которые в противном случае должны были заблокироваться, к примеру, при открытии файла, как в нашем примере. Для открытия файла с O_NONBLOCK
можно использовать программу cat_nonblock
, расположенную в каталоге examples/other
.
$ sudo insmod sleep.ko
$ cat_nonblock /proc/sleep
Last input:
$ tail -f /proc/sleep &
Last input:
Last input:
Last input:
Last input:
Last input:
Last input:
Last input:
tail: /proc/sleep: file truncated
[1] 6540
$ cat_nonblock /proc/sleep
Open would block
$ kill %1
[1]+ Terminated tail -f /proc/sleep
$ cat_nonblock /proc/sleep
Last input:
$
/*
* sleep.c – создаем файл /proc, и если его одновременно будут пытаться
* открыть несколько процессов, все их отправляем в ожидание.
*/
#include /* Для работы с ядром. */
#include /* Для модуля. */
#include /* Необходим для использования procfs */
#include /* Для усыпления процессов и их пробуждения. */
#include /* Для get_user и put_user. */
#include
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
#define HAVE_PROC_OPS
#endif
/* Здесь мы храним последнее полученное сообщение, подтверждая возможность
* обработки ввода.
*/
#define MESSAGE_LENGTH 80
static char message[MESSAGE_LENGTH];
static struct proc_dir_entry *our_proc_file;
#define PROC_ENTRY_FILENAME "sleep"
/* Так как мы используем структуру файловых операций, то не можем
* задействовать специальную файловую систему proc и должны
* использовать стандартную функцию чтения, которой эта функция и является.
*/
static ssize_t module_output(struct file *file, /* см. include/linux/fs.h */
char __user *buf, /* Буфер для данных
(в сегменте пользователя). */
size_t len, /* Длина буфера. */
loff_t *offset)
{
static int finished = 0;
int i;
char output_msg[MESSAGE_LENGTH + 30];
/* Возвращаем 0, обозначая конец файла.
*/
if (finished) {
finished = 0;
return 0;
}
sprintf(output_msg, "Last input:%s\n", message);
for (i = 0; i < len && output_msg[i]; i++)
put_user(output_msg[i], buf + i);
finished = 1;
return i; /* Возвращаем количество "считанных” байт. */
}
/* Эта функция получает ввод от пользователя, когда он производит запись
* в файл /proc.
*/
static ssize_t module_input(struct file *file, /* Сам файл. */
const char __user *buf, /* Буфер с вводом. */
size_t length, /* Длина буфера. */
loff_t *offset) /* Cмещение до файла – игнорируется. */
{
int i;
/* Помещение ввода в Message, где позднее его сможет использовать
* module_output.
*/
for (i = 0; i < MESSAGE_LENGTH - 1 && i < length; i++)
get_user(message[i], buf + i);
/* Нам нужна стандартная строка, завершающаяся нулем. */
message[i] = '\0';
/* Нужно вернуть количество использованных во вводе символов. */
return i;
}
/* 1, если файл сейчас уже кем-то открыт. */
static atomic_t already_open = ATOMIC_INIT(0);
/* Очередь процессов, ожидающих доступа к файлу. */
static DECLARE_WAIT_QUEUE_HEAD(waitq);
/* Вызывается при открытии файла /proc. */
static int module_open(struct inode *inode, struct file *file)
{
/* Если флаги при открытии файла содержат O_NONBLOCK, значит процесс
* не хочет ждать доступности этого файла. В таком случае, если файл
* уже открыт, нужно будет не блокировать процесс, который
* предпочитает оставаться открытым, а вернуть -EAGAIN, сообщив ему,
* что попытку нужно повторить позже.
*/
if ((file->f_flags & O_NONBLOCK) && atomic_read(&already_open))
return -EAGAIN;
/* Это подходящее место для try_module_get(THIS_MODULE), так как,
* если процесс находится в цикле в модуле ядра, то этот модуль
* извлекать нельзя.
*/
try_module_get(THIS_MODULE);
while (atomic_cmpxchg(&already_open, 0, 1)) {
int i, is_sig = 0;
/* Эта функция отправляет текущий процесс, включая любые системные
* вызовы, например наши, в ожидание. Выполнение продолжится сразу
* после вызова этой функции либо при вызове
* wake_up(&waitq) (это делает только module_close при закрытии
* файла), либо при отправке процессу сигнала вроде Ctrl+C.
*/
wait_event_interruptible(waitq, !atomic_read(&already_open));
/* Если пробуждение произошло из-за получения сигнала, который не
* блокируется, вернуть -EINTR (провал системного вызова). Это
* позволяет завершать или останавливать процессы.
*/
for (i = 0; i < _NSIG_WORDS && !is_sig; i++)
is_sig = current->pending.signal.sig[i] & ~current->blocked.sig[i];
if (is_sig) {
/* Важно поместить module_put(THIS_MODULE) сюда, так как
* для процессов, где окажется прервана операция open(),
* соответствующей операции close() не будет. Если не
* декрементировать счетчик использования здесь, у нас
* останется в нем положительный счет, который мы никак уже
* не приведем к нулю. В итоге у нас получится бессмертный
* модуль, для извлечения которого потребуется перезагрузка.
*/
module_put(THIS_MODULE);
return -EINTR;
}
}
return 0; /* Разрешение доступа. */
}
/* Вызывается при закрытии файла /proc. */
static int module_close(struct inode *inode, struct file *file)
{
/* Устанавливаем already_open на нуль, чтобы один из процессов в waitq
* мог установить already_open обратно на один и открыть файл. В итоге
* остальные процессы при вызове будут видеть, что already_open
* равен одному, в связи с чем возвращаться в ожидание.
*/
atomic_set(&already_open, 0);
/* Пробуждение всех процессов в waitq, чтобы очередной ожидающий мог
* получить доступ к файлу.
*/
wake_up(&waitq);
module_put(THIS_MODULE);
return 0; /* Успех. */
}
/* Структуры для регистрации в качестве файла /proc с указателями на все
* связанные функции.
*/
/* Файловые операции нашего файла /proc. Здесь размещаются указатели на
* все функции, вызываемые, когда кто-то пытается произвести действия с
* файлом. NULL означает, что мы не хотим выполнять какое-то действие.
*/
#ifdef HAVE_PROC_OPS
static const struct proc_ops file_ops_4_our_proc_file = {
.proc_read = module_output, /* "Считывание" из файла. */
.proc_write = module_input, /* "Запись" в файл. */
.proc_open = module_open, /* Вызывается при открытии файла /proc */
.proc_release = module_close, /* Вызывается при его закрытии. */
};
#else
static const struct file_operations file_ops_4_our_proc_file = {
.read = module_output,
.write = module_input,
.open = module_open,
.release = module_close,
};
#endif
/* Инициализация модуля – регистрация файла /proc. */
static int __init sleep_init(void)
{
our_proc_file =
proc_create(PROC_ENTRY_FILENAME, 0644, NULL, &file_ops_4_our_proc_file);
if (our_proc_file == NULL) {
remove_proc_entry(PROC_ENTRY_FILENAME, NULL);
pr_debug("Error: Could not initialize /proc/%s\n", PROC_ENTRY_FILENAME);
return -ENOMEM;
}
proc_set_size(our_proc_file, 80);
proc_set_user(our_proc_file, GLOBAL_ROOT_UID, GLOBAL_ROOT_GID);
pr_info("/proc/%s created\n", PROC_ENTRY_FILENAME);
return 0;
}
/* Очистка – снятие регистрации файла из /proc. Это может быть опасно,
* если в waitq еще есть ожидающие процессы, потому что они находятся
* внутри функции open(), которая будет выгружена. В 10 главе я объясняю,
* как в подобном случае избежать извлечения модуля.
*/
static void __exit sleep_exit(void)
{
remove_proc_entry(PROC_ENTRY_FILENAME, NULL);
pr_debug("/proc/%s removed\n", PROC_ENTRY_FILENAME);
}
module_init(sleep_init);
module_exit(sleep_exit);
MODULE_LICENSE("GPL");
/*
* cat_nonblock.c – открывает файл и отображает содержимое, но в случае
* необходимости ожидания ввода выходит.
*/
#include /* Для errno. */
#include /* Для открытия. */
#include /* Стандартный ввод-вывод. */
#include /* Для выхода. */
#include /* Для считывания.*/
#define MAX_BYTES 1024 * 4
int main(int argc, char *argv[])
{
int fd; /* Дескриптор считываемого файла. */
size_t bytes; /* Количество считываемых байт. */
char buffer[MAX_BYTES]; /* Буфер для этих байт. */
/* Использование. */
if (argc != 2) {
printf("Usage: %s \n", argv[0]);
puts("Reads the content of a file, but doesn't wait for input");
exit(-1);
}
/* Открытие файла для считывания в неблокирующемся режиме. */
fd = open(argv[1], O_RDONLY | O_NONBLOCK);
/* Если открытие провалилось. */
if (fd == -1) {
puts(errno == EAGAIN ? "Open would block" : "Open failed");
exit(-1);
}
/* Считывание файла и вывод его содержимого. */
do {
/* Считывание символов из файла. */
bytes = read(fd, buffer, MAX_BYTES);
/* В случае ошибки сообщить о ней и завершиться. */
if (bytes == -1) {
if (errno == EAGAIN)
puts("Normally I'd block, but you told me not to");
else
puts("Another read error");
exit(-1);
}
/* Вывод символов. */
if (bytes > 0) {
for (int i = 0; i < bytes; i++)
putchar(buffer[i]);
}
/* Пока нет ошибок, и файл не закончился. */
} while (bytes > 0);
return 0;
}
▍ 11.2 Завершение потоков
Иногда в модуле, имеющем несколько потоков, одно действие должно совершиться перед другим. И вместо использования команд /bin/sleep
ядро реализует это другим способом, поддерживающим таймауты или прерывания.
В примере ниже стартуют два потока, но один должен сработать раньше.
/*
* completions.c
*/
#include
#include
#include
#include
#include
static struct {
struct completion crank_comp;
struct completion flywheel_comp;
} machine;
static int machine_crank_thread(void *arg)
{
pr_info("Turn the crank\n");
complete_all(&machine.crank_comp);
complete_and_exit(&machine.crank_comp, 0);
}
static int machine_flywheel_spinup_thread(void *arg)
{
wait_for_completion(&machine.crank_comp);
pr_info("Flywheel spins up\n");
complete_all(&machine.flywheel_comp);
complete_and_exit(&machine.flywheel_comp, 0);
}
static int completions_init(void)
{
struct task_struct *crank_thread;
struct task_struct *flywheel_thread;
pr_info("completions example\n");
init_completion(&machine.crank_comp);
init_completion(&machine.flywheel_comp);
crank_thread = kthread_create(machine_crank_thread, NULL, "KThread Crank");
if (IS_ERR(crank_thread))
goto ERROR_THREAD_1;
flywheel_thread = kthread_create(machine_flywheel_spinup_thread, NULL,
"KThread Flywheel");
if (IS_ERR(flywheel_thread))
goto ERROR_THREAD_2;
wake_up_process(flywheel_thread);
wake_up_process(crank_thread);
return 0;
ERROR_THREAD_2:
kthread_stop(crank_thread);
ERROR_THREAD_1:
return -1;
}
static void completions_exit(void)
{
wait_for_completion(&machine.crank_comp);
wait_for_completion(&machine.flywheel_comp);
pr_info("completions exit\n");
}
module_init(completions_init);
module_exit(completions_exit);
MODULE_DESCRIPTION("Completions example");
MODULE_LICENSE("GPL");
Структура machine
хранит состояния завершения для этих двух потоков. В точке выхода каждого из них обновляется соответствующее состояние. При этом для потока flywheel
используется wait_for_completion
, чтобы он не запустился преждевременно.
Так что, хоть flywheel_thread
и стартует первым, загрузив модуль и выполнив dmesg
, вы должны заметить, что сначала всегда происходит поворот рычага (crank
), потому что поток маховика (flywheel
) ожидает его завершения.
У функции wait_for_completion
есть и другие вариации, которые включают таймауты и прерывания, но этого базового механизма вполне достаточно для множества типичных ситуаций без добавления излишней сложности.
12. Избегание коллизий и взаимных блокировок
Если процессы, выполняющиеся на разных ядрах или в разных потоках, попытаются обратиться к одной и той же области памяти, то вполне могут случиться странности, либо система просто заблокируется. Для избежания этого в ядре существуют специальные функции взаимного исключения (мьютексы). Они показывают, «занят» или «свободен» в данный момент фрагмент кода, исключая тем самым одновременные попытки его выполнения.
▍ 12.1 Мьютексы
Используются мьютексы ядра аналогично тому, как они развертываются в пользовательской среде. И в большинстве случаев для избежания коллизий этого вполне может оказаться достаточно.
/*
* example_mutex.c
*/
#include
#include
#include
#include
static DEFINE_MUTEX(mymutex);
static int example_mutex_init(void)
{
int ret;
pr_info("example_mutex init\n");
ret = mutex_trylock(&mymutex);
if (ret != 0) {
pr_info("mutex is locked\n");
if (mutex_is_locked(&mymutex) == 0)
pr_info("The mutex failed to lock!\n");
mutex_unlock(&mymutex);
pr_info("mutex is unlocked\n");
} else
pr_info("Failed to lock\n");
return 0;
}
static void example_mutex_exit(void)
{
pr_info("example_mutex exit\n");
}
module_init(example_mutex_init);
module_exit(example_mutex_exit);
MODULE_DESCRIPTION("Mutex example");
MODULE_LICENSE("GPL");
▍ 12.2 Спин-блокировки
Спин-блокировки, или спинлоки, блокируют ЦПУ, на котором выполняется код, занимая 100% его ресурсов. В связи с этим механизм спинлоков желательно использовать только для кода, на выполнение которого требуется не более нескольких миллисекунд, чтобы с позиции пользователя не вызвать заметного замедления работы.
Примером в данном случае является ситуация irq safe, когда прерывания, происходящие во время блокировки, не забываются, а повторно активируются при ее снятии, используя переменную flags
для сохранения своего состояния.
/*
* example_spinlock.c
*/
#include
#include
#include
#include
#include
static DEFINE_SPINLOCK(sl_static);
static spinlock_t sl_dynamic;
static void example_spinlock_static(void)
{
unsigned long flags;
spin_lock_irqsave(&sl_static, flags);
pr_info("Locked static spinlock\n");
/* Безопасное выполнение задачи. Поскольку задействуется 100% ЦПУ,
* выполнение кода должно занимать не более нескольких миллисекунд.
*/
spin_unlock_irqrestore(&sl_static, flags);
pr_info("Unlocked static spinlock\n");
}
static void example_spinlock_dynamic(void)
{
unsigned long flags;
spin_lock_init(&sl_dynamic);
spin_lock_irqsave(&sl_dynamic, flags);
pr_info("Locked dynamic spinlock\n");
/* Безопасное выполнение задачи. Поскольку задействуется 100% ЦПУ,
* выполнение кода должно занимать не более нескольких миллисекунд.
*/
spin_unlock_irqrestore(&sl_dynamic, flags);
pr_info("Unlocked dynamic spinlock\n");
}
static int example_spinlock_init(void)
{
pr_info("example spinlock started\n");
example_spinlock_static();
example_spinlock_dynamic();
return 0;
}
static void example_spinlock_exit(void)
{
pr_info("example spinlock exit\n");
}
module_init(example_spinlock_init);
module_exit(example_spinlock_exit);
MODULE_DESCRIPTION("Spinlock example");
MODULE_LICENSE("GPL");
▍ 12.3 Блокировки для чтения и записи
Блокировки для выполнения чтения и записи — это специализированные спинлоки, позволяющие эксклюзивно считывать или производить запись. Подобно предыдущему примеру, код ниже показывает ситуацию irq safe, когда в случае активации аппаратными прерываниями других функций, которое также могут выполнять нужные вам чтение/запись, эти функции не нарушат текущую логику выполнения. Как и прежде, будет правильным решением, устанавливать подобную блокировку для максимально коротких задач, чтобы они не подвешивали систему и не вызывали недовольство пользователей относительно тирании вашего модуля.
/*
* example_rwlock.c
*/
#include
#include
#include
static DEFINE_RWLOCK(myrwlock);
static void example_read_lock(void)
{
unsigned long flags;
read_lock_irqsave(&myrwlock, flags);
pr_info("Read Locked\n");
/* Считывание. */
read_unlock_irqrestore(&myrwlock, flags);
pr_info("Read Unlocked\n");
}
static void example_write_lock(void)
{
unsigned long flags;
write_lock_irqsave(&myrwlock, flags);
pr_info("Write Locked\n");
/* Запись. */
write_unlock_irqrestore(&myrwlock, flags);
pr_info("Write Unlocked\n");
}
static int example_rwlock_init(void)
{
pr_info("example_rwlock started\n");
example_read_lock();
example_write_lock();
return 0;
}
static void example_rwlock_exit(void)
{
pr_info("example_rwlock exit\n");
}
module_init(example_rwlock_init);
module_exit(example_rwlock_exit);
MODULE_DESCRIPTION("Read/Write locks example");
MODULE_LICENSE("GPL");
Конечно же, если вы уверены, что аппаратные прерывания не активируют никакие функции, которые могли бы нарушить логику, то можете использовать более простые read_lock(&myrwlock)
и read_unlock(&myrwlock)
либо соответствующие функции записи.
▍ 12.4 Атомарные операции
Если вы выполняете простую арифметику: сложение, вычитание или побитовые операции, тогда многоядерный и гиперпоточный мир может предложить еще один способ, как не позволить другим компонентам системы вмешаться в ваше действо. С помощью атомарных операций вы можете обеспечить, чтобы ваше сложение, вычитание или инвертирование битов произошли успешно и не были перезаписаны какими-либо сторонними процессами. Вот пример:
/*
* example_atomic.c
*/
#include
#include
#include
#define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c"
#define BYTE_TO_BINARY(byte) \
((byte & 0x80) ? '1' : '0'), ((byte & 0x40) ? '1' : '0'), \
((byte & 0x20) ? '1' : '0'), ((byte & 0x10) ? '1' : '0'), \
((byte & 0x08) ? '1' : '0'), ((byte & 0x04) ? '1' : '0'), \
((byte & 0x02) ? '1' : '0'), ((byte & 0x01) ? '1' : '0')
static void atomic_add_subtract(void)
{
atomic_t debbie;
atomic_t chris = ATOMIC_INIT(50);
atomic_set(&debbie, 45);
/* Вычитание единицы. */
atomic_dec(&debbie);
atomic_add(7, &debbie);
/* Прибавление единицы. */
atomic_inc(&debbie);
pr_info("chris: %d, debbie: %d\n", atomic_read(&chris),
atomic_read(&debbie));
}
static void atomic_bitwise(void)
{
unsigned long word = 0;
pr_info("Bits 0: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
set_bit(3, &word);
set_bit(5, &word);
pr_info("Bits 1: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
clear_bit(5, &word);
pr_info("Bits 2: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
change_bit(3, &word);
pr_info("Bits 3: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
if (test_and_set_bit(3, &word))
pr_info("wrong\n");
pr_info("Bits 4: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
word = 255;
pr_info("Bits 5: " BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(word));
}
static int example_atomic_init(void)
{
pr_info("example_atomic started\n");
atomic_add_subtract();
atomic_bitwise();
return 0;
}
static void example_atomic_exit(void)
{
pr_info("example_atomic exit\n");
}
module_init(example_atomic_init);
module_exit(example_atomic_exit);
MODULE_DESCRIPTION("Atomic operations example");
MODULE_LICENSE("GPL");
До того, как в стандарте С11 появились встроенные атомарные типы, ядро уже предоставляло небольшой их набор, которым можно было воспользоваться с помощью хитрого архитектурно-зависимого кода.
Реализация же атомарных типов в С11 позволяет ядру отказаться от этих специфичных команд, сделав его код более внятным для людей, которые данный стандарт понимают. Но есть здесь и кое-какие проблемы, например модель памяти ядра не соответствует модели, формируемой атомарными операциями в С11. Подробнее эта тема раскрыта в следующих ресурсах:
▍ Продолжение
В следующей части мы поговорим о замене макроса print
, создадим мигающий светодиодами модуль, а также разберем систему планирования задач.
▍ Готовые части руководства:
Конкурс статей от RUVDS.COM. Три денежные номинации. Главный приз — 100 000 рублей.