Triển khai Bottom-half bằng Workqueue

Giới thiệu

Trong bài học trước, chúng ta đã tìm hiểu về cơ chế tasklet. Cơ chế này cho phép thực hiện các công việc dưới bottom-half ở một thời điểm thích hợp trong tương lai. Bên cạnh tasklet, cơ chế workqueue cũng hay được sử dụng.

Bài học hôm nay sẽ tập trung vào các nội dung sau:

  • Workqueue là gì? Cơ chế workqueue khác gì so với tasklet?
  • Linux kernel biểu diễn một workqueue như thế nào?
  • Sử dụng workqueue như thế nào để triển khai bottom half khi lập trình device driver.

Workqueue

Workqueue là tên của một cơ chế lập lịch, giúp thực hiện một công việc tại một thời điểm trong tương lai. Khái niệm workqueue cũng là tên của một thành phần trong Linux kernel. Khi device driver cần thực thi một hàm trong tương lai, device driver chỉ cần tạo ra một công việc liên kết với hàm đó, rồi gửi công việc ấy tới workqueue. Dưới đây, ta sẽ dùng khái niệm work item thay cho "công việc" để tránh nhầm lẫn.

Hình 1 Workqueue trong hệ thống Linux

Trong hệ thống, có thể có nhiều workqueue. Dựa trên nguồn gốc, chúng có thể được phân làm 2 loại: workqueue do người dùng định nghĩa và workqueue do hệ thống định nghĩa.

Nếu CPU có N core, thì mỗi một workqueue chứa N pool workqueue. Hiểu nôm na, mỗi pool workqueue là một kho chứa bao gồm một worker và các work item. Worker sẽ lần lượt lấy từng work item ra và thực hiện. Khi không còn work item nào để thực hiện, worker sẽ ngồi chơi.

Tuy nhiên, ở đây có một vấn đề. Giả sử hệ thống chứa M workqueue, thì số lượng worker sẽ là M*N. Nếu M khá lớn thì số lượng worker cũng khá lớn, điều này gây lãng phí. Lý do lãng phí là vì, chỉ khi nào xảy ra ngắt thì các device driver mới gửi các work item tới workqueue, điều này khiến cho các worker thường xuyên ngồi chơi. Để tránh lãng phí, Linux kernel xây dựng thêm khái niệm worker pool. Theo đó, ứng với một core sẽ có 2 worker pool: worker pool H và worker pool N. Mỗi worker pool sẽ gồm một worker và một danh sách các work item (còn gọi là worklist). Các work item này có thể đến từ bất kì workqueue nào. Các work item có độ ưu tiên cao sẽ được gửi tới cho worker pool (H). Còn các work item có độ ưu tiên bình thường sẽ được gửi tới cho worker pool (N).

Về mục đích, cơ chế workqueue tương tự như cơ chế tasklet. Tuy nhiên, các công việc được lập lịch theo workqueue vẫn có những điểm khác so với tasklet:

  • Theo cơ chế tasklet, core nào lập lịch cho công việc thì chính core đó sẽ thực hiện công việc ấy trong tương lai. Điều này không bắt buộc trong cơ chế workqueue.
  • Theo cơ chế tasklet, core phải thực hiện công việc từ đầu tới cuối, không được phép thực hiện đan xen các công việc khác (tức là thực thi trong atomic context). Điều này không bắt buộc trong cơ chế workqueue. Do đó, trong quá trình xử lý công việc, ta có thể gọi hàm sleep hoặc schedule.

Biểu diễn workqueue trong Linux

Để xây dựng cơ chế workqueue, Linux kernel sử dụng một tập các cấu trúc dữ liệu. Lập trình viên không cần phải nắm được tất cả các cấu trúc ấy, và cũng không cần phải hiểu chi tiết từng cấu trúc. Ta chỉ cần nắm được một số cấu trúc quan trọng, và chỉ cần hiểu chúng mô tả cái gì.

Cấu trúc dữ liệu

Ý nghĩa

work_struct

Mô tả các thuộc tính của một non-delayed work item. Các work item này được thêm vào worklist ngay lập tức.

Cấu trúc này tương đương với cấu trúc tasklet_struct trong bài học trước.

delayed_work

Mô tả các thuộc tính của một delayed work item. Các work item này được thêm vào worklist sau một khoảng thời gian.

workqueue_struct

Mô tả các thuộc tính của một workqueue.

pool_workqueue

Mô tả các thuộc tính của một pool workqueue.

worker_pool

Mô tả các thuộc tính của một worker pool.

worker

Mô tả các thuộc tính của một worker.

Trong số các cấu trúc trên, lập trình viên sẽ thường xuyên làm việc với work_struct, delayed_workworkqueue_struct.

struct work_struct {
	/* @data sẽ được truyền cho hàm @func */
	atomic_long_t data;

	/* @entry dùng để liên kết công việc này với worklist */
	struct list_head entry;

	/*
	 * @func là hàm của device driver. Hàm này sẽ
	 * xử lý các công việc thuộc bottom-half
	 */
	work_func_t func;
};

struct delayed_work {
	/*
	 * Sau một khoảng thời gian, work item @work
	 * được gửi tới workqueue @wq
	 */
	struct work_struct work;

	/*
	 * @timer giống như đồng hồ báo thức. Sau một khoảng
	 * thời gian, @work sẽ được gửi tới workqueue @wq
	 */
	struct timer_list timer;

	/* workqueue sẽ tiếp nhận work item @work */
	struct workqueue_struct *wq;

	/* CPU @cpu sẽ thực thi work item @work */
	int cpu;
};

struct workqueue_struct {
	/* Danh sách các pool workque của workqueue này */
	struct list_head	pwqs;

	/* @list giúp liên kết workqueue này với các workqueue khác của hệ thống */
	struct list_head	list;

	/* Tên của workqueue này */
	char name[WQ_NAME_LEN];

	/* Địa chỉ của từng pool workque của workqueue này */
	struct pool_workqueue __percpu *cpu_pwqs;
};

Cách sử dụng workqueue

Linux kernel cung cấp các hàm cho phép chúng ta sử dụng workqueue, bao gồm:

  • Tạo/hủy một workqueue mới.
  • Tạo một work item.
  • Gửi một work item tới cho workqueue.
  • Hủy một work item đã gửi cho workqueue.

Mặc định, Linux tạo sẵn một loại workqueue có tên là events. Tuy nhiên, nếu vẫn muốn tạo một loại workqueue của riêng mình thì ta có thể dùng hàm create_workqueue. Khi không cần dùng nữa, ta có thể gọi hàm destroy_workqueue để hủy workqueue đó đi.

/*
 * Hàm create_workqueue
 * Chức năng: tạo ra một workqueue mới
 * Tham số đầu vào:
 *   @name: tên của workqueue.
 * Giá trị trả về:
 *   nếu thành công, trả về địa chỉ của workqueue
 *   nếu không tạo được workqueue, trả về NULL
 * Chú ý: hàm này thường được gọi bởi hàm khởi tạo của driver
 */
struct workqueue_struct *create_workqueue(const char *name)

/*
 * Hàm destroy_workqueue
 * Chức năng: hủy bỏ một workqueue
 * Tham số đầu vào:
 *   @name: tên của workqueue.
 * Giá trị trả về:
 *   nếu thành công, trả về địa chỉ của workqueue
 *   nếu không tạo được workqueue, trả về NULL
 * Chú ý: hàm này thường được gọi bởi hàm kết thúc của driver
 */
void destroy_workqueue(struct workqueue_struct *wq)

Để tạo ra một non-delayed work item, ta sử dụng macro DECLARE_WORKINIT_WORK. Để tạo ra một delayed work item, ta sử dụng macro  DECLARE_DELAYED_WORKINIT_DELAYED_WORK.

/*
 * Hàm DECLARE_WORK
 * Chức năng: tạo ra một work item
 * Tham số đầu vào:
 *   @n: tên của work item.
 *   @f: tên của hàm. Hàm này mô tả nội dung của work item
 * Giá trị trả về:
 *   biến cấu trúc work_struct có tên là @n, và khởi tạo
 *   trường func của cấu trúc đó bằng @f.
 * Chú ý: hàm @f phải tuân theo nguyên mẫu sau:
 *            void work_handler(struct work_struct* )
 */
DECLARE_WORK (n, f)
DECLARE_DELAYED_WORK (n, f)

/*
 * Hàm INIT_WORK
 * Chức năng: khởi tạo một biến có kiểu cấu trúc work_struct
 * Tham số đầu vào:
 *   @_work: địa chỉ của biến cấu trúc work_struct đai diện cho work item
 *   @_func: tên của hàm. Hàm này mô tả nội dung của work item
 * Giá trị trả về:
 *   Không có
 * Chú ý: hàm @_func phải tuân theo nguyên mẫu sau:
 *            void work_handler(struct work_struct* )
 */
INIT_WORK (_work, _func)
INIT_DELAYED_WORK (_work, _func)

Sau khi đã tạo ra một work item, chúng ta cần gửi work item này tới một workqueue nào đó. Nếu ta muốn gửi work item này tới workqueue của hệ thống (có tên là events), thì Linux kernel hỗ trợ các hàm schedule_work, schedule_work_on, schedule_delayed_work, schedule_delayed_work_on.

/*
 * Chức năng:
 *   Hàm schedule_work: gửi work item tới workqueue events
 *   Hàm schedule_work_on: gửi work item tới workqueue events.
 *           Work item này sẽ được thực thi bởi core thứ @cpu
 * Tham số đầu vào:
 *   @work: địa chỉ của biến cấu trúc work_struct đai diện cho work item
 *   @cpu: core của CPU sẽ thực thi work item
 * Giá trị trả về:
 *   nếu work item đó đã gửi tới workqueue rồi, hàm này sẽ trả về 0.
 *   nếu work item đó chưa được gửi, trả về một giá trị khác 0.
 */
int schedule_work( struct work_struct *work )
int schedule_work_on(int cpu, struct work_struct *work )

/*
 * Chức năng:
 *   Hàm schedule_delayed_work: chờ một khoảng thời gian,
 *            rồi gửi work item tới workqueue events
 *   Hàm schedule_delayed_work_on: chờ một khoảng thời gian,
 *            rồi gửi work item tới workqueue events.
 *            Work item này sẽ được thực thi bởi core thứ @cpu
 * Tham số đầu vào:
 *   @work: địa chỉ của biến cấu trúc delayed_work đai diện cho work item
 *   @delay: thời gian chờ trước khi gửi work item (tính theo đơn vị jiffy).
 *   @cpu: core của CPU sẽ thực thi work item
 * Giá trị trả về:
 *   nếu work item đó đã gửi tới workqueue rồi, hàm này sẽ trả về 0.
 *   nếu work item đó chưa được gửi, trả về một giá trị khác 0.
 */
int schedule_delayed_work(struct delayed_work *dwork, unsigned long delay)
int schedule_delayed_work_on (int cpu, struct delayed_work *dwork, unsigned long delay)

Còn nếu muốn gửi work item tới workqueue do ta tạo ra, Linux cung cấp các hàm tương ứng sau:

int queue_work(struct workqueue_struct *wq, struct work_struct *work)
int queue_work_on(int cpu, struct workqueue_struct *wq, struct work_struct *work)
int queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay)
int queue_delayed_work_on(int cpu, struct workqueue_struct *wq, struct delayed_work *work, unsigned long delay)

Nếu ta muốn hủy một work item nào đó trong hệ thống, ta có thể sử dụng các hàm cancel_work_sync hoặc cancel_delayed_work_sync:

/*
 * Chức năng:
 *    Hàm cancel_work_sync: hủy bỏ một work item được gửi bởi hàm
 *                          schedule_work() hoặc schedule_work_on()
 *                          hoặc queue_work() hoặc queue_work_on().
 *    Hàm cancel_delayed_work_sync: hủy bỏ một work item được gửi bởi hàm
 *                          schedule_delayed_work() hoặc schedule_delayed_work_on()
 *                          hoặc queue_delayed_work() hoặc queue_delayed_work_on().
 * Tham số đầu vào:
 *   @work: địa chỉ của biến cấu trúc work_struct.
 *          Biến này đại diện cho work item cần hủy bỏ
 *   @delay_work: địa chỉ của biến cấu trúc delayed_work.
 *          Biến này đại diện cho work item cần hủy bỏ
 * Giá trị trả về:
 *   nếu hủy bỏ thành công, hàm này sẽ trả về một giá trị >= 0.
 *   nếu hủy bỏ không thành công, trả về một giá trị < 0.
 */
int cancel_work_sync(struct work_struct *work)
int cancel_delayed_work_sync( struct delayed_work *dwork )

Case study

Trong ví dụ này, ta sẽ tìm hiểu cách sử dụng workqueue events để lập lịch các công việc dưới bottom-half. Đây là một workqueue do hệ thống định nghĩa sẵn, ta chỉ việc sử dụng. Để biết cách tạo một workqueue của riêng mình, các bạn có thể tham khảo ví dụ khác tại đây.

Ví dụ này dựa trên ví dụ trong bài học Triển khai top-half. Vì vậy, ta tạo thư mục cho bài học hôm nay như sau:

cd /home/ubuntu
cd ldd/phan_5
cp -r bai_5_1 bai_5_4
cd bai_5_4

Ý tưởng ở đây là, mỗi khi có tín hiệu Interrupt, hàm ISR sẽ gửi một non-delayed work item và một delayed work item tới workqueue events. Như đã trình bày ở trên, để tạo ra một work item, ta có 2 cách:

  • Sử dụng macro DECLARE_WORK (hoặc DECLARE_DELAYED_WORK). Cách này được gọi là cấp phát tĩnh một work item. Ví dụ này sẽ sử dụng cách này để tạo ra một work item.
  • Khai báo một biến kiểu cấu trúc work_struct (hoặc delayed_work_struct), sau đó sử dụng macro INIT_WORK (hoặc INIT_DELAYED_WORK) để khởi tạo cho biến đó. Cách này được gọi là cấp phát động một work item. Để hiểu cách cấp phát này, bạn có thể tham khảo ví dụ khác tại đây.

Trước tiên, để có thể sử dụng các hàm của Linux kernel, chúng ta cần thêm vào thư viện <linux/workqueue.h>.

Tiếp theo, ta sẽ tạo ra hàm vchar_hw_bh_task để xử lý các công việc dưới bottom-half. Sau đó, ta sẽ sử dụng cách thức cấp phát tĩnh để tạo ra một non-delayed work item và một delayed work item. Cả 2 work item này cùng liên kết với hàm vchar_hw_bh_task. Bên trong hàm xử lý ngắt vchar_hw_isr, ta thực hiện:

  • Gọi hàm schedule_work_on để lập lịch cho hàm vchar_hw_bh_task được thực thi trên CPU0 khi có Interrupt.
  • Gọi hàm schedule_delayed_work_on để lập lịch cho hàm vchar_hw_bh_task được thực hiện trên CPU1 sau 2 giây kể từ khi có Interrupt.

Cuối cùng, trong hàm vchar_driver_exit, ta gọi hàm cancel_work_sync và cancel_delayed_work_sync để xóa bỏ các work item trước khi gỡ bỏ driver ra khỏi Linux kernel.

Bây giờ, ta gõ lệnh make để biên dịch char driver. Sau khi biên dịch thành công, ta sử dụng lệnh sudo insmod vchar_driver.ko để lắp driver vào trong kernel. Kiểm tra kernel log bằng lệnh dmesg, ta thấy non-delayed work item được thực thi trên CPU0, còn delayed work item được thực thi trên CPU 1 (hình 2).

Hình 2. Ví dụ về sử dụng tasklet để lập lịch thực thi công việc dưới bottom-half

Kết luận

Tương tự Tasklet, workqueue là một cơ chế lập lịch thực thi các công việc dưới bottom-half. Khác với tasklet, cơ chế workqueue cho phép các work item được thực thi trên bất cứ core nào và không cần phải trong ngữ cảnh atomic context.

Khái niệm workqueue cũng dùng để chỉ một thành phần của kernel có nhiệm vụ lập lịch các công việc dưới bottom-half. Khi xảy ra Interrupt, device driver sẽ gửi một work item tới workqueue.

  • Linux kernel biểu diễn một work item bằng cấu trúc work_struct, và cung cấp macro DECLARE_WORK (hoặc DECLARE_DELAYED_WORK) để tạo ra một work item.
  • Linux kernel biểu diễn một workqueue bằng cấu trúc workqueue_struct, và cung cấp hàm create_workqueue để tạo ra một workqueue.
  • Linux kernel cung cấp các hàm schedule_work, schedule_work_on, schedule_delayed_work, schedule_delayed_work_on để gửi work item tới workqueue của hệ thống.
  • Linux kernel cũng cung cấp các hàm queue_work, queue_work_on, queue_delayed_work, queue_delayed_work_on để gửi work item tới workqueue của người dùng.

** 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