Пятая часть последней версии руководства по написанию модулей ядра от 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.

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

12. Избегание коллизий и взаимных блокировок


Если процессы, выполняющиеся на разных ядрах или в разных потоках, попытаются обратиться к одной и той же области памяти, то вполне могут случиться странности, либо система просто заблокируется. Для избежания этого в ядре существуют специальные функции взаимного исключения (мьютексы). Они показывают, «занят» или «свободен» в данный момент фрагмент кода, исключая тем самым одновременные попытки его выполнения.

▍ 12.1 Мьютексы


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

example_mutex.c
/* 
 * example_mutex.c 
 */ 
#include <linux/init.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/mutex.h> 
 
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
/* 
 * example_spinlock.c 
 */ 
#include <linux/init.h> 
#include <linux/interrupt.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/spinlock.h> 
 
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
/* 
 * example_rwlock.c 
 */ 
#include <linux/interrupt.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
 
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
/* 
 * example_atomic.c 
 */ 
#include <linux/interrupt.h> 
#include <linux/kernel.h> 
#include <linux/module.h> 
 
#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 рублей.

Комментарии (0)