Những lợi ích của một hệ điều hành không ưu tiên là gì? và giá cho những lợi ích này?


14

Đối với MCU kim loại được khoanh vùng, So sánh với mã tự chế với vòng lặp nền cộng với kiến ​​trúc ngắt hẹn giờ, lợi ích của hệ điều hành không phòng ngừa là gì? Điều gì trong số những lợi ích này đủ hấp dẫn để một dự án chấp nhận một hệ điều hành không ưu tiên, thay vì sử dụng mã tự chế với kiến ​​trúc vòng lặp nền?
.

Giải thích cho câu hỏi:

Tôi thực sự đánh giá cao tất cả những người đã trả lời câu hỏi của tôi. Tôi cảm thấy câu trả lời đã gần như ở đó. Tôi thêm lời giải thích này vào câu hỏi của mình ở đây cho thấy sự cân nhắc của riêng tôi và có thể giúp thu hẹp câu hỏi hoặc làm cho nó chính xác hơn.

Những gì tôi đang cố gắng làm là hiểu cách chọn RTOS phù hợp nhất cho một dự án nói chung.
Để đạt được điều này, hiểu rõ hơn về các khái niệm cơ bản và lợi ích hấp dẫn nhất từ ​​các loại RTOS khác nhau và mức giá tương ứng sẽ giúp ích, vì không có RTOS tốt nhất cho tất cả các ứng dụng.
Tôi đã đọc sách về HĐH vài năm trước nhưng tôi không còn mang theo chúng nữa. Tôi đã tìm kiếm trên internet trước khi tôi đăng câu hỏi của mình ở đây và thấy thông tin này hữu ích nhất: http://www.ustudy.in/node/5456 .
Có rất nhiều thông tin hữu ích khác như phần giới thiệu trong trang web của RTOS khác nhau, các bài viết so sánh lịch trình ưu tiên và lập lịch không ưu tiên, v.v.
Nhưng tôi đã không tìm thấy bất kỳ chủ đề nào được đề cập khi chọn RTOS không ưu tiên và khi nào tốt hơn chỉ nên viết mã của riêng bạn bằng cách sử dụng ngắt hẹn giờ và vòng lặp nền.
Tôi chắc chắn có câu trả lời của riêng mình nhưng tôi không đủ hài lòng với chúng.
Tôi thực sự muốn biết câu trả lời hoặc quan điểm từ những người xuất chúng hơn, đặc biệt là trong thực tiễn ngành công nghiệp.

Sự hiểu biết của tôi cho đến nay là: cho
dù sử dụng hay không sử dụng HĐH, một số loại mã lập lịch nhất định luôn luôn cần thiết, thậm chí nó ở dạng mã như:

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

Lợi ích 1:
Một hệ điều hành không ưu tiên chỉ định cách thức / phong cách lập trình cho mã lập lịch, để các kỹ sư có thể chia sẻ cùng một quan điểm ngay cả khi họ không ở trong cùng một dự án trước đó. Sau đó, với cùng quan điểm về nhiệm vụ khái niệm, các kỹ sư có thể làm việc trên các nhiệm vụ khác nhau và kiểm tra chúng, lập hồ sơ độc lập hết mức có thể.
Nhưng chúng ta thực sự có thể kiếm được bao nhiêu từ việc này? Nếu các kỹ sư đang làm việc trong cùng một dự án, họ có thể tìm cách chia sẻ cùng một quan điểm mà không cần sử dụng một hệ điều hành không ưu tiên.
Nếu một kỹ sư đến từ một dự án hoặc công ty khác, anh ta sẽ đạt được lợi ích nếu anh ta biết hệ điều hành trước đó. Nhưng nếu anh ta không làm thế, thì một lần nữa, dường như anh ta không học được một hệ điều hành mới hay một đoạn mã mới nào.

Lợi ích 2:
Nếu mã hệ điều hành đã được kiểm tra tốt, vì vậy nó sẽ tiết kiệm thời gian từ việc gỡ lỗi. Đây thực sự là một lợi ích tốt.
Nhưng nếu ứng dụng chỉ có khoảng 5 tác vụ, tôi nghĩ sẽ không thực sự lộn xộn khi viết mã của riêng bạn bằng cách sử dụng ngắt hẹn giờ và vòng lặp nền.

Một hệ điều hành không ưu tiên ở đây được đề cập đến một hệ điều hành thương mại / miễn phí / kế thừa với một bộ lập lịch không ưu tiên.
Khi tôi đăng câu hỏi này, tôi chủ yếu nghĩ về một số HĐH nhất định như:
(1) KISS Kernel (Một RTOS nhỏ không khước từ - được yêu cầu bởi trang web của nó)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (RTOS nhẹ - được tuyên bố bởi trang web của nó)
(3) FreeRTOS (Đó là RTOS được ưu tiên, nhưng theo tôi hiểu, nó cũng có thể được định cấu hình là RTOS không phòng ngừa)
(4) uC / OS (tương tự FreeRTOS)
(5) ) mã hệ điều hành / lịch trình kế thừa ở một số công ty (thường được thực hiện và duy trì bởi công ty trong nội bộ)
(Không thể thêm nhiều liên kết vì giới hạn từ tài khoản StackOverflow mới)

Theo tôi hiểu, một hệ điều hành không phòng ngừa là một tập hợp các mã này:
(1) một bộ lập lịch sử dụng chiến lược không phòng ngừa.
(2) các phương tiện để liên lạc giữa các tác vụ, mutex, đồng bộ hóa và kiểm soát thời gian.
(3) quản lý bộ nhớ.
(4) Các cơ sở hữu ích khác / thư viện như File System, mạng stack, GUI và vv (FreeRTOS và UC / OS cung cấp này, nhưng tôi không chắc chắn nếu họ vẫn làm việc khi scheduler được cấu hình như không ưu tiên)
Một số chúng không phải lúc nào cũng ở đó Nhưng lịch trình là phải.


Đó là khá nhiều trong một tóm tắt. Nếu bạn có một khối lượng công việc cần phải đa luồng và bạn có thể đủ khả năng chi trả, hãy sử dụng một hệ điều hành luồng. Mặt khác, "trình lập lịch biểu" dựa trên thời gian hoặc nhiệm vụ đơn giản đủ cho hầu hết các trường hợp. Và để tìm hiểu xem đa nhiệm ưu tiên hay hợp tác là tốt nhất ... Tôi đoán nó sẽ vượt qua và bạn muốn kiểm soát bao nhiêu đối với đa nhiệm bạn cần làm.
akohlsmith

Câu trả lời:


13

Điều này có vẻ hơi lạc đề nhưng tôi sẽ cố gắng điều khiển nó đi đúng hướng.

Đa nhiệm ưu tiên có nghĩa là hệ điều hành hoặc nhân có thể tạm dừng luồng hiện đang chạy và chuyển sang luồng khác dựa trên bất kỳ lịch trình nào mà nó có tại chỗ. Hầu hết các luồng đang chạy không có khái niệm rằng có những thứ khác đang diễn ra trên hệ thống và điều này có nghĩa là gì đối với mã của bạn là bạn phải cẩn thận thiết kế nó để nếu kernel quyết định tạm dừng một luồng ở giữa Hoạt động nhiều bước (giả sử thay đổi đầu ra PWM, chọn kênh ADC mới, trạng thái đọc từ thiết bị ngoại vi I2C, v.v.) và để một luồng khác chạy trong một thời gian, hai luồng này không can thiệp lẫn nhau.

Một ví dụ tùy ý: giả sử bạn chưa quen với các hệ thống nhúng đa luồng và bạn có một hệ thống nhỏ với I2C ADC, LCD SPI và I2C EEPROM. Bạn đã quyết định rằng sẽ có hai ý tưởng hay: có một luồng đọc từ ADC và ghi mẫu vào EEPROM, và một mẫu đọc 10 mẫu cuối cùng, lấy trung bình chúng và hiển thị chúng trên màn hình LCD SPI. Thiết kế thiếu kinh nghiệm sẽ trông giống như thế này (đơn giản hóa rất nhiều):

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

Đây là một ví dụ rất thô sơ và nhanh chóng. Đừng viết mã như thế này!

Bây giờ hãy nhớ rằng, hệ điều hành đa nhiệm ưu tiên có thể tạm dừng một trong các luồng này tại bất kỳ dòng nào trong mã (thực tế là tại bất kỳ lệnh lắp ráp nào) và cho thời gian xử lý luồng khác.

Nghĩ về điều đó. Hãy tưởng tượng điều gì sẽ xảy ra nếu HĐH quyết định tạm dừng adc_thread()giữa cài đặt địa chỉ EE để ghi và ghi dữ liệu thực tế. lcd_thread()sẽ chạy, chạy xung quanh với thiết bị ngoại vi I2C để đọc dữ liệu cần thiết và khi nàoadc_thread() đến lượt chạy lại, EEPROM sẽ không ở trạng thái như cũ. Mọi thứ sẽ không hoạt động tốt cả. Tồi tệ hơn, nó thậm chí có thể hoạt động hầu hết thời gian, nhưng không phải tất cả thời gian, và bạn sẽ phát điên khi cố gắng tìm ra lý do tại sao mã của bạn không hoạt động khi nó LOOKS như vậy!

Đó là một ví dụ điển hình nhất; HĐH có thể quyết định chọn trước i2c_write()từ adc_thread()ngữ cảnh và bắt đầu chạy lại từlcd_thread() ngữ cảnh! Mọi thứ có thể trở nên thực sự lộn xộn rất nhanh.

Khi bạn viết mã để làm việc trong môi trường đa nhiệm ưu tiên, bạn phải sử dụng các cơ chế khóa để đảm bảo rằng nếu mã của bạn bị treo ở thời điểm không phù hợp thì tất cả địa ngục sẽ không bị hỏng.

Mặt khác, đa nhiệm hợp tác, có nghĩa là mỗi luồng được kiểm soát khi nó từ bỏ thời gian thực hiện. Việc mã hóa đơn giản hơn, nhưng mã phải được thiết kế cẩn thận để đảm bảo tất cả các luồng có đủ thời gian để chạy. Một ví dụ khác:

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

Mã đó sẽ không hoạt động như bạn nghĩ, hoặc thậm chí nếu nó có vẻ hoạt động, nó sẽ không hoạt động khi tốc độ dữ liệu của chuỗi echo tăng lên. Một lần nữa, hãy dành một phút để xem xét nó.

echo_thread()chờ đợi một byte xuất hiện tại UART và sau đó lấy nó và đợi cho đến khi có chỗ để viết nó, sau đó viết nó. Sau khi xong, nó cho phép các luồng khác chạy. seconds_counter()sẽ tăng số đếm, đợi 1000ms và sau đó cho các luồng khác một cơ hội để chạy. Nếu hai byte đi vào UART trong khi đósleep() xảy ra , bạn có thể bỏ lỡ việc nhìn thấy chúng bởi vì UART giả định của chúng tôi không có FIFO để lưu trữ các ký tự trong khi CPU đang bận làm việc khác.

Cách chính xác để thực hiện ví dụ rất kém này sẽ là đặt bất cứ yield_cpu()nơi nào bạn có một vòng lặp bận rộn. Điều này sẽ giúp mọi thứ di chuyển cùng, nhưng có thể gây ra các vấn đề khác. ví dụ: nếu thời gian là quan trọng và bạn nhường CPU cho một luồng khác mất nhiều thời gian hơn bạn mong đợi, bạn có thể bỏ thời gian của mình. Một hệ điều hành đa nhiệm ưu tiên sẽ không gặp phải vấn đề này bởi vì nó buộc phải tạm dừng các luồng để đảm bảo tất cả các luồng được lên lịch chính xác.

Bây giờ những gì phải làm với một bộ đếm thời gian và vòng lặp nền? Bộ đếm thời gian và vòng lặp nền rất giống với ví dụ đa nhiệm hợp tác ở trên:

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

Điều này có vẻ khá gần với ví dụ phân luồng hợp tác; bạn có một bộ đếm thời gian thiết lập các sự kiện và một vòng lặp chính tìm kiếm chúng và tác động lên chúng theo kiểu nguyên tử. Bạn không phải lo lắng về các "luồng" ADC và LCD can thiệp lẫn nhau vì cái này sẽ không bao giờ làm gián đoạn cái kia. Bạn vẫn phải lo lắng về một "chủ đề" mất quá nhiều thời gian; ví dụ: điều gì xảy ra nếu get_adc_data()mất 30ms? bạn sẽ bỏ lỡ ba cơ hội để kiểm tra một nhân vật và lặp lại nó.

Việc thực hiện vòng lặp + bộ đếm thời gian thường đơn giản hơn rất nhiều so với việc sử dụng một hạt nhân đa nhiệm hợp tác do mã của bạn có thể được thiết kế cụ thể hơn cho nhiệm vụ trong tay. Bạn không thực sự đa nhiệm nhiều như thiết kế một hệ thống cố định trong đó bạn dành cho mỗi hệ thống con một thời gian để thực hiện các nhiệm vụ của nó theo một cách rất cụ thể và có thể dự đoán được. Ngay cả một hệ thống đa nhiệm hợp tác cũng phải có cấu trúc nhiệm vụ chung cho từng luồng và luồng tiếp theo để chạy được xác định bởi chức năng lập lịch có thể trở nên khá phức tạp.

Các cơ chế khóa cho cả ba hệ thống là như nhau, nhưng chi phí cần thiết cho mỗi hệ thống là khá khác nhau.

Cá nhân, tôi hầu như luôn luôn mã theo tiêu chuẩn cuối cùng này, việc thực hiện vòng lặp + hẹn giờ. Tôi thấy luồng là một cái gì đó nên được sử dụng rất ít. Nó không chỉ phức tạp hơn để viết và gỡ lỗi, mà còn đòi hỏi nhiều chi phí hơn (một hạt nhân đa nhiệm được ưu tiên luôn luôn lớn hơn một bộ đếm thời gian đơn giản ngu ngốc và theo dõi sự kiện vòng lặp chính).

Cũng có một câu nói rằng bất cứ ai làm việc trên các chủ đề sẽ đánh giá cao:

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

:-)


Cảm ơn bạn rất nhiều vì đã trả lời của bạn với các ví dụ chi tiết, akohlsmith. Tuy nhiên, tôi không thể kết luận từ câu trả lời của bạn tại sao bạn chọn bộ đếm thời gian đơn giản và kiến ​​trúc vòng lặp nền thay vì đa nhiệm hợp tác . Đừng hiểu lầm tôi. Tôi thực sự đánh giá cao câu trả lời của bạn, nơi cung cấp rất nhiều thông tin hữu ích về việc lên lịch khác nhau. Tôi chỉ không có điểm.
hailang

Bạn có thể vui lòng làm việc này nhiều hơn một chút?
hailang

Cảm ơn, akohlsmith. Tôi thích câu bạn đặt ở cuối. Phải mất một lúc tôi mới nhận ra nó :) Quay lại điểm câu trả lời của bạn, bạn hầu như luôn luôn viết mã cho vòng lặp + thực hiện hẹn giờ. Sau đó, trong các trường hợp bạn đã từ bỏ việc triển khai này và chuyển sang HĐH không phòng ngừa, điều gì đã khiến bạn làm như vậy?
hailang

Tôi đã sử dụng cả hệ thống đa nhiệm ưu tiên và hợp tác khi tôi đang chạy hệ điều hành của người khác. Linux, ThreadX, ucOS-ii hoặc QNX. Ngay cả trong một số tình huống đó, tôi đã sử dụng vòng lặp sự kiện + vòng lặp đơn giản và hiệu quả ( poll()xuất hiện ngay lập tức).
akohlsmith

Tôi không phải là một fan hâm mộ của luồng hoặc đa nhiệm trong nhúng, nhưng tôi biết rằng đối với các hệ thống phức tạp, đó là lựa chọn lành mạnh duy nhất. Các hệ điều hành vi mô đóng hộp cung cấp cho bạn một cách nhanh chóng để khởi động và vận hành và đôi khi cũng cung cấp trình điều khiển thiết bị.
akohlsmith

6

Đa tác vụ có thể là một sự trừu tượng hóa hữu ích trong rất nhiều dự án vi điều khiển, mặc dù một bộ lập lịch trước thực sự sẽ quá nặng và không cần thiết trong hầu hết các trường hợp. Tôi đã thực hiện tốt hơn 100 dự án vi điều khiển. Tôi đã sử dụng hợp tác nhiều lần, nhưng chuyển đổi nhiệm vụ trước với hành lý liên quan của nó cho đến nay vẫn chưa phù hợp.

Các vấn đề với nhiệm vụ phủ đầu như được áp dụng cho nhiệm vụ hợp tác là:

  1. Nặng hơn nhiều. Lập lịch tác vụ ưu tiên phức tạp hơn, chiếm nhiều không gian mã hơn và mất nhiều chu kỳ hơn. Họ cũng yêu cầu ít nhất một ngắt. Đó thường là một gánh nặng không thể chấp nhận được trên ứng dụng.

  2. Mutexes được yêu cầu xung quanh các cấu trúc có thể được truy cập đồng thời. Trong một hệ thống hợp tác, bạn không được gọi TASK_YIELD ở giữa hoạt động nguyên tử. Điều này ảnh hưởng đến hàng đợi, trạng thái toàn cầu được chia sẻ và xâm nhập vào rất nhiều nơi.

Nói chung, việc giao một nhiệm vụ cho một công việc cụ thể có ý nghĩa khi CPU có thể hỗ trợ việc này và công việc đủ phức tạp với đủ hoạt động phụ thuộc vào lịch sử, việc chia nó thành một vài sự kiện riêng lẻ sẽ gây cồng kềnh. Đây thường là trường hợp khi xử lý một luồng đầu vào truyền thông. Những thứ như vậy thường được thúc đẩy mạnh mẽ tùy thuộc vào một số đầu vào trước đó. Ví dụ, có thể có các byte opcode theo sau là các byte dữ liệu duy nhất cho mỗi opcode. Sau đó, có vấn đề của các byte đến với bạn khi một cái gì đó khác cảm thấy muốn gửi chúng. Với một tác vụ riêng xử lý luồng đầu vào, bạn có thể làm cho nó xuất hiện trong mã tác vụ như thể bạn đang đi ra ngoài và nhận byte tiếp theo.

Nhìn chung, các tác vụ rất hữu ích khi có nhiều bối cảnh trạng thái. Nhiệm vụ cơ bản là các máy trạng thái với PC là biến trạng thái.

Nhiều điều mà một vi mô phải làm có thể được thể hiện như là phản ứng với một tập hợp các sự kiện. Kết quả là, tôi thường có một vòng lặp sự kiện chính. Điều này kiểm tra từng sự kiện có thể theo trình tự, sau đó nhảy trở lại đầu trang và thực hiện lại tất cả. Khi xử lý một sự kiện mất nhiều hơn chỉ một vài chu kỳ, tôi thường quay lại bắt đầu vòng lặp sự kiện sau khi xử lý sự kiện. Điều này có hiệu lực có nghĩa là các sự kiện có mức độ ưu tiên ngụ ý dựa trên vị trí chúng được kiểm tra trong danh sách. Trên nhiều hệ thống đơn giản, điều này là đủ tốt.

Đôi khi bạn nhận được một chút nhiệm vụ phức tạp hơn. Chúng thường có thể được chia thành một chuỗi gồm một số lượng nhỏ những việc riêng biệt phải làm. Bạn có thể sử dụng cờ nội bộ làm sự kiện trong những trường hợp đó. Tôi đã thực hiện loại điều này nhiều lần trên các PIC cấp thấp.

Ví dụ, nếu bạn có cấu trúc sự kiện cơ bản như trên nhưng cũng phải phản hồi một luồng lệnh qua UART, thì thật hữu ích khi có một tác vụ riêng xử lý luồng UART nhận được. Một số bộ vi điều khiển có tài nguyên phần cứng hạn chế cho đa tác vụ, như PIC 16 không thể đọc hoặc viết ngăn xếp cuộc gọi của chính nó. Trong những trường hợp như vậy, tôi sử dụng cái mà tôi gọi là tác vụ giả cho bộ xử lý lệnh UART. Vòng lặp sự kiện chính vẫn xử lý mọi thứ khác, nhưng một trong những sự kiện cần xử lý là một byte mới đã được UART nhận. Trong trường hợp đó, nó nhảy đến một thói quen chạy tác vụ giả này. Mô-đun lệnh UART chứa mã tác vụ và địa chỉ thực hiện và một vài giá trị đăng ký của tác vụ được lưu trong RAM trong mô-đun đó. Mã nhảy đến bởi vòng lặp sự kiện lưu các thanh ghi hiện tại, tải các thanh ghi tác vụ đã lưu, và nhảy đến địa chỉ khởi động lại nhiệm vụ. Mã tác vụ gọi một macro YIELD thực hiện ngược lại, sau đó cuối cùng sẽ quay trở lại bắt đầu vòng lặp sự kiện chính. Trong một số trường hợp, vòng lặp sự kiện chính chạy tác vụ giả một lần trên mỗi lần vượt qua, thường là ở phía dưới để biến nó thành sự kiện ưu tiên thấp.

Trên PIC 18 trở lên, tôi sử dụng một hệ thống tác vụ hợp tác thực sự vì ngăn xếp cuộc gọi có thể đọc và ghi được bằng phần sụn. Trên các hệ thống này, địa chỉ khởi động lại, một vài trạng thái khác và con trỏ ngăn xếp dữ liệu được giữ trong bộ nhớ đệm cho mỗi tác vụ. Để cho tất cả các tác vụ khác chạy một lần, một tác vụ gọi TASK_YIELD. Điều này sẽ lưu trạng thái tác vụ hiện tại, xem qua danh sách cho tác vụ khả dụng tiếp theo, tải trạng thái của nó, sau đó chạy nó.

Trong kiến ​​trúc này, vòng lặp sự kiện chính chỉ là một nhiệm vụ khác, với một lệnh gọi TASK_YIELD ở đầu vòng lặp.

Tất cả mã đa tác vụ của tôi cho PIC đều có sẵn miễn phí. Để xem nó, hãy cài đặt bản phát hành Công cụ phát triển PIC tại http://www.embedinc.com/pic/dload.htm . Tìm kiếm các tệp có "tác vụ" trong tên của chúng trong thư mục SOURCE> PIC cho PIC 8 bit và thư mục SOURCE> DSPIC cho PIC 16 bit.


mutexes vẫn có thể cần thiết trong các hệ thống đa nhiệm hợp tác, mặc dù nó rất hiếm. Ví dụ điển hình là ISR cần truy cập vào phần quan trọng. Điều này hầu như luôn có thể tránh được thông qua thiết kế tốt hơn hoặc chọn một thùng chứa dữ liệu phù hợp cho dữ liệu quan trọng.
akohlsmith

@akoh: Có, tôi đã sử dụng mutexes trong một số trường hợp để xử lý tài nguyên được chia sẻ, như truy cập vào bus SPI. Quan điểm của tôi là các mutexes vốn không bắt buộc trong phạm vi chúng nằm trong một hệ thống ưu tiên. Tôi không có ý nói rằng chúng không bao giờ cần thiết hoặc không bao giờ được sử dụng trong một hệ thống hợp tác. Ngoài ra, một mutex trong một hệ thống hợp tác có thể đơn giản như quay trong vòng lặp TASK_YIELD kiểm tra một bit. Trong một hệ thống ưu tiên, chúng thường cần được tích hợp vào kernel.
Olin Lathrop

@OlinLathrop: Tôi nghĩ rằng lợi thế đáng kể nhất của các hệ thống không phòng ngừa khi nói đến mutexes là chúng chỉ được yêu cầu khi tương tác trực tiếp với các ngắt (mà về bản chất là phòng ngừa) hoặc khi một trong hai thời điểm cần giữ tài nguyên được bảo vệ vượt quá thời gian người ta muốn dành giữa các cuộc gọi "nhường" hoặc người ta muốn giữ một tài nguyên được bảo vệ xung quanh một cuộc gọi mà "có thể" mang lại (ví dụ: "ghi dữ liệu vào một tệp"). Trong một số trường hợp khi có lợi suất trong một cuộc gọi "ghi dữ liệu" sẽ là một vấn đề, tôi đã bao gồm ...
supercat

... một phương pháp để kiểm tra lượng dữ liệu có thể được ghi ngay lập tức và một phương pháp (có thể mang lại hiệu quả) để đảm bảo rằng có sẵn một số lượng (tiến hành khai hoang các khối flash bẩn và đợi cho đến khi một số thích hợp được lấy lại) .
supercat

Xin chào Olin, tôi thích trả lời của bạn rất nhiều. Thông tin của nó vượt xa câu hỏi của tôi. Nó bao gồm rất nhiều kinh nghiệm thực tế.
hailang

1

Chỉnh sửa: (Tôi sẽ để lại bài viết trước đây của tôi bên dưới; có thể nó sẽ giúp được ai đó vào một ngày nào đó.)

Các hệ điều hành đa nhiệm thuộc bất kỳ loại nào và các Dịch vụ gián đoạn không - hoặc không nên - kiến ​​trúc hệ thống cạnh tranh. Chúng có nghĩa là cho các công việc khác nhau ở các cấp độ khác nhau của hệ thống. Các ngắt thực sự dành cho các chuỗi mã ngắn để xử lý các công việc tức thời như khởi động lại thiết bị, có thể bỏ phiếu cho các thiết bị không bị gián đoạn, chấm công trong phần mềm, v.v. Người ta thường cho rằng nền sẽ xử lý thêm không còn quan trọng sau thời gian nữa nhu cầu ngay lập tức đã được đáp ứng. Nếu tất cả những gì bạn cần làm là khởi động lại bộ hẹn giờ và bật đèn LED hoặc phát xung một thiết bị khác, ISR thường có thể thực hiện tất cả trong nền trước một cách an toàn. Mặt khác, nó cần thông báo nền (bằng cách đặt cờ hoặc xếp hàng tin nhắn) rằng cần phải làm gì đó và giải phóng bộ xử lý.

Tôi đã thấy các cấu trúc chương trình rất đơn giản có vòng lặp nền chỉ là một vòng lặp nhàn rỗi : for(;;){ ; }. Tất cả các công việc đã được thực hiện trong bộ đếm thời gian ISR. Điều này có thể hoạt động khi chương trình cần lặp lại một số hoạt động liên tục được đảm bảo hoàn thành trong ít hơn một khoảng thời gian hẹn giờ; một số loại hạn chế xử lý tín hiệu đến với tâm trí.

Cá nhân, tôi viết các ISR để dọn sạch và để nền chiếm lấy mọi thứ cần làm, ngay cả khi điều đó đơn giản như nhân và thêm có thể được thực hiện trong một phần của khoảng thời gian. Tại sao? Một ngày nào đó, tôi sẽ có ý tưởng sáng sủa để thêm một chức năng "đơn giản" khác vào chương trình của mình và "quái gì, nó sẽ chỉ mất một ISR ngắn để làm điều đó" và đột nhiên kiến ​​trúc đơn giản trước đây của tôi phát triển một số tương tác tôi đã lên kế hoạch trên và xảy ra không nhất quán. Những thứ đó không thú vị để gỡ lỗi.


(Trước đây đã đăng so sánh hai loại đa tác vụ)

Chuyển đổi tác vụ: MT ưu tiên đảm nhiệm việc chuyển đổi tác vụ cho bạn bao gồm đảm bảo không có luồng nào bị thiếu CPU và các luồng có mức độ ưu tiên cao sẽ chạy ngay khi chúng sẵn sàng. Cooperative MT yêu cầu lập trình viên đảm bảo không có luồng nào giữ bộ xử lý quá lâu tại một thời điểm. Bạn cũng sẽ phải quyết định bao lâu là quá dài. Điều đó cũng có nghĩa là bất cứ khi nào bạn sửa đổi mã, bạn sẽ cần phải biết liệu có bất kỳ phân đoạn mã nào hiện vượt quá lượng tử thời gian đó hay không.

Bảo vệ các hoạt động phi nguyên tử: Với PMT, bạn sẽ phải đảm bảo các giao dịch hoán đổi luồng không xảy ra ở giữa các hoạt động không được phân chia. Chẳng hạn, đọc / ghi các cặp đăng ký thiết bị nhất định phải được xử lý theo một thứ tự cụ thể hoặc trong một khoảng thời gian tối đa. Với CMT, điều này khá dễ dàng - chỉ cần không mang lại bộ xử lý ở giữa hoạt động như vậy.

Gỡ lỗi: Nói chung dễ dàng hơn với CMT, vì bạn lập kế hoạch khi / nơi chuyển đổi luồng sẽ xảy ra. Các điều kiện chạy đua giữa các luồng và lỗi liên quan đến các hoạt động không an toàn của luồng với PMT đặc biệt khó gỡ lỗi vì các thay đổi của luồng là xác suất, do đó không thể lặp lại.

Hiểu mã: Các chủ đề được viết cho một PMT được viết khá nhiều như thể chúng có thể đứng một mình. Các chủ đề được viết cho CMT được viết dưới dạng phân đoạn và tùy thuộc vào cấu trúc chương trình bạn chọn, người đọc có thể khó theo dõi hơn.

Sử dụng mã thư viện không an toàn luồng: Bạn sẽ cần xác minh rằng mỗi chức năng thư viện bạn gọi theo luồng PMT an toàn. printf () và scanf () và các biến thể của chúng hầu như không phải là chủ đề an toàn. Với CMT, bạn sẽ biết rằng sẽ không có thay đổi luồng nào xảy ra trừ khi bạn đặc biệt mang lại bộ xử lý.

Một hệ thống điều khiển máy trạng thái hữu hạn để điều khiển thiết bị cơ học và / hoặc theo dõi các sự kiện bên ngoài thường là ứng cử viên tốt cho CMT, vì tại mỗi sự kiện, không có nhiều việc phải làm - khởi động hoặc dừng động cơ, đặt cờ, chọn trạng thái tiếp theo , v.v ... Do đó, các chức năng thay đổi trạng thái vốn đã ngắn gọn.

Một cách tiếp cận hỗn hợp có thể hoạt động thực sự tốt trong các loại hệ thống này: CMT để quản lý máy trạng thái (và do đó, hầu hết phần cứng) chạy dưới dạng một luồng và một hoặc hai luồng nữa để thực hiện bất kỳ tính toán chạy nào được khởi động bởi trạng thái thay đổi.


Cảm ơn bạn đã trả lời, JRobert. Nhưng nó không phù hợp với câu hỏi của tôi. Nó so sánh hệ điều hành ưu tiên so với hệ điều hành không phòng ngừa, nhưng nó không so sánh hệ điều hành không ưu tiên so với hệ điều hành không ưu tiên.
hailang

Phải - xin lỗi. Chỉnh sửa của tôi nên giải quyết câu hỏi của bạn tốt hơn.
JRobert
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.