In the previous article, we introduced multitasking. Today it's time to consider the topic of character device drivers.
Specifically, today we will write a terminal driver, a mechanism for deferred processing of interrupts, and consider the topic of handlers for the upper and lower halves of interrupts.
We start by creating a device structure, then introduce basic file I / O support, consider the io_buf structure and functions for working with files from stdio.h.
Table of contents
Build system (make, gcc, gas). Initial boot (multiboot). Launch (qemu). C library (strcpy, memcpy, strext). C library (sprintf, strcpy, strcmp, strtok, va_list ...). Building the library in kernel mode and user application mode. The kernel system log. Video memory Output to the terminal (kprintf, kpanic, kassert). Dynamic memory, heap (kmalloc, kfree). Organization of memory and interrupt handling (GDT, IDT, PIC, syscall). Exceptions Virtual memory (page directory and page table). Process. Scheduler. Multitasking. System calls (kill, exit, ps).
Character device drivers. System calls (ioctl, fopen, fread, fwrite). C library (fopen, fclose, fprintf, fscanf).
The file system of the kernel (initrd), elf and its internals. System calls (exec). Shell as a complete program for the kernel. User protection mode (ring3). Task state segment (tss).
Character Device Drivers
It all starts with the appearance of the symbolic device. As you recall, in Linux Device Drivers, the device definition looked like this:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops;
The main point is to assign the device the implementation of file I / O functions.
We will get by with one structure, but the meaning will be similar:
extern struct dev_t { struct clist_head_t list_head; char name[8]; void* base_r; void* base_w; dev_read_cb_t read_cb; dev_write_cb_t write_cb; dev_ioctl_cb_t ioctl_cb; struct clist_definition_t ih_list; };
Each device corresponds to half the list of interrupts that are called when interrupts are generated.
In Linux, such halves are called upper, on the contrary, lower (lower level).
Personally, it seemed more logical to me and I accidentally remembered the terms the other way around. We describe each element of the list of lower halves of interruptions as
extern struct ih_low_t { struct clist_head_t list_head; int number; ih_low_cb_t handler; };
Upon initialization, the driver will register its device through the dev_register function, in other words add a new device to the ring list:
extern void dev_register(struct dev_t* dev) { struct clist_head_t* entry; struct dev_t* device; entry = clist_insert_entry_after(&dev_list, dev_list.head); device = (struct dev_t*)entry->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; }
For all this to work somehow, we need the rudiment of the file system. At first, we will only have files for character devices.
Those. opening the file will be equivalent to creating a FILE structure from stdio for the corresponding driver file.
In this case, the file names will match the device name. We define the concept of a file descriptor in our C library (stdio.h).
struct io_buf_t { int fd; char* base; char* ptr; bool is_eof; void* file; }; #define FILE struct io_buf_t
For simplicity, let all open files be stored in a ring list for now. The list item is described as follows:
extern struct file_t { struct clist_head_t list_head; struct io_buf_t io_buf; char name[8]; int mod_rw; struct dev_t* dev; };
For each open file, we will store a link to the device. We implement a ring list of open files and implement read / write / ioctl system calls.
When opening the file, we just need to assign the initial positions of the read and write buffers from the driver to the io_buf_t structure, and, accordingly, associate file operations with the device driver.
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; 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; } entry = clist_insert_entry_after(&file_list, file_list.head); file = (struct file_t*)entry->data; dev = dev_find_by_name(path); if (dev != null) { 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 { file->dev = null; unreachable(); } 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; }
The file operations read / write / ioctl are defined by one pattern using the read system call as an example.
The very system calls that we learned to write in the last lesson will simply call these functions.
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; if (file->dev != null) { return file->dev->read_cb(&file->io_buf, buff, size); } else { unreachable(); } return 0; }
In short, they will simply pull callbacks from the device definition. Now we will write the terminal driver.
Terminal driver
We need a screen output buffer and keyboard input buffer, as well as a couple of flags for input and output modes.
static const char* tty_dev_name = TTY_DEV_NAME; static char tty_output_buff[VIDEO_SCREEN_SIZE]; static char tty_input_buff[VIDEO_SCREEN_WIDTH]; char* tty_output_buff_ptr = tty_output_buff; char* tty_input_buff_ptr = tty_input_buff; bool read_line_mode = false; bool is_echo = false;
We write the function of creating a device. It simply puts down callbacks of file operations and handlers of the lower halves of interruptions, after which it registers the device in a ring list.
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)); 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; 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); }
The lower half-interrupt handler for the keyboard is defined as follows:
static void tty_keyboard_ih_low(int number, struct ih_low_data_t* data) { 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') { *tty_output_buff_ptr++ = ch; } 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); }
Here we just put the entered character in the keyboard buffer. At the end, we register the deferred call of the processor of the upper halves of the keyboard interruptions. This is done by sending a message (IPC) to the kernel thread.
The kernel thread itself is pretty simple:
void dq_task() { struct message_t msg; for (;;) { kreceive(TID_DQ, &msg); switch (msg.type) { case IPC_MSG_TYPE_DQ_SCHED: 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); }
Using it, the handler of the upper halves of the keyboard interrupt will be called. Its purpose is to duplicate the character on the screen by copying the output buffer to the video memory.
static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); }
Now it remains to write the I / O functions themselves, called from file operations.
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; } 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++); } }
Character-by-character operations are not much more complicated and I donβt think they need commenting.
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') { *tty_output_buff_ptr++ = ch; } else { 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; } 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'; } }
It remains only to control the input and output modes to implement ioctl.
static void tty_ioctl(struct io_buf_t* io_buf, int command) { char* hello_msg = MSG_KERNEL_NAME; switch (command) { case IOCTL_INIT: if (io_buf->base == tty_output_buff) { kmode(false); 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) { 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) { tty_input_buff_ptr = tty_input_buff; io_buf->ptr = io_buf->base; io_buf->is_eof = true; } break; case IOCTL_FLUSH: 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: 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: if (io_buf->base == tty_input_buff) { is_echo = true; } else if (io_buf->base == tty_output_buff) { unreachable(); } break; default: unreachable(); } }
Now we implement file input output at the level of our library C.
extern FILE* fopen(const char* file, int mod_rw) { FILE* result = null; asm_syscall(SYSCALL_OPEN, file, mod_rw, &result); return result; } extern void fclose(FILE* file) { asm_syscall(SYSCALL_CLOSE, file); } extern u_int fread(FILE* file, char* buff, u_int size) { return asm_syscall(SYSCALL_READ, file, buff, size); } extern void fwrite(FILE* file, const char* data, u_int size) { asm_syscall(SYSCALL_WRITE, file, data, size); }
Well, here are a few high-level functions:
extern void uvnprintf(const char* format, u_int n, va_list list) { char buff[VIDEO_SCREEN_WIDTH]; vsnprintf(buff, n, format, list); uputs(buff); } extern void uscanf(char* buff, ...) { u_int readed = 0; do { readed = fread(stdin, buff, 255); } while (readed == 0); buff[readed - 1] = '\0'; uprintf("\n"); uflush(); }
In order not to fool with format reading so far, we will always just read into the line as if the% s flag was given. I was too lazy to introduce a new task status to wait for file descriptors, so we just try to read something in an endless loop until we succeed.
That's all. Now you can safely fasten drivers to your kernel!
References
Watch the
video tutorial for more information.
β Source code
in git repository (you need lesson8 branch)
Bibliography
- James Molloy. Roll your own toy UNIX-clone OS.
- Zubkov. Assembler for DOS, Windows, Unix
- Kalashnikov. Assembler is easy!
- Tanenbaum. Operating Systems. Implementation and development.
- Robert Love. Linux kernel Description of the development process.