MyTetra Share
Делитесь знаниями!
11. Блокировка процессов и потоков
Время создания: 20.09.2022 09:52
Текстовые метки: linux, ядро, модуль, программирование, язык, C, Си, пособие, документация
Раздел: Компьютер - Linux - Ядро - Пособие по программированию модулей ядра Linux
Запись: xintrea/mytetra_syncro/master/base/1663656733hea6m8yal2/text.html на raw.github.com

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.ko


/*

* sleep.c – создаем файл /proc, и если его одновременно будут пытаться

* открыть несколько процессов, все их отправляем в ожидание.

*/

#include <linux/kernel.h> /* Для работы с ядром. */

#include <linux/module.h> /* Для модуля. */

#include <linux/proc_fs.h> /* Необходим для использования procfs */

#include <linux/sched.h> /* Для усыпления процессов и их пробуждения. */

#include <linux/uaccess.h> /* Для get_user и put_user. */

#include <linux/version.h>

#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);


cat_nonblock.c


MODULE_LICENSE("GPL");

/*

* cat_nonblock.c – открывает файл и отображает содержимое, но в случае

* необходимости ожидания ввода выходит.

*/

#include <errno.h> /* Для errno. */

#include <fcntl.h> /* Для открытия. */

#include <stdio.h> /* Стандартный ввод-вывод. */

#include <stdlib.h> /* Для выхода. */

#include <unistd.h> /* Для считывания.*/

#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 <filename>\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 ядро реализует это другим способом, поддерживающим таймауты или прерывания.

В примере ниже стартуют два потока, но один должен сработать раньше.

completion.c


/*

* completions.c

*/

#include <linux/completion.h>

#include <linux/init.h>

#include <linux/kernel.h>

#include <linux/kthread.h>

#include <linux/module.h>

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 есть и другие вариации, которые включают таймауты и прерывания, но этого базового механизма вполне достаточно для множества типичных ситуаций без добавления излишней сложности.


 
MyTetra Share v.0.59
Яндекс индекс цитирования