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
retvolatile
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 ưu | Với biến thường | Với volatile |
|---|---|---|
| Giữ giá trị trong thanh ghi qua nhiều lần dùng | Được | Không |
| Bỏ lần đọc mà kết quả không dùng tới | Được | Không |
| Bỏ lần ghi bị lần ghi sau đè lên | Được | Không |
| Đổi thứ tự với truy cập volatile khác | Được | Khô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 | _Atomic | mutex | |
|---|---|---|---|
| Ngăn trình biên dịch tối ưu | Có | Có | Có |
| Bảo đảm đọc sửa ghi nguyên tử | Không | Có | Có |
| Sinh rào cản bộ nhớ | Không | Có | Có |
| Bảo vệ được nhiều biến cùng lúc | Không | Không | Có |
| Chi phí khi không tranh chấp | Không | Thấp | Trung bình |
| Dùng cho | Phần cứng, ISR, tín hiệu | Một biến chia sẻ giữa luồng | Nhiề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à volatile | p là volatile | Dùng khi |
|---|---|---|---|
| volatile int *p; | Có | Không | Trỏ tới thanh ghi phần cứng. Đây là trường hợp thường gặp |
| int *volatile p; | Không | Có | 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ử
- Viết hai hàm chờ, có và không có
volatile, rồi so hợp ngữ ở-O2. - Xem hợp ngữ của
++demvớivolatile int demvà đếm số lệnh. - Chạy hai luồng cùng tăng một biến
volatilemột triệu lần và xem kết quả cuối. - Đổi sang
_Atomicvà xác nhận kết quả luôn đúng. - Khai báo ba biến thể con trỏ
volatilevà thử gán để xem cái nào bị trình biên dịch từ chối. - Đ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ànhjmptớ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 *pnghĩa là thứptrỏ tới là volatile, cònint *volatile pnghĩa là chínhp.- 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.