Warning
Si tiene alguna duda sobre la exactitud del contenido de esta traducción, la única referencia válida es la documentación oficial en inglés. Además, por defecto, los enlaces a documentos redirigen a la documentación en inglés, incluso si existe una versión traducida. Consulte el índice para más información.
- Original:
Documentation/process/4.Coding.rst
- Translator:
Carlos Bilbao <carlos.bilbao.osdev@gmail.com> and Avadhut Naik <avadhut.naik@amd.com>
4. Conseguir el código correcto¶
Si bien hay mucho que decir a favor de un proceso de diseño sólido y orientado a la comunidad, la prueba de cualquier proyecto de desarrollo del kernel está en el código resultante. Es el código lo que será examinado por otros desarrolladores y lo que será incluido (o no) en el árbol principal. Por lo tanto, es la calidad de este código lo que determinará el éxito final del proyecto.
Esta sección examinará el proceso de programación. Comenzaremos observando algunas de las maneras en que los desarrolladores del kernel pueden cometer errores. Luego, el enfoque se dirigirá hacia hacer las cosas bien y las herramientas que pueden ayudar en dicha búsqueda.
4.1. Problemas¶
4.1.1. Estilo de programación¶
El kernel ha tenido durante mucho tiempo un estilo de programación estándar, descrito en la documentación del kernel en Linux kernel coding style. Durante gran parte de ese tiempo, las políticas descritas en ese archivo se tomaban como, en el mejor de los casos, orientativas. Como resultado, hay una cantidad considerable de código en el kernel que no cumple con las pautas de estilo de programación. La presencia de ese código lleva a dos peligros independientes para los desarrolladores del kernel.
El primero de estos es creer que los estándares de programación del kernel no importan y no se aplican. La realidad es que agregar nuevo código al kernel es muy difícil si ese código no está escrito de acuerdo con el estándar; muchos desarrolladores solicitarán que el código sea reformateado antes de revisarlo. Una base de código tan grande como el kernel requiere cierta uniformidad para que los desarrolladores puedan comprender rápidamente cualquier parte de él. Así que ya no hay lugar para el código con formato extraño.
Ocasionalmente, el estilo de programación del kernel entrará en conflicto con el estilo obligatorio de un empleador. En tales casos, el estilo del kernel tendrá que prevalecer antes de que el código pueda ser fusionado. Incluir código en el kernel significa renunciar a cierto grado de control de varias maneras, incluida la forma en que se formatea el código.
La otra trampa es asumir que el código que ya está en el kernel necesita urgentemente correcciones de estilo de programación. Los desarrolladores pueden comenzar a generar parches de reformateo como una forma de familiarizarse con el proceso o como una forma de incluir su nombre en los registros de cambios del kernel, o ambos. Pero las correcciones puramente de estilo de programación son vistas como ruido por la comunidad de desarrollo; tienden a recibir una recepción adversa. Por lo tanto, este tipo de parche es mejor evitarlo. Es natural corregir el estilo de una parte del código mientras se trabaja en él por otras razones, pero los cambios de estilo de programación no deben hacerse por sí mismos.
El documento de estilo de programación tampoco debe leerse como una ley absoluta que nunca puede transgredirse. Si hay una buena razón para ir en contra del estilo (una línea que se vuelve mucho menos legible si se divide para ajustarse al límite de 80 columnas, por ejemplo), perfecto.
Tenga en cuenta que también puedes usar la herramienta clang-format para ayudarle con estas reglas, para reformatear rápidamente partes de su código automáticamente y para revisar archivos completos a fin de detectar errores de estilo de programación, errores tipográficos y posibles mejoras. También es útil para ordenar #includes, alinear variables/macros, reformatear texto y otras tareas similares. Consulte el archivo clang-format para más detalles.
Algunas configuraciones básicas del editor, como la indentación y los finales de línea, se configurarán automáticamente si utilizas un editor compatible con EditorConfig. Consulte el sitio web oficial de EditorConfig para obtener más información: https://editorconfig.org/
4.1.2. Capas de abstracción¶
Los profesores de ciencias de la computación enseñan a los estudiantes a hacer un uso extensivo de capas de abstracción en nombre de la flexibilidad y el ocultamiento de la información. Sin duda, el kernel hace un uso extensivo de la abstracción; ningún proyecto que involucre varios millones de líneas de código podría sobrevivir de otra manera. Pero la experiencia ha demostrado que una abstracción excesiva o prematura puede ser tan perjudicial como la optimización prematura. La abstracción debe usarse en la medida necesaria y ya.
A un nivel simple, considere una función que tiene un argumento que siempre se pasa como cero por todos los que la invocan. Uno podría mantener ese argumento por si alguien eventualmente necesita usar la flexibilidad adicional que proporciona. Sin embargo, para entonces, es probable que el código que implementa este argumento adicional se haya roto de alguna manera sutil que nunca se notó, porque nunca se ha utilizado. O, cuando surge la necesidad de flexibilidad adicional, no lo hace de una manera que coincida con la expectativa temprana del programador. Los desarrolladores del kernel rutinariamente enviarán parches para eliminar argumentos no utilizados; en general, no deberían añadirse en primer lugar.
Las capas de abstracción que ocultan el acceso al hardware, a menudo para permitir que la mayor parte de un controlador se utilice con varios sistemas operativos, son especialmente mal vistas. Dichas capas oscurecen el código y pueden imponer una penalización en el rendimiento; no pertenecen al kernel de Linux.
Por otro lado, si se encuentra copiando cantidades significativas de código de otro subsistema del kernel, es hora de preguntar si, de hecho, tendría sentido extraer parte de ese código en una biblioteca separada o implementar esa funcionalidad a un nivel superior. No tiene sentido replicar el mismo código en todo el kernel.
4.1.3. Uso de #ifdef y del preprocesador en general¶
El preprocesador de C tiene una tentación poderosa para algunos programadores de C, quienes lo ven como una forma de programar eficientemente una gran cantidad de flexibilidad en un archivo fuente. Pero el preprocesador no es C, y el uso intensivo de él da como resultado un código mucho más difícil de leer para otros y más difícil de verificar por el compilador para su corrección. El uso intensivo del preprocesador es asi siempre un signo de un código que necesita algo de limpieza.
La compilación condicional con #ifdef es, de hecho, una característica poderosa, y se usa dentro del kernel. Pero hay poco deseo de ver código que sté salpicado liberalmente con bloques #ifdef. Como regla general, el uso de #ifdef debe limitarse a los archivos de encabezado siempre que sea posible. El código condicionalmente compilado puede confinarse a funciones que, si el código no va a estar presente, simplemente se convierten en vacías. El compilador luego optimizará silenciosamente la llamada a la función vacía. El resultado es un código mucho más limpio y fácil de seguir.
Las macros del preprocesador de C presentan varios peligros, incluida la posible evaluación múltiple de expresiones con efectos secundarios y la falta de seguridad de tipos. Si te sientes tentado a definir una macro, considera crear una función en línea en su lugar. El código resultante será el mismo, pero las funciones en línea son más fáciles de leer, no evalúan sus argumentos varias veces y permiten que el compilador realice comprobaciones de tipo en los argumentos y el valor de retorno.
4.1.4. Funciones en línea¶
Las funciones en línea presentan su propio peligro, sin embargo. Los programadores pueden enamorarse de la eficiencia percibida al evitar una llamada a función y llenar un archivo fuente con funciones en línea. Esas funciones, sin embargo, pueden en realidad reducir el rendimiento. Dado que su código se replica en cada sitio de llamada, terminan hinchando el tamaño del kernel compilado. Eso, a su vez, crea presión en las cachés de memoria del procesador, lo que puede ralentizar la ejecución de manera drástica Las funciones en línea, como regla, deben ser bastante pequeñas y relativamente raras. El costo de una llamada a función, después de todo, no es tan alto; la creación de un gran número de funciones en línea es un ejemplo clásico de optimización prematura.
En general, los programadores del kernel ignoran los efectos de caché bajo su propio riesgo. El clásico intercambio de tiempo/espacio que se enseña en las clases de estructuras de datos iniciales a menudo no se aplica al hardware contemporáneo. El espacio es tiempo, en el sentido de que un programa más grande se ejecutará más lentamente que uno más compacto.
Los compiladores más recientes toman un papel cada vez más activo al decidir si una función dada debe realmente ser en línea o no. Por lo tanto, la colocación liberal de palabras clave “inline” puede no solo ser excesiva; también podría ser irrelevante.
4.1.5. Bloqueo¶
En mayo de 2006, la pila de red “Devicescape” fue, con gran fanfarria, lanzada bajo la licencia GPL y puesta a disposición para su inclusión en el kernel principal. Esta donación fue una noticia bienvenida; el soporte para redes inalámbricas en Linux se consideraba, en el mejor de los casos, deficiente, y la pila de Devicescape ofrecía la promesa de solucionar esa situación. Sin embargo, este código no fue incluido en el kernel principal hasta junio de 2007 (versión 2.6.22). ¿Qué sucedió?
Este código mostró varios signos de haber sido desarrollado a puertas cerradas en una empresa. Pero un problema importante en particular fue que no estaba diseñado para funcionar en sistemas multiprocesador. Antes de que esta pila de red (ahora llamada mac80211) pudiera fusionarse, se tuvo que implementar un esquema de bloqueo en ella.
Hubo un tiempo en que se podía desarrollar código para el kernel de Linux sin pensar en los problemas de concurrencia que presentan los sistemas multiprocesador. Ahora, sin embargo, este documento se está escribiendo en una computadora portátil con dos núcleos. Incluso en sistemas de un solo procesador, el trabajo que se está realizando para mejorar la capacidad de respuesta aumentará el nivel de concurrencia dentro del kernel. Los días en que se podía escribir código para el kernel sin pensar en el bloqueo han quedado atrás.
Cualquier recurso (estructuras de datos, registros de hardware, etc.) que pueda ser accedido concurrentemente por más de un hilo debe estar protegido por un bloqueo. El nuevo código debe escribirse teniendo en cuenta este requisito; implementar el bloqueo después de que el código ya ha sido desarrollado es una tarea mucho más difícil. Los desarrolladores del kernel deben tomarse el tiempo para comprender bien los primitivos de bloqueo disponibles para elegir la herramienta adecuada para el trabajo. El código que muestre una falta de atención a la concurrencia tendrá un camino difícil para ser incluido en el kernel principal.
4.1.6. Regresiones¶
Un último peligro que vale la pena mencionar es el siguiente: puede ser tentador realizar un cambio (que puede traer grandes mejoras) que cause un problema para los usuarios existentes. Este tipo de cambio se llama una “regresión”, y las regresiones se han vuelto muy mal recibidas en el kernel principal. Con pocas excepciones, los cambios que causan regresiones serán revertidos si la regresión no se puede solucionar de manera oportuna. Es mucho mejor evitar la regresión desde el principio.
A menudo se argumenta que una regresión puede justificarse si hace que las cosas funcionen para más personas de las que crea problemas. ¿Por qué no hacer un cambio si trae nueva funcionalidad a diez sistemas por cada uno que rompe? La mejor respuesta a esta pregunta fue expresada por Linus en julio de 2007 (traducido):
Entonces, no arreglamos errores introduciendo nuevos problemas. Eso
lleva a la locura, y nadie sabe si realmente se avanza. ¿Es dos pasos
adelante, uno atrás, o un paso adelante y dos atrás?
(https://lwn.net/Articles/243460/).
Un tipo de regresión especialmente mal recibido es cualquier tipo de cambio en la ABI del espacio de usuario. Una vez que se ha exportado una interfaz al espacio de usuario, debe ser soportada indefinidamente. Este hecho hace que la creación de interfaces para el espacio de usuario sea particularmente desafiante: dado que no pueden cambiarse de manera incompatible, deben hacerse bien desde el principio. Por esta razón, siempre se requiere una gran cantidad de reflexión, documentación clara y una amplia revisión para las interfaces del espacio de usuario.
4.1.7. Herramientas de verificación de código¶
Por ahora, al menos, escribir código libre de errores sigue siendo un ideal que pocos de nosotros podemos alcanzar. Sin embargo, lo que podemos esperar hacer es detectar y corregir tantos de esos errores como sea posible antes de que nuestro código se integre en el kernel principal. Con ese fin, los desarrolladores del kernel han reunido una impresionante variedad de herramientas que pueden detectar una amplia variedad de problemas oscuros de manera automatizada. Cualquier problema detectado por el ordenador es un problema que no afectará a un usuario más adelante, por lo que es lógico que las herramientas automatizadas se utilicen siempre que sea posible.
El primer paso es simplemente prestar atención a las advertencias producidas por el compilador. Las versiones contemporáneas de gcc pueden detectar (y advertir sobre) una gran cantidad de errores potenciales. Con bastante frecuencia, estas advertencias apuntan a problemas reales. El código enviado para revisión no debería, por regla general, producir ninguna advertencia del compilador. Al silenciar las advertencias, tenga cuidado de comprender la causa real e intente evitar “correcciones” que hagan desaparecer la advertencia sin abordar su causa.
Tenga en cuenta que no todas las advertencias del compilador están habilitadas de forma predeterminada. Compile el kernel con “make KCFLAGS=-W” para obtener el conjunto completo.
El kernel proporciona varias opciones de configuración que activan funciones de depuración; la mayoría de estas se encuentran en el submenú “kernel hacking”. Varias de estas opciones deben estar activadas para cualquier kernel utilizado para desarrollo o pruebas. En particular, debería activar:
FRAME_WARN para obtener advertencias sobre marcos de pila más grandes que una cantidad determinada. La salida generada puede ser extensa, pero no es necesario preocuparse por las advertencias de otras partes del kernel.
DEBUG_OBJECTS agregará código para rastrear la vida útil de varios objetos creados por el kernel y advertir cuando se realicen cosas fuera de orden. Si está agregando un subsistema que crea (y exporta) objetos complejos propios, considere agregar soporte para la infraestructura de depuración de objetos.
DEBUG_SLAB puede encontrar una variedad de errores en la asignación y uso de memoria; debe usarse en la mayoría de los kernels de desarrollo.
DEBUG_SPINLOCK, DEBUG_ATOMIC_SLEEP y DEBUG_MUTEXES encontrarán una serie de errores comunes de bloqueo.
Hay bastantes otras opciones de depuración, algunas de las cuales se discutirán más adelante. Algunas de ellas tienen un impacto significativo en el rendimiento y no deben usarse todo el tiempo. Pero dedicar tiempo a aprender las opciones disponibles probablemente será recompensado muchas veces en poco tiempo.
Una de las herramientas de depuración más pesadas es el verificador de bloqueos, o “lockdep”. Esta herramienta rastreará la adquisición y liberación de cada bloqueo (spinlock o mutex) en el sistema, el orden en que se adquieren los bloqueos en relación entre sí, el entorno actual de interrupción, y más. Luego, puede asegurarse de que los bloqueos siempre se adquieran en el mismo orden, que las mismas suposiciones de interrupción se apliquen en todas las situaciones, y así sucesivamente. En otras palabras, lockdep puede encontrar varios escenarios en los que el sistema podría, en raras ocasiones, bloquearse. Este tipo de problema puede ser doloroso (tanto para desarrolladores como para usuarios) en un sistema desplegado; lockdep permite encontrarlos de manera automatizada con anticipación. El código con cualquier tipo de bloqueo no trivial debe ejecutarse con lockdep habilitado antes de ser enviado para su inclusión.
Como programador diligente del kernel, sin duda alguna, verificará el estado de retorno de cualquier operación (como una asignación de memoria) que pueda fallar. Sin embargo, el hecho es que las rutas de recuperación de fallos resultantes probablemente no hayan sido probadas en absoluto. El código no probado tiende a ser código roto; podría tener mucha más confianza en su código si todas esas rutas de manejo de errores se hubieran ejercitado algunas veces.
El kernel proporciona un marco de inyección de fallos que puede hacer precisamente eso, especialmente donde están involucradas las asignaciones de memoria. Con la inyección de fallos habilitada, un porcentaje configurable de las asignaciones de memoria fallarán; estas fallas pueden restringirse a un rango específico de código. Ejecutar con la inyección de fallos habilitada permite al programador ver cómo responde el código cuando las cosas van mal. Consulte Fault injection capabilities infrastructure para obtener más información sobre cómo utilizar esta funcionalidad.
Otros tipos de errores se pueden encontrar con la herramienta de análisis estático “sparse”. Con sparse, el programador puede recibir advertencias sobre confusiones entre direcciones del espacio de usuario y del kernel, mezcla de cantidades big-endian y little-endian, el paso de valores enteros donde se espera un conjunto de banderas de bits, y así sucesivamente. Sparse debe instalarse por separado (puede encontrarse en https://sparse.wiki.kernel.org/index.php/Main_Page si su distribución no lo empaqueta); luego, puede ejecutarse en el código agregando “C=1” a su comando make.
La herramienta “Coccinelle” (http://coccinelle.lip6.fr/) puede encontrar una amplia variedad de posibles problemas de codificación; también puede proponer correcciones para esos problemas. Bastantes “parches semánticos” para el kernel se han empaquetado en el directorio scripts/coccinelle; ejecutar “make coccicheck” ejecutará esos parches semánticos e informará sobre cualquier problema encontrado. Consulte: ref:Coccinelle <devtools_coccinelle> para obtener más información.
Otros tipos de errores de portabilidad se encuentran mejor compilando su código para otras arquitecturas. Si no tiene un sistema S/390 o una placa de desarrollo Blackfin a mano, aún puede realizar el paso de compilación. Un gran conjunto de compiladores cruzados para sistemas x86 se puede encontrar en
Muchos sistemas de compilación disponibles comercialmente también se pueden utilizar para compilar código de kernel para una amplia gama de arquitecturas.
Los desarrolladores del kernel son afortunados: tienen acceso a una variedad de herramientas de verificación de código de la que los desarrolladores de la mayoría de los otros sistemas pueden estar celosos. Pero todas esas herramientas no servirán de nada si no las usa. El resultado final de ignorar estas herramientas es simple: alguien más puede notificarle de un problema en su código a través de un “oportuno” comentario en la lista de correo o, peor aún, el código problemático podría ser eliminado. Es mucho más fácil usar estas herramientas en primer lugar.
4.1.8. Documentación¶
La documentación a menudo ha sido más la excepción que la regla en el desarrollo del kernel. Aun así, una documentación adecuada ayudará a facilitar la integración de nuevo código en el kernel, hará la vida más fácil a otros desarrolladores, y será útil para sus usuarios. En muchos casos, la inclusión de documentación se ha vuelto esencialmente obligatoria.
La primera pieza de documentación para cualquier parche es su changelog asociado. Las entradas de registro deben describir el problema que se está esolviendo, la forma de la solución, las personas que trabajaron en el parche, cualquier efecto relevante en el rendimiento, y cualquier otra cosa que pueda ser necesaria para entender el parche. Asegúrese de que el changelog diga por qué el parche vale la pena ser aplicado; un sorprendente número de desarrolladores no proporciona esa información.
Cualquier código que agregue una nueva interfaz para el espacio de usuario, incluidos los nuevos archivos de sysfs o /proc, debe incluir documentación de esa interfaz que permita a los desarrolladores del espacio de usuario saber con qué están trabajando. Consulte Documentation/ABI/README para una descripción de cómo debe formatearse esta documentación y qué información debe proporcionarse.
El archivo Documentation/admin-guide/kernel-parameters.rst describe todos los parámetros de arranque del kernel. Cualquier parche que agregue nuevos parámetros debe agregar las entradas correspondientes a este archivo.
Cualquier nueva opción de configuración debe ir acompañada de un texto de ayuda que explique claramente las opciones y cuándo el usuario podría querer seleccionarlas.
La información de la API interna para muchos subsistemas está documentada mediante comentarios especialmente formateados; estos comentarios pueden extraerse y formatearse de diversas maneras mediante el script “kernel-doc”. Si está trabajando dentro de un subsistema que tiene comentarios de kerneldoc, debe mantenerlos y agregarlos según corresponda para las funciones disponibles externamente. Incluso en áreas que no han sido tan documentadas, no hay ningún inconveniente en agregar comentarios de kerneldoc para el futuro; de hecho, esta puede ser una actividad útil para desarrolladores de kernel principiantes. El formato de estos comentarios, junto con alguna información sobre cómo crear plantillas de kerneldoc, se puede encontrar en Documentation/doc-guide/.
Cualquiera que lea una cantidad significativa de código existente del kernel notará que, a menudo, los comentarios son notables por su ausencia. Una vez más, las expectativas para el nuevo código son más altas que en el pasado; integrar código sin comentarios será más difícil. Dicho esto, hay poco deseo de tener código excesivamente comentado. El código en sí debe ser legible, con comentarios que expliquen los aspectos más sutiles.
Ciertas cosas siempre deben comentarse. El uso de barreras de memoria debe ir acompañado de una línea que explique por qué la barrera es necesaria. Las reglas de bloqueo para las estructuras de datos generalmente necesitan explicarse en algún lugar. Las estructuras de datos importantes en general necesitan documentación completa. Las dependencias no obvias entre fragmentos de código separados deben señalarse. Cualquier cosa que pueda tentar a un maintainer de código a hacer una “limpieza” incorrecta necesita un comentario que explique por qué se hace de esa manera. Y así sucesivamente.
4.1.9. Cambios en la API interna¶
La interfaz binaria proporcionada por el kernel al espacio de usuario no se puede romper, excepto en las circunstancias más graves. Las interfaces de programación internas del kernel, en cambio, son altamente fluidas y pueden cambiarse cuando surge la necesidad. Si usted se encuentra teniendo que hacer un rodeo alrededor de una API del kernel, o simplemente no utilizando una funcionalidad específica porque no cumple con sus necesidades, eso puede ser una señal de que la API necesita cambiar. Como desarrollador del kernel, usted está autorizado a hacer esos cambios.
Hay, por supuesto, algunas condiciones. Los cambios en la API se pueden hacer, pero necesitan estar bien justificados. Entonces, cualquier parche que realice un cambio en la API interna debe ir acompañado de una descripción de cuál es el cambio y por qué es necesario. Este tipo de cambio también debe desglosarse en un parche separado, en lugar de estar enterrado dentro de un parche más grande.
La otra condición es que un desarrollador que cambia una API interna generalmente está encargado de la tarea de corregir cualquier código dentro del árbol del kernel que se vea afectado por el cambio. Para una función ampliamente utilizada, este deber puede llevar a literalmente cientos o miles de cambios, muchos de los cuales probablemente entren en conflicto con el trabajo que otros desarrolladores están realizando. No hace falta decir que esto puede ser un trabajo grande, por lo que es mejor asegurarse de que la justificación sea sólida. Tenga en cuenta que la herramienta Coccinelle puede ayudar con los cambios de API a gran escala.
Cuando se realice un cambio incompatible en la API, siempre que sea posible, se debe asegurar que el código que no ha sido actualizado sea detectado por el compilador. Esto le ayudará a estar seguro de que ha encontrado todos los usos en el árbol de esa interfaz. También alertará a los desarrolladores de código fuera del árbol de que hay un cambio al que necesitan responder. Apoyar el código fuera del árbol no es algo de lo que los desarrolladores del kernel deban preocuparse, pero tampoco tenemos que dificultarles la vida más de lo necesario.