В предыдущей статье мы ввели многозадачность. Сегодня пришло время рассмотреть тему драйверов символьных устройств.
Конкретно сегодня мы напишем драйвер терминала, механизм отложенной обработки прерываний, рассмотрим тему обработчиков верхних и нижних половин прерываний.
Начнем с создания структуры устройства, затем введем базовую поддержку файлового ввода-вывода, рассмотрим структуру 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 определение устройства выглядело так:
Основной смысл в том, чтобы назначить устройству реализации функций файлового ввода-вывода.
Мы обойдемся одной структурой, но смысл будет похожий:
Каждому устройству соответствует половин список прерываний, вызываемых в момент генерации прерываний.
В Linux такие половины называются верхними, у нас наоборот, нижними (более низкий уровень).
Лично мне это показалось более логичным и я ненароком запомнил термины наоборот. Каждый элемент списка нижних половин прерываний опишем так:
При инициализации драйвер будет регистрировать свое устройство через функцию dev_register, другими словами добавлять новое устройство в кольцевой список:
Чтобы все это хоть как-то заработало нам понадобится зачаток файловой системы. На первых порах, файлы у нас будут только для символьных устройств.
Т.е. открытие файла будет эквивалентно созданию структуры FILE из stdio для соответствующего файлу драйвера.
При этом, имена файлов будут совпадать с именем устройств. Определим понятие файлового дескриптора в нашей библиотеки C (stdio.h).
Для простоты пусть пока все открытые файлы будут храниться в кольцевом списке. Элемент списка опишем так:
Для каждого открытого файла мы будем хранить ссылку на устройство. Реализуем кольцевой список открытых файлов и реализуем системные вызовы read/write/ioctl.
При открытии файла нам нужно просто структуре io_buf_t присвоить начальные позиции буферов чтения и записи из драйвера, ну и соответственно связать файловые операции с драйвером устройства.
Файловые операции read/write/ioctl определим по одному шаблону на примере системного вызова read.
Сами системные вызовы которые мы научились писать в прошлом уроке будут просто вызывать эти функции.
Короче говоря они просто будут дергать коллбэки из определения устройства. Теперь напишем драйвер терминала.
Нам понадобится буфер вывода на экран и буфер ввода с клавиатуры, а также пару флагов для режимов ввода и вывода.
Напишем функцию создания устройства. Она просто проставляет коллбэки файловых операций и обработчики нижних половин прерываний, после чего регистрирует устройство в кольцевом списке.
Нижний обработчик половин прерываний для клавиатуры определим так:
Тут мы просто кладем введенный символ в буффер к��авиатуры. В конце регистрируем отложенный вызов обработчика верхних половин прерываний клавиатуры. Делается это посылкой сообщения (IPC) потоку ядра.
Сам же поток ядра довольно прост:
С помощью него будет вызываться обработчик верхних половин прерываний клавиатуры. Его целью является дублирование символа на экран через копирование буфера вывода в видеопамять.
Теперь осталось написать сами функции ввода-вывода, вызываемые из файловых операций.
Посимвольные операции не сильно сложнее и в комментировании думаю не нуждаются.
Осталось только для управления режимами ввода и вывода реализовать ioctl.
Теперь реализуем файловый ввод вывод на уровне нашей библиотеки С.
Ну и немного высокоуровневых функций приведу тут:
Чтобы пока не морочиться с форматным чтением, будем всегда просто читать в строку как будто дан флаг %s. Мне было лень вводить новый статус задачи для ожидания файловых дескрипторов, поэтому просто в бесконечном цикле пытаемся считать что-нибудь пока нам это не удасться.
На этом все. Теперь ты можешь смело прикручивать драйвера к своему ядру!
Смотри видеоурок для дополнительной информации.
→ Исходный код в git репозиторий (тебе нужна ветка lesson8)
Конкретно сегодня мы напишем драйвер терминала, механизм отложенной обработки прерываний, рассмотрим тему обработчиков верхних и нижних половин прерываний.
Начнем с создания структуры устройства, затем введем базовую поддержку файлового ввода-вывода, рассмотрим структуру 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)
Список литературы
- James Molloy. Roll your own toy UNIX-clone OS.
- Зубков. Ассемблер для DOS, Windows, Unix
- Калашников. Ассемблер — это просто!
- Таненбаум. Операционные системы. Реализация и разработка.
- Роберт Лав. Ядро Linux. Описание процесса разработки.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Почему вам не нравятся видеоуроки?
25%Трудно понять область кода, которая разбирается3
41.67%Не успеваю читать код с экрана5
33.33%Кто это сказал? Все норм.4
Проголосовали 12 пользователей. Воздержались 18 пользователей.
