Red de conocimiento informático - Conocimiento informático - Cómo escribir un módulo simple del kernel de Linux y un controlador de dispositivo

Cómo escribir un módulo simple del kernel de Linux y un controlador de dispositivo

Cómo escribir un controlador de dispositivo Linux

Mirando hacia atrás, he estado aprendiendo el sistema operativo Linux durante casi un año. Lo he aprendido poco a poco, así que debería intentarlo. eso. Lo que escribí. ¡Se puede considerar que te deja algunos recuerdos y recuerdos! Como soy completamente autodidacta, si el siguiente contenido no es apropiado, por favor dame tu consejo.

Linux es una variante del sistema operativo Unix. Los principios e ideas para escribir controladores en Linux son completamente similares a los de otros sistemas Unix, pero los controladores en entornos DOS o Windows son muy diferentes. Diseñar un controlador en un entorno Linux es de idea simple, fácil de operar y potente en función. Sin embargo, tiene pocas funciones compatibles y solo puede depender de funciones en el kernel. Algunas operaciones comunes deben escribirse usted mismo y la depuración es necesaria. inconveniente.

El siguiente texto proviene principalmente de khg, el controlador de dispositivo Write Linux de Johnsonm, la Guía de ensamblaje en línea de Brennan, The Linux a-z y alguna información sobre el controlador de dispositivo en Tsinghua bbs.

1. El concepto de controlador de dispositivo Linux

La llamada al sistema es la interfaz entre el kernel del sistema operativo y el programa de aplicación, y el controlador de dispositivo es la interfaz entre el kernel del sistema operativo y el hardware de la máquina. El controlador del dispositivo protege los detalles del hardware del programa de aplicación, de modo que desde la perspectiva del programa de aplicación, el dispositivo de hardware es solo un archivo de dispositivo y el programa de aplicación puede operar el dispositivo de hardware como un archivo normal. El controlador del dispositivo es parte del kernel y realiza las siguientes funciones:

1. Inicializar y liberar el dispositivo.

2. Transfiera datos desde el kernel al hardware y lea datos del hardware.

3. Lea los datos enviados por la aplicación al archivo del dispositivo y envíe los datos solicitados por la aplicación.

4. Detectar y manejar errores en los equipos.

Hay tres tipos principales de archivos de dispositivo en el sistema operativo Linux: uno es un dispositivo de caracteres, el otro es un dispositivo de bloque y el tercero es un dispositivo de red. La principal diferencia entre un dispositivo de caracteres y un dispositivo de bloque es que cuando se realiza una solicitud de lectura/escritura a un dispositivo de caracteres, la E/S del hardware real generalmente ocurre inmediatamente. Este no es el caso con un dispositivo de bloque. memoria del sistema como búfer Cuando el usuario Si la solicitud del dispositivo del proceso puede cumplir con los requisitos del usuario, se devolverán los datos solicitados. De lo contrario, se llamará a la función de solicitud para realizar la operación de E/S real. Los dispositivos de bloque están diseñados principalmente para dispositivos lentos, como discos, para evitar perder demasiado tiempo de CPU esperando.

Como se mencionó, el proceso de usuario se ocupa del hardware real a través de archivos de dispositivo. Cada archivo de dispositivo tiene su atributo de archivo (c/b), que indica si es un dispositivo de caracteres o un dispositivo de bloque. Además, cada archivo tiene dos números de dispositivo, el primero es el número de dispositivo principal, que identifica al controlador, y el segundo. Es el número de dispositivo menor, que identifica diferentes dispositivos de hardware que usan el mismo controlador de dispositivo. Por ejemplo, si hay dos disquetes, puede usar el número de dispositivo menor para distinguirlos. El número de dispositivo principal del archivo del dispositivo debe ser coherente con el número de dispositivo principal solicitado por el controlador del dispositivo al registrarse; de ​​lo contrario, el proceso del usuario no podrá acceder al controlador.

Finalmente, debe mencionarse que cuando el proceso del usuario llama al controlador, el sistema ingresa al estado central y ya no es una programación preventiva. En otras palabras, el sistema debe regresar de la subfunción del conductor antes de poder realizar otro trabajo. Si su controlador se atasca en un bucle infinito, desafortunadamente todo lo que tiene que hacer es reiniciar la máquina y luego realizar un fsck largo.

Al leer/escribir, primero verifica el contenido del búfer. Si los datos en el búfer no se han procesado, el contenido se procesa primero.

Cómo escribir un controlador de dispositivo en el sistema operativo Linux

2. Análisis de ejemplo

Escribamos el controlador de dispositivo de caracteres más simple. Aunque no hace nada, le permite comprender cómo funcionan los controladores de dispositivos Linux. Ingrese el siguiente código C en su máquina y obtendrá un controlador de dispositivo real.

#define __NO_VERSION__

#include

#include

char kernel_version [] = UTS_RELEASE;

Esta sección define cierta información de la versión. Aunque no es muy útil, es esencial. Johnsonm dijo que todos los controladores deberían incluir al principio, lo cual generalmente es mejor de usar.

Dado que el proceso del usuario trata con el hardware a través del archivo del dispositivo, el método de operación del archivo del dispositivo no es más que algunas llamadas al sistema, como abrir, leer, escribir, cerrar..., nota, No es fopen, fread, pero ¿Cómo asociar llamadas al sistema con controladores? Esto requiere comprender una estructura de datos muy clave:

struct file_operations

{

int (* buscar) (struct inode *, struct file *, off_t, int);

int (*read) (struct inode *, struct file *, char, int); (* escribir) (struct inode *, struct file *, off_t, int);

int (*readdir) (struct inode *, struct file *, struct dirent *, int);

int (*select) (struct inode *, struct file *, int, select_table *);

int (*ioctl) (struct inode *, struct file *, unsined int, unsigned long)

int (*mmap) (struct inode *, struct file *, struct vm_area_struct *

int (*open) (struct inode *, struct file *); /p >

int (*liberación) (inodo de estructura *, archivo de estructura *);

int (*fsync) (inodo de estructura *, archivo de estructura *); int (*fasync) (inodo de estructura *, archivo de estructura *, int (*check_media_change) (inodo de estructura *, archivo de estructura *); ) ( dev_t dev);

}

El nombre de cada miembro de esta estructura corresponde a una llamada al sistema. Cuando el proceso de usuario utiliza llamadas al sistema para realizar operaciones como lectura/escritura en el archivo del dispositivo, la llamada al sistema encuentra el controlador de dispositivo correspondiente a través del número de dispositivo principal del archivo del dispositivo, luego lee el puntero de función correspondiente de esta estructura de datos y luego transfiere el control a esta función. Este es el principio básico de cómo funcionan los controladores de dispositivos Linux. Dado que este es el caso, el trabajo principal al escribir un controlador de dispositivo es escribir subfunciones y completar los diversos campos de file_operatives.

Comencemos a escribir la subrutina.

#include

#include

#include

p>

#include

#include

#include

unsigned int test_major = 0;

static int read_test(struct inode *node, struct file *file, char *buf, int count)

{

int izquierda;

if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT)

return -EFAULT; for(izquierda = recuento; izquierda > 0; izquierda--)

{

__put_user(1,buf,1);

buf++; >

}

recuento de retornos;

}

Esta función está preparada para llamadas de lectura. Cuando se llama a read, se llama a read_test(), que escribe todos los unos en el búfer del usuario. buf es un parámetro de la llamada de lectura. Es una dirección en el espacio de proceso del usuario. Pero cuando se llama a read_test, el sistema ingresa al estado central. Por lo tanto, no se puede utilizar la dirección buf y se debe utilizar __put_user(). Esta es una función proporcionada por el kernel para transmitir datos al usuario. También hay muchas funciones con funciones similares. Consulte "Diseño e implementación del kernel de Linux" (segunda edición) de Robert. Sin embargo, antes de copiar datos al espacio del usuario, debe verificar que buf esté disponible. Esto utiliza la función verificar_area.

static int write_tibet(struct inode *inode, struct file *file, const char *buf, int count)

{

recuento de retornos; >

}

static int open_tibet(struct inode *inode, struct file *file)

{

MOD_INC_USE_COUNT

; return 0;

}

static void release_tibet(struct inode *inode, struct file *file)

{

MOD_DEC_USE_COUNT <; /p>

}

Todas estas funciones no son operativas. Las llamadas reales no hacen nada cuando ocurren, solo proporcionan punteros de función a las estructuras subyacentes.

estructura file_operaciones test_fops = {

NULL,

lectura_prueba,

escritura_prueba,

NULL, /* test_readdir */

NULL,

NULL, /* test_ioctl */

NULL, /* test_mmap */

open_test,

release_test,

NULL, /* test_fsync */

NULL, /* test_fasync */

/* nada más, rellenar con NULLs */

};

De esta manera, se puede decir que está escrito el cuerpo principal del controlador del dispositivo. Ahora necesitamos integrar el controlador en el kernel. Los controladores se pueden compilar de dos maneras. Una es compilarlo en el kernel y la otra es compilarlo en módulos. Si lo compila en el kernel, aumentará el tamaño del kernel, cambiará el archivo fuente del kernel y no se podrá desinstalar dinámicamente. lo cual no favorece la depuración, por lo que se recomienda utilizar el método del módulo.

int init_module(void)

{

int resultado

resultado = Register_chrdev(0, "prueba", &test_fops);

if (resultado < 0) {

printk(KERN_INFO "prueba: no se puede obtener el número mayor\n");

devolver resultado

p>

p>

}

if (test_major == 0) test_major = resultado; /* dinámico */

return 0; >}

Cuando se utiliza el comando insmod para transferir el módulo compilado a la memoria, se llama a la función init_module. Aquí, init_module solo hace una cosa: registrar un dispositivo de caracteres en la tabla de dispositivos de caracteres del sistema. Register_chrdev requiere tres parámetros. El primer parámetro es el número de dispositivo que desea obtener. Si es cero, el sistema seleccionará un número de dispositivo desocupado y lo devolverá. El segundo parámetro es el nombre del archivo del dispositivo y el tercer parámetro se utiliza para registrar el puntero de la función en la que el controlador realmente realiza la operación.

Si el registro es exitoso, se devuelve el número de dispositivo principal del dispositivo. Si el registro no es exitoso, se devuelve un valor negativo.

void cleanup_module(void)

{

unregister_chrdev(test_major, "prueba"

}

Cuando se usa rmmod para desinstalar el módulo, se llama a la función cleanup_module, que libera la entrada ocupada por la prueba del dispositivo de caracteres en la tabla de dispositivos de caracteres del sistema.

Se puede decir que se escribe un dispositivo de caracteres extremadamente simple y el nombre del archivo se llamará test.c.

Compile a continuación:

$ gcc -O2 -DMODULE -D__KERNEL__ -c test.c

El archivo test.o obtenido es un controlador de dispositivo.

Si el controlador del dispositivo tiene varios archivos, compile cada archivo de acuerdo con la línea de comando anterior y luego

ld -r file1.o file2.o -o modulename.

El controlador ha sido compilado, ahora instálelo en el sistema.

$ insmod –f test.o

Si la instalación se realiza correctamente, puede ver la prueba del dispositivo en el archivo /proc/devices y su número de dispositivo principal. Para desinstalar, ejecute:

$ rmmod test

El siguiente paso es crear el archivo del dispositivo.

mknod /dev/test c major minor

c se refiere al dispositivo del carácter, major es el número de dispositivo principal, que se ve en /proc/devices.

Utilice el comando de shell

$ cat /proc/devices

para obtener el número de dispositivo principal. Puede agregar la línea de comando anterior a su script de shell. .

Menor es el número del dispositivo esclavo, simplemente configúrelo en 0.

Ya podemos acceder a nuestro controlador a través del archivo del dispositivo. Escribe un pequeño programa de prueba.

#include

#include

#include

#include

main()

{

int testdev

int

int i;

p>

char buf[10];

testdev = open("/dev/test", O_RDWR

if ( testdev == -1 )

{

printf("No se puede abrir el archivo \n"); /p>

}

read(testdev,buf,10);

for (i = 0; i < 10;i++)

printf ("%d\n",buf[i] );

close(testdev);

}

Compilar y ejecutar, ver si todos los 1 son. ¿impreso?

Lo anterior es sólo una simple demostración. Un controlador verdaderamente práctico es mucho más complicado y necesita abordar cuestiones como interrupciones, dma, puertos de E/S, etc. Éstas son las verdaderas dificultades. Consulte la siguiente sección para saber cómo lidiar con situaciones reales.

Cómo escribir controladores de dispositivos en el sistema operativo Linux

3. Algunos problemas específicos en los controladores de dispositivos

1. Puerto de E/S.

La cuestión del hardware es inseparable de los puertos de E/S. Los dispositivos isa antiguos a menudo ocupan puertos de E/S reales. En Linux, el sistema operativo no protege los puertos de E/S, es decir, ningún controlador. puede funcionar en cualquier puerto de E/S, lo que fácilmente puede causar confusión. Cada conductor debe evitar por sí solo el mal uso de los puertos.

Hay dos funciones importantes del núcleo que pueden garantizar que el controlador haga esto.

1) check_region(int io_port, int off_set)

Esta función verifica la tabla de E/S del sistema para ver si otros controladores ocupan un determinado puerto de E/S.

Parámetro 1: La dirección base del puerto de E/S,

Parámetro 2: El rango ocupado por el puerto de E/S.

Valor de retorno: 0, no ocupado, no-0, ya ocupado.

2) request_region(int io_port, int off_set, char *devname)

Si este puerto de E/S no está ocupado, se puede utilizar en nuestro controlador. Antes de su uso se debe registrar en el sistema para evitar que sea ocupado por otros programas.

Después del registro, podrá ver el puerto de E/S que registró en el archivo /proc/ioports.

Parámetro 1: La dirección base del puerto io.

Parámetro 2: El rango ocupado por el puerto io.

Parámetro 3: Utilice el nombre del dispositivo de esta dirección io.

Después de registrar el puerto de E/S, puede utilizar inb(), outb() y otras funciones de forma segura para acceder a él.

En algunos dispositivos PCI, los puertos de E/S están asignados a una sección de memoria. Acceder a estos puertos equivale a acceder a una sección de memoria. A menudo, necesitamos obtener la dirección física de un fragmento de memoria.

2. Operación de memoria

Para asignar memoria dinámicamente en el controlador del dispositivo, use kmalloc en lugar de malloc, o use get_free_pages para solicitar páginas directamente. Kfree o free_pages se utiliza para liberar memoria. ¡Tenga en cuenta que funciones como kmalloc devuelven direcciones físicas!

Tenga en cuenta que kmalloc solo puede abrir un máximo de 128k-16, y la estructura del descriptor de página ocupa 16 bytes.

Los puertos de E/S asignados en memoria, los registros o la RAM de dispositivos de hardware (como la memoria de vídeo) generalmente ocupan un espacio de direcciones superior a F0000000. No se puede acceder a él directamente en el controlador. La dirección después de la reasignación debe obtenerse a través de la función del núcleo vremap.

Además, muchos hardware requieren una memoria continua relativamente grande para la transmisión DMA. Este programa debe residir siempre en la memoria y no se puede cambiar a un archivo. Pero kmalloc sólo puede abrir hasta 128k de memoria.

Esto se puede solucionar sacrificando algo de memoria del sistema.

3. Manejo de interrupciones

Igual que manejar puertos de E/S. Para utilizar una interrupción, primero debe registrarla en el sistema.

int request_irq(unsigned int irq, void(*handle)(int, void *, struct pt_regs *),

unsigned int long flags, const char *device

p>

p>

irq: es la interrupción a solicitar.

handle: puntero de función de procesamiento de interrupción.

flags: SA_INTERRUPT solicita una interrupción rápida, 0 interrupción normal.

dispositivo: nombre del dispositivo.

Si el registro es exitoso, se devuelve 0. En este momento, la interrupción que solicitó se puede ver en el archivo /proc/interrupts.

4. Algunas preguntas comunes.

Para las operaciones de hardware, a veces la sincronización es muy importante (para preguntas específicas sobre la sincronización, consulte el manual del chip del dispositivo específico. Por ejemplo, el chip de la tarjeta de red RTL8139). Pero si escribe algunas operaciones de hardware de bajo nivel en lenguaje C, gcc a menudo optimizará su programa, por lo que se producirán errores de sincronización. Si está escrito en ensamblador, gcc también optimizará el código ensamblador a menos que se modifique con la palabra clave volátil. La forma más segura es desactivar la optimización. Por supuesto, esto solo funciona para parte del código que usted mismo escribió. Si no optimiza todo su código, encontrará que el controlador no se cargará en absoluto. Esto se debe a que algunas funciones extendidas de gcc se utilizan al compilar el controlador, y estas funciones extendidas solo pueden reflejarse después de agregar opciones de optimización.

Escrito al final: Aprender Linux no es una tarea fácil, porque requiere mucho esfuerzo y una buena base en el lenguaje C. Sin embargo, aprender Linux también es algo muy interesante. y el "humor" de muchos maestros, que debes experimentar por ti mismo, O(∩_∩)O~ ¡Jaja!