В предыдущей статье мы ввели многозадачность. Сегодня пришло время рассмотреть тему драйверов символьных устройств.

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

Начнем с создания структуры устройства, затем введем базовую поддержку файлового ввода-вывода, рассмотрим структуру io_buf и функции для работы с файлами из stdio.h.

Оглавление


Система сборки (make, gcc, gas). Первоначальная загрузка (multiboot). Запуск (qemu). Библиотека C (strcpy, memcpy, strext). Библиотека C (sprintf, strcpy, strcmp, strtok, va_list ...). Сборка библиотеки в режиме ядра и в режиме пользовательского приложения. Системный журнал ядра. Видеопамять. Вывод на терминал (kprintf, kpanic, kassert). Динамическая память, куча (kmalloc, kfree). Организация памяти и обработка прерываний (GDT, IDT, PIC, syscall). Исключения. Виртуальная память (каталог страниц и таблица страниц). Процесс. Планировщик. Многозадачность. Системные вызовы (kill, exit, ps).

Драйверы символьных устройств. Системные вызовы (ioctl, fopen, fread, fwrite). Библиотека C (fopen, fclose, fprintf, fscanf).

Файловая система ядра (initrd), elf и его внутренности. Системные вызовы (exec). Оболочка как полноценная программа для ядра. Пользовательский режим защиты (ring3). Сегмент состояния задачи (tss).

Драйверы символьных устройств


Начинается все с об]явления символьного устройства. Как ты помнишь, в Linux Device Drivers определение устройства выглядело так:

struct cdev *my_cdev = cdev_alloc( );
my_cdev->ops = &my_fops;

Основной смысл в том, чтобы назначить устройству реализации функций файлового ввода-вывода.

Мы обойдемся одной структурой, но смысл будет похожий:

extern struct dev_t {
    struct clist_head_t list_head; /* should be at first */
    char name[8]; /* device name */
    void* base_r; /* base read address */
    void* base_w; /* base write address */
    dev_read_cb_t read_cb; /* read handler */
    dev_write_cb_t write_cb; /* write handler */
    dev_ioctl_cb_t ioctl_cb; /* device specific command handler */
    struct clist_definition_t ih_list; /* low half interrupt handlers */
};

Каждому устройству соответствует половин список прерываний, вызываемых в момент генерации прерываний.

В Linux такие половины называются верхними, у нас наоборот, нижними (более низкий уровень).

Лично мне это показалось более логичным и я ненароком запомнил термины наоборот. Каждый элемент списка нижних половин прерываний опишем так:

extern struct ih_low_t {
    struct clist_head_t list_head; /* should be at first */
    int number; /* interrupt number */
    ih_low_cb_t handler; /* interrupt handler */
};

При инициализации драйвер будет регистрировать свое устройство через функцию dev_register, другими словами добавлять новое устройство в кольцевой список:

extern void dev_register(struct dev_t* dev)
{
    struct clist_head_t* entry;
    struct dev_t* device;

    /* create list entry */
    entry = clist_insert_entry_after(&dev_list, dev_list.head);
    device = (struct dev_t*)entry->data;

    /* fill data */
    strncpy(device->name, dev->name, sizeof(dev->name));
    device->base_r = dev->base_r;
    device->base_w = dev->base_w;
    device->read_cb = dev->read_cb;
    device->write_cb = dev->write_cb;
    device->ioctl_cb = dev->ioctl_cb;
    device->ih_list.head = dev->ih_list.head;
    device->ih_list.slot_size = dev->ih_list.slot_size;
}

Чтобы все это хоть как-то заработало нам понадобится зачаток файловой системы. На первых порах, файлы у нас будут только для символьных устройств.

Т.е. открытие файла будет эквивалентно созданию структуры FILE из stdio для соответствующего файлу драйвера.

При этом, имена файлов будут совпадать с именем устройств. Определим понятие файлового дескриптора в нашей библиотеки C (stdio.h).

struct io_buf_t {
    int fd; /* file descriptor */
    char* base; /* buffer beginning */
    char* ptr; /* position in buffer */
    bool is_eof; /* whether end of file */
    void* file; /* file definition */
};

#define FILE struct io_buf_t

Для простоты пусть пока все открытые файлы будут храниться в кольцевом списке. Элемент списка опишем так:

extern struct file_t {
    struct clist_head_t list_head; /* should be at first */
    struct io_buf_t io_buf; /* file handler */
    char name[8]; /* file name */
    int mod_rw; /* whether read or write */
    struct dev_t* dev; /* whether device driver */
};

Для каждого открытого файла мы будем хранить ссылку на устройство. Реализуем кольцевой список открытых файлов и реализуем системные вызовы read/write/ioctl.

При открытии файла нам нужно просто структуре io_buf_t присвоить начальные позиции буферов чтения и записи из драйвера, ну и соответственно связать файловые операции с драйвером устройства.

extern struct io_buf_t* file_open(char* path, int mod_rw)
{
    struct clist_head_t* entry;
    struct file_t* file;
    struct dev_t* dev;

    /* try to find already opened file */
    entry = clist_find(&file_list, file_list_by_name_detector, path, mod_rw);
    file = (struct file_t*)entry->data;
    if (entry != null) {
        return &file->io_buf;
    }

    /* create list entry */
    entry = clist_insert_entry_after(&file_list, file_list.head);
    file = (struct file_t*)entry->data;

    /* whether file is device */
    dev = dev_find_by_name(path);
    if (dev != null) {
        /* device */
        file->dev = dev;

        if (mod_rw == MOD_R) {
            file->io_buf.base = dev->base_r;
        } else if (mod_rw == MOD_W) {
            file->io_buf.base = dev->base_w;
        }
    } else {
        /* fs node */
        file->dev = null;
        unreachable(); /* fs in not implemented yet */
    }

    /* fill data */
    file->mod_rw = mod_rw;
    file->io_buf.fd = next_fd++;
    file->io_buf.ptr = file->io_buf.base;
    file->io_buf.is_eof = false;
    file->io_buf.file = file;
    strncpy(file->name, path, sizeof(file->name));

    return &file->io_buf;
}

Файловые операции read/write/ioctl определим по одному шаблону на примере системного вызова read.

Сами системные вызовы которые мы научились писать в прошлом уроке будут просто вызывать эти функции.

extern size_t file_read(struct io_buf_t* io_buf, char* buff, u_int size)
{
    struct file_t* file;

    file = (struct file_t*)io_buf->file;

    /* whether file is device */
    if (file->dev != null) {
        /* device */
        return file->dev->read_cb(&file->io_buf, buff, size);
    } else {
        /* fs node */
        unreachable(); /* fs in not implemented yet */
    }

    return 0;
}

Короче говоря они просто будут дергать коллбэки из определения устройства. Теперь напишем драйвер терминала.

Драйвер терминала


Нам понадобится буфер вывода на экран и буфер ввода с клавиатуры, а также пару флагов для режимов ввода и вывода.

static const char* tty_dev_name = TTY_DEV_NAME; /* teletype device name */
static char tty_output_buff[VIDEO_SCREEN_SIZE]; /* teletype output buffer */
static char tty_input_buff[VIDEO_SCREEN_WIDTH]; /* teletype input buffer */
char* tty_output_buff_ptr = tty_output_buff;
char* tty_input_buff_ptr = tty_input_buff;
bool read_line_mode = false; /* whether read only whole line */
bool is_echo = false; /* whether to put readed symbol to stdout */

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

extern void tty_init()
{
    struct clist_head_t* entry;
    struct dev_t dev;
    struct ih_low_t* ih_low;

    memset(tty_output_buff, 0, sizeof(VIDEO_SCREEN_SIZE));
    memset(tty_input_buff, 0, sizeof(VIDEO_SCREEN_WIDTH));

    /* register teletype device */
    strcpy(dev.name, tty_dev_name);
    dev.base_r = tty_input_buff;
    dev.base_w = tty_output_buff;
    dev.read_cb = tty_read;
    dev.write_cb = tty_write;
    dev.ioctl_cb = tty_ioctl;
    dev.ih_list.head = null;

    /* add interrupt handlers */
    dev.ih_list.slot_size = sizeof(struct ih_low_t);
    entry = clist_insert_entry_after(&dev.ih_list, dev.ih_list.head);
    ih_low = (struct ih_low_t*)entry->data;
    ih_low->number = INT_KEYBOARD;
    ih_low->handler = tty_keyboard_ih_low;

    dev_register(&dev);
}

Нижний обработчик половин прерываний для клавиатуры определим так:

/*
 * Key press low half handler
 */
static void tty_keyboard_ih_low(int number, struct ih_low_data_t* data)
{
    /* write character to input buffer */
    char* keycode = data->data;
    int index = *keycode;
    assert(index < 128);
    char ch = keyboard_map[index];
    *tty_input_buff_ptr++ = ch;

    if (is_echo && ch != '\n') {
      /* echo character to screen */
      *tty_output_buff_ptr++ = ch;
    }

    /* register deffered execution */
    struct message_t msg;
    msg.type = IPC_MSG_TYPE_DQ_SCHED;
    msg.len = 4;
    *((size_t *)msg.data) = (size_t)tty_keyboard_ih_high;
    ksend(TID_DQ, &msg);
}

Тут мы просто кладем введенный символ в буффер клавиатуры. В конце регистрируем отложенный вызов обработчика верхних половин прерываний клавиатуры. Делается это посылкой сообщения (IPC) потоку ядра.

Сам же поток ядра довольно прост:

/*
 * Deferred queue execution scheduler
 * This task running in kernel mode
 */
void dq_task() {
    struct message_t msg;

    for (;;) {
        kreceive(TID_DQ, &msg);

        switch (msg.type) {
            case IPC_MSG_TYPE_DQ_SCHED:
                /* do deffered callback execution */
                assert(msg.len == 4);
                dq_handler_t handler = (dq_handler_t)*((size_t*)msg.data);
                assert((size_t)handler < KERNEL_CODE_END_ADDR);
                printf(MSG_DQ_SCHED, handler);
                handler(msg);
                break;
        }
    }

    exit(0);
}

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

/*
 * Key press high half handler
 */
static void tty_keyboard_ih_high(struct message_t *msg)
{
    video_flush(tty_output_buff);
}

Теперь осталось написать сами функции ввода-вывода, вызываемые из файловых операций.

/*
 * Read line from tty to string
 */
static u_int tty_read(struct io_buf_t* io_buf, void* buffer, u_int size)
{
    char* ptr = buffer;

    assert((size_t)io_buf->ptr <= (size_t)tty_input_buff_ptr);
    assert((size_t)tty_input_buff_ptr >= (size_t)tty_input_buff);
    assert(size > 0);

    io_buf->is_eof = (size_t)io_buf->ptr == (size_t)tty_input_buff_ptr;
    if (read_line_mode) {
        io_buf->is_eof = !strchr(io_buf->ptr, '\n');
    }

    for (int i = 0; i < size - 1 && !io_buf->is_eof; ++i) {
        char ch = tty_read_ch(io_buf);
        *ptr++ = ch;

        if (read_line_mode && ch == '\n') {
            break;
        }
    }

    return (size_t)ptr - (size_t)buffer;
}

/*
 * Write to tty
 */
static void tty_write(struct io_buf_t* io_buf, void* data, u_int size)
{
    char* ptr = data;

    for (int i = 0; i < size && !io_buf->is_eof; ++i) {
        tty_write_ch(io_buf, *ptr++);
    }
}

Посимвольные операции не сильно сложнее и в комментировании думаю не нуждаются.

/*
 * Write single character to tty
 */
static void tty_write_ch(struct io_buf_t* io_buf, char ch)
{
    if ((size_t)tty_output_buff_ptr - (size_t)tty_output_buff + 1 < VIDEO_SCREEN_SIZE) {
        if (ch != '\n') {
            /* regular character */
            *tty_output_buff_ptr++ = ch;
        } else {
            /* new line character */
            int line_pos = ((size_t)tty_output_buff_ptr - (size_t)tty_output_buff) % VIDEO_SCREEN_WIDTH;
            for (int j = 0; j < VIDEO_SCREEN_WIDTH - line_pos; ++j) {
                *tty_output_buff_ptr++ = ' ';
            }
        }
    } else {
        tty_output_buff_ptr = video_scroll(tty_output_buff, tty_output_buff_ptr);
        tty_write_ch(io_buf, ch);
    }

    io_buf->ptr = tty_output_buff_ptr;
}

/*
 * Read single character from tty
 */
static char tty_read_ch(struct io_buf_t* io_buf)
{
    if ((size_t)io_buf->ptr < (size_t)tty_input_buff_ptr) {
        return *io_buf->ptr++;
    } else {
        io_buf->is_eof = true;
        return '\0';
    }
}

Осталось только для управления режимами ввода и вывода реализовать ioctl.

/*
 * Teletype specific command
 */
static void tty_ioctl(struct io_buf_t* io_buf, int command)
{
    char* hello_msg = MSG_KERNEL_NAME;

    switch (command) {
    case IOCTL_INIT: /* prepare video device */
        if (io_buf->base == tty_output_buff) {
            kmode(false); /* detach syslog from screen */
            tty_output_buff_ptr = video_clear(io_buf->base);
            io_buf->ptr = tty_output_buff_ptr;
            tty_write(io_buf, hello_msg, strlen(hello_msg));
            video_flush(io_buf->base);
            io_buf->ptr = tty_output_buff_ptr;
        } else if (io_buf->base == tty_input_buff) {
            unreachable();
        }
        break;
    case IOCTL_CLEAR:
        if (io_buf->base == tty_output_buff) {
            /* fill output buffer with spaces */
            tty_output_buff_ptr = video_clear(io_buf->base);
            video_flush(io_buf->base);
            io_buf->ptr = tty_output_buff_ptr;
        } else if (io_buf->base == tty_input_buff) {
            /* clear input buffer */
            tty_input_buff_ptr = tty_input_buff;
            io_buf->ptr = io_buf->base;
            io_buf->is_eof = true;
        }
        break;
    case IOCTL_FLUSH: /* flush buffer to screen */
        if (io_buf->base == tty_output_buff) {
            video_flush(io_buf->base);
        } else if (io_buf->base == tty_input_buff) {
            unreachable();
        }
        break;
    case IOCTL_READ_MODE_LINE: /* read only whole line */
        if (io_buf->base == tty_input_buff) {
            read_line_mode = true;
        } else if (io_buf->base == tty_output_buff) {
            unreachable();
        }
        break;
    case IOCTL_READ_MODE_ECHO: /* put readed symbol to stdout */
        if (io_buf->base == tty_input_buff) {
            is_echo = true;
        } else if (io_buf->base == tty_output_buff) {
            unreachable();
        }
        break;
    default:
        unreachable();
    }
}

Теперь реализуем файловый ввод вывод на уровне нашей библиотеки С.

/*
 * Api - Open file
 */
extern FILE* fopen(const char* file, int mod_rw)
{
    FILE* result = null;
    asm_syscall(SYSCALL_OPEN, file, mod_rw, &result);
    return result;
}

/*
 * Api - Close file
 */
extern void fclose(FILE* file)
{
    asm_syscall(SYSCALL_CLOSE, file);
}

/*
 * Api - Read from file to buffer
 */
extern u_int fread(FILE* file, char* buff, u_int size)
{
    return asm_syscall(SYSCALL_READ, file, buff, size);
}

/*
 * Api - Write data to file
 */
extern void fwrite(FILE* file, const char* data, u_int size)
{
    asm_syscall(SYSCALL_WRITE, file, data, size);
}

Ну и немного высокоуровневых функций приведу тут:

/*
 * Api - Print user message
 */
extern void uvnprintf(const char* format, u_int n, va_list list)
{
    char buff[VIDEO_SCREEN_WIDTH];
    vsnprintf(buff, n, format, list);
    uputs(buff);
}

/*
 * Api - Read from file to string
 */
extern void uscanf(char* buff, ...)
{
    u_int readed = 0;
    do {
        readed = fread(stdin, buff, 255);
    } while (readed == 0);
    buff[readed - 1] = '\0'; /* erase new line character */
    uprintf("\n");
    uflush();
}

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

На этом все. Теперь ты можешь смело прикручивать драйвера к своему ядру!

Ссылки


Смотри видеоурок для дополнительной информации.

> Исходный код в git репозиторий (тебе нужна ветка lesson8)

Список литературы


  1. James Molloy. Roll your own toy UNIX-clone OS.
  2. Зубков. Ассемблер для DOS, Windows, Unix
  3. Калашников. Ассемблер — это просто!
  4. Таненбаум. Операционные системы. Реализация и разработка.
  5. Роберт Лав. Ядро Linux. Описание процесса разработки.

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


  1. Komei
    24.09.2019 00:57
    +1

    Почему вам не нравятся видеоуроки?
    Напишу свой вариант ответа: потому что читать удобнее. Вот удобнее и всё тут.


  1. assembled
    25.09.2019 10:19

    entry = clist_find(&file_list, file_list_by_name_detector, path, mod_rw);
        file = (struct file_t*)entry->data;
        if (entry != null) {
            return &file->io_buf;
        }
    Вы обращаетесь к данным по указателю до проверки на null.

    Почему вам не нравятся видеоуроки?

    Кмк, формат статьи более удобен, я могу свободно перемещаться по статье глазами и скроллить, если я вдруг не въехал, я могу просто перечитать предложение еще раз, без всяких лишних действий, вдобавок в случае кода его можно без проблем копипастить.
    Заголовок спойлера
    В добавок я не очень люблю людей и не собираюсь слушать чей-то там бубнеж. Извините.


  1. assembled
    25.09.2019 10:53

    #define FILE struct io_buf_t

    Никогда не понимал, зачем использовать #define вместо typedef?