Đồng bộ hóa và Xử lý Cạnh tranh trong Nhân Linux

Trong hệ thống đa luồng và đa lõi, việc đảm bảo tính toàn vẹn dữ liệu khi nhiều thực thể truy cập tài nguyên chung là một yêu cầu thiết yếu. Các cơ chế đồng bộ hóa như nguyên tử, khóa xoay, đèn hiệu và mutex cung cấp các giải pháp khác nhau tùy theo đặc điểm thời gian chiếm dụng, ngữ cảnh thực thi và mức độ an toàn cần thiết.

1. Khái niệm cơ bản

Đồng thời (Concurrency) xảy ra khi hai hoặc nhiều luồng, tiến trình hoặc xử lý gián đoạn (interrupt handler) có khả năng thực thi song song — dù trên cùng một lõi (thông qua chuyển đổi ngữ cảnh) hay trên nhiều lõi vật lý.

Cạnh tranh (Race condition) xuất hiện khi các thực thể đồng thời truy cập vào một tài nguyên chia sẻ (biến toàn cục, vùng nhớ phần cứng, cấu trúc dữ liệu) mà không có cơ chế kiểm soát truy cập, dẫn đến trạng thái không xác định hoặc sai lệch dữ liệu.

2. Thao tác nguyên tử

Một thao tác được coi là nguyên tử nếu nó không thể bị chia nhỏ thành các bước con có thể bị ngắt giữa chừng bởi luồng khác. Đây là nền tảng cho mọi cơ chế đồng bộ hóa cấp thấp.

2.1 Biến nguyên tử kiểu số nguyên

Nhân Linux cung cấp kiểu atomic_t để thao tác an toàn trên giá trị 32-bit:

atomic_t counter = ATOMIC_INIT(5);  // Khởi tạo với giá trị 5
atomic_add(3, &counter);           // Tăng thêm 3 → giá trị trở thành 8
int current = atomic_read(&counter); // Đọc giá trị hiện tại: 8
atomic_dec_and_test(&counter);      // Giảm 1 và kiểm tra xem có bằng 0 không

Với kiến trúc 64-bit, sử dụng atomic64_t cùng các hàm tương ứng như atomic64_inc(), atomic64_xchg().

2.2 Thao tác bit nguyên tử

Khi cần điều khiển từng bit trong một vùng nhớ, các hàm như sau đảm bảo tính nguyên tử:

  • set_bit(7, &flags): bật bit thứ 7 của biến flags
  • test_and_clear_bit(3, &status): xóa bit thứ 3 và trả về giá trị cũ
  • change_bit(0, &control): đảo trạng thái bit 0

3. Khóa xoay (Spinlock)

Khóa xoay phù hợp khi thời gian giữ khóa ngắn (vài microsecond), vì luồng chờ sẽ liên tục kiểm tra trạng thái khóa — tiêu tốn chu kỳ CPU nhưng tránh chi phí chuyển ngữ cảnh.

3.1 Định nghĩa và khởi tạo

spinlock_t my_lock;
spin_lock_init(&my_lock); // Hoặc dùng macro: DEFINE_SPINLOCK(my_lock);

3.2 Sử dụng trong ngữ cảnh khác nhau

Do khóa xoay vô hiệu hóa cơ chế chiếm quyền (preemption), việc kết hợp với quản lý ngắt là bắt buộc để tránh deadlock:

// Trong luồng người dùng (process context)
unsigned long irq_flags;
spin_lock_irqsave(&my_lock, irq_flags);
// ... vùng giới hạn (critical section)
spin_unlock_irqrestore(&my_lock, irq_flags);

// Trong xử lý ngắt (interrupt context)
spin_lock(&my_lock);
// ... xử lý nhanh
spin_unlock(&my_lock);

Lưu ý: Không được gọi hàm gây ngủ (như wait_event(), msleep()) bên trong vùng giới hạn được bảo vệ bởi spinlock.

4. Đèn hiệu (Semaphore)

Đèn hiệu hỗ trợ đồng bộ hóa với khả năng chặn (blocking), thích hợp khi thời gian chiếm dụng tài nguyên dài hơn. Giá trị ban đầu xác định loại đèn hiệu:

  • Đèn hiệu nhị phân: khởi tạo với giá trị 1 → dùng cho loại trừ lẫn nhau (mutual exclusion)
  • Đèn hiệu đếm: khởi tạo với giá trị N > 1 → kiểm soát tối đa N luồng đồng thời truy cập
struct semaphore sem;
sema_init(&sem, 1); // Đèn hiệu nhị phân

if (down_interruptible(&sem) == 0) {
    // Thành công: vào vùng giới hạn
    // ...
    up(&sem); // Giải phóng
}

Không được sử dụng đèn hiệu trong interrupt context do hàm down() có thể đưa luồng vào trạng thái ngủ.

5. Mutex

Linux cung cấp struct mutex như một cơ chế loại trừ lẫn nhau chuyên biệt hơn đèn hiệu, với các tính năng bổ sung như phát hiện lỗi khóa lặp (recursive lock) và theo dõi chủ sở hữu.

struct mutex data_mutex;
mutex_init(&data_mutex);

mutex_lock(&data_mutex);
// Truy cập dữ liệu chia sẻ
mutex_unlock(&data_mutex);

Mutex cũng không thể dùng trong interrupt context, nhưng cho phép gọi các hàm gây chặn bên trong vùng giới hạn — điều kiện cần khi phải thực hiện I/O hoặc chờ sự kiện.

Thẻ: linux-kernel Concurrency Synchronization spinlock semaphore

Đăng vào ngày 8 tháng 6 lúc 19:00