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

Ngoài spinlock và mutex lock, ta cũng có thể áp dụng kỹ thuật semaphore để bảo vệ dữ liệu trong vital useful resource. Không chỉ là một kỹ thuật đồng bộ tài nguyên, semaphore cũng được biết đến là một kỹ thuật đồng bộ hoạt động.

Do phạm vi của khóa học, bài học này chỉ trình bày về semaphore với tư cách là một kỹ thuật đồng bộ tài nguyên:

  • Giới thiệu semaphore là gì, có cấu tạo như thế nào, hoạt động ra sao, bảo vệ vital useful resource như thế nào?
  • Sử dụng kỹ thuật semaphore trong lập trình gadget driver như thế nào?
  • Cần chú ý những gì khi sử dụng kỹ thuật semaphore?

Semaphore là gì?

Semaphore là một cấu trúc dữ liệu, được dùng để đồng bộ tài nguyên và đồng bộ hoạt động.

Khi được dùng với mục đích đồng bộ tài nguyên, semaphore tương tự như một bộ các chìa khóa dự phòng. Nếu một thread lấy được một chiếc chìa khóa, thread đó được phép truy cập 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ự phòng. Nhờ vậy, race situation sẽ bị ngăn chặn.

pGhWV9FYNmPkAn7c5yR_WvagTA9KbSl4c6c-ygjGgomQZ114qdyx5uGFucqpSoCmV0-Mcd_jCDkWzzemlcCDc_FjYIxfz0V2naJwxXnnfK_QGi6fWr2UWU2p2s-cmD_yXiNUyYqSW7ytmQdd53UAnL8

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

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

Semaphore gồm 2 thành phần chính: biến depend 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 vital useful resource. Biến @lock là * một spinlock bảo vệ @depend và @wait_list trong cấu trúc semaphore. */ raw_spinlock_t lock; /* * Biến depend vừa thể hiện trạng thái của semaphore, vừa thể hiện * trạng thái của vital useful resource. * > 0: semaphore đang ở trạng thái AVAILABLE, * còn vital useful resource đang ở trạng thái READY. * depend cũng thể hiện còn bao nhiêu thread nữa được phép * sử dụng vital useful resource. * = 0: semaphore đang ở trạng thái UNAVAILABLE, * còn vital useful resource đang ở trạng thái BUSY. */ unsigned int depend; // 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 depend, semaphore được chia làm 2 loại là counting semaphorebinary semaphore.

  • Nếu giá trị cực đại của biến depend lớn hơn 1, thì semaphore được gọi là counting semaphore. Giá trị cực đại của biến depend thể hiện số lượng thread tối đa được phép sử dụng vital useful resource tại cùng một thời điểm.
  • Nếu biến depend 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 ra sao?

dzRoodrH4xEchdDoZWhIfttCSiGueRllGhpSRFI74-uTRG5qzK45TT0fbG_Hu5BEWRe7uOiWfBS_sx4PJd-aaNvL01QIVXH7hGVmhs22eo5BqBSDQ3pdSzP1ppMxetmiNWfAD2tzNyOqjxg1t0dDeRo

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

Khi depend đ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 depend 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 vital part của thread (nói theo ngôn ngữ của CPU), hay thread bắt đầu sử dụng vital useful resource (nói theo ngôn ngữ của Linux kernel).

Có Thể Bạn Quan Tâm :   Book ship là gì? Những điều cần biết về Book ship

Khi depend đ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 vital useful 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 depend được tăng thêm 1 đơn vị, tức là semaphore chuyển sang trạng thái AVAILABLE.

Semaphore bảo vệ vital useful 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 situation. Trong khi lập trình gadget driver, ta đặt hàm downup lần lượt vào trước và sau vital part của mỗi thread.

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 vital useful 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 cập R trong khi B đang truy cập R.
    • Trước khi thực thi các lệnh trong vital part 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 vital part 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 cập 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 đầ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 binary semaphore, đồng nghĩa với việc, tối đa chỉ có một thread được phép sử dụng vital useful resource. Do đó, race situation sẽ không xảy ra và vital useful resource được bảo vệ.

Có Thể Bạn Quan Tâm :   Gal game là gì? 4 điểm đặc trưng phổ biến của tựa game này

Để khai báo và khởi tạo giá trị cho binary semaphore ngay từ lúc biên dịch (compile time), ta 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 vital useful resource, * còn my_semaphore đại diện cho bộ các chìa khóa bảo vệ vital useful 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 vital part của thread để ngăn không cho race situation xảy ra.

down(&my_semaphore); /* vital part 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 (sign) trong lúc * đang chờ semaphore. * * Xét trường hợp tiến trình P trên consumer area yêu cầu gadget driver * đọc/ghi dữ liệu trong vital useful 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 (sign), 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; /* vital part của kernel thread */ up(&my_semaphore);

Ngoài ra, Linux kernel hỗ trợ 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);

Có Thể Bạn Quan Tâm :   Tin Tức

Chú ý khi sử dụng semaphore

Khi triển khai giải pháp này, ta cần chú ý 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 vital part 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 hoàn toàn phù hợp để áp dụng 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 đượ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 có thể giải phóng semaphore mặc dù nó không phải là người đã chiếm dụng. Đ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.

Trong ví dụ này, chúng ta sẽ áp dụng kỹ thuật semaphore để 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 /house/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 <linux/semaphore.h>.

8D0lCyovqovmMyvufop05voyDg1jLJQVTQm5urBU6fsMR8NiH9PliSTJDfbz8GD-fTCZxj_yQXz8ucMGKjpFRIEcDjiOcHjAY7UHO7kTxX02ITbyfJmJ9yF5t9itqeAsnx71z2v1AFY6bzaoJEZv7Cc

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.

vKL65sa_uAlmSrjvrLPKNWagAfhDqkl_6LaEwggu8rO8G3SCPn0IvdK2-jLvs-ffpO8NQ9dGeu1qvH1OxdKIElx8a5V9sYTjZtwdmmRfFXtKX1cuCe3KgEWKJ9jhzeBYMAbpDxdKPmO0BOgksEfdO_M

Sau đó, trong hàm vchar_driver_init, ta khởi tạo semaphore này để tạo ra binary semaphore:

xvBQtdH2rFXN-vFhEbxgPTnqrmgtsEoPVMQM0UNBJEX9ZNpCgNLFB0XH-oJphQfj_Sez3Xp4FqV_2GVMIAI6PNyIp90i5VuBQjLfd_isdxriZZVolA7AI3yHRCL4Ysoe-qOLqj_ebgwpE7ZcFpJbw70

Cuối cùng, ta thêm hàm downup lần lượt vào trước vào sau vùng vital part.

dHoEIQhyDhBy89UKlF2mfQ_YnIXMfwRa0gqrITDl7blGRJ92BRPhix44Y-UyRvINILL_TUb2HqoBiKmR0mTz0iqzfSGdTn0NRAJIEKE-Bf6TVZJzTrNE06d2j5ZvX61BjfrfEuZPTq1EI3Tj-kLWQmg

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.

jU6ChPXQdwq1dHTAuMhppg4b3Z5OEGRQVwmG4og36B-dsuELsBmmS8qV2c-hv_czkp36jpnAPt3gsgDIgCS3pu0libw5EnlLmmPXF4qsF7FwV-BVK-DlSlKpsg5mzenXvdCB1QwB3fkXV0QTMddOEwg

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

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 depend và hàng đợi wait_list. Biến depend giúp kiểm soát số lượng thread còn lại được phép truy cập vào vital useful 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 vital useful resource.

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

Back to top button