Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 32.128 phút đọc

volatile

Sau bài này bạn sẽ làm được

  • Chỉ ra vòng lặp chờ bị tối ưu thành vòng lặp vô tận
  • Kể bốn trường hợp bắt buộc dùng volatile
  • Nêu vì sao volatile không bảo đảm tính nguyên tử
  • Đặt volatile đúng chỗ trong khai báo con trỏ

volatile nói với trình biên dịch đúng một điều: giá trị này có thể đổi mà không do mã của bạn. Nó là từ khóa quyết định khi làm hệ nhúng, và cũng là từ khóa bị hiểu sai nhiều nhất trong C.

#Vòng lặp chờ biến thành vòng lặp vô tận

vo.c
unsigned dem;

void cho_thuong(void) {
    unsigned *p = &dem;

    while (*p == 0) { }
}

void cho_volatile(void) {
    volatile unsigned *p = &dem;

    while (*p == 0) { }
}
terminal
gcc -O2 -S -masm=intel -o - vo.c
cho_thuong:
    mov  eax, DWORD PTR dem[rip]     ; doc MOT lan
    test eax, eax
    jne  .L5
.L4:
    jmp  .L4                         ; VONG LAP VO TAN
.L5:
    ret

cho_volatile:
    lea  rdx, dem[rip]
.L8:
    mov  eax, DWORD PTR [rdx]        ; doc LAI moi vong
    test eax, eax
    je   .L8
    ret
volatile
Từ khóa nói rằng mọi lần đọc phải thật sự đọc từ bộ nhớ, và mọi lần ghi phải thật sự ghi ra bộ nhớ. Trình biên dịch không được gộp, bỏ, hay đổi thứ tự các truy cập đó với nhau.
Tối ưuVới biến thườngVới volatile
Giữ giá trị trong thanh ghi qua nhiều lần dùngĐượcKhông
Bỏ lần đọc mà kết quả không dùng tớiĐượcKhông
Bỏ lần ghi bị lần ghi sau đè lênĐượcKhông
Đổi thứ tự với truy cập volatile khácĐượcKhông
Đổi thứ tự với truy cập biến thườngĐượcĐược
Chia một lần ghi 32 bit thành hai lần 16 bitĐượcĐược, chuẩn không cấm

#Bốn trường hợp bắt buộc

Một: thanh ghi phần cứng

#define UART_SR (*(volatile uint32_t *)0x40011000)
#define UART_DR (*(volatile uint32_t *)0x40011004)

void gui_byte(uint8_t b) {
    while (!(UART_SR & (1u << 7))) { }    /* cho co TXE, PHAN CUNG dat */

    UART_DR = b;
}

/* Khong co volatile:
     - vong lap cho thanh vong lap vo tan, nhu vi du dau bai
     - hoac trinh bien dich bo han lenh doc vi ket qua khong dung

   Bai 47.3 cua Phan 18 da noi ky ve dieu nay. */

Hai: biến sửa trong hàm xử lý ngắt

volatile int g_co_ngat = 0;

void ISR(void) { g_co_ngat = 1; }         /* ngat dat co */

int main(void) {
    bat_ngat();

    while (!g_co_ngat) { }                /* main doc co */

    xu_ly();
}

/* Trinh bien dich khong biet ISR ton tai, vi khong ai goi no
   tu ma C. No la mot muc trong bang vector. Nen no coi
   g_co_ngat khong bao gio doi. */

Ba: biến sửa trong hàm xử lý tín hiệu

#include <signal.h>

volatile sig_atomic_t dang_chay = 1;

static void xu_ly_tin_hieu(int s) { (void)s; dang_chay = 0; }

int main(void) {
    signal(SIGINT, xu_ly_tin_hieu);

    while (dang_chay) { lam_viec(); }

    don_dep();
}

/* Kieu PHAI la sig_atomic_t: chuan chi bao dam kieu do doc ghi
   duoc tron ven trong ngu canh xu ly tin hieu.

   Va PHAI co volatile, vi ham xu ly duoc goi bat dong bo. */

Bốn: biến bị sửa qua setjmp và longjmp

#include <setjmp.h>

jmp_buf buf;

int main(void) {
    volatile int x = 0;          /* PHAI co volatile */
    int y = 0;                   /* gia tri KHONG XAC DINH sau longjmp */

    if (setjmp(buf) == 0) {
        x = 1;
        y = 1;

        longjmp(buf, 1);
    }

    /* Toi day x chac chan la 1.
       y co the la 0 hoac 1, tuy trinh bien dich giu no
       trong thanh ghi hay trong ngan xep. */

    return 0;
}

/* Chuan noi: bien tu dong khong volatile ma bi sua sau setjmp
   co gia tri khong xac dinh sau longjmp. */

#Nó không bảo đảm tính nguyên tử

at.c
volatile int dem;

void tang(void) { ++dem; }
terminal
gcc -O2 -S -masm=intel -o - at.c
tang:
    mov  eax, DWORD PTR dem[rip]    ; DOC
    add  eax, 1                      ; SUA
    mov  DWORD PTR dem[rip], eax     ; GHI
    ret
volatile_Atomicmutex
Ngăn trình biên dịch tối ưuCóCóCó
Bảo đảm đọc sửa ghi nguyên tửKhôngCóCó
Sinh rào cản bộ nhớKhôngCóCó
Bảo vệ được nhiều biến cùng lúcKhôngKhôngCó
Chi phí khi không tranh chấpKhôngThấpTrung bình
Dùng choPhần cứng, ISR, tín hiệuMột biến chia sẻ giữa luồngNhiều biến, hoặc thao tác phức tạp

#Đặt volatile đúng chỗ

volatile int *p;          /* thu p TRO TOI la volatile */
int *volatile p;          /* CHINH p la volatile */
volatile int *volatile p; /* ca hai */
Khai báo*p là volatilep là volatileDùng khi
volatile int *p;CóKhôngTrỏ tới thanh ghi phần cứng. Đây là trường hợp thường gặp
int *volatile p;KhôngCóChính con trỏ bị phần cứng hoặc ISR sửa. Rất hiếm
volatile int *volatile p;CóCóCả hai, ví dụ con trỏ vòng do DMA cập nhật

#Ba chỗ lạm dụng

Một: dùng volatile thay cho công cụ đồng bộ

Đã bàn ở mục trên. Nếu có hai luồng thì dùng _Atomic hoặc mutex, không dùng volatile.

Hai: rắc volatile để chữa một lỗi không hiểu

/* "Chuong trinh chay sai o -O2 nhung dung o -O0.
    Toi them volatile vao va no chay dung roi." */

/* Gan nhu chac chan ban dang co mot HANH VI KHONG XAC DINH
   o cho khac, va volatile chi lam trinh bien dich toi uu it di
   nen loi tam thoi khong lo ra.

   Bon buoc dung:
     1. bien dich voi -fsanitize=address,undefined
     2. chay valgrind
     3. bat -Wall -Wextra -Wpedantic va sua het canh bao
     4. neu van chua ra, doc lai ma tim UB: tran so nguyen,
        con tro treo, doc bien chua khoi tao

   Phan 13 danh ca mot chuong cho viec nay. */

Ba: dùng volatile để chống tối ưu khi đo tốc độ

/* Muc dich chinh dang: ngan trinh bien dich xoa ma dang do */

volatile int ket_qua;

for (int i = 0; i < N; ++i) ket_qua = tinh(i);

/* Cach nay CHAY, nhung no cung lam cham vong lap vi moi vong
   phai ghi that ra bo nho.

   Cach tot hon tren GCC va Clang: */

static inline void giu_lai(void *p) {
    __asm__ volatile ("" : : "r"(p) : "memory");
}

int ket_qua;

for (int i = 0; i < N; ++i) { ket_qua = tinh(i); giu_lai(&ket_qua); }

/* Doan hop ngu rong noi voi trinh bien dich rang no da doc p
   va co the sua bo nho, nen ket_qua khong bi xoa. Nhung khong
   sinh ra mot lenh nao. */

Tự làm thử

  1. Viết hai hàm chờ, có và không có volatile, rồi so hợp ngữ ở -O2.
  2. Xem hợp ngữ của ++dem với volatile int dem và đếm số lệnh.
  3. Chạy hai luồng cùng tăng một biến volatile một triệu lần và xem kết quả cuối.
  4. Đổi sang _Atomic và xác nhận kết quả luôn đúng.
  5. Khai báo ba biến thể con trỏ volatile và thử gán để xem cái nào bị trình biên dịch từ chối.
  6. Đo tốc độ ghi vào mảng thường và mảng volatile ở -O3.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Không có volatile, vòng lặp chờ bị tối ưu thành jmp tới chính nó, tức treo hẳn chứ không phải chạy sai.
  • Bốn trường hợp bắt buộc: thanh ghi phần cứng, biến sửa trong ISR, biến sửa trong hàm xử lý tín hiệu, và biến sống qua longjmp.
  • volatile int dem; ++dem; sinh ra ba lệnh riêng, nên nó không nguyên tử và không dùng được cho đa luồng.
  • volatile int *p nghĩa là thứ p trỏ tới là volatile, còn int *volatile p nghĩa là chính p.
  • Thêm volatile để chữa một lỗi mà bạn không hiểu là đang giấu một hành vi không xác định ở chỗ khác.