Jakie są zalety niepostępującego systemu operacyjnego? a cena za te świadczenia?

14

W przypadku MCU z obnażonym metalem, w porównaniu do domowej roboty kodu z pętlą w tle i architekturą przerwania timera, jakie są korzyści z nie-zapobiegawczego systemu operacyjnego? Jakie spośród tych korzyści są na tyle atrakcyjne, że projekt może przyjąć nieprzeszkadzający system operacyjny, a nie używać domowego kodu z architekturą pętli tła?
.

Wyjaśnienie do pytania:

Naprawdę doceniam wszystkich, którzy odpowiedzieli na moje pytanie. Wydaje mi się, że odpowiedź już prawie nadeszła. Dodaję to wyjaśnienie do mojego pytania tutaj, które pokazuje moje własne rozważania i może pomóc zawęzić pytanie lub uczynić je bardziej precyzyjnym.

Staram się zrozumieć, jak wybrać najbardziej odpowiedni RTOS dla danego projektu.
Aby to osiągnąć, pomocne będzie lepsze zrozumienie podstawowych pojęć i najbardziej atrakcyjnych korzyści z różnych rodzajów RTOS i odpowiedniej ceny, ponieważ nie ma najlepszej RTOS dla wszystkich aplikacji.
Kilka lat temu czytałem książki o systemie operacyjnym, ale nie mam ich już przy sobie. Szukałem w Internecie, zanim opublikowałem tutaj pytanie, i okazało się, że ta informacja była najbardziej pomocna: http://www.ustudy.in/node/5456 .
Istnieje wiele innych pomocnych informacji, takich jak wprowadzenie na stronie internetowej różnych RTOS, artykuły porównujące planowanie wyprzedzające i planowanie nieprzewidziane itp.
Ale nie znalazłem żadnego tematu wspomnianego, kiedy wybrać nieprzekazujący RTOS, a kiedy lepiej jest po prostu napisać własny kod, używając przerwania timera i pętli tła.
Mam pewne własne odpowiedzi, ale nie jestem z nich wystarczająco zadowolony.
Naprawdę chciałbym poznać odpowiedź lub opinię bardziej doświadczonych ludzi, szczególnie w praktyce przemysłowej.

Jak dotąd rozumiem:
bez względu na to, czy korzystasz z systemu operacyjnego, zawsze potrzebujesz pewnych kodów planowania, nawet w postaci kodu takiego jak:

    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;  
    }  

Korzyść 1:
nieprzekazujący system operacyjny określa sposób / styl programowania dla kodu planowania, aby inżynierowie mogli dzielić ten sam widok, nawet jeśli wcześniej nie byli w tym samym projekcie. Następnie, mając ten sam pogląd na temat zadania koncepcyjnego, inżynierowie mogą pracować nad różnymi zadaniami i testować je, profilować je w jak największym stopniu niezależnie.
Ale ile naprawdę możemy na tym zyskać? Jeśli inżynierowie pracują nad tym samym projektem, mogą znaleźć sposób, aby dobrze dzielić ten sam widok bez korzystania z nieprzeszkadzającego systemu operacyjnego.
Jeśli jeden inżynier pochodzi z innego projektu lub firmy, odniesie korzyść, jeśli wcześniej znał system operacyjny. Ale jeśli tego nie zrobił, to nie wydaje się, żeby uczynił nowy system operacyjny lub nowy fragment kodu.

Korzyści 2:
Jeśli kod systemu operacyjnego został dobrze przetestowany, oszczędza to czas od debugowania. To naprawdę dobra zaleta.
Ale jeśli aplikacja ma tylko około 5 zadań, myślę, że pisanie własnego kodu za pomocą przerwania timera i pętli tła nie jest zbyt trudne.

Nie-zapobiegawczy system operacyjny jest tutaj określany jako komercyjny / wolny / starszy system operacyjny z nieprzewidzianym harmonogramem.
Kiedy opublikowałem to pytanie, myślę głównie o niektórych systemach operacyjnych, takich jak:
(1) KISS Kernel (Mały niepostępny system RTOS - zgłoszony przez jego stronę internetową)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (lekki RTOS - deklarowany przez swoją stronę internetową)
(3) FreeRTOS (To jest prewencyjny RTOS, ale jak rozumiem, może być również skonfigurowany jako nieprzekazowy RTOS)
(4) uC / OS (podobny do FreeRTOS)
(5) ) starszy kod systemu operacyjnego / harmonogramu w niektórych firmach (zazwyczaj tworzony i obsługiwany przez firmę wewnętrznie)
(Nie można dodać więcej linków, ponieważ ograniczenia z nowego konta StackOverflow)

Jak rozumiem, nieprzenikliwy system operacyjny to zbiór następujących kodów:
(1) program planujący korzystający ze strategii nieprewencyjnej.
(2) udogodnienia do komunikacji między zadaniami, muteksu, synchronizacji i kontroli czasu.
(3) zarządzanie pamięcią.
(4) inne przydatne narzędzia / biblioteki, takie jak system plików, stos sieciowy, GUI itp. (FreeRTOS i uC / OS to zapewniają, ale nie jestem pewien, czy nadal działają, gdy harmonogram jest skonfigurowany jako zapobiegawczy)
Niektóre z nie zawsze tam są. Ale harmonogram jest koniecznością.

grad
źródło
To w skrócie wszystko. Jeśli masz obciążenie wymagające wielowątkowości i możesz sobie pozwolić na koszty ogólne, użyj systemu operacyjnego do obsługi wątków. W przeciwnym razie w większości przypadków wystarczy prosty „harmonogram” zorientowany na czas lub zadanie. I dowiedzieć się, czy najlepsze jest zapobiegawcze czy kooperacyjne wielozadaniowość ... Myślę, że sprowadza się to do narzutu i tego, ile chcesz mieć kontroli nad wielozadaniowością.
akohlsmith,

Odpowiedzi:

13

Pachnie to trochę nie na temat, ale postaram się skierować to z powrotem na właściwe tory.

Zapobiegawcza wielozadaniowość oznacza, że ​​system operacyjny lub jądro może zawiesić aktualnie działający wątek i przejść do innego w oparciu o dowolną heurystykę planowania. Najczęściej działające wątki nie mają pojęcia, że ​​w systemie dzieją się inne rzeczy, a to oznacza dla twojego kodu to, że musisz uważnie go zaprojektować, aby jeśli jądro zdecydowało się zawiesić wątek w środku wieloetapowa operacja (np. zmiana wyjścia PWM, wybór nowego kanału ADC, odczyt stanu z urządzenia peryferyjnego I2C itp.) i pozostawienie na jakiś czas innego wątku, aby te dwa wątki nie kolidowały ze sobą.

Dowolny przykład: załóżmy, że jesteś nowy w wielowątkowych systemach osadzonych i masz mały system z I2C ADC, SPI LCD i I2C EEPROM. Zdecydowałeś, że dobrym pomysłem będzie posiadanie dwóch wątków: jednego, który odczytuje z ADC i zapisuje próbki do EEPROM, a drugi, który odczytuje 10 ostatnich próbek, uśrednia je i wyświetla na wyświetlaczu SPI LCD. Niedoświadczony projekt wyglądałby mniej więcej tak (rażąco uproszczony):

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);
    }
}

To bardzo prymitywny i szybki przykład. Nie koduj w ten sposób!

Teraz pamiętaj, że wyprzedzający wielozadaniowy system operacyjny może zawiesić jeden z tych wątków w dowolnym wierszu kodu (właściwie w dowolnej instrukcji asemblacji) i dać czas drugiemu wątkowi na uruchomienie.

Pomyśl o tym. Wyobraź sobie, co by się stało, gdyby system operacyjny zdecydował się zawiesić adc_thread()między ustawieniem adresu EE, aby zapisywać i zapisywać rzeczywiste dane. lcd_thread()działał, kręcił się wokół urządzenia peryferyjnego I2C, aby odczytać potrzebne dane, a gdy adc_thread()znów nadejdzie jego kolej, pamięć EEPROM nie będzie w tym samym stanie, w jakim została. Wszystko nie działałoby zbyt dobrze. Co gorsza, może nawet działać przez większość czasu, ale nie przez cały czas, i oszalałbyś, próbując dowiedzieć się, dlaczego twój kod nie działa, gdy WYGLĄDA tak, jak powinien!

To najlepszy przykład; system operacyjny może zdecydować o uprzedzeniu i2c_write()z adc_thread()kontekstu i ponownym uruchomieniu go z lcd_thread()kontekstu! Sprawy mogą się naprawdę szybko popsuć.

Kiedy piszesz kod do pracy w środowisku wyprzedzającym wielozadaniowość, musisz użyć mechanizmów blokujących, aby upewnić się, że jeśli Twój kod zostanie zawieszony w nieodpowiednim czasie, całe piekło się nie rozpadnie.

Z kolei wielozadaniowość kooperacyjna oznacza, że ​​każdy wątek kontroluje, kiedy rezygnuje z czasu wykonywania. Kodowanie jest prostsze, ale kod musi być starannie zaprojektowany, aby upewnić się, że wszystkie wątki mają wystarczająco dużo czasu na uruchomienie. Kolejny wymyślony przykład:

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();
    }
}

Ten kod nie będzie działał tak, jak myślisz, a nawet jeśli wydaje się, że działa, nie będzie działał wraz ze wzrostem szybkości przesyłania danych wątku echa. Jeszcze raz, spójrzmy na to.

echo_thread()czeka na pojawienie się bajtu w UART, a następnie pobiera go i czeka, aż będzie miejsce na napisanie go, a następnie zapisuje. Po wykonaniu tej czynności można uruchomić inne wątki. seconds_counter()zwiększy licznik, odczekaj 1000 ms, a następnie da innym wątkom szansę na uruchomienie. Jeśli dwa bajty wejdą do UART podczas tak długiego czasu sleep(), możesz przegapić ich zobaczenie, ponieważ nasz hipotetyczny UART nie ma FIFO do przechowywania znaków, gdy procesor jest zajęty robieniem innych rzeczy.

Prawidłowym sposobem zaimplementowania tego bardzo kiepskiego przykładu byłoby umieszczenie yield_cpu()gdziekolwiek masz zajętą ​​pętlę. Pomoże to w rozwoju, ale może powodować inne problemy. np. jeśli czas ma kluczowe znaczenie, a procesor przechodzi na inny wątek, który trwa dłużej niż się spodziewasz, możesz zostać odrzucony. System operacyjny z wyprzedzeniem wielozadaniowości nie miałby tego problemu, ponieważ siłą zawiesza wątki, aby upewnić się, że wszystkie wątki są poprawnie zaplanowane.

Co to ma wspólnego z zegarem i pętlą tła? Czasomierz i pętla tła są bardzo podobne do powyższego przykładu wielozadaniowości kooperacyjnej:

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;
        }
    };
}

Wygląda to bardzo podobnie do kooperacyjnego przykładu wątków; masz licznik czasu, który konfiguruje zdarzenia, i główną pętlę, która ich szuka i działa na nie w sposób atomowy. Nie musisz się martwić, że „wątki” ADC i LCD zakłócają się wzajemnie, ponieważ jeden nigdy nie będzie przeszkadzał drugiemu. Nadal musisz się martwić, że „wątek” będzie trwał zbyt długo; np. co się stanie, jeśli get_adc_data()zajmie 30ms? stracisz trzy możliwości sprawdzenia postaci i powtórzenia jej.

Implementacja pętli + timera jest często o wiele prostsza do wdrożenia niż mikrojądro współpracujące z wieloma zadaniami, ponieważ kod można zaprojektować bardziej specyficznie dla danego zadania. Nie jest tak naprawdę wielozadaniowość, jak projektowanie stałego systemu, w którym każdy podsystem ma trochę czasu na wykonanie swoich zadań w bardzo konkretny i przewidywalny sposób. Nawet system wielozadaniowy współpracujący musi mieć ogólną strukturę zadań dla każdego wątku, a następny wątek do uruchomienia jest określany przez funkcję planowania, która może stać się dość złożona.

Mechanizmy blokujące dla wszystkich trzech systemów są takie same, ale narzut wymagany dla każdego z nich jest zupełnie inny.

Osobiście prawie zawsze koduję do tego ostatniego standardu, implementacji pętli + timera. Uważam, że gwintowanie powinno być stosowane bardzo oszczędnie. Pisanie i debugowanie jest nie tylko bardziej skomplikowane, ale także wymaga większego nakładu pracy (zapobiegawcze wielozadaniowe mikrojądro zawsze będzie większe niż głupio prosty licznik zdarzeń i obserwator zdarzeń w głównej pętli).

Jest też powiedzenie, że każdy, kto pracuje nad wątkami, doceni:

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

:-)

akohlsmith
źródło
Dziękuję bardzo za odpowiedź ze szczegółowymi przykładami, johnsmith. Nie mogę jednak wywnioskować z twojej odpowiedzi, dlaczego wybrałeś prostą architekturę timera i pętli tła, zamiast współpracy wielozadaniowej . Nie zrozum mnie źle. Naprawdę doceniam twoją odpowiedź, która zawiera wiele przydatnych informacji na temat różnych harmonogramów. Po prostu nie mam racji.
grad
Czy mógłbyś popracować nad tym trochę więcej?
grad
Dzięki, akohlsmith. Podoba mi się zdanie, które umieściłeś na końcu. Rozpoznanie go zajęło mi trochę czasu :) Wracając do odpowiedzi, prawie zawsze kodujesz do pętli + implementacja timera. A zatem, w przypadkach, w których zrezygnowałeś z tej implementacji i przeszedłeś na system operacyjny nieprzewidujący, co spowodowało, że to zrobiłeś?
grad
Korzystałem zarówno z systemów współpracy wielozadaniowej, jak i prewencyjnych, gdy korzystałem z systemu operacyjnego innej osoby. Linux, ThreadX, ucOS-ii lub QNX. Nawet w niektórych z tych sytuacji korzystałem z prostej i skutecznej pętli czasomierza + zdarzenia ( poll()od razu przychodzi mi na myśl).
akohlsmith,
Nie jestem fanem osadzania wątków ani wielozadaniowości we wbudowanych, ale wiem, że w przypadku złożonych systemów jest to jedyna rozsądna opcja. Konserwowane mikro-systemy operacyjne zapewniają szybki sposób na uruchomienie i często także sterowniki urządzeń.
akohlsmith,
6

Wielozadaniowość może być przydatną abstrakcją w wielu projektach mikrokontrolerów, chociaż prawdziwy harmonogram wyprzedzający byłby zbyt ciężki i niepotrzebny w większości przypadków. Zrobiłem dobrze ponad 100 projektów mikrokontrolerów. Wielokrotnie korzystałem z funkcji współpracy, ale uprzedzające przełączanie zadań z powiązanym bagażem nie było jak dotąd właściwe.

Problemy z wyprzedzającym zadawaniem w stosunku do wspólnego zadawania są następujące:

  1. Znacznie więcej wagi ciężkiej. Zapobiegawcze harmonogramy zadań są bardziej skomplikowane, zajmują więcej miejsca w kodzie i wymagają więcej cykli. Wymagają również co najmniej jednego przerwania. Jest to często niedopuszczalne obciążenie dla aplikacji.

  2. Muteksy są wymagane wokół struktur, do których można uzyskać dostęp jednocześnie. W systemie kooperacyjnym po prostu nie wywołujesz TASK_YIELD w trakcie operacji atomowej. Wpływa to na kolejki, wspólny stan globalny i łączy się w wielu miejscach.

Zasadniczo poświęcenie zadania konkretnemu zadaniu ma sens, gdy procesor może to obsłużyć, a zadanie jest wystarczająco skomplikowane z wystarczającą operacją zależną od historii, więc rozbicie go na kilka osobnych zdarzeń byłoby kłopotliwe. Zasadniczo dzieje się tak w przypadku obsługi wejściowego strumienia komunikacyjnego. Takie rzeczy są zwykle silnie zależne od stanu w zależności od niektórych wcześniejszych danych wejściowych. Na przykład mogą istnieć bajty kodu operacji, po których następują bajty danych unikalne dla każdego kodu operacji. Następnie pojawia się problem, że bajty przychodzą do ciebie, gdy coś innego ma ochotę je wysłać. Dzięki osobnemu zadaniu obsługującemu strumień wejściowy, możesz sprawić, aby pojawił się w kodzie zadania tak, jakbyś wychodził i pobierał następny bajt.

Ogólnie rzecz biorąc, zadania są przydatne, gdy istnieje wiele kontekstów państwowych. Zadania są w zasadzie maszynami stanu, przy czym PC jest zmienną stanu.

Wiele rzeczy, które musi zrobić mikro, można wyrazić jako reagowanie na zestaw zdarzeń. W rezultacie zazwyczaj mam główną pętlę zdarzeń. To sprawdza kolejno każde możliwe zdarzenie, a następnie przeskakuje z powrotem na górę i robi to wszystko ponownie. Gdy obsługa zdarzenia zajmuje więcej niż kilka cykli, po zakończeniu obsługi zwykle przeskakuję z powrotem na początek pętli zdarzenia. Oznacza to, że zdarzenia mają domyślny priorytet na podstawie tego, gdzie są sprawdzane na liście. W wielu prostych systemach jest to wystarczająco dobre.

Czasami dostajesz trochę bardziej skomplikowane zadania. Często można je podzielić na sekwencję niewielkiej liczby osobnych rzeczy do zrobienia. W takich przypadkach można użyć flag wewnętrznych jako zdarzeń. Robiłem takie rzeczy wiele razy na niskiej jakości PIC.

Jeśli masz podstawową strukturę zdarzeń jak wyżej, ale musisz także na przykład odpowiedzieć na strumień poleceń przez UART, wtedy przydatne jest, aby osobne zadanie obsługiwało odebrany strumień UART. Niektóre mikrokontrolery mają ograniczone zasoby sprzętowe do wielozadaniowości, na przykład PIC 16, który nie może odczytać ani zapisać własnego stosu wywołań. W takich przypadkach używam tak zwanego pseudo-zadania dla procesora poleceń UART. Główna pętla zdarzeń nadal obsługuje wszystko inne, ale jednym z jej zdarzeń jest to, że UART odebrał nowe bajty. W takim przypadku przeskakuje do procedury, która uruchamia to pseudo-zadanie. Moduł poleceń UART zawiera kod zadania, a adres wykonania i kilka wartości rejestru zadania są zapisywane w pamięci RAM w tym module. Kod przeskakiwany przez pętlę zdarzeń zapisuje bieżące rejestry, ładuje zapisane rejestry zadań, i przeskakuje na adres restartu zadania. Kod zadania wywołuje makro YIELD, które wykonuje operację odwrotną, która następnie ostatecznie przeskakuje z powrotem na początek głównej pętli zdarzeń. W niektórych przypadkach główna pętla zdarzeń uruchamia pseudo-zadanie raz na przebieg, zwykle u dołu, aby było to zdarzenie o niskim priorytecie.

Na PIC 18 i nowszych używam prawdziwego systemu zadań współpracy, ponieważ stos wywołań jest możliwy do odczytu i zapisu przez oprogramowanie układowe. W tych systemach adres restartu, kilka innych elementów stanu i wskaźnik stosu danych są przechowywane w buforze pamięci dla każdego zadania. Aby wszystkie pozostałe zadania mogły zostać uruchomione raz, zadanie wywołuje TASK_YIELD. Zapisuje bieżący stan zadania, przegląda listę następnego dostępnego zadania, ładuje jego stan, a następnie uruchamia.

W tej architekturze główna pętla zdarzeń jest po prostu kolejnym zadaniem, z wywołaniem TASK_YIELD na górze pętli.

Cały mój wielozadaniowy kod dla PIC jest dostępny za darmo. Aby to zobaczyć, zainstaluj wersję PIC Development Tools na stronie http://www.embedinc.com/pic/dload.htm . Poszukaj plików z „zadaniem” w ich nazwach w katalogu SOURCE> PIC dla 8-bitowych PIC oraz w katalogu SOURCE> DSPIC dla 16-bitowych PIC.

Olin Lathrop
źródło
muteksy wciąż mogą być konieczne w systemach wielozadaniowych współpracujących, chociaż jest to rzadkie. Typowym przykładem jest ISR, który potrzebuje dostępu do sekcji krytycznej. Prawie zawsze można tego uniknąć poprzez lepsze zaprojektowanie lub wybranie odpowiedniego kontenera danych dla danych krytycznych.
akohlsmith,
@akoh: Tak, kilkakrotnie korzystałem z muteksów do obsługi współdzielonego zasobu, takiego jak dostęp do magistrali SPI. Chodzi mi o to, że muteksy nie są z natury wymagane w zakresie, w jakim znajdują się w systemie wyprzedzającym. Nie chciałem powiedzieć, że nigdy nie są potrzebne ani nigdy nie są używane w systemie współpracy. Również muteks w systemie kooperacyjnym może być tak prosty, jak wirowanie w pętli TASK_YIELD sprawdzającej pojedynczy bit. W systemie wyprzedzającym zazwyczaj muszą być wbudowane w jądro.
Olin Lathrop,
@OlinLathrop: Myślę, że najbardziej znaczącą zaletą systemów nieprewencyjnych, jeśli chodzi o muteksy, jest to, że są one wymagane tylko podczas bezpośredniej interakcji z przerwaniami (które z natury są wyprzedzające) lub gdy albo potrzeba, aby trzymać strzeżony zasób przekracza czas, jaki chce się spędzić między wywołaniami „wydajnością” lub chce trzymać chroniony zasób wokół połączenia, który „może” przynieść (np. „zapisać dane do pliku”). W niektórych przypadkach, gdy o wydajność w obrębie „Zapis danych” rozmowy byłby problem, podaję ...
SuperCat
... metoda sprawdzania, ile danych można natychmiast zapisać, oraz metoda (która może się przydać) w celu zapewnienia dostępności pewnej ilości (przyspieszenie odzyskiwania brudnych bloków flash i czekanie, aż odpowiednia liczba zostanie odzyskana) .
supercat,
Cześć Olin, bardzo podoba mi się twoja odpowiedź. Jego informacje są daleko poza moimi pytaniami. Zawiera wiele praktycznych doświadczeń.
grad
1

Edycja: (Opuszczę mój wcześniejszy post poniżej; może kiedyś komuś pomoże.)

Systemy operacyjne wielozadaniowe dowolnego rodzaju i procedury obsługi przerwań nie są - lub nie powinny - być konkurencyjną architekturą systemu. Są przeznaczone do różnych zadań na różnych poziomach systemu. Przerwania są tak naprawdę przeznaczone dla krótkich sekwencji kodu do obsługi natychmiastowych obowiązków, takich jak ponowne uruchomienie urządzenia, być może odpytywanie urządzeń nieprzeszkadzających, mierzenie czasu w oprogramowaniu itp. Zazwyczaj zakłada się, że tło wykona dalsze przetwarzanie, które nie jest już krytyczne po czasie natychmiastowe potrzeby zostały zaspokojone. Jeśli wszystko, co musisz zrobić, to zrestartować stoper i przełączyć diodę LED lub impulsować inne urządzenie, ISR zwykle może to wszystko bezpiecznie zrobić na pierwszym planie. W przeciwnym razie musi poinformować tło (ustawiając flagę lub ustawiając w kolejce wiadomość), że coś musi zrobić, i zwolnić procesor.

Widziałem bardzo proste struktury programu, którego pętla tło jest tylko bezczynne pętla: for(;;){ ; }. Cała praca została wykonana w ISR timera. Może to działać, gdy program musi powtórzyć pewną stałą operację, która gwarantuje zakończenie w czasie krótszym niż czas; przychodzą na myśl pewne ograniczone rodzaje przetwarzania sygnału.

Osobiście piszę ISR, które oczyszczają wyjście, i pozwalam, aby tło przejęło wszystko, co wymaga zrobienia, nawet jeśli jest to tak proste jak pomnożenie i dodać, że można to zrobić w ułamku czasu. Dlaczego? Któregoś dnia wpadnę na świetny pomysł, aby dodać kolejną „prostą” funkcję do mojego programu i „cholera, wystarczy krótki ISR, aby to zrobić” i nagle moja wcześniej prosta architektura wywołuje interakcje, których nie planowałem i dzieje się niekonsekwentnie. Debugowanie nie jest zbyt zabawne.


(Wcześniej opublikowane porównanie dwóch rodzajów wielozadaniowości)

Przełączanie zadań: Funkcja wyprzedzająca MT zajmuje się przełączaniem zadań, w tym upewniając się, że żaden wątek nie jest pozbawiony procesora, a wątki o wysokim priorytecie uruchomią się, gdy tylko będą gotowe. Cooperative MT wymaga od programisty upewnienia się, że żaden wątek nie utrzymuje procesora zbyt długo. Musisz także zdecydować, jak długo jest za długi. Oznacza to również, że za każdym razem, gdy modyfikujesz kod, musisz wiedzieć, czy jakikolwiek segment kodu przekracza teraz kwant czasowy.

Ochrona operacji nieatomowych: dzięki PMT musisz upewnić się, że zamiana wątków nie występuje w trakcie operacji, których nie można dzielić. Odczytywanie / zapisywanie określonych par rejestrów urządzeń, które muszą być obsługiwane na przykład w określonej kolejności lub w maksymalnym czasie. Z CMT jest to dość łatwe - po prostu nie poddawaj procesora w trakcie takiej operacji.

Debugowanie: Generalnie łatwiejsze dzięki CMT, ponieważ planujesz, kiedy / gdzie nastąpi przełączenie wątku. Warunki wyścigu między wątkami i błędami związane z niechronionymi wątkami operacjami z PMT są szczególnie trudne do debugowania, ponieważ zmiany wątków są probabilistyczne, więc nie powtarzalne.

Zrozumienie kodu: Wątki napisane dla PMT są napisane tak, jakby mogły być samodzielne. Wątki napisane dla CMT są zapisywane jako segmenty i zależnie od wybranej struktury programu, czytelnikowi może być trudniej.

Używanie kodu biblioteki, który nie jest bezpieczny dla wątków: Musisz sprawdzić, czy każda funkcja biblioteki, którą wywołujesz, jest bezpieczna dla wątków PMT. printf () i scanf () i ich warianty prawie zawsze nie są bezpieczne dla wątków. Dzięki CMT będziesz wiedział, że nie nastąpi zmiana nici, z wyjątkiem sytuacji, gdy specjalnie podasz procesor.

System napędzany maszyną o skończonym stanie do sterowania urządzeniem mechanicznym i / lub śledzenia zdarzeń zewnętrznych jest często dobrym kandydatem do CMT, ponieważ na każdym wydarzeniu nie ma wiele do zrobienia - uruchomić lub zatrzymać silnik, ustawić flagę, wybrać następny stan itd. Zatem funkcje zmiany stanu są z natury krótkie.

Podejście hybrydowe może działać naprawdę dobrze w tego rodzaju systemach: CMT do zarządzania maszyną stanową (a zatem i większością sprzętu) działającą jako jeden wątek oraz jeden lub dwa kolejne wątki do wykonywania dłuższych obliczeń uruchomionych przez stan zmiana.

JRobert
źródło
Dziękuję za odpowiedź, JRobert. Ale to nie jest dostosowane do mojego pytania. Porównuje system wyprzedzający z systemem operacyjnym nieprzeszkadzającym, ale nie porównuje systemu operacyjnego bez systemu zapobiegawczego z systemem innym niż system operacyjny.
grad
Racja - przepraszam. Moja edycja powinna lepiej odpowiedzieć na twoje pytanie.
JRobert