Desenvolvimento de sistema operacional Unix-like - Drivers de dispositivos de caracteres (8)

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; /* 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 */ }; 

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; /* should be at first */ int number; /* interrupt number */ ih_low_cb_t handler; /* interrupt 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; /* 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; } 

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; /* 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 

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; /* 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 */ }; 

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; /* 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; } 

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; /* 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; } 

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; /* 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 */ 

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)); /* 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); } 

O manipulador de interrupção inferior para o teclado é definido da seguinte maneira:

 /* * 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); } 

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:

 /* * 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); } 

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.

 /* * Key press high half handler */ 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.

 /* * 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++); } } 

As operações caractere por caractere não são muito mais complicadas e não acho que elas precisem ser comentadas.

 /* * 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'; } } 

Resta apenas controlar os modos de entrada e saída para implementar o 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(); } } 

Agora, implementamos a saída de entrada de arquivo no nível da nossa biblioteca C.

 /* * 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); } 

Bem, aqui estão algumas funções de alto nível:

 /* * 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(); } 

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


  1. James Molloy. Role seu próprio sistema operacional clone do UNIX de brinquedo.
  2. Zubkov. Assembler para DOS, Windows, Unix
  3. Kalashnikov. Assembler é fácil!
  4. Tanenbaum. Sistemas operacionais. Implementação e desenvolvimento.
  5. Robert Love. Kernel Linux Descrição do processo de desenvolvimento.

Source: https://habr.com/ru/post/pt468509/


All Articles