Atomic linux là gì

Giới thiệu

Như đã nói trong bài học trước, để ngăn chặn hiện tượng race condition, khi lập trình device driver, ta phải tuân theo nguyên tắc mutual exclusion: tại một thời điểm, chỉ có một thread được phép truy cập critical resource. Linux đã xây dựng một số kỹ thuật đồng bộ dựa theo nguyên tắc này.

Đôi khi, critical resource chỉ là một biến hay một bit, và thao tác truy cập cũng đơn giản (tăng, giảm,…). Đối với những trường hợp như vậy, Linux cung cấp kỹ thuật atomic để ngăn không cho race condition xảy ra. Bài học hôm nay sẽ trình bày về cơ chế cũng như cách sử dụng kỹ thuật atomic trong lập trình device driver.

Cơ chế

Để hiểu về kỹ thuật atomic, ta xét ví dụ sau. Giả sử, device driver có 2 thread. Trong quá trình hoạt động, cả hai đều có nhu cầu sử dụng biến toàn cục a. Giả sử, mỗi khi truy cập biến a, các thread sẽ thực thi lệnh ++a. Thực chất, sau khi biên dịch, lệnh này được tách nhỏ thành 3 lệnh assembly sau:

  • Lệnh load R, [a]: CPU lấy giá trị của biến a trên RAM vào thanh ghi R
  • Lệnh inc R: CPU tăng giá trị của thanh ghi R thêm 1 đơn vị
  • Lệnh store R, [a]: CPU ghi giá trị của thanh ghi R trở lại biến a trên RAM

Nếu cả hai thread truy cập biến a cùng lúc, thì race condition rất có thể sẽ xảy ra. Xét tình huống sau (giả sử ban đầu a = 0):

Thời điểm

Thread 1 (thực thi bởi CPU0)

Thread 2 (thực thi bởi CPU1)

T1

load R, [a]

(sau lệnh này, thanh ghi R của CPU0 = 0, a = 0)

T2

inc R

(sau lệnh này, thanh ghi R của CPU0 = 1)

load R, [a]

(sau lệnh này, thanh ghi R của CPU1 = 0, a = 0)

T3

store R, [a]

(sau lệnh này, a = 1)

inc R

(sau lệnh này, thanh ghi R của CPU1 = 1)

T4

store R, [a]

(sau lệnh này, a = 1)

Lẽ ra, kết quả cuối cùng của a phải bằng 2 thì nó lại chỉ bằng 1. Để giải quyết vấn đề này, ta có thể sử dụng thao tác atomic. Thao tác atomic sẽ gộp 3 thao tác nhỏ thành 1 thao tác duy nhất.

Thời điểm

Thread 1

Thread 2

T1

load, inc, store [a]

(sau lệnh này, a = 1)

T2

load, inc, store [a]

(sau lệnh này, a = 2)

Chú ý rằng, do giới hạn về phần cứng, nên 2 lõi không thể truy cập RAM đồng thời được. Chính vì vậy, 2 lệnh trên xảy ra ở 2 thời điểm khác nhau.

Linux kernel hỗ trợ 2 nhóm thao tác atomic. Một nhóm dùng để truy cập critical resource là số nguyên (atomic integer operation). Một nhóm dùng để truy cập critical resource là bit (atomic bitwise operation). Dưới đây, chúng ta sẽ cùng tìm hiểu cách sử dụng 2 nhóm này.

Các thao tác atomic trên số nguyên

Khi critical resource là một số nguyên, và ta muốn áp dụng kỹ thuật atomic để bảo vệ critical resource đó, ta cần khai báo số nguyên này theo kiểu atomic_t, chứ không phải theo kiểu int.

Dưới đây là các thao tác atomic làm việc với số nguyên có kiểu atomic_t. Hầu hết các kiến trúc vi xử lý đều chứa những thao tác này trong tập lệnh.

Các thao tác

Ý nghĩa

atomic_t u = ATOMIC_INIT(int i)

Khai báo một biến atomic u rồi khởi tạo giá trị của u bằng i

int atomic_read(atomic_t *u)

Đọc giá trị của biến atomic u

void atomic_set(atomic_t *u, int i)

Gán giá trị i cho biến atomic u

void atomic_add(int i, atomic_t *u)

Tăng u thêm i đơn vị

void atomic_sub(int i, atomic_t *u)

Giảm u đi i đơn vị

void atomic_inc(atomic_t *u)

Tăng u thêm 1 đơn vị

void atomic_dec(atomic_t *u)

Giảm u đi 1 đơn vị

int atomic_sub_and_test(int i, atomic_t *u)

Giảm u đi i đơn vị, và trả về TRUE nếu hiệu bằng 0

int atomic_add_negative(int i, atomic_t *u)

Tăng u thêm i đơn vị và trả về TRUE nếu tổng là một số âm

int atomic_inc_and_test(atomic_t *u)

Tăng u thêm 1 đơn vị và trả về true nếu kết quả bằng 0

int atomic_dec_and_test(atomic_t *u)

Giảm u đi 1 đơn vị và trả về true nếu kết quả bằng 0

int atomic_add_return(int i, atomic_t *u)

Tăng u thêm i đơn vị và trả về kết quả

int atomic_sub_return(int i, atomic_t *u)

Giảm u đi i đơn vị và trả về kết quả

int atomic_inc_return(atomic_t *u)

Tăng u thêm 1 đơn vị và trả về kết quả

int atomic_dec_return(atomic_t *u)

Giảm u đi 1 đơn vị và trả về kết quả

Các thao tác atomic trên bit

Khi critical resource là một bit, và ta muốn áp dụng kỹ thuật atomic để bảo vệ critical resource đó, ta có thể sử dụng các thao tác sau:

Các thao tác

Ý nghĩa

void set_bit(int nr, void *addr)

Thiết lập bit thứ nr của biến có địa chỉ addr

void clear_bit(int nr, void *addr)

Xóa bit thứ nr của biến có địa chỉ addr

void change_bit(int nr, void *addr)

Đảo bit thứ nr của biến có địa chỉ addr

int test_and_set_bit(int nr, void *addr)

Thiết lập bit thứ nr của biến có địa chỉ addr và trả về giá trị cũ của bit thứ nr

int test_and_clear_bit(int nr, void *addr)

Xóa bit thứ nr của biến có địa chỉ addr và trả về giá trị cũ của bit thứ nr

int test_and_change_bit(int nr, void *addr)

Đảo bit thứ nr của biến có địa chỉ addr và trả về giá trị cũ của bit thứ nr

int test_bit(int nr, void *addr)

Trả về giá trị của bit thứ nr của biến có địa chỉ addr

Dưới đây là một ví dụ về sử dụng các thao tác này:

unsigned long a = 0
set_bit(0, &a); //thiết lập bit 0 của biến a bằng 1
change_bit(1, &a); //đảo bit 1 của biến a
clear_bit(1, &a); //xóa bit 1 của biến a

Case study

Trong ví dụ này, chúng ta sẽ áp dụng các thao tác atomic trên số nguyên để cải thiện vchar driver trong bài hôm trước. Đầu tiên, ta tạo thư mục cho bài học ngày hôm nay như sau:

cd /home/ubuntu/ldd/phan_6
cp -r bai_6_1 bai_6_2
cd bai_6_2

Sau đó, trong vchar_driver.c, ta thay đổi lại khai báo của biến critical_resource.

Atomic linux là gì

Cuối cùng, ta thay thế các thao tác cộng/trừ thông thường bằng các thao tác atomic để tránh xảy ra race condition trên biến này.

Atomic linux là gì

Bây giờ, ta gõ lệnh make để biên dịch lại char driver. Sau khi biên dịch thành công, ta thực hiện kiểm tra như hình 1 dưới đây và thấy rằng, kết quả cuối cùng của biến critical_resource đúng bằng 3,145,728:

Atomic linux là gì

Hình 1. Sử dụng các thao tác atomic giúp ngăn ngừa race condition trên biến critical_resource

Kết luận

Khi critical resource chỉ là một số nguyên hoặc một bit, và thao tác truy cập cũng đơn giản (cộng, trừ, tăng, giảm,…), lập trình viên nên áp dụng kỹ thuật atomic để tránh xảy ra race condition.

Trong các bài học tiếp theo, ta sẽ cùng tìm hiểu các kỹ thuật đồng bộ khác cho phép bảo vệ các crtical resource phức tạp hơn.