Chào mừng bạn đến với Vimentor!

Hỏi đáp
Đăng ký

Giải pháp mutex lock

Giới thiệu

Trong bài học trước, ta đã tìm hiểu cách sử dụng spinlock để phòng tránh race condition. Do kỹ thuật spinlock áp dụng cơ chế chờ bận (busy-waiting), nên sẽ rất lãng phí thời gian của CPU nếu phải chờ đợi lâu.

Để khắc phục nhược điểm trên, Linux kernel hỗ trợ một kỹ thuật khác, có tên là mutex lock. Bài học hôm nay sẽ trình bày về kỹ thuật này:

  • Mutex lock là gì, có cấu tạo như thế nào, hoạt động ra sao, bảo vệ critical resource như thế nào?
  • Sử dụng kỹ thuật mutex lock trong lập trình device driver như thế nào?
  • Cần chú ý những gì khi sử dụng kỹ thuật mutex lock?

Giới thiệu về mutex lock

Mutex lock là gì?

Mutex lock là một cấu trúc dữ liệu, được Linux kernel xây dựng theo nguyên tắc mutual exclusion, dùng để ngăn chặn race condition xảy ra trên các cấu trúc dữ liệu khác. Nói nôm na, mutex lock đảm bảo rằng: tại một thời điểm bất kì, chỉ có tối đa một thread truy cập vào critical resource.

Hình 1 Mutex lock giống như một ổ khóa dùng để bảo vệ critical resource

Mutex lock có cấu tạo như thế nào?

Mutex lock gồm 3 thành phần chính: biến count, biến owner và hàng đợi wait_list. Dựa vào đó, Linux kernel đã xây dựng cấu trúc mutex để biểu diễn một mutex lock.

struct mutex {
    /*
     * Biến count lưu trạng thái của mutex lock, cũng như trạng thái của
     * critical resource.
     *    - Nếu count = 1, thì trạng thái của mutex lock đang là UNLOCKED,
     *      còn trạng thái của critical resource đang là AVAILABLE.
     *    - Nếu count < 1, thì trạng thái của mutex lock đang là LOCKED,
     *      còn trạng thái của critical resource đang là UNAVAILABLE.
     *        count = 0: không có thread nào đang phải đợi để được
     *                   sử dụng critical resource.
     *        count < 0: có thread đang phải đợi để được sử dụng
     *                   critical resource.
     */
    atomic_t		count;

    /*
     * Hàng đợi wait_list có thể bị nhiều thread truy cập đồng thời.
     * wait_lock là một spinlock bảo vệ wait_list
     */
    spinlock_t		wait_lock;

    /*
     * Hàng đợi wait_list chứa danh sách các thread đang phải đợi để
     * chiếm được mutex lock, cũng chính là danh sách các thread
     * đang phải đợi để được sử dụng critical resource.
     */
    struct list_head	wait_list;

    /*
     * Biến owner mô tả thread đang chiếm dụng mutex lock, cũng chính là
     * thread đang sử dụng critical resource.
     */
    struct task_struct	*owner;
    ...
};

Mutex lock hoạt động ra sao?

Hình 2. Sơ đồ biểu diễn các trạng thái của một mutex lock

Khi count đang bằng 1 (tức là mutex lock đang ở trạng thái UNLOCKED), nếu một thread gọi hàm mutex_lock, thì:

  • biến count bị giảm thành 0 (tức là mutex lock bị chuyển sang trạng thái LOCKED). Ta nói rằng thread đã khóa mutex lock lại.
  • biến owner được thiết lập bằng thread đó. Ta nói rằng thread đã chiếm dụng mutex lock.
  • CPU bắt đầu thực thi critical section của thread (nói theo ngôn ngữ của CPU), hay thread đang sử dụng critical resource (nói theo ngôn ngữ của Linux kernel).

Khi count đang nhỏ hơn 1 (tức là đang ở trạng thái LOCKED), nếu một thread gọi hàm mutex_lock, thì:

  • biến count sẽ giảm xuống 1 đơn vị.
  • CPU tạm ngừng thực thi thread này rồi chuyển sang thực thi thread khác (nói theo ngôn ngữ của CPU). Hay nói theo ngôn ngữ của Linux kernel, thread được thêm vào hàng đợi wait_list và sẽ đi ngủ, sau đó Linux kernel sẽ lập lịch cho thread khác. Do đó, ta nói rằng, mutex lock áp dụng cơ chế sleep-waiting, tức là mutex lock thuộc loại sleep lock, trái với spinlock thuộc loại busy lock.

Khi count đang nhỏ hơn 1 (tức là đang ở trạng thái LOCKED), nếu một thread A gọi hàm mutex_unlock, thì:

  • biến owner được thiết lập thành NULL. Ta nói rằng, thread A đã giải phóng mutex lock.
  • biến count sẽ được tăng thành 1 hoặc được thiết lập bằng 1 (tức là mutex lock chuyển sang trạng thái UNLOCKED). Ta nói rằng thread A đã mở khóa mutex lock.
  • nếu hàng đợi wait_list không rỗng và giả sử thread B nằm ở đầu hàng đợi, CPU sẽ chuyển sang thực thi thread B (nói theo ngôn ngữ của CPU). Hay nói theo ngôn ngữ của Linux kernel, Linux kernel sẽ đánh thức thread B dậy. Sau khi thức dậy, thread B sẽ chuyển mutex lock sang trạng thái LOCKED (thay đổi biến count  thành -1 nếu vẫn còn các thread khác đang đợi, hoặc thành 0 nếu không còn thread nào đang đợi). Sau đó, thread B chiếm lấy mutex lock rồi bắt đầu sử dụng critical resource.

Mutex lock bảo vệ critical resource như thế nào?

Trong khi lập trình device driver, ta đặt hàm mutex_lock và mutex_unlock lần lượt vào trước và sau critical section của mỗi thread. Việc làm này giúp bảo vệ critical resource. Để thấy được điều này, ta xét ví dụ sau. Giả sử, hệ thống có kernel thread A và B được thực thi riêng biệt trên 2 lõi CPU0 và CPU1. Cả 2 thread đều có nhu cầu sử dụng critical resource R, và tài nguyên R được bảo vệ bằng mutex lock M. Xét 2 trường hợp sau:

  • Trường hợp 1: A muốn truy cập R trong khi B đang sử dụng R.
    • Trước khi thực thi các lệnh trong critical section của thread A, CPU0 sẽ thực thi hàm mutex_lock và thấy rằng M đang ở trạng thái LOCKED. Khi đó, CPU0 sẽ dừng thực thi thread A rồi chuyển sang thực thi một thread C nào đó.
    • Sau khi thực thi xong critical section của thread B, CPU1 thực thi tiếp hàm mutex_unlock để chuyển M sang trạng thái UNLOCKED. Lúc này, thread A sẽ chiếm lấy M và CPU0 tiếp tục thực thi thread A.
  • Trường hợp 2: cả A và B đồng thời muốn truy cập R.
    • Khi đó, cả 2 thread đồng thời thực thi hàm mutex_lock. Tuy nhiên, do hàm mutex_lock dùng thao tác atomic để thay đổi biến count, nên chỉ có một trong hai thread chiếm được M.
    • Thread nào chiếm được M trước thì sẽ sử dụng R trước. Thread nào không chiếm được M thì sẽ đi ngủ cho đến khi thread đầu tiên sử dụng xong R.

Như vậy, tại bất cứ thời điểm nào, tối đa chỉ có một thread được phép chiếm dụng mutex lock, đồng nghĩa với việc, tối đa chỉ có một thread được phép sử dụng critical resource. Do đó, race condition sẽ không xảy ra và critical resource được bảo vệ.

Sử dụng các mutex lock kernel API trong lập trình device driver

Để khai báo và khởi tạo giá trị cho mutex lock ngay từ lúc biên dịch (compile time), ta có thể sử dụng macro DEFINE_MUTEX. Ví dụ:

DEFINE_MUTEX(my_mutexlock); //khởi tạo trạng thái UNLOCKED cho my_mutexlock

Tuy nhiên, mutex lock thường nằm trong một cấu trúc lớn hơn và được cấp phát bộ nhớ trong quá trình chạy (run time). Do đó, ta sẽ dùng hàm mutex_init để khởi tạo giá trị cho mutex lock. Ta thường gọi hàm mutex_init trong hàm khởi tạo của driver. Ví dụ:

/*
 * Khi ta muốn bảo vệ dữ liệu trong cấu trúc my_struct, ta sẽ nhúng
 * biến cấu trúc kiểu mutex vào trong cấu trúc my_struct.
 * Biến cấu trúc my_struct_t đại diện cho critical resource,
 * còn my_mutexlock đại diện cho ổ khóa bảo vệ critical resource.
 */
struct my_struct {
    ...
    struct mutex my_mutexlock;
    ...
} my_struct_t;

int init_driver_func() {
    ...
    mutex_init(&my_struct_t.my_mutexlock);
    ...
}

Sau khi đã khai báo và khởi tạo mutex lock, ta đặt hàm mutex_lock và mutex_unlock lần lượt vào trước và sau critical section của thread để ngăn không cho race condition xảy ra.

mutex_lock(&my_mutexlock);

/* critical section của kernel thread */

mutex_unlock(&my_mutexlock);

Đôi khi, ta có thể sử dụng hàm mutex_lock_interruptible thay cho hàm mutex_lock. Cách sử dụng như sau:

/*
 * Ta có thể sử dụng "int mutex_lock_interruptible(struct mutex *lock)"
 * thay cho hàm "void mutex_lock(struct mutex *lock)".
 * Nếu chiếm được mutex lock, hàm này sẽ trả về 0.
 * Nếu chưa chiếm được, thread (gọi hàm này) sẽ bị tạm ngừng hoạt động.
 * Nếu thread đang tạm ngừng hoạt động mà có một tín hiệu, hàm này trả về -EINTR.
 *
 * Khi nào sử dụng mutex_lock_interruptible thay cho mutex_lock?
 * Đó là khi ta muốn thread tiếp nhận các tín hiệu (signal) trong lúc
 * đang chờ mutex lock.
 *
 * Xét trường hợp tiến trình P trên user space yêu cầu device driver
 * đọc/ghi dữ liệu trong critical resource R. Khi đó, tương ứng với P,
 * sẽ có một kernel thread T định truy cập vào R. Nếu kernel thread T'
 * đang truy cập R, thread T sẽ bị tạm dừng tại hàm mutex_lock_interruptible.
 * Ta nói, thread T đang bị blocking bởi hàm mutex_lock_interruptible.
 * Nếu lúc này người dùng tạo một tín hiệu (signal), ví dụ nhấn tổ hợp
 * CTRL + C để hủy tiến trình P, thì hàm mutex_lock_interruptible
 * sẽ trả luôn về -EINTR mà không blocking thread T nữa. Điều này giúp hủy
 * tiến trình P luôn mà không phải chờ đợi thread T' giải phóng mutex lock.
 */
if (mutex_lock_interruptible(&my_mutexlock))
    return -ERESTARTSYS;

/* critical section của kernel thread */

mutex_unlock(&my_mutexlock);

Ngoài ra, Linux kernel còn hỗ trợ 2 hàm hữu dụng khác là  mutex_trylockmutex_is_locked.

/*
 * hàm: mutex_trylock
 * chức năng: yêu cầu chiếm giữ mutex lock. Nếu không thể chiếm được,
 *            trả luôn về cho thread gọi hàm này. Thread gọi hàm này
 *            sẽ không chờ đợi mutex lock nữa (non-blocking). 
 * tham số đầu vào:
 *    *lock    [IO]: là địa chỉ của vùng nhớ chứa cấu trúc mutex.
 * giá trị trả về:
 *    Nếu chiếm được mutex lock, trả về một số dương.
 *    Nếu không chiếm được mutex lock, trả về 0.
 */
int mutex_trylock(struct mutex *lock);

/*
 * hàm: mutex_is_locked
 * chức năng: kiểm tra xem mutex lock có đang bị chiếm dụng không. 
 * tham số đầu vào:
 *    *lock    [IO]: là địa chỉ của vùng nhớ chứa cấu trúc mutex.
 * giá trị trả về:
 *    Nếu mutex lock đang không bị thread nào chiếm dụng, trả về 0.
 *    Nếu mutex lock đang bị một thread chiếm dụng, trả về số dương.
 */
int mutex_is_locked(struct mutex *lock);

Chú ý khi sử dụng mutex lock

Khi triển khai giải pháp này, ta cần chú ý mấy điểm sau:

  • Do mutex lock áp dụng cơ chế chờ đợi sleep-waiting, nên ta chỉ sử dụng kỹ thuật này khi khoảng thời gian chờ đợi dài. Thông thường, nếu critical section chứa lời gọi hàm sleep/schedule hoặc gồm nhiều câu lệnh, thì có thể áp dụng mutex lock.
  • Ta chỉ nên sử dụng kỹ thuật này trong các thread được phép đi ngủ, ví dụ như các kernel thread thông thường, hoặc bottom-half được triển khai bằng workqueue. Ta không nên sử dụng kỹ thuật này trong các thread không được phép đi ngủ, ví dụ như ISR, hoặc bottom-half được triển khai bằng tasklet/softirq.
  • Khi lập trình, ta phải sử dụng cặp mutex_lock/mutex_unlock trong cùng một thread. Nghĩa là thread nào gọi hàm mutex_lock thì chính thread đó phải gọi hàm mutex_unlock. Nói nôm na, ai chiếm đoạt mutex lock thì người đó phải giải phóng mutex lock. Hai lỗi có thể gặp phải trong lập trình đó là:
    • Sau khi gọi hàm mutex_lock, bạn kiểm tra một điều kiện nào đó và thấy có lỗi, thế là bạn return luôn mà quên không gọi hàm mutex_unlock. Tham khảo ví dụ tại đây.
    • Gọi hàm mutex_lock trong một thread và gọi hàm mutex_unlock trong một thread khác.
  • Không được đệ quy mutex lock. Nói nôm na, nếu một thread đang chiếm dụng một mutex lock thì không được phép gọi hàm mutex_lock để đòi chính cái mutex lock đó. Nếu vi phạm, hệ thống sẽ rơi vào tình trạng deadlock.
  • Luôn khởi tạo mutex lock trước khi sử dụng.
  • Khi đang chiếm dụng một spinlock, ta không được gọi hàm mutex_lock hoặc mutex_lock_interruptible để đòi lấy một mutex lock.

Case study

Trong ví dụ này, chúng ta sẽ áp dụng kỹ thuật đồng bộ mutex lock để 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_4

Bây giờ, ta tiến hành sửa file vchar_driver.c. Đầu tiên, để triển khai mutex lock, ta cần tham chiếu tới thư viện <linux/mutex.h>.

Tiếp theo, ta khai báo biến vchar_mutexlock trong cấu trúc _vchar_drv. Mutex lock này giúp bảo vệ dữ liệu trong biến critical_resource.

Sau đó, trong hàm vchar_driver_init, ta khởi tạo mutex lock này:

Cuối cùng, ta thêm hàm mutex_lockmutex_unlock vào trước vào sau vùng critical section.

 

Bây giờ, ta gõ lệnh make để biên dịch lại vchar driver. Sau khi biên dịch thành công, ta thực hiện kiểm tra như hình 3 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.

Hình 3. Sử dụng kỹ thuật mutex lock giúp ngăn ngừa race condition trên biến critical_resource

Kết luận

Mutex lock giống như một chiếc ổ khóa thuộc loại sleep lock giúp bảo vệ critical resource. Một thread muốn truy cập vào một critical resource thì trước hết, thread đó phải chiếm được mutex lock. Nếu không chiếm được, thread đó phải chờ đợi theo cơ chế sleep-waiting.

Ta chỉ nên sử dụng kỹ thuật này khi mà thời gian chờ đợi ổ khóa mutex lock dài. Kỹ thuật này phù hợp với các thread được phép đi ngủ (như các thread thông thường hoặc các bottom-half được triển khai bằng workqueue). Khác với kỹ thuật spinlock, nếu vận dụng kỹ thuật này, ta được phép gọi hàm sleep hoặc schedule trong critical section của thread.

** Nếu bạn muốn viết các nội dung đặt biệt thì hãy làm theo hướng dẫn sau

Xem thêm 10 bình luận
Viết blog mới của bạn
Báo lỗi trang
Đang tải