Triển khai Bottom-half bằng Tasklet

Giới thiệu

Trong bài học trước, chúng ta đã biết rằng, các công việc khẩn cấp sẽ được thực hiện trong ISR. Nếu vẫn còn những công việc khác cần làm nhưng không gấp, thì ta sẽ lập lịch làm sau. Ví dụ, khi nhận được một gói tin, card mạng sẽ gửi tín hiệu ngắt tới CPU. Việc đọc gói tin từ card mạng vào trong RAM cần phải được thực hiện nhanh chóng. Còn việc xử lý gói tin đó có thể thực hiện sau.

Trong chương này, ta sẽ tìm hiểu 2 cơ chế lập lịch các công việc cần nhưng không gấp, đó là taskletworkqueueBài học hôm nay sẽ giới thiệu về tasklet, cách Linux kernel biểu diễn một tasklet và cách sử dụng tasklet trong lập trình device driver.

Tasklet

Tasklet là một công việc sẽ được thực thi trong tương lai. Khái niệm tasklet cũng được đặt tên cho một cơ chế lập lịch công việc. Chú ý rằng, bản chất của "công việc" chính là một chuỗi các hàm mà CPU sẽ thực thi. Dưới đây là một số điểm cần lưu ý khi lập lịch công việc theo cơ chế tasklet:

  • Mỗi công việc chỉ được thực thi bởi chính core đã lập lịch công việc đó. Điều này giúp tối ưu sử dụng bộ nhớ cache. Tuy vậy, đôi khi nó lại không tận dụng được việc sử dụng CPU, vì có thể các core khác đang rảnh rỗi.
  • Các công việc khác nhau có thể được thực hiện đồng thời trên nhiều core khác nhau của CPU. Nhưng tại một thời điểm, cùng một công việc không thể được thực hiện đồng thời trên nhiều core.
  • Mỗi công việc sẽ chiếm dụng một core từ khi bắt đầu đến khi kết thúc công việc. Do đó, ta nói công việc sẽ được thực thi ở ngữ cảnh atomic context (hay non-preemptive). Vì lý do này, trong quá trình xử lý công việc, ta không được gọi hàm sleep (dù trực tiếp hay gián tiếp).

Để hiểu hơn, ta xét ví dụ trong hình 1. Giả sử hệ thống có một CPU gồm 4 core. Tại thời điểm t = 0, core 1 nhận tín hiệu Interrupt A. Tại thời điểm t = 5 us, core 1 bắt đầu thực thi hàm ISR. Sau khi xử lý xong công việc thuộc phần top-half, tại thời điểm t = 10 us, hàm ISR lập lịch cho công việc B thuộc phần bottom-half theo cơ chế tasklet. Công việc B được thực hiện liền mạch trong khoảng thời gian từ t = 15 us cho đến t = 100 us.

Hình 1 Ví dụ minh họa công việc được lập lịch theo cơ chế tasklet

Giả sử, tại thời điểm t = 25 us, core 2 lại nhận tín hiệu Interrupt A. Tương tự, core 2 sẽ bắt đầu thực thi ISR từ thời điểm t = 30 us đến t = 35 us. Sau đó, ISR lập lịch công việc B theo cơ chế tasklet. Do core 1 cũng đang thực thi công việc B, nên core 2 chỉ thực thi công việc B sau thời điểm t = 100 us.

Biểu diễn tasklet trong Linux

Linux kernel sử dụng cấu trúc tasklet_struct để biểu diễn một tasklet.

#include <linux/interrupt.h>
/*
 * Cấu trúc tasklet_struct: mô tả một công việc cần làm
 * @next: Linux lưu một danh sách các tasklet trong hệ thống.
 *        Con trỏ @next trỏ đến tasklet tiếp theo trong danh sách.
 * @state: lưu trạng thái của tasklet này. Ví dụ:
 *            TASKLET_STATE_SCHED nghĩa là công việc đã được lập lịch.
 *            TASKLET_STATE_RUN nghĩa là công việc đang được thực thi.
 * @count: bằng số lần gọi hàm tasklet_disable.
 *            Nếu bằng 0 thì có nghĩa là tasklet được phép hoạt động.
 *            Nếu khác 0 thì có nghĩa là tasklet không được hoạt động.
 * @func: là địa chỉ của hàm thực thi công việc dưới bottom-half.
 * @data là tham số sẽ truyền cho hàm @func.
 */
struct tasklet_struct
{
     struct tasklet_struct *next;
     unsigned long state;
     atomic_t count;
     void (*func)(unsigned long);
     unsigned long data;
};

Cách sử dụng tasklet

Linux kernel cung cấp các hàm cho phép tạo/hủy tasklet, khởi tạo tasklet, lập lịch cho tasklet, tạm dừng/khôi phục hoạt động của tasklet.

Để tạo ra một tasklet, ta có 2 cách:

  • Cách cấp phát động tasklet: Đầu tiên, ta khai báo biến tasklet_struct. Sau đó, ta sử dụng hàm tasklet_init để khởi tạo cho cấu trúc ấy.
  • Cách cấp phát tĩnh tasklet: Ta có thể sử dụng macro DECLARE_TASKLET hoặc DECLARE_TASKLET_DISABLE để vừa khai báo, vừa khởi tạo cấu trúc tasklet_struct.
#include <linux/interrupt.h>
/*
 * Hàm tasklet_init.
 * Chức năng: khởi tạo tasklet.
 * Tham số đầu vào:
 *   @t: địa chỉ của tasklet cần khởi tạo.
 *   @func: địa chỉ của hàm thực thi công việc dưới bottom half.
 *   @data: tham số truyền vào cho @func.
 */
void tasklet_init (struct tasklet_struct *t,
                   void(*)(unsigned long) func,
                   unsigned long data);


/*
 * DECLARE_TASKLET: tạo ra một tasklet ở trạng thái hoạt động (active).
 * DECLARE_TASKLET_DISABLE: tạo ra một tasklet ở trạng thái chưa
 *                          hoạt động (inactive).
 * Tham số đầu vào:
 *    @name: tasklet có tên là @name sẽ được tạo ra.
 *    @function: tên của hàm thực thi công việc dưới bottom half.
 *    @data: tham số truyền cho hàm @function.
 */
DECLARE_TASKLET(name, function, data)
DECLARE_TASKLET_DISABLE(name, function, data)

Nếu muốn chuyển tasklet từ trạng thái không hoạt động (inactive) chuyển sang trạng thái hoạt động (active), ta sẽ sử dụng hàm tasklet_enable. Ta cũng có thể làm cho một tasklet ngưng hoạt động bằng hàm tasklet_disable hoặc tasklet_disable_nosync.

/*
 * Hàm tasklet_enable
 * Chức năng: chuyển tasklet sang trạng thái hoạt động
 * Tham số đầu vào:
 *    @t: địa chỉ của tasklet cần chuyển trạng thái.
 */
void tasklet_enable(struct tasklet_struct *t)

/*
 * Hàm tasklet_disable và tasklet_disable_nosync
 * Chức năng: chuyển tasklet sang trạng thái ngưng hoạt động
 * Tham số đầu vào:
 *    @t: địa chỉ của tasklet cần chuyển trạng thái.
 * Chú ý:
 *    Hàm tasklet_disable sẽ chờ cho tới khi thực thi xong tasklet,
 *    sau đó mới làm tasklet đó ngừng hoạt động.
 *    Hàm tasklet_disable_nosync sẽ làm cho tasklet đó ngừng hoạt động
 *    ngay lập tức.
 *    Nếu ta đã gọi hàm tasklet_disable (hoặc tasklet_disable_nosync)
 *    bao nhiêu lần, thì ta cũng cần gọi hàm tasklet_enable bấy nhiêu lần.
 *    Điều này được phản ánh qua trường @count của cấu trúc tasklet_struct.
 */
void tasklet_disable(struct tasklet_struct *t)
void tasklet_disable_nosync(struct tasklet_struct *t)

Linux hỗ trợ các hàm tasklet_scheduletasklet_hi_schedule để lập lịch một tasklet. Còn nếu ta muốn hủy tasklet, Linux cung cấp hàm tasklet_kill. Ta thường gọi hàm tasklet_kill bên trong hàm kết thúc của device driver.

/*
 * Hàm tasklet_schedule: lập lịch cho tasklet với độ ưu tiên trung bình
 * Hàm tasklet_hi_schedule: lập lịch cho tasklet với độ ưu tiên cao
 * Tham số đầu vào:
 *    @t: địa chỉ của tasklet cần được lập lịch.
 */
void tasklet_schedule(struct tasklet_struct *t)
void tasklet_hi_schedule (struct tasklet_struct *t)

/*
 * Hàm tasklet_kill: hủy tasklet
 * Tham số đầu vào:
 *    @t: địa chỉ của tasklet muốn hủy bỏ.
 */
void tasklet_kill( struct tasklet_struct *t )

Case study

Trong ví dụ này, ta sẽ tìm hiểu cách sử dụng cơ chế tasklet để lập lịch cho các công việc dưới bottom-half. Ví dụ này dựa trên 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_2
cd bai_5_2

Đầu tiên, 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ẽ tạo ra một tasklet. Như đã trình bày, có 2 phương pháp tạo tasklet, đó là cấp phát động và cấp phát tĩnh. Ví dụ này sẽ trình bày cách cấp phát tĩnh một tasklet có tên là vchar_static_tasklet. Về phương pháp cấp phát động tasklet, bạn tham khảo ví dụ tại đây.

Tiếp theo, trong hàm xử lý ngắt vchar_hw_isr, ta gọi hàm tasklet_schedule để lập lịch cho hàm vchar_hw_bh_task mỗi khi xảy ra Interrupt.

Cuối cùng, trong hàm vchar_driver_exit, ta gọi hàm tasklet_kill để hủy bỏ tasklet vchar_static_tasklet 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.

Sử dụng lệnh dmesg để quan sát kernel log, ta thấy rằng, cứ mỗi khi xảy ra Interrupt, hàm vchar_hw_bh_task đã được lập lịch để thực hiện các công việc dưới bottom-half. Cũng chú ý rằng, core 0 lập lịch cho vchar_hw_bh_task thì cũng chính core 0 thực thi hàm này.

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

Tasklet là một công việc cần được thực hiên trong tương lai. Đó cũng là tên một phương pháp lập lịch cho các công việc dưới bottom-half.

  • Linux kernel sử dụng cấu trúc tasklet_struct để biểu diễn một tasklet.
  • Linux kernel cung cấp các:
    • Macro DECLARE_TASKLET và hàm tasklet_init để khai báo và khởi tạo một tasklet.
    • Hàm tasklet_enabletasklet_disable để làm cho một tasklet hoạt động hay ngừng hoạt động.
    • Hàm tasklet_schedule để lập lịch cho tasklet. Hàm này thường được gọi bởi ISR.
    • Hàm tasklet_kill để hủy bỏ một tasklet.

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