在上一篇文章中,我们介绍了多任务处理。 今天是时候考虑字符设备驱动程序主题了。
具体来说,今天,我们将编写一个终端驱动程序,这是一种用于延迟处理中断的机制,并考虑上半部分和下半部分中断的处理程序主题。
我们首先创建一个设备结构,然后介绍基本的文件I / O支持,考虑io_buf结构和用于处理stdio.h中文件的功能。
目录
构建系统(make,gcc,gas)。 初始引导(多次引导)。 启动(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及其内部文件系统。 系统调用(执行)。 Shell作为内核的完整程序。 用户保护模式(ring3)。 任务状态段(tss)。
角色驱动
这一切都始于符号设备的出现。 您还记得,在Linux设备驱动程序中,设备定义如下所示:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops;
要点是为设备分配文件I / O功能的实现。
我们将采用一种结构,但是含义将相似:
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; };
每个设备对应于生成中断时调用的中断列表的一半。
在Linux中,这样的一半称为上半部分,相反,称为下半部分(下半部分)。
就我个人而言,这似乎更加合乎逻辑,而我无意中又想起了这些术语。 我们将下半部分中断列表中的每个元素描述为
extern struct ih_low_t { struct clist_head_t list_head; int number; ih_low_cb_t handler; };
初始化后,驱动程序将通过dev_register函数注册其设备,换句话说,将新设备添加到环列表:
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; }
为了使所有这些都能正常工作,我们需要文件系统的基础。 首先,我们将只包含用于字符设备的文件。
即 打开文件等同于从stdio为相应的驱动程序文件创建FILE结构。
在这种情况下,文件名将与设备名匹配。 我们在C库(stdio.h)中定义文件描述符的概念。
struct io_buf_t { int fd; char* base; char* ptr; bool is_eof; void* file; }; #define FILE struct io_buf_t
为了简单起见,现在让所有打开的文件都存储在铃声列表中。 列表项描述如下:
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; };
对于每个打开的文件,我们将存储指向设备的链接。 我们实现了一个打开文件的列表,并实现了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; 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; }
以读取系统调用为例,通过一种模式定义文件读/写/ ioctl操作。
我们在上一课中学到的系统调用将仅调用这些函数。
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; }
简而言之,它们将仅从设备定义中提取回调。 现在我们将编写终端驱动程序。
终端驱动
我们需要一个屏幕输出缓冲区和键盘输入缓冲区,以及几个用于输入和输出模式的标志。
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;
我们编写了创建设备的功能。 它只是放下文件操作和下半部分中断的处理程序的回调,然后在环列表中注册该设备。
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); }
键盘的下部中断处理程序定义如下:
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); }
在这里,我们只是将输入的字符放在键盘缓冲区中。 最后,我们注册键盘中断上半部分的处理器的延迟调用。 这是通过将消息(IPC)发送到内核线程来完成的。
内核线程本身非常简单:
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); }
使用它,将调用键盘中断的上半部分的处理程序。 其目的是通过将输出缓冲区复制到视频存储器来在屏幕上复制字符。
static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); }
现在剩下的工作就是自己编写I / O函数,从文件操作中调用。
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++); } }
逐个字符的操作并不复杂,我认为它们不需要注释。
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'; } }
仅需控制输入和输出模式即可实现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(); } }
现在,我们在库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); }
好吧,这里有一些高级功能:
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(); }
为了避免到目前为止对格式读取的痴迷,我们将始终只是读入该行,就像给出了%s标志一样。 我懒得介绍一个新的任务状态来等待文件描述符,所以我们只是尝试无限循环地读取某些内容,直到成功。
仅此而已。 现在,您可以安全地将驱动程序固定到内核了!
参考文献
观看
视频教程以获取更多信息。
→
git存储库中的源代码(您需要lesson8分支)
参考文献
- 詹姆斯·莫洛伊(James Molloy)。 滚动自己的玩具UNIX克隆操作系统。
- 祖布科夫。 DOS,Windows,Unix的汇编器
- 卡拉什尼科夫。 汇编程序很简单!
- Tanenbaum。 操作系统。 实施与开发。
- 罗伯特·洛夫(Robert Love)。 Linux内核 开发过程的描述。