No artigo anterior, apresentamos a multitarefa. Hoje é hora de considerar o tópico dos drivers de dispositivo de caracteres.
Especificamente, hoje escreveremos um driver de terminal, um mecanismo para processamento diferido de interrupções, e consideraremos o tópico de manipuladores para as metades superior e inferior das interrupções.
Começamos criando uma estrutura de dispositivo, depois introduzimos o suporte básico de E / S de arquivo, considere a estrutura e as funções io_buf para trabalhar com arquivos de stdio.h.
Sumário
Construa o sistema (marca, gcc, gás). Inicialização inicial (inicialização múltipla). Iniciar (qemu). Biblioteca C (strcpy, memcpy, strext). Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Construindo a biblioteca no modo kernel e no modo de aplicativo do usuário. O log do sistema do kernel. Memória de vídeo Saída para o terminal (kprintf, kpanic, kassert). Memória dinâmica, heap (kmalloc, kfree). Organização da memória e manipulação de interrupções (GDT, IDT, PIC, syscall). Exceções Memória virtual (diretório e tabela de páginas). Processo. Planejador Multitarefa. Chamadas do sistema (interrupção, saída, ps).
Drivers de dispositivo de caracteres. Chamadas do sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).O sistema de arquivos do kernel (initrd), elf e seus internos. Chamadas do sistema (exec). Shell como um programa completo para o kernel. Modo de proteção do usuário (anel3). Segmento de status da tarefa (tss).
Drivers de caracteres
Tudo começa com a aparência de um dispositivo simbólico. Como você se lembra, em Linux Device Drivers, a definição do dispositivo era assim:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops;
O ponto principal é atribuir ao dispositivo a implementação das funções de E / S do arquivo.
Vamos nos dar bem com uma estrutura, mas o significado será semelhante:
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; };
Cada dispositivo corresponde a metade da lista de interrupções chamadas quando as interrupções são geradas.
No Linux, essas metades são chamadas superior, pelo contrário, inferior (nível inferior).
Pessoalmente, parecia mais lógico para mim e acidentalmente me lembrei dos termos do contrário. Descrevemos cada elemento da lista de metades inferiores de interrupções da seguinte forma:
extern struct ih_low_t { struct clist_head_t list_head; int number; ih_low_cb_t handler; };
Após a inicialização, o driver registrará seu dispositivo através da função dev_register, ou seja, adicione um novo dispositivo à lista de toques:
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; }
Para que tudo isso funcione de alguma forma, precisamos do rudimento do sistema de arquivos. Inicialmente, teremos apenas arquivos para dispositivos de caracteres.
I.e. abrir o arquivo será equivalente a criar uma estrutura FILE a partir do stdio para o arquivo de driver correspondente.
Nesse caso, os nomes dos arquivos corresponderão ao nome do dispositivo. Definimos o conceito de um descritor de arquivo em nossa biblioteca C (stdio.h).
struct io_buf_t { int fd; char* base; char* ptr; bool is_eof; void* file; }; #define FILE struct io_buf_t
Por uma questão de simplicidade, permita que todos os arquivos abertos sejam armazenados em uma lista de toque por enquanto. O item da lista é descrito da seguinte maneira:
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; };
Para cada arquivo aberto, armazenaremos um link para o dispositivo. Implementamos uma lista circular de arquivos abertos e implementamos as chamadas de sistema de leitura / gravação / ioctl.
Ao abrir um arquivo, basta atribuir as posições iniciais dos buffers de leitura e gravação do driver à estrutura io_buf_t e, consequentemente, associar operações de arquivo ao driver de dispositivo.
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; }
As operações de arquivo read / write / ioctl são definidas por um padrão, usando a chamada de leitura do sistema como exemplo.
As chamadas de sistema que aprendemos a escrever na última lição simplesmente chamarão essas funções.
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; }
Em resumo, eles simplesmente receberão retornos de chamada da definição do dispositivo. Agora vamos escrever o driver do terminal.
Driver de terminal
Precisamos de um buffer de saída de tela e de entrada do teclado, além de alguns sinalizadores para os modos de entrada e saída.
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;
Escrevemos a função de criar um dispositivo. Simplesmente coloca retornos de chamada de operações de arquivo e manipuladores das metades inferiores de interrupções e depois registra o dispositivo em uma lista de toques.
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); }
O manipulador de interrupção inferior para o teclado é definido da seguinte maneira:
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); }
Aqui simplesmente colocamos o caractere inserido no buffer do teclado. No final, registramos a chamada adiada do processador das metades superiores das interrupções do teclado. Isso é feito enviando uma mensagem (IPC) para o thread do kernel.
O próprio thread do kernel é bem simples:
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); }
Utilizando-o, será chamado o manipulador das metades superiores da interrupção do teclado. Seu objetivo é duplicar um caractere na tela, copiando o buffer de saída para a memória de vídeo.
static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); }
Agora resta gravar as próprias funções de E / S, chamadas a partir de operações de arquivo.
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++); } }
As operações caractere por caractere não são muito mais complicadas e não acho que elas precisem ser comentadas.
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'; } }
Resta apenas controlar os modos de entrada e saída para implementar o 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(); } }
Agora, implementamos a saída de entrada de arquivo no nível da nossa biblioteca 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); }
Bem, aqui estão algumas funções de alto nível:
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(); }
Para não nos enganarmos com a leitura de formatos até o momento, sempre leremos a linha como se a bandeira% s tivesse sido dada. Eu estava com preguiça de introduzir um novo status de tarefa para aguardar os descritores de arquivo, por isso tentamos ler algo em um loop infinito até conseguirmos.
Só isso. Agora você pode fixar com segurança os drivers no seu kernel!
Referências
Assista ao
tutorial em vídeo para obter mais informações.
→ Código fonte
no repositório git (você precisa da lição 8)
Referências
- James Molloy. Role seu próprio sistema operacional clone do UNIX de brinquedo.
- Zubkov. Assembler para DOS, Windows, Unix
- Kalashnikov. Assembler é fácil!
- Tanenbaum. Sistemas operacionais. Implementação e desenvolvimento.
- Robert Love. Kernel Linux Descrição do processo de desenvolvimento.