Creo que puedo ver de dónde vienes. (Después de leer sus comentarios.) Este es más revelador para mí:
...¿ves a la gente que hace la arquitectura del software y evita los globales en todo el costo en estos diminutos sistemas integrados? Puedes despellejar al gato o los globos me parecen más rápidos.
(También estoy imaginando los núcleos de la serie 8051/8031/8052/8032, por ahora.)
Tomemos un método muy simple para crear colas de sistema operativo para un sistema operativo muy simple. Necesitamos una cola lista y una cola de sueño, como mínimo. (Podríamos añadir colas de semáforos, pero no lo hagamos porque quiero mantener esto al mínimo.) También queremos apoyar un número limitado de procesos. (Esto es, después de todo, un pequeño MCU sin mucha memoria disponible.)
Para empezar, definamos algunas constantes:
#define NPROC 10
#define TAILPRIORITY 10000 /* you will see the need later */
En cuanto a las colas, vamos con un enfoque de lista vinculada y reconocemos el hecho de que necesitamos la inserción y la supresión y nos gustaría que estas operaciones fueran bastante fáciles de realizar. Así que concluimos que queremos que ambos siguiente y prev para cada entrada de la cola. También queremos apoyar un campo prioritario:
typedef struct proc_s proc_t;
typedef struct proc_s {
int priority;
proc_t *next;
proc_t *prev;
} proc_t;
Ahora, podemos definir esto, estáticamente (el alcance podría mantenerse a nivel de archivo):
proc_t readyhead, readytail, sleephead, sleeptail, freehead, freetail, proc[NPROC];
Veamos ahora lo que se necesita para quitar e insertar un proceso en una cola (Nótese que las variables de cola lista y dormida son tipos de proc_t completo, no sólo punteros a ellas. Esto simplifica el siguiente código). También añadiré un "getfirst" porque lo necesitamos para algunos propósitos.
/* Assumes that 'item' actually resides within a queue. */
proc_t * remove( proc_t * item ) {
item->prev->next= item->next;
item->next->prev= item->prev;
return item;
}
/* Not to be used if 'item' is already in another queue. */
/* Priority insertion assumes that all queues end in a tail */
proc_t * insert( proc_t * queue, proc_t * item, int priority ) {
proc_t *n, *p;
for ( n= queue->next; n->priority < priority; n= n->next ) ;
item->next= n;
item->prev= p= n->prev;
item->priority= priority;
p->next= item;
n->prev= item;
return item;
}
proc_t * getfirst( proc_t * queue ) {
if ( queue->next->priority == TAILPRIORITY )
return NULL;
return remove( queue->next );
}
Todo lo anterior supone, por supuesto, que los punteros de cabeza y cola de la cola estén debidamente inicializados y que las entradas en proc[] se inserten primero en la cola libre y se eliminen secuencialmente. (También se supone que la cola siempre tiene una "prioridad" que es el valor más grande posible y es mayor que cualquier proceso válido puede poseer: PRIORIDAD DE LA COLA).
¿Qué más podríamos hacer? Sin romper esto en pedazos como arriba, aquí hay otra oportunidad:
#define NPROC (10)
#define TAILPRIORITY (10000)
#define READYQUEUE (NPROC)
#define SLEEPQUEUE (NPROC+2)
int next[PROC+4];
int prev[PROC+4];
int prio[PROC+4];
int remove( int item ) {
int n= next[item], p= prev[item];
next[p]= n;
prev[n]= p;
return item;
}
int insert( int queue, int item, int priority ) {
int n, p;
for ( n= next[queue]; prio[n] < priority; n= next[n] ) ;
next[item]= n;
prev[item]= p= prev[n];
prio[item]= priority;
next[p]= item;
prev[n]= item;
return item;
}
int getfirst( int queue ) {
if ( next[queue] > NPROC )
return -1;
return remove( next[queue] );
}
El compilador C ahora conoce de antemano sobre la dirección de las matrices next[] y prev[] y prio[]. getfirst() ya no necesita depender de un valor de prioridad especial (aunque insert() sigue requiriendo algo parecido, aunque eso también podría cambiarse ahora).
¿Importa esto desde el punto de vista del tamaño del código y/o del rendimiento? Tal vez. Depende del compilador. Para sonreír, pruebe estos dos enfoques diferentes con el compilador SDCC y eche un vistazo al código ensamblador generado para cada uno. ¿Qué te parece?
¿Qué hay del caso de la legibilidad? ¿Cuál es más legible para ti? (No me refería a "particularmente legible" o "particularmente ilegible" sino a "consistente con cada uno".) ¿Qué hay del caso de la legibilidad? ¿Cuál es más legible para ti? (No me refería a "particularmente legible" o "particularmente ilegible", sino a "consistentes entre sí".)
¿Y qué hay de lo sostenible? ¿Y si necesitara ampliar el tipo de nodo de la lista de enlaces? ¿Sería más sostenible añadir otro array (2º ejemplo de código fuente)? ¿O más sostenible añadir otro elemento a una estructura (1er ejemplo de código fuente)? ¿Haría tanta diferencia, en absoluto?
Supongamos que estás pasando un nodo de lista enlazado. ¿Es mejor pasar un puntero o un índice? Tenga en cuenta que pasar un puntero permite a una función acceder a cualquier elemento de la estructura, incluso si no se supone que lo haga. Pero pasar un índice podría permitir colocar la visibilidad de partes específicas "en otro lugar" de modo que si hubiera un intento, el compilador podría emitir un error. Pero hay otras consideraciones, por supuesto. ¿Qué argumentos ve, a favor y en contra?
Y así va.
¿Personalmente? Encuentro que la consistencia del estilo es quizás lo más importante. Puedo acostumbrarme a casi cualquier estilo de codificación incluso a los que no me gustan mucho, en absoluto. Siempre y cuando la programación sea consistente es mayormente una cuestión de acostumbrarse a ello y seguir después de eso. Sin embargo, si la programación cambia de un modo de pensar a otro y luego a otro y hay poca o ninguna consistencia en el código, encuentro bastante difícil de leer y/o mantener bien. Así que esto es probablemente lo más importante para mí. Establecer un estilo y luego mantener la coherencia con el estilo.
Hay algunas áreas que están llenas de problemas. Por ejemplo, usar el montón en un sistema integrado en lugar de la memoria estática. (Especialmente cierto, supongo, para la familia 8051.) Es muy, muy fácil para un compilador y enlazador calcular el espacio total requerido para los arreglos estáticos y hacerte saber si puede caber en el procesador que estás usando en ese momento. Pero es muy difícil encontrar errores de memoria similares si la única forma de averiguarlo es ejecutar el programa y asegurarse de que se ejercita todo el código condicional variado necesario para forzar la combinación correcta de eventos para superar la memoria, cuando se utiliza heap.
No hay nada intrínsecamente malo con el montón. Pero en la práctica, con los sistemas integrados, necesita una buena justificación, creo. Así que buscaría criterios explícitos que justificaran su uso, si es que se usa.
Así que también busco el pensamiento artesanal en clave. ¿Por qué se hicieron ciertas elecciones? ¿Muestran buen juicio? En los casos en que se requieren pruebas extraordinarias para algún uso, ¿se han proporcionado esas pruebas y tienen sentido? Etc.
Sin embargo, en el caso de la familia 8051, las instrucciones necesarias para referenciar directamente las direcciones específicas asignadas son tan pequeñas (espacio de código) y tan rápidas, que todos los buenos compiladores de C para ello (un número muy reducido de empresas, por cierto) proporcionarán un mecanismo para el análisis estático de la ruta de llamada, de modo que se puedan asignar a las variables de función locales (con alguna posibilidad de éxito) direcciones fijas. Así que en este caso particular, esperaría encontrar un uso mucho mayor de la estática, ya sea en el ámbito de los archivos locales o en el ámbito global. Simplemente tiene demasiado sentido en el 8051.
Las transiciones de las que he formado parte han pasado de 4-8 bits de ALU y anchos de memoria a 32/64/128, con 32 bits bastante comunes ahora. La definición de qué tipo de controlador puedes encontrar en un pequeño dispositivo también ha cambiado y no me sorprende en absoluto encontrar que Linux se utiliza para poco más que parpadear unos pocos LED en un núcleo ARM7TDMI 16/32. (O incluso una superescala Cortex-A53.) Así que lo que es apropiado a nivel de codificación C variará un poco.
Espero consistencia en el código fuente y un razonamiento bien pensado para las elecciones de diseño que se hagan. Eso es lo más importante, para mí. Más allá de eso, soy un poco más flexible en cuanto a mi opinión.
1 votos
W
4 votos
Se utilizan punteros cuando procede. La pregunta es poco clara, demasiado amplia y basada en opiniones.
0 votos
Véase la segunda parte del comentario. Esto es algo que se suele aprender cuando se aprende a programar y se adquiere cierta experiencia.
0 votos
Parece una respuesta bastante religiosa. "Hijo mío, el sol brillará y el señor se mostrará y tú sabrás cuándo ha llegado lo 'apropiado'". Super condescendiente. Podría mallockearlo todo y puntearlo entero, no creo que tenga ninguna ganancia.
0 votos
Esto queda cubierto por lo de "demasiado amplio" y "basado en opiniones". Puedes llamarlo "religioso" y "condescendiente".
1 votos
Dice usted que podrían surgir problemas a medida que el sistema crezca. Sin embargo, la mayoría de los sistemas embebidos no pueden crecer demasiado debido a los limitados recursos disponibles. Yo evitaría las variables globales si programara un sistema Linux embebido, pero no veo un problema serio con ellas en un sistema bare-metal de pocos KB.
0 votos
No puedo ni empezar a contar el número de veces que he visto debatir esto durante meses seguidos. Una de las preguntas más importantes que hago cuando veo a alguien empezar este tipo de cosas es: "¿Qué consideras que es la 'programación embebida' y cómo la distingues del resto de la programación?". La respuesta suele ser muy reveladora.
0 votos
Creo que Photon tiene una buena demarcación, pocos-kB de memoria es un buen punto de partida de estar incrustado. No ejecutar un sistema operativo es otro buen indicador. Personalmente, he estado trabajando en pequeños PICs de 8bit y 8051s. Ahora bien, si ejecutas un RTOS realmente básico en un 32 bits, me tienes si eso es parte del santo canon de los dispositivos embebidos.
2 votos
@Leroy105 Algunos parecen definir incrustado como "simplemente otra forma de decir cualquier programación dirigida a cualquier producto que un usuario no utiliza como un dispositivo de propósito general destinado a ejecutar programas." Lo que convierte la definición en una cuestión de lo que piense/haga un usuario y elimina cualquiera de las importantísimas distinciones que importan a los propios programadores. Para mí, lo que marca la diferencia es el conjunto de herramientas necesarias y el conjunto de habilidades y conocimientos personales y variados que se exigen al programador (no sólo sobre programación). (Espero un buen conocimiento del enlazador, por ejemplo).
3 votos
En la programación embebida también existen argumentos para minimizar o incluso eliminar por completo el uso de punteros, especialmente cuando la fiabilidad es importante. MISRA-C, por ejemplo, impone restricciones al uso de punteros.