En sistemas x86, ¿dónde se almacena el bloque de control de procesos en el espacio del kernel?
Linux simplifica el mecanismo de segmentación para que la dirección virtual sea siempre la misma que la dirección lineal, por lo que el espacio de direcciones virtuales de Linux también es 0~4G. El kernel utiliza los bytes de 1G superiores (desde la dirección virtual 0xC0000000 a 0xFFFFFFFF) y se denominan "espacio del kernel". Los bytes 3G inferiores (desde la dirección virtual 0x00000000 a 0xBFFFFFFF) son utilizados por cada proceso y se denominan "espacio de usuario". Dado que cada proceso puede acceder al kernel a través de llamadas al sistema, todos los procesos del sistema pueden usar el kernel de Linux***. Entonces, desde una perspectiva específica del proceso, cada proceso puede tener 4 gigabytes de espacio virtual.
Linux utiliza un mecanismo de protección de dos niveles: el nivel 0 protege el kernel y el nivel 3 protege los programas del usuario. Como puede ver en el diagrama (que no se puede representar gráficamente aquí), cada proceso tiene su propio espacio de usuario privado (0 a 3G) que no es visible para otros procesos en el sistema. Todos los procesos y kernels comparten el máximo de 1 GB de espacio virtual del kernel.
El espacio del kernel almacena el código y los datos del kernel, mientras que el espacio de usuario del proceso almacena el código y los datos de los programas del usuario. Ya sea en el espacio del kernel o en el espacio del usuario, están ubicados en el espacio virtual.
Aunque el espacio del kernel ocupa hasta 1 GB de bytes por espacio virtual, el mapeo a la memoria física siempre comienza en la dirección más baja (0x00000000). Para el espacio del kernel, el mapeo de direcciones es un mapeo lineal muy simple, 0xC0000000 es el desplazamiento entre la dirección física y la dirección lineal, que se llama PAGE_OFFSET en el código de Linux.
¿Cómo se comunican entre sí el espacio del kernel y el espacio del usuario?
El espacio del kernel y el espacio del usuario generalmente se comunican a través de llamadas al sistema.
¿Cómo saber si un controlador es un controlador en modo usuario o un controlador en modo kernel? ¿Cuáles son los criterios para juzgar?
Los controladores en modo espacio de usuario generalmente completan el acceso al hardware a través de llamadas al sistema, como mapear el espacio io del controlador al espacio del usuario a través de llamadas al sistema. Por lo tanto, la base principal para el juicio son las llamadas al sistema.
Hay demasiadas diferencias entre el espacio del kernel y el espacio del usuario para enumerarlas todas. Por ejemplo, la lista vinculada en el modo de usuario es diferente de la lista vinculada en el modo de usuario printf y en el modo kernel printk; El modo de usuario es virtual y relativamente independiente del espacio de cada aplicación, pero el estado del núcleo no es independiente del estado del núcleo, por lo que los programadores deben tener mucho cuidado. Etcétera.
También existen muchos métodos de comunicación entre los programas en modo usuario y los programas en modo kernel, no solo las llamadas al sistema. Las llamadas al sistema son en realidad una mala elección porque requieren un número de llamada del sistema, y este número requiere Distribución uniforme. .
Esto se puede lograr mediante ioctl, sysfs, proc, etc.
Al realizar el desarrollo a nivel de sistema de controladores de dispositivos, módulos de funciones del kernel, etc., generalmente es necesario intercambiar información entre el kernel y los programas del usuario. Este artículo resume varios métodos de intercambio de información de uso común y utiliza ejemplos simples para demostrar las características y el uso de cada método. Algunos de estos métodos son muy familiares para todos, mientras que otros sólo pueden utilizarse en condiciones especiales. Al comparar y contrastar estos métodos, podemos profundizar nuestra comprensión del kernel de Linux y, lo que es más importante, hacernos más competentes en las técnicas de desarrollo de aplicaciones a nivel del kernel de Linux.
Espacio del kernel y espacio del usuario
Como desarrollador de Linux, primero debes conocer la diferencia entre el espacio del kernel y el espacio del usuario. Hay mucha información sobre este tema, por lo que la cubriremos brevemente aquí:
La arquitectura informática moderna a menudo incluye mecanismos de protección para la gestión del almacenamiento. Se proporciona protección para evitar que las tareas del sistema accedan a áreas de almacenamiento que pertenecen a otras tareas o al sistema operativo.
Por ejemplo, en la arquitectura IntelX86, los niveles de privilegio se proporcionan como un mecanismo de protección para restringir el acceso a las áreas de almacenamiento mediante la distinción de niveles de privilegio. Según esta arquitectura, el sistema operativo Linux se divide: una parte del software principal se ejecuta con un nivel de privilegio más alto, independientemente de las aplicaciones ordinarias (Linux ejecuta el kernel usando el nivel de privilegio 3 en los sistemas Intel).
En cambio, otras partes se ejecutan como aplicaciones en el espacio del usuario. Solo pueden ver algunos de los recursos del sistema que pueden usar, no pueden acceder a ciertas funciones del sistema, no pueden acceder directamente al hardware, no pueden acceder directamente al espacio del kernel y, por supuesto, tienen otras restricciones de uso específicas. (Linux utiliza el nivel 0 de permiso del sistema Intel para ejecutar programas de usuario).
Poner el espacio del usuario y el espacio del kernel bajo este mecanismo de acceso asimétrico es efectivo desde una perspectiva de seguridad porque puede evitar que usuarios malintencionados espíen y que usuarios inferiores programas, haciendo así que el sistema funcione más estable y confiable. Sin embargo, si a los programas de usuario se les prohíbe completamente acceder y utilizar recursos en el espacio del kernel, nuestro sistema no podrá proporcionar ninguna funcionalidad significativa. Para facilitar que los programas de usuario utilicen recursos que solo se pueden controlar completamente en el espacio del kernel sin violar los permisos anteriores, hemos definido una interfaz de acceso estándar desde la propia arquitectura del hardware al sistema operativo. Para obtener más información sobre los sistemas X86, consulte la Referencia 1.
Las arquitecturas de hardware generalmente proporcionan un mecanismo de "puerta". El significado de "puerta" es que cuando ocurren ciertos eventos, las aplicaciones con pocos privilegios pueden acceder al espacio del kernel con altos privilegios a través de la "puerta". Para Intel El uso de "puerta del sistema" a través de "llamada al sistema" no requiere permisos especiales, pero la ubicación específica en el kernel no es arbitraria, sino que se especifica mediante la "llamada al sistema". Esta es una restricción que garantiza que el kernel sea seguro y confiable. Podemos entender claramente este mecanismo: como turista, puedes comprar un billete y pedir ir al parque safari, pero debes sentarte en el autobús turístico y seguir la ruta prescrita. Por supuesto, no puedes salir del coche porque es demasiado peligroso y te costará la vida o asustará a los animales salvajes.
Por razones de eficiencia y tamaño del código, los programas del kernel no pueden usar funciones de biblioteca estándar (por supuesto, existen otras preocupaciones; consulte la Referencia 2 para obtener más detalles), por lo que el desarrollo del kernel no es tan conveniente como el desarrollo de programas de usuario.
Interacción entre el espacio del kernel y el espacio del usuario
Hoy en día, cada vez más aplicaciones necesitan escribir programas a nivel de kernel y de usuario para completar tareas específicas de forma conjunta, generalmente utilizando el siguiente modelo: Primero, escriba un programa de servicio del kernel y utilice los privilegios y servicios proporcionados por el espacio del kernel para recibir, procesar y almacenar en caché los datos, luego, escriba un programa de usuario para interactuar con el programa de servicio del kernel completado previamente. Luego, escriba un programa de usuario para interactuar con el programa de servicio del kernel completado previamente. Específicamente, el programa de usuario se puede usar para configurar los parámetros del programa de servicio del kernel, extraer datos proporcionados por el programa de servicio del kernel y, por supuesto, también se puede usar para ingresar datos que serán procesados por el programa de servicio del kernel.
Las aplicaciones típicas incluyen Netfilter (programa de administración del kernel: firewall) e Iptable (programa de nivel de usuario: programa de configuración de reglas) IPSEC (programa de administración del kernel: parte del protocolo VPN) e IKE (programa de nivel de usuario: VPN); procesamiento de negociación de claves de cifrado); por supuesto, también hay una gran cantidad de controladores de dispositivos y el software de aplicación correspondiente. Los programas a nivel de kernel y los programas a nivel de usuario utilizan todo este software de aplicación para realizar tareas específicas juntos mediante el intercambio de información entre sí.
Métodos de interacción de información
El intercambio de información entre el programa de usuario y el kernel es bidireccional, lo que significa que la información se puede enviar activamente desde el espacio del usuario al espacio del kernel, y los datos también pueden ser enviado desde el kernel El espacio se envía al espacio del usuario. Por supuesto, los programas de usuario también pueden extraer datos del núcleo de forma activa. El siguiente es un resumen de cómo interactúan el kernel y los usuarios.
Según el iniciador de la transferencia de información, la interacción de información se puede dividir en dos categorías: los usuarios transmiten/extraen datos al kernel y el kernel envía solicitudes al espacio del usuario. Echemos un vistazo primero:
Interacciones de información iniciadas por programas a nivel de usuario.
(1) Escriba su propia llamada al sistema
Como se puede ver en la sección anterior, las llamadas al sistema son la forma más básica para que los programas de nivel de usuario accedan al kernel. Actualmente, Linux proporciona alrededor de 200 llamadas al sistema estándar y nos permite agregar nuestras propias llamadas al sistema para intercambiar información con el kernel. Por ejemplo, queremos crear un sistema de registro de llamadas al sistema para registrar todas las operaciones de llamadas al sistema para la detección de intrusiones. En este punto, podemos escribir un programa de servicio del kernel. Este programa es responsable de recopilar todas las solicitudes de llamadas al sistema y registrar la información de estas llamadas en el búfer integrado del kernel. No podemos implementar procedimientos complejos de detección de intrusiones en el kernel, por lo que los registros deben extraerse del búfer al espacio del usuario. La forma más sencilla es escribir una nueva llamada al sistema para implementar la extracción del búfer. Una vez que se implementan la rutina de servicio del kernel y las nuevas llamadas al sistema, podemos escribir programas de usuario en el espacio de usuario para realizar tareas de detección de intrusiones. El programa de detección de intrusiones puede programar, turnarse o realizar nuevas llamadas al sistema cuando sea necesario para extraer datos del kernel y luego realizar la detección de intrusiones (para conocer pasos y códigos específicos, consulte la cuarta edición de la revista electrónica del sitio web Linux Kernel Journey). ).
(2) Escribir controladores
Una de las características de Linux/UNIX es que todo es un archivo. El sistema define una interfaz de controlador simple y bien definida a través de la cual los programas cliente pueden interactuar con los controladores del kernel de manera uniforme. La mayoría de los usuarios y desarrolladores del sistema ya están familiarizados con esta interfaz y el proceso de desarrollo correspondiente.
Los controladores se ejecutan en el espacio del kernel y las aplicaciones del espacio del usuario interactúan con ellos a través de archivos en el directorio /dev/ del sistema de archivos. Este es el proceso de operación de archivos con el que estamos familiarizados: open() -- read()() -- write() -- ioctl() --close(). (Cabe señalar que no todos los controladores del kernel tienen esta interfaz, y los controladores de red y varias pilas de protocolos se usan de diferentes maneras. Por ejemplo, aunque la programación de interfaz establecida también tiene el concepto open() y close(), etc., pero su La implementación del kernel y el uso externo son muy diferentes de los controladores normales). Para obtener más detalles sobre esta parte de la programación, consulte las Referencias 3 y 4.
Este artículo no se centra en la respuesta a interrupciones, la administración de dispositivos, el procesamiento de datos y otros trabajos realizados por el controlador del dispositivo en el kernel, sino en la parte del programa que interactúa con los programas de nivel de usuario. El sistema operativo define una interfaz interactiva unificada para este propósito, a saber, las mencionadas anteriormente open(), read(), write(), ioctl() y close(), etc. Cada conductor hace lo que tiene que hacer. Cada controlador se implementa de forma independiente según sus propias necesidades, ocultando las funciones y servicios que proporciona bajo esta interfaz unificada. El programa cliente selecciona el controlador o servicio que necesita (en realidad selecciona el archivo en el directorio /dev/) e interactúa con el controlador en el kernel de acuerdo con la interfaz anterior y el proceso de operación de archivos. De hecho, es más fácil de explicar utilizando conceptos orientados a objetos. El sistema define una interfaz abstracta y cada controlador específico es la implementación de esta interfaz.
Por lo tanto, los controladores son una de las formas más importantes de interactuar con el espacio del usuario y la información del kernel. De hecho, ioctl, lectura y escritura son esencialmente llamadas al sistema, pero el kernel las normaliza. Por lo tanto, los usuarios no necesitan modificar el código del kernel y recompilar un nuevo kernel para agregar nuevas llamadas al sistema. Para usar dispositivos virtuales, los usuarios solo necesitan instalar el nuevo dispositivo virtual en el kernel a través del método modular (insmod activado), que es. uso conveniente.
En Linux, los dispositivos se pueden dividir aproximadamente en dispositivos de caracteres, dispositivos de bloque e interfaces de red (los dispositivos de caracteres incluyen aquellos a los que se debe acceder secuencialmente, como flujos de bytes, como terminales de caracteres, puertos serie, etc. ). Los dispositivos de bloque se refieren a dispositivos a los que se puede acceder de forma aleatoria, como los discos duros; las interfaces de red se refieren a tarjetas de red y pilas de protocolos comunes y otros servicios complejos de entrada y salida de red). Nuestro sistema de registro de llamadas también es fácil de implementar como controlador basado en caracteres. Podemos escribir la parte del kernel que recopila y registra información como un controlador de dispositivo de caracteres.
Aunque no existe una contraparte física real para el dispositivo, esto no es un problema: un controlador de dispositivo Linux debe ser una abstracción de software que pueda proporcionar servicios en combinación con hardware, o puede proporcionar servicios completamente como software puro (por supuesto, el uso de memoria es algo que no podemos evitar). En el controlador, podemos usar open para iniciar el servicio, read() para devolver los registros procesados, ioctl() para configurar el formato de registro, etc., y close() para detener el servicio. Luego cree un archivo de dispositivo en el directorio /dev/ correspondiente al nuevo controlador del sistema de registro de llamadas que agregamos al kernel.
(3) Utilice el sistema de archivos proc
Proc es un sistema de archivos especial proporcionado por Linux. El propósito de su introducción es proporcionar una forma conveniente para la interacción entre el usuario y. el núcleo. Utiliza el sistema de archivos como interfaz, lo que permite a las aplicaciones acceder de forma segura y cómoda al estado actual del sistema y a otros datos del kernel a través de operaciones de archivos.
El sistema de archivos proc se utiliza principalmente para monitorear, administrar y depurar sistemas. Muchas herramientas de administración que utilizamos (como ps y top) utilizan proc para leer la información del kernel. Además de leer la información del kernel, el sistema de archivos proc también proporciona capacidades de escritura. Por lo tanto, también podemos usarlo para ingresar información al kernel. Por ejemplo, al modificar el archivo de configuración de parámetros del sistema (/proc/sys) en el sistema de archivos proc, podemos cambiar directa y dinámicamente los parámetros del kernel en tiempo de ejecución, por ejemplo, mediante el siguiente comando:
echo 1 > /proc /sys/net/ip_v4/ip_forward
Al activar el interruptor que controla el interruptor de reenvío de IP en el kernel, podemos habilitar la función de enrutamiento en el sistema Linux en ejecución. Asimismo, existen muchas opciones del kernel que se pueden consultar y ajustar directamente a través del sistema de archivos proc.
Además de las entradas de archivos ya proporcionadas por el sistema, proc también proporciona una interfaz que nos permite crear nuevas entradas en el kernel para compartir datos de información con programas de usuario. Por ejemplo, podemos crear una nueva entrada de archivo en el sistema de archivos proc para el registrador de llamadas al sistema (que puede ser un controlador o un módulo del kernel), mostrando la cantidad de llamadas al sistema utilizadas, con qué frecuencia se usa cada llamada al sistema, etc. También podemos agregar entradas adicionales en el sistema de archivos proc para mostrar la cantidad de llamadas al sistema utilizadas. También puede agregar entradas adicionales para establecer reglas de registro, como no registrar el uso de la llamada al sistema abierto. Consulte la Referencia 7 para obtener más detalles sobre el uso del sistema de archivos proc.
(4) Uso de sistemas de archivos virtuales
Algunos desarrolladores del kernel creen que el uso de llamadas al sistema ioctl() a menudo hace que las llamadas al sistema carezcan de sentido y sean difíciles de controlar. Poner información en el sistema de archivos proc puede resultar confuso, por lo que también desaconsejan el uso del sistema de archivos proc. Recomiendan implementar un sistema de archivos virtual aislado en lugar de ioctl() y /proc porque la interfaz del sistema de archivos es clara, accesible al espacio del usuario y el uso de un sistema de archivos virtual hace que sea más fácil y eficiente usar scripts para realizar tareas de administración del sistema.
Tomemos como ejemplo cómo modificar la información del kernel a través del sistema de archivos virtual. Podemos implementar un sistema de archivos virtual llamado sagafs, donde el registro de archivos corresponde al registro de llamadas del sistema almacenado por el kernel. Podemos obtener información de registro a través de métodos comunes de acceso a archivos: como
# cat /sagafs/log
Utilice el sistema de archivos virtual (VFS) para lograr la interacción de la información, lo que hace que la administración del sistema sea más conveniente y claro. Sin embargo, algunos programadores pueden decir que la interfaz API de VFS es relativamente compleja y difícil de dominar. No se preocupe, el kernel 2.5 proporciona una rutina llamada libfs que ayuda a encapsular el funcionamiento general de VFS para usuarios que no están familiarizados con los sistemas de archivos. Para obtener más información sobre cómo interactuar con VFS, consulte Recursos.
(5) Uso del mapeo de memoria
Linux proporciona a los programas de usuario acceso directo a la memoria a través del mecanismo de mapeo de memoria. El mapeo de memoria se refiere a mapear una parte específica del espacio de memoria en el kernel al espacio de memoria de un programa a nivel de usuario.
En otras palabras, el espacio del usuario y el espacio del kernel comparten la misma memoria. El efecto intuitivo de esto es obvio: cualquier cambio de datos almacenado por el kernel en esta dirección puede ser encontrado y utilizado inmediatamente por el usuario, sin copiar los datos en absoluto. En el caso de las interacciones de llamadas al sistema, para muchas aplicaciones de tiempo crítico y de transferencia intensiva de datos, debe haber un paso de copia de datos en el proceso, ya sea copiar los datos del kernel al búfer del usuario o simplemente copiar los datos del usuario. al búfer del núcleo.
Desarrollamos un controlador para un dispositivo de muestreo de alta velocidad que requiere muestreo en tiempo real de 16 bits a 20 Mbps y una tasa de repetición de 1 KHz, lo que requiere mucho muestreo, DMA y procesamiento cada milisegundo. El método de copia simplemente no puede lograr esto. En este punto, el mapeo de memoria se convierte en la única opción: reservamos un espacio en la memoria y lo configuramos como una cola en anillo para que el dispositivo de muestreo genere datos en modo DMA. Luego, este espacio de memoria se asigna a un programa de procesamiento de datos que se ejecuta en el espacio del usuario, de modo que los datos recién recibidos por el dispositivo de muestreo y transmitidos al host puedan ser procesados inmediatamente por el programa del espacio del usuario.
De hecho, el mapeo de memoria se usa a menudo exactamente cuando el kernel y el espacio del usuario necesitan interactuar con grandes cantidades de datos muy rápidamente, especialmente en aplicaciones en tiempo real.
El área de memoria virtual de un servidor del sistema X Window puede verse como un ejemplo clásico del uso de mapeo de memoria: el servidor X necesita intercambiar grandes cantidades de datos con la memoria de video, y el La asignación de memoria de visualización de gráficos directamente al espacio del usuario puede mejorar significativamente el rendimiento en comparación con lseek/write.
No todos los tipos de aplicaciones son adecuados para mmap. Por ejemplo, los dispositivos de caracteres basados en transmisión de datos (como puertos serie y ratones) no son adecuados para mmap. Además, esta maldita forma de disfrutar de la memoria también tiene el problema de una mala sincronización. Dado que no existe un mecanismo de sincronización especial para los programas de usuario y los programas del kernel, ****, la lectura y escritura de datos deben diseñarse con mucho cuidado para garantizar que no se produzcan "ejecuciones en seco".
mmap se basa completamente en el concepto de disfrutar de la memoria, por lo que proporciona más comodidad, pero también es particularmente difícil de controlar.
Interacciones iniciadas por el kernel
En las interacciones iniciadas por el kernel, lo que más nos interesa es cómo el kernel envía mensajes a los programas de usuario y cómo los programas de usuario reciben estos mensajes. Específicamente, las preguntas generalmente. Concéntrese en los siguientes aspectos: ¿Puede el kernel llamar a programas de usuario? ¿Puede el kernel señalar un evento a un programa de usuario?
La mayor diferencia entre los métodos interactivos descritos anteriormente es que están activos, en lugar de esperar a que las llamadas al sistema devuelvan información pasivamente.
(1) Llamar al programa de usuario desde el espacio del kernel.
Incluso en el kernel, a veces necesitamos realizar operaciones que solo el nivel de usuario puede realizar, como abrir un archivo para leer datos específicos o ejecutar un programa de usuario para realizar una determinada función. Dado que la mayoría de los datos y funciones ya existen o se han implementado en el espacio del usuario, no es necesario gastar muchos recursos repitiendo estas operaciones. Además, el kernel está diseñado para tener una mejor resiliencia o rendimiento para soportar cambios desconocidos pero posibles, lo que a su vez requiere el uso de recursos de espacio del usuario para completar la tarea. Por ejemplo, la parte del kernel que carga módulos dinámicamente requiere llamar a kmod, pero es imposible ordenar todos los módulos del kernel al compilar kmod (en cuyo caso cargar módulos dinámicamente no tendría sentido), por lo que es imposible saber dónde y cómo cargar módulos posteriores. Por lo tanto, se utiliza la siguiente estrategia para la carga dinámica de módulos: la tarea de carga en realidad se realiza con la ayuda del programa modprobe ubicado en el espacio del usuario; en el caso más simple, modprobe llama a insmod y le pasa al kernel el nombre del módulo como un parámetro y luego use este método para cargar los módulos requeridos.
El inicio de programas de usuario en el kernel aún se realiza a través del prototipo de llamada al sistema execve, pero la llamada ocurre en el espacio del kernel, mientras que las llamadas normales al sistema se realizan en el espacio del usuario.
Si la llamada al sistema está parametrizada, surgirán problemas: porque en el código de ejecución de la llamada al sistema, se debe verificar la validez de los parámetros, y esta verificación requiere que todos los parámetros estén entre el espacio de usuario - 0x0000000 - 0xC0000000, por lo que todos los parámetros debe estar en el espacio del usuario. Entre 0xC0000000 - 0xC0000000, por lo que si pasamos parámetros del kernel (dirección mayor que 0xC0000000), la verificación rechazará nuestra solicitud de llamada. Para resolver este problema, podemos usar la macro set_fs para modificar la estrategia de verificación para permitir que la dirección del parámetro sea la dirección del kernel. Esto permitirá que el kernel utilice llamadas al sistema directamente.
Por ejemplo, set_fs(KERNEL_DS):
Antes de que kmod ejecute el código de modprobe llamando a execve, debe usar... .
set_fs(KERNEL_DS);
/* Vamos, vamos, vamos...
set_fs(KERNEL_DS);
/* Vaya, vaya, vaya... "-s", "-k", "--", (char*)module_name, NULL }, envp es { "HOME=/", "TERM=linux", "PATH= /sbin:/usr/sbin:/bin:/usr/bin", NULL }.
También se puede abrir un archivo desde el kernel usando la llamada al sistema open con parámetros. Todo lo que se requiere es llamar primero a la macro set_fs.
(2) Utilice la llamada al sistema brk para exportar datos del kernel
El método principal para transferir datos entre el kernel y el espacio del usuario es usar get_user(ptr) y put_user(datum, ptr) Procedimiento de ejemplo. Por lo tanto, se pueden encontrar en la mayoría de las llamadas al sistema que requieren pasar datos. Pero si no pasamos datos del kernel a través de una llamada al sistema iniciada por el programa de usuario, es decir, sin proporcionar explícitamente la ubicación del búfer en el espacio del usuario, ¿cómo podemos pasar datos del kernel al espacio del usuario?
Obviamente, ya no podemos usar put_user() directamente porque no podemos especificar un búfer de destino para ello. Por lo tanto, debemos tomar prestada la llamada al sistema brk y el espacio de proceso actual: brk se usa para establecer el tamaño del espacio del montón de procesos. Cada proceso tiene un espacio de montón independiente y las funciones de asignación de memoria dinámica (como malloc) en realidad obtienen memoria del espacio de montón del proceso. Usaremos brk para expandir un nuevo búfer temporal en el espacio de almacenamiento dinámico del proceso actual y luego usaremos put_user para exportar los datos del kernel a este espacio de usuario determinado.
¿Recuerdas cómo acabamos de llamar al programa de usuario en el kernel? Allí, nuestra operación se salta la verificación de parámetros. Con este enfoque, podemos hacer lo contrario: expandir un espacio en el montón del proceso actual, copiar los parámetros que usará la llamada al sistema al espacio de usuario recién expandido a través de put_user () y luego, cuando llame a execve, use la dirección. de este espacio recién abierto como parámetro para que la barrera de verificación de parámetros ya no exista.
char * program_path = "/bin/ls" ;
/* Encuentra la posición superior actual del montón*/
mmm=current->mm ->brk ;
/* Utilice brk para extender un nuevo búfer de 256 bytes en la parte superior del montón*/
ret = brk(*(void)(mmm+256 ));
p>/* Copie los parámetros requeridos por execve al nuevo buffer*/
put_user((void*)2,program_path,strlen(program_path)+1) ;
/* ¡Programa /bin/ls ejecutado exitosamente! */
execve((char*)(mmm+2));
/* Sitio de recuperación*/
tmp = brk((void*) mmm);
Este método no tiene generalidad (específicamente, ¿tiene algún efecto negativo?) y sólo puede usarse como un truco, pero no es difícil de ver: si estás familiarizado con el kernel estructura, ¡puedes hacer muchas cosas inesperadas!
(3) Uso de señales
El uso de señales en el kernel se centra principalmente en notificar al usuario que se ha producido un error importante en el programa y cerrar por la fuerza el proceso actual. esta vez, el kernel notificará enviando la señal SIGKILL. Cuando finaliza el proceso, el kernel envía una señal usando la rutina send_sign(pid, sig). Se puede ver que el número de proceso (pid) debe conocerse de antemano para enviarlo. una señal Por lo tanto, si se va a enviar una señal desde el kernel para notificar de forma asincrónica al proceso del usuario que realice tareas, entonces se debe conocer de antemano el número de proceso del proceso del usuario. Buscar el ID de proceso de un proceso específico mientras el kernel se está ejecutando es una tarea laboriosa que puede requerir atravesar toda la cadena de bloques de control de procesos. Por lo tanto, señalar a un proceso de usuario específico es una mala idea y generalmente no se usa en el kernel. El único caso en el que se utilizan señales en el kernel es para notificar al proceso actual (el pid se puede obtener fácilmente de la variable actual) para que realice alguna acción común, como terminar. Por lo tanto, es de poca utilidad para los desarrolladores del kernel.
Existe una situación similar para las operaciones de mensajes. No diré mucho más aquí.
Resumen Los intercambios de información iniciados por programas a nivel de usuario a través de llamadas estándar o interfaces de controlador generalmente involucran llamadas al sistema. No hay muchas situaciones en las que el núcleo inicie el intercambio de información. Dado que no existe una interfaz estándar, su funcionamiento es muy incómodo. Por lo tanto, en general, los primeros métodos presentados en este artículo se utilizarán para la interacción de información tanto como sea posible. Después de todo, por diseño, el kernel se define como un proveedor de servicios pasivo en relación con los programas a nivel de cliente. Por lo tanto, nuestro propio desarrollo también debería intentar seguir este principio de diseño.