Chi tiết bài học Giải pháp semaphore

Giải pháp semaphore

Giới thiệu

Ngoài spinlock và mutex lock, ta cũng hoàn toàn có thể vận dụng kỹ thuật semaphore để bảo vệ tài liệu trong critical resource. Không chỉ là một kỹ thuật đồng nhất tài nguyên, semaphore cũng được biết đến là một kỹ thuật đồng nhất hoạt động giải trí .
Do khoanh vùng phạm vi của khóa học, bài học kinh nghiệm này chỉ trình diễn về semaphore với tư cách là một kỹ thuật đồng điệu tài nguyên :

  • Giới thiệu semaphore là gì, có cấu trúc như thế nào, hoạt động giải trí ra làm sao, bảo vệ critical resource như thế nào ?

  • Sử dụng kỹ thuật semaphore trong lập trình device driver như thế nào ?

  • Cần chú ý quan tâm những gì khi sử dụng kỹ thuật semaphore ?

Giới thiệu về semaphore

Semaphore là gì ?

Semaphore là một cấu trúc tài liệu, được dùng để đồng điệu tài nguyên và đồng nhất hoạt động giải trí .
Khi được dùng với mục tiêu đồng điệu tài nguyên, semaphore tương tự như như một bộ những chìa khóa dự trữ. Nếu một thread lấy được một chiếc chìa khóa, thread đó được phép truy vấn vào tài nguyên. Nhưng nếu không còn chiếc chìa khóa nào, thread đó phải đợi cho tới khi một thread khác trả lại chìa khóa dự trữ. Nhờ vậy, race condition sẽ bị ngăn ngừa .

Hình 1. Sử dụng semaphore để đồng nhất tài nguyên

Semaphore có cấu trúc như thế nào ?

Semaphore gồm 2 thành phần chính: biến count và hàng đợi wait_list. Linux kernel sử dụng cấu trúc semaphore để biểu diễn một semaphore.

struct semaphore {
    /*
     * Do cấu trúc semaphore cũng bị nhiều thread truy cập đồng thời,
     * nên semaphore cũng được xem là một critical resource. Biến @lock là
     * một spinlock bảo vệ @count và @wait_list trong cấu trúc semaphore.
     */
    raw_spinlock_t		lock;

    /*
     * Biến count vừa thể hiện trạng thái của semaphore, vừa thể hiện
     * trạng thái của critical resource.
     *   > 0: semaphore đang ở trạng thái AVAILABLE,
     *        còn critical resource đang ở trạng thái READY.
     *        count cũng thể hiện còn bao nhiêu thread nữa được phép
     *        sử dụng critical resource.
     *   = 0: semaphore đang ở trạng thái UNAVAILABLE,
     *        còn critical resource đang ở trạng thái BUSY.
     */
    unsigned int		count;

    // wait_list là danh sách các thread đang chờ đợi để có được semaphore
    struct list_head	wait_list;
};

Căn cứ vào giá trị của biến count, semaphore được chia làm 2 loại là counting semaphorebinary semaphore.

  • Nếu giá trị cực đại của biến count lớn hơn 1, thì semaphore được gọi là counting semaphore. Giá trị cực đại của biến count thể hiện số lượng thread tối đa được phép sử dụng critical resource tại cùng một thời điểm.

  • Nếu biến count chỉ có hai giá trị 0 và 1, thì semaphore được gọi là binary semaphore. Binary semaphore có một số nét tương đồng với mutex lock.

Semaphore hoạt động giải trí thế nào ?

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

Khi count đang lớn hơn 0, tức là semaphore đang ở trạng thái AVAILABLE, nếu một thread gọi hàm down, thì biến count bị giảm đi 1 đơn vị (nếu hiệu bằng 0 thì semaphore chuyển sang trạng thái UNAVAILABLE). Sau đó, CPU bắt đầu thực thi critical section của thread (nói theo ngôn ngữ của CPU), hay thread bắt đầu sử dụng critical resource (nói theo ngôn ngữ của Linux kernel).

Khi count đang bằng 0, tức là semaphore đang ở trạng thái UNAVAILABLE, nếu một thread gọi hàm down, thì CPU tạm dừ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à đi ngủ, sau đó Linux kernel sẽ lập lịch cho thread khác. Do đó, ta nói rằng, semaphore áp dụng cơ chế sleep-waiting.

Khi wait_list vẫn còn ít nhất một thread đang phải đợi, nếu một thread A gọi hàm up, thì CPU sẽ chuyển sang thực thi thread B nằm ở vị trí đầu tiên trong hàng đợi wait_list (nói theo ngôn ngữ của CPU). Hay nói theo ngôn ngữ của Linux kernel, Linux kernel đánh thức thread B dậy, sau đó thread B bắt đầu sử dụng critical resource.

Khi wait_list không còn thread nào chờ đợi, nếu một thread gọi hàm up, thì biến count được tăng thêm 1 đơn vị, tức là semaphore chuyển sang trạng thái AVAILABLE.

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

Vì hoạt động của binary semaphore tương tự như mutex lock, nên loại semaphore này thường được sử dụng để đồng bộ dữ liệu, phòng tránh race condition. Trong khi lập trình device driver, ta đặt hàm down và up lần lượt vào trước và sau critical section của mỗi thread.

Giả sử, mạng lưới hệ thống có kernel thread A và B được thực thi riêng không liên quan gì đến nhau trên 2 lõi CPU0 và CPU1. Cả 2 thread đều có nhu yếu sử dụng critical resource R, và tài nguyên R được bảo vệ bằng binary semaphore S. Xét 2 trường hợp :

  • Trường hợp 1 : A muốn truy vấn R trong khi B đang truy vấn 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 down và thấy rằng S đang ở trạng thái UNAVAILABLE. Khi đó, CPU0 sẽ tạm 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 up để đánh thức thread A dậy 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 vấn R .

    • Khi đó, cả 2 thread đồng thời thực thi hàm down. Tuy nhiên, do semaphore được bảo vệ bằng một spinlock, nên chỉ có một trong hai thread chiếm được S.

    • Thread nào chiếm được S trước thì sẽ sử dụng R trước. Thread nào không chiếm được S thì sẽ đi ngủ cho đến khi thread tiên phong sử dụng xong R .

Như vậy, tại bất kỳ thời gian nào, tối đa chỉ có một thread được phép chiếm hữu binary semaphore, đồng nghĩa tương quan 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 semaphore kernel API trong lập trình device driver

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

DEFINE_SEMAPHORE(my_semaphore); //khởi tạo trạng thái AVAILABLE cho my_semaphore

Tuy nhiên, semaphore 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 sema_init để khởi tạo giá trị cho semaphore. Ta thường gọi hàm sema_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 semaphore 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_semaphore đại diện cho bộ các chìa khóa bảo vệ critical resource.
 */
struct my_struct {
    ...
    struct semaphore my_semaphore;
    ...
} my_struct_t;

int init_driver_func() {
    ...
     //Giá trị khởi tạo lớn hơn hoặc bằng 0
    sema_init(&my_struct_t.my_semaphore, 1);
    ...
}

Sau khi đã khai báo và khởi tạo semaphore, ta có thể sử dụng cặp hàm downup 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.

down(&my_semaphore);

/* critical section của kernel thread */

up(&my_semaphore);

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

/*
 * Ta có thể sử dụng "int down_interruptible(struct semaphore *sem)"
 * thay cho hàm "void down(struct semaphore *sem)".
 * Nếu chiếm được semaphore, 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 down_interruptible thay cho down?
 * Đó là khi ta muốn thread tiếp nhận các tín hiệu (signal) trong lúc
 * đang chờ semaphore.
 *
 * 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 down_interruptible.
 * Ta nói, thread T đang bị blocking bởi hàm down_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 down_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 semaphore.
 */
if (down_interruptible(&my_semaphore))
    return -ERESTARTSYS;

/* critical section của kernel thread */

up(&my_semaphore);

Ngoài ra, Linux kernel tương hỗ hàm down_trylock .

/*
 * hàm: down_trylock
 * chức năng: yêu cầu chiếm giữ semaphore. 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 semaphore nữa (non-blocking). 
 * tham số đầu vào:
 *    *sem [IO]: là địa chỉ của vùng nhớ chứa cấu trúc semaphore.
 * giá trị trả về:
 *    Nếu chiếm được semaphore, trả về 0.
 *    Nếu không chiếm được semaphore (do thread khác đã chiếm rồi), trả về 1.
 */
int down_trylock(struct semaphore *sem);

Chú ý khi sử dụng semaphore

Khi tiến hành giải pháp này, ta cần quan tâm mấy điểm sau :

  • Do semaphore á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 semaphore.

  • Kỹ thuật này trọn vẹn tương thích để vận dụng trong những thread được phép đi ngủ, ví dụ như những kernel thread thường thì, hoặc bottom-half được tiến hành bằng workqueue .

  • Ta không được phép gọi hàm down hoặc down_interruptible trong ISR, hoặc bottom-half được triển khai bằng tasklet/softirq. Tuy vậy, hàm down_trylockup vẫn có thể được gọi từ ISR.

  • Một thread hoàn toàn có thể giải phóng semaphore mặc dầu nó không phải là người đã chiếm hữu. Điều này khác so với kỹ thuật spinlock và mutex lock .

  • Trong khi đang chiếm dụng một spinlock, ta không được gọi hàm down_interruptible hoặc down để lấy một semaphore.

Case study

Trong ví dụ này, tất cả chúng ta sẽ vận dụng kỹ thuật semaphore để cải tổ vchar driver trong bài hôm trước. Đầu tiên, ta tạo thư mục cho bài học kinh nghiệm ngày thời điểm ngày hôm nay như sau :

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

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

Tiếp theo, ta thêm biến vchar_semaphore trong cấu trúc _vchar_drv. Semaphore 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 semaphore này để tạo ra binary semaphore:

Cuối cùng, ta thêm hàm downup lần lượt 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. Tuy nhiên, có thể thấy rằng, nếu áp dụng kỹ thuật semaphore, thời gian để hoàn thành bài toán lâu hơn rất nhiều so với kỹ thuật spinlock và mutex lock.

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

Kết luận

Semaphore là một cấu trúc, vừa dùng để đồng bộ tài nguyên, vừa dùng để đồng bộ hoạt động. Semaphore gồm 2 thành phần chính là biến count và hàng đợi wait_list. Biến count giúp kiểm soát số lượng thread còn lại được phép truy cập vào critical resource. Còn hàng đợi wait_list chứa danh sách các thread đang phải chờ đợi trước khi có thể truy cập critical resource.

Semaphore gồm 2 loại là binary semaphore và counting semaphore. Hoạt động của binary semaphore tựa như như mutex lock, do đó thường được sử dụng để phòng tránh race condition. Điểm độc lạ điển hình nổi bật so với mutex lock đó là : một thread hoàn toàn có thể giải phóng semaphore mặc dầu thread đó chưa hề chiếm hữu semphore .

Source: https://tuvi365.net
Category: BLOG

Related Posts

Trả lời

Email của bạn sẽ không được hiển thị công khai.