Quali sono i vantaggi di un sistema operativo non preventivo? e il prezzo per questi vantaggi?


14

Per un MCU in metallo scoperto, rispetto al codice casalingo con loop in background più architettura di interruzione del timer, quali sono i vantaggi di un sistema operativo non preventivo? Quali tra questi vantaggi sono abbastanza interessanti da consentire a un progetto di adottare un sistema operativo non preventivo, piuttosto che utilizzare codice fatto in casa con architettura a ciclo di background?
.

Spiegazione alla domanda:

Apprezzo molto tutti coloro che hanno risposto alla mia domanda. Sento che la risposta è stata quasi lì. Aggiungo questa spiegazione alla mia domanda qui che mostra la mia considerazione e può aiutare a restringere la domanda o renderla più precisa.

Quello che sto cercando di fare è capire come scegliere l'RTOS più appropriato per un progetto in generale.
Per raggiungere questo obiettivo, una migliore comprensione dei concetti di base e i vantaggi più interessanti di diversi tipi di RTOS e il prezzo corrispondente aiuteranno, poiché non esiste un RTOS migliore per tutte le applicazioni.
Ho letto libri sul sistema operativo alcuni anni fa ma non li ho più con me. Ho cercato su Internet prima di pubblicare la mia domanda qui e ho trovato queste informazioni molto utili: http://www.ustudy.in/node/5456 .
Ci sono molte altre informazioni utili come le presentazioni nel sito Web di diversi RTOS, articoli che confrontano la pianificazione preventiva e la pianificazione non preventiva, ecc.
Ma non ho trovato alcun argomento menzionato quando scegliere un RTOS non preventivo e quando è meglio scrivere il proprio codice usando l'interruzione del timer e il ciclo in background.
Ho certe mie risposte ma non sono abbastanza soddisfatto di loro.
Mi piacerebbe davvero conoscere la risposta o il punto di vista di persone più esperte, soprattutto nelle pratiche del settore.

Finora la mia comprensione è:
indipendentemente dall'uso o meno di un sistema operativo, alcuni tipi di codici di pianificazione sono sempre necessari, anche sotto forma di codice come:

    in the timer interrupt which occurs every 10ms  
    if(it's 10ms)  
    {  
      call function A / execute task A;  
    }  
    if(it's 50ms)  
    {  
      call function B / execute task B;  
    }  

Vantaggio 1:
un sistema operativo non preventivo indica il modo / lo stile di programmazione per il codice di pianificazione, in modo che gli ingegneri possano condividere la stessa vista anche se prima non si trovavano nello stesso progetto. Quindi, con la stessa visione delle attività concettuali, gli ingegneri possono lavorare su diverse attività e testarle, profilandole il più indipendentemente possibile.
Ma quanto possiamo davvero guadagnare da questo? Se gli ingegneri stanno lavorando allo stesso progetto, possono trovare il modo di condividere bene la stessa vista senza usare un sistema operativo non preventivo.
Se un ingegnere proviene da un altro progetto o società, otterrà il vantaggio se avesse già conosciuto il sistema operativo. Ma se non lo facesse, di nuovo, sembra non fare molta differenza per lui imparare un nuovo sistema operativo o un nuovo codice.

Vantaggio 2:
se il codice del sistema operativo è stato testato bene, si risparmia tempo dal debug. Questo è davvero un buon vantaggio.
Ma se l'applicazione ha solo circa 5 compiti, penso che non sia davvero complicato scrivere il proprio codice usando l'interruzione del timer e il ciclo in background.

Un sistema operativo non preventivo in questo caso è riferito a un sistema operativo commerciale / libero / legacy con uno scheduler non preventivo.
Quando ho pubblicato questa domanda, penso principalmente a determinati sistemi operativi come:
(1) KISS Kernel (A Small NonPreemptive RTOS - rivendicato dal suo sito Web)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (RTOS leggero - rivendicato dal suo sito Web)
(3) FreeRTOS (È un RTOS preventivo, ma a quanto ho capito, può essere configurato anche come RTOS non preventivo)
(4) uC / OS (simile a FreeRTOS)
(5 ) codice legacy del sistema operativo / scheduler in alcune società (solitamente creato e gestito internamente dalla società)
(Impossibile aggiungere più collegamenti a causa della limitazione dal nuovo account StackOverflow)

A quanto ho capito, un sistema operativo non preventivo è una raccolta di questi codici:
(1) uno scheduler che utilizza una strategia non preventiva.
(2) strutture per la comunicazione tra compiti, mutex, sincronizzazione e controllo del tempo.
(3) gestione della memoria.
(4) altre utili strutture / librerie come File System, stack di rete, GUI e così via (FreeRTOS e uC / OS forniscono questi, ma non sono sicuro che funzionino ancora quando lo scheduler è configurato come non preventivo)
Alcuni di loro non sono sempre lì. Ma lo scheduler è un must.


È praticamente tutto in breve. Se hai un carico di lavoro che deve essere multithread e puoi permetterti il ​​sovraccarico, usa un sistema operativo di threading. In caso contrario, nella maggior parte dei casi è sufficiente un semplice "scheduler" basato su attività o attività. E per capire se il multitasking preventivo o cooperativo è il migliore ... Immagino che dipenda dal sovraccarico e da quanto controllo vuoi avere sul multitasking che devi fare.
akohlsmith l'

Risposte:


13

Questo puzza un po 'fuori tema, ma proverò a rimetterlo in carreggiata.

Multitasking preventivo significa che il sistema operativo o il kernel possono sospendere il thread attualmente in esecuzione e passare a un altro in base a qualsiasi euristica di pianificazione che ha in atto. La maggior parte delle volte che i thread in esecuzione non hanno idea che ci siano altre cose in corso nel sistema, e ciò che questo significa per il tuo codice è che devi stare attento a progettarlo in modo che se il kernel decida di sospendere un thread nel mezzo di un operazione in più passaggi (ad esempio, cambiare un'uscita PWM, selezionare un nuovo canale ADC, leggere lo stato da una periferica I2C, ecc.) e lasciare che un altro thread venga eseguito per un po ', che questi due thread non interferiscano l'uno con l'altro.

Un esempio arbitrario: supponiamo che tu sia nuovo nei sistemi embedded multithread e che tu abbia un piccolo sistema con un ADC I2C, un LCD SPI e una EEPROM I2C. Hai deciso che sarebbe una buona idea avere due thread: uno che legge dall'ADC e scrive i campioni nella EEPROM e uno che legge gli ultimi 10 campioni, li media e li visualizza sul display LCD SPI. Il design inesperto sarebbe simile a questo (grossolanamente semplificato):

char i2c_read(int i2c_address, char databyte)
{
    turn_on_i2c_peripheral();
    wait_for_clock_to_stabilize();

    i2c_generate_start();
    i2c_set_data(i2c_address | I2C_READ);
    i2c_go();
    wait_for_ack();
    i2c_set_data(databyte);
    i2c_go();
    wait_for_ack();
    i2c_generate_start();
    i2c_get_byte();
    i2c_generate_nak();
    i2c_stop();
    turn_off_i2c_peripheral();
}

char i2c_write(int i2c_address, char databyte)
{
    turn_on_i2c_peripheral();
    wait_for_clock_to_stabilize();

    i2c_generate_start();
    i2c_set_data(i2c_address | I2C_WRITE);
    i2c_go();
    wait_for_ack();
    i2c_set_data(databyte);
    i2c_go();
    wait_for_ack();
    i2c_generate_start();
    i2c_get_byte();
    i2c_generate_nak();
    i2c_stop();
    turn_off_i2c_peripheral();
}

adc_thread()
{
    int value, sample_number;

    sample_number = 0;

    while (1) {
        value = i2c_read(ADC_ADDR);
        i2c_write(EE_ADDR, EE_ADDR_REG, sample_number);
        i2c_write(EE_ADDR, EE_DATA_REG, value);

        if (sample_number < 10) {
            ++sample_number;
        } else {
            sample_number = 0;
        }
    };
}

lcd_thread()
{
    int i, avg, sample, hundreds, tens, ones;

    while (1) {
        avg = 0;
        for (i=0; i<10; i++) {
            i2c_write(EE_ADDR, EE_ADDR_REG, i);
            sample = i2c_read(EE_ADDR, EE_DATA_REG);
            avg += sample;
        }

        /* calculate average */
        avg /= 10;

        /* convert to numeric digits for display */
        hundreds = avg / 100;
        tens = (avg % 100) / 10;
        ones = (avg % 10);

        spi_write(CS_LCD, LCD_CLEAR);
        spi_write(CS_LCD, '0' + hundreds);
        spi_write(CS_LCD, '0' + tens);
        spi_write(CS_LCD, '0' + ones);
    }
}

Questo è un esempio molto rozzo e veloce. Non codificare in questo modo!

Ora ricorda, il sistema operativo multitasking preventivo può sospendere uno di questi thread in qualsiasi riga del codice (in realtà in qualsiasi istruzione di assemblaggio) e concedere il tempo di esecuzione all'altro thread.

Pensaci. Immagina cosa accadrebbe se il sistema operativo decidesse di sospendere adc_thread()tra l'impostazione dell'indirizzo EE per scrivere e scrivere i dati effettivi. lcd_thread()correva, andava in giro con la periferica I2C per leggere i dati di cui aveva bisogno e, quando adc_thread()si avviava di nuovo per funzionare, la EEPROM non si trovava nello stesso stato in cui era rimasta. Le cose non funzionerebbero affatto bene. Peggio ancora, potrebbe anche funzionare la maggior parte delle volte, ma non tutte le volte, e impazziresti cercando di capire perché il tuo codice non funziona quando sembra che dovrebbe!

Questo è un esempio nel migliore dei casi; il sistema operativo potrebbe decidere di uscire i2c_write()dal adc_thread()contesto e ricominciare a eseguirlo dal lcd_thread()contesto! Le cose possono diventare davvero disordinate molto velocemente.

Quando si scrive codice per funzionare in un ambiente multitasking preventivo, è necessario utilizzare i meccanismi di blocco per assicurarsi che se il codice viene sospeso in un momento inopportuno, l'inferno non si scatena.

Il multitasking cooperativo, d'altra parte, significa che ogni thread ha il controllo di quando rinuncia al suo tempo di esecuzione. La codifica è più semplice, ma il codice deve essere progettato con cura per assicurarsi che tutti i thread abbiano abbastanza tempo per essere eseguiti. Un altro esempio inventato:

char getch()
{
    while (! (*uart_status & DATA_AVAILABLE)) {
        /* do nothing */
    }

    return *uart_data_reg;
}

void putch(char data)
{
    while (! (*uart_status & SHIFT_REG_EMPTY)) {
        /* do nothing */
    }

    *uart_data_reg = data;
}

void echo_thread()
{
    char data;

    while (1) {
        data = getch();
        putch(data);
        yield_cpu();
    }
}

void seconds_counter()
{
    int count = 0;

    while (1) {
        ++count;
        sleep_ms(1000);
        yield_cpu();
    }
}

Quel codice non funzionerà come pensi, o anche se sembra funzionare, non funzionerà all'aumentare della velocità dei dati del thread dell'eco. Ancora una volta, prendiamoci un minuto per guardarlo.

echo_thread()attende che un byte appaia in una UART, quindi lo ottiene e attende fino a quando non c'è spazio per scriverlo, quindi lo scrive. Fatto ciò, consente agli altri thread di girare. seconds_counter()incrementerà un conteggio, attenderà 1000 ms e quindi darà agli altri thread la possibilità di essere eseguito. Se due byte entrano nell'UART mentre sleep()sta succedendo così tanto tempo, potresti non vederli perché il nostro ipotetico UART non ha FIFO per memorizzare i caratteri mentre la CPU è impegnata a fare altre cose.

Il modo corretto di implementare questo esempio molto scarso sarebbe quello di mettere yield_cpu()ovunque un ciclo indaffarato. Questo aiuterà le cose a muoversi, ma potrebbe causare altri problemi. ad esempio, se il tempismo è fondamentale e si assegna la CPU a un altro thread che impiega più tempo del previsto, è possibile che il tempismo venga annullato. Un sistema operativo multitasking preventivo non avrebbe questo problema perché sospende forzatamente i thread per assicurarsi che tutti i thread siano pianificati correttamente.

Cosa c'entra questo con un timer e un loop in background? Il timer e il loop di sfondo sono molto simili all'esempio cooperativo multitasking sopra:

void timer_isr(void)
{
    ++ticks;
    if ((ticks % 10)) == 0) {
        ten_ms_flag = TRUE;
    }

    if ((ticks % 100) == 0) {
        onehundred_ms_flag = TRUE;
    }

    if ((ticks % 1000) == 0) {
        one_second_flag = TRUE;
    }
}

void main(void)
{
    /* initialization of timer ISR, etc. */

    while (1) {
        if (ten_ms_flag) {
            if (kbhit()) {
                putch(getch());
            }
            ten_ms_flag = FALSE;
        }

        if (onehundred_ms_flag) {
                    get_adc_data();
            onehundred_ms_flag = FALSE;
        }

        if (one_second_flag) {
            ++count;
                    update_lcd();
            one_second_flag = FALSE;
        }
    };
}

Questo sembra abbastanza vicino all'esempio di threading cooperativo; hai un timer che imposta eventi e un ciclo principale che li cerca e agisce su di essi in modo atomico. Non devi preoccuparti che i "thread" di ADC e LCD interferiscano l'uno con l'altro perché l'uno non interromperà mai l'altro. Devi ancora preoccuparti che un "thread" impieghi troppo tempo; ad es. cosa succede se get_adc_data()impiega 30 ms? perderai tre opportunità per verificare la presenza di un personaggio e fargli eco.

L'implementazione di loop + timer è spesso molto più semplice da implementare rispetto a un microkernel cooperativo multitasking poiché il codice può essere progettato più specifico per l'attività a portata di mano. In realtà non stai multitasking, ma stai progettando un sistema fisso in cui dai tempo a ciascun sottosistema di svolgere i suoi compiti in modo molto specifico e prevedibile. Anche un sistema multitasking cooperativo deve avere una struttura di attività generica per ogni thread e il thread successivo da eseguire è determinato da una funzione di pianificazione che può diventare piuttosto complessa.

I meccanismi di blocco per tutti e tre i sistemi sono gli stessi, ma il sovraccarico richiesto per ciascuno è abbastanza diverso.

Personalmente, scrivo quasi sempre l'ultimo standard, l'implementazione di loop + timer. Trovo che il threading sia qualcosa che dovrebbe essere usato con parsimonia. Non solo è più complesso scrivere e debug, ma richiede anche più overhead (un microkernel multitasking preventivo sarà sempre più grande di un timer stupidamente semplice e di un follower di eventi di loop principale).

C'è anche un detto che chiunque lavori sui thread apprezzerà:

if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls

:-)


Grazie mille per la tua risposta con esempi dettagliati, akohlsmith. Tuttavia, non posso concludere dalla tua risposta perché hai scelto l'architettura semplice di timer e loop di sfondo piuttosto che il multitasking cooperativo . Non fraintendetemi. Apprezzo molto la tua risposta, che fornisce molte informazioni utili su diverse pianificazioni. Non ho capito.
Hailang

Potresti per favore lavorare un po 'di più su questo?
Hailang

Grazie, akohlsmith. Mi piace la frase che hai messo alla fine. Mi ci è voluto un po 'per riconoscerlo :) Tornando al punto della tua risposta, quasi sempre codifichi l'implementazione loop + timer. Quindi, nei casi in cui hai rinunciato a questa implementazione e sei passato a un sistema operativo non preventivo, cosa ti ha spinto a farlo?
Hailang

Sono andato con sistemi multitasking cooperativi e preventivi quando stavo eseguendo il sistema operativo di qualcun altro. Linux, ThreadX, ucOS-ii o QNX. Anche in alcune di quelle situazioni, ho usato il semplice ed efficace loop + evento (mi poll()viene subito in mente).
akohlsmith l'

Non sono un fan del threading o del multitasking in embedded, ma so che per sistemi complessi è l'unica opzione sana. I sistemi micro-operativi in ​​scatola offrono un modo rapido per avviare e far funzionare le cose e spesso forniscono anche driver di dispositivo.
akohlsmith l'

6

Il multi-tasking può essere un'astrazione utile in molti progetti di microcontrollori, anche se un vero programmatore preventivo sarebbe troppo pesante e non necessario nella maggior parte dei casi. Ho realizzato oltre 100 progetti di microcontrollori. Ho usato il tasking cooperativo diverse volte, ma il passaggio preventivo di attività con il relativo bagaglio associato non è stato finora appropriato.

I problemi con il tasking preventivo come applicato al tasking cooperativo sono:

  1. Molto più pesante. I programmatori di attività preventive sono più complicati, occupano più spazio di codice e richiedono più cicli. Richiedono anche almeno un interrupt. Questo è spesso un onere inaccettabile per l'applicazione.

  2. I mutex sono richiesti intorno alle strutture a cui è possibile accedere contemporaneamente. In un sistema cooperativo, non devi semplicemente chiamare TASK_YIELD nel mezzo di quella che dovrebbe essere un'operazione atomica. Ciò ha effetto sulle code, ha condiviso lo stato globale e si insinua in molti luoghi.

In generale, dedicare un'attività a un determinato lavoro ha senso quando la CPU può supportarlo e il lavoro è abbastanza complicato con abbastanza operazioni dipendenti dalla storia che dividerlo in alcuni singoli eventi separati sarebbe ingombrante. Questo è generalmente il caso della gestione di un flusso di input delle comunicazioni. Tali cose sono generalmente fortemente guidate dallo stato a seconda di alcuni input precedenti. Ad esempio, potrebbero essere presenti byte di codice operativo seguiti da byte di dati univoci per ciascun codice operativo. Quindi c'è il problema di questi byte che ti arrivano quando qualcos'altro ha voglia di inviarli. Con un'attività separata che gestisce il flusso di input, è possibile farlo apparire nel codice attività come se si stesse uscendo e ottenendo il byte successivo.

Complessivamente, le attività sono utili quando c'è molto contesto statale. Le attività sono sostanzialmente macchine a stati con il PC come variabile di stato.

Molte cose che una micro deve fare possono essere espresse come risposta a una serie di eventi. Di conseguenza, di solito ho un ciclo di eventi principale. Questo controlla ogni possibile evento in sequenza, quindi torna all'inizio e fa di nuovo tutto. Quando la gestione di un evento richiede più di pochi cicli, di solito torno indietro all'inizio del ciclo di eventi dopo aver gestito l'evento. Questo in effetti significa che gli eventi hanno una priorità implicita in base a dove vengono controllati nell'elenco. Su molti sistemi semplici, questo è abbastanza buono.

A volte ottieni compiti un po 'più complicati. Questi possono spesso essere suddivisi in una sequenza di un numero limitato di cose separate da fare. In questi casi è possibile utilizzare flag interni come eventi. Ho fatto questo genere di cose molte volte su PIC di fascia bassa.

Se si dispone della struttura di eventi di base come sopra ma si deve anche rispondere a un flusso di comandi su UART, ad esempio, è utile disporre di un'attività separata per gestire il flusso UART ricevuto. Alcuni microcontrollori hanno risorse hardware limitate per il multi-tasking, come un PIC 16 che non è in grado di leggere o scrivere il proprio stack di chiamate. In tali casi, utilizzo quello che chiamo pseudo-task per il processore dei comandi UART. Il ciclo di eventi principale gestisce ancora tutto il resto, ma uno dei suoi eventi da gestire è che UART ha ricevuto un nuovo byte. In tal caso, passa a una routine che esegue questa pseudo-attività. Il modulo di comando UART contiene il codice dell'attività e l'indirizzo di esecuzione e alcuni valori di registro dell'attività vengono salvati nella RAM in quel modulo. Il codice saltato dal loop degli eventi salva i registri correnti, carica i registri delle attività salvati, e passa all'indirizzo di riavvio dell'attività. Il codice attività richiama una macro YIELD che fa il contrario, che alla fine ritorna all'inizio del ciclo di eventi principale. In alcuni casi il ciclo di eventi principale esegue lo pseudo-task una volta per passaggio, in genere in fondo per renderlo un evento a bassa priorità.

Su un PIC 18 e versioni successive, utilizzo un vero sistema di tasking cooperativo poiché lo stack di chiamate è leggibile e scrivibile dal firmware. Su questi sistemi, l'indirizzo di riavvio, alcune altre parti di stato e il puntatore dello stack di dati sono conservati in un buffer di memoria per ogni attività. Per consentire a tutte le altre attività di essere eseguite una volta, un'attività chiama TASK_YIELD. Ciò consente di salvare lo stato dell'attività corrente, scorrere l'elenco per l'attività disponibile successiva, caricarne lo stato, quindi eseguirlo.

In questa architettura, il ciclo degli eventi principale è solo un altro compito, con una chiamata a TASK_YIELD nella parte superiore del ciclo.

Tutto il mio codice multi-tasking per PIC è disponibile gratuitamente. Per vederlo, installa la versione PIC Development Tools su http://www.embedinc.com/pic/dload.htm . Cerca i file con "task" nei loro nomi nella directory SOURCE> PIC per i PIC a 8 bit e la directory SOURCE> DSPIC per i PIC a 16 bit.


i mutex possono ancora essere necessari nei sistemi cooperativi multitasking, sebbene sia raro. L'esempio tipico è un ISR che necessita dell'accesso a una sezione critica. Questo può quasi sempre essere evitato attraverso una migliore progettazione o scegliendo un contenitore di dati appropriato per i dati critici.
akohlsmith,

@akoh: Sì, in alcune occasioni ho usato i mutex per gestire una risorsa condivisa, come l'accesso al bus SPI. Il mio punto era che i mutex non sono intrinsecamente richiesti nella misura in cui si trovano in un sistema preventivo. Non intendevo dire che non sono mai necessari o mai utilizzati in un sistema cooperativo. Inoltre, un mutex in un sistema cooperativo può essere semplice come girare in un loop TASK_YIELD controllando un singolo bit. In un sistema preventivo, in genere devono essere integrati nel kernel.
Olin Lathrop,

@OlinLathrop: Penso che il vantaggio più significativo dei sistemi non preventivi quando si tratta di mutex sia che sono richiesti solo quando si interagisce direttamente con gli interrupt (che per loro natura sono preventivi) o quando è necessario il tempo di trattenere una risorsa protetta supera il tempo che si vuole trascorrere tra le chiamate "yield", oppure si vuole trattenere una risorsa protetta attorno a una chiamata che "potrebbe" produrre (ad es. "scrivere dati su un file"). In alcune occasioni in cui avere un rendimento in una chiamata "Scrivi dati" sarebbe stato un problema, ho incluso ...
Supercat

... un metodo per verificare la quantità di dati che possono essere scritti immediatamente e un metodo (che potrebbe probabilmente produrre) per garantire la disponibilità di una certa quantità (accelerando il recupero di blocchi flash sporchi e aspettando fino a quando un numero adeguato è stato recuperato) .
supercat

Ciao Olin, mi piace molto la tua risposta. Le sue informazioni vanno ben oltre le mie domande. Include molte esperienze pratiche.
Hailang

1

Modifica: (Lascerò il mio post precedente qui sotto; forse un giorno aiuterà qualcuno.)

Sistemi operativi multitasking di qualsiasi tipo e routine di servizio di interruzione non sono - o non dovrebbero essere - architetture di sistema concorrenti. Sono pensati per diversi lavori a diversi livelli del sistema. Gli interrupt sono realmente destinati a brevi sequenze di codici per gestire faccende immediate come il riavvio di un dispositivo, il polling di dispositivi non interrompibili, il cronometraggio nel software, ecc. Di solito si presume che lo sfondo eseguirà ulteriori elaborazioni che non sono più critiche dal punto di vista temporale dopo bisogni immediati sono stati soddisfatti. Se tutto ciò che devi fare è riavviare un timer e attivare o disattivare un LED o un altro dispositivo, l'ISR di solito può fare tutto in primo piano in modo sicuro. Altrimenti deve informare lo sfondo (impostando un flag o accodando un messaggio) che qualcosa deve fare e rilasciare il processore.

Ho visto molto semplici strutture di programma il cui ciclo di fondo è solo un ciclo di inattività: for(;;){ ; }. Tutto il lavoro è stato svolto nel timer ISR. Questo può funzionare quando il programma deve ripetere alcune operazioni costanti che sono garantite per finire in meno di un periodo di tempo; vengono in mente alcuni tipi limitati di elaborazione del segnale.

Personalmente, scrivo ISR che puliscono e escono, e lascio che lo sfondo prenda il sopravvento su qualsiasi altra cosa abbia bisogno di fare, anche se è semplice come moltiplicare e aggiungere ciò che potrebbe essere fatto in una frazione di un periodo di tempo. Perché? Un giorno, avrò la brillante idea di aggiungere un'altra funzione "semplice" al mio programma e "diamine, ci vorrà solo un breve ISR per farlo" e improvvisamente la mia architettura precedentemente semplice fa crescere alcune interazioni che non avevo pianificato e accade in modo incoerente. Non sono molto divertenti per il debug.


(Confronto precedentemente pubblicato di due tipi di multi-tasking)

Commutazione delle attività: MT preventiva si occupa della commutazione delle attività per te, incluso assicurarsi che nessun thread sia affamato di CPU e che i thread ad alta priorità vengano eseguiti non appena sono pronti. Cooperative MT richiede al programmatore di assicurarsi che nessun thread mantenga il processore troppo a lungo alla volta. Dovrai anche decidere quanto tempo è troppo lungo. Ciò significa anche che ogni volta che modifichi il codice, dovrai essere consapevole del fatto che qualsiasi segmento di codice ora superi quel quantum temporale.

Protezione delle operazioni non atomiche: con un PMT, dovrai assicurarti che non si verifichino scambi di thread nel mezzo di operazioni che non devono essere divise. Lettura / scrittura di determinate coppie di registri di dispositivi che devono essere gestite in un ordine particolare o entro un tempo massimo, ad esempio. Con CMT è abbastanza semplice: non cedere il processore nel mezzo di un'operazione del genere.

Debug: generalmente più semplice con CMT, poiché si pianifica quando / dove si verificheranno le opzioni di thread. Le condizioni di competizione tra thread e bug relativi a operazioni non thread-safe con un PMT sono particolarmente difficili da eseguire il debug perché le modifiche ai thread sono probabilistiche, quindi non ripetibili.

Comprensione del codice: i thread scritti per un PMT sono praticamente scritti come se potessero essere autonomi. I thread scritti per un CMT sono scritti come segmenti e, a seconda della struttura del programma scelta, può essere più difficile da seguire per un lettore.

Utilizzo del codice libreria non thread-safe: è necessario verificare che ciascuna funzione di libreria chiamata in un thread thread-PMT sicuro. printf () e scanf () e le loro varianti non sono quasi sempre thread-safe. Con un CMT, saprai che non si verificherà alcun cambio di thread, tranne quando si produce in modo specifico il processore.

Un sistema basato su macchine a stati finiti per controllare un dispositivo meccanico e / o tracciare eventi esterni sono spesso buoni candidati per CMT, poiché ad ogni evento non c'è molto da fare: avviare o arrestare un motore, impostare un flag, scegliere lo stato successivo , ecc. Pertanto, le funzioni di cambio di stato sono intrinsecamente brevi.

Un approccio ibrido può funzionare davvero bene in questi tipi di sistemi: CMT per gestire la macchina a stati (e quindi la maggior parte dell'hardware) in esecuzione come un thread e uno o due più thread per eseguire calcoli in esecuzione più lunghi avviati da uno stato modificare.


Grazie per la tua risposta, JRobert. Ma non è su misura per la mia domanda. Confronta il sistema operativo preventivo e il sistema operativo non preventivo, ma non confronta il sistema operativo non preventivo rispetto al sistema operativo non.
Hailang

Giusto - scusa. La mia modifica dovrebbe rispondere meglio alla tua domanda.
JRobert,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.