Top Half, Bottom Half và Softirqs Trong bài trước, chúng ta đã có một ví dụ về việc viết một kernel module đăng kí một hàm xử lý ngắt. Trong bài này, chúng ta tiếp tục tìm hiểu một số chủ đề chuyên sâu hơn trong phần ngắt và ngoại lệ. Top Half và Bottom Half Như đã đề cập ở những bài trước, việc xử lý một ngắt cần được thực hiện một cách nhanh chóng, vì trong quá trình xử lý, ngắt đó tạm thời bị vô hiệu hóa. Tuy nhiên, khi một ngắt xảy ra, thông thường hệ thống cần phải thực hiện một số lượng lớn công việc tương ứng với ngắt đó. Ta có thể thấy, việc một ngắt cần được thực hiện một cách nhanh chóng, và việc hệ thống cần thực hiện một số lượng lớn công việc khi ngắt xảy ra trở nên mâu thuẫn với nhau. Để đáp ứng được cả hai tiêu chí vấn đề này, quá trình xử lý một ngắt được chia làm hai giai đoạn (hoặc gọi là hai nửa - halves). Giai đoạn đầu được gọi là top half. Top half được chạy ngay lập tức khi có ngắt xảy ra ở phần cứng, và thực hiện một vài công việc quan trọng, có tính khẩn cấp về mặt thời gian (ví dụ như gửi phản hồi ngắt, hoặc cài đặt một số thông số phần cứng). Những công việc không mang tính khẩn cấp về mặt thời gian khác mà có thể delay một chút, sẽ được thực hiện sau, trong thời gian thích hợp hơn, với tất cả các ngắt vẫn được bật. Cùng lấy một ví dụ về một thiết bị phần cứng quen thuộc để làm rõ về hai khái niệm này: Card mạng. Khi một card mạng nhận được một gói tin, nó sẽ tạo ra một ngắt để thông báo cho kernel biết Hey kernel! Có gói tin mới đến thiết bị. Hàm xử lý ngắt của kernel sẽ chạy, gửi phản hồi lại cho phần cứng Tao đã nhận được ngắt rồi, sao chép gói tin từ card mạng vào bộ nhớ chính, và thiết lập lại card mạng để card mạng tiếp tục nhận được những gói tin tiếp theo. Tất cả những việc trên là những việc quan trọng, khẩn cấp về mặt thời gian, và đặc trưng riêng cho phần cứng. Chúng cần được thực thi ngay lập tức khi hệ thống nhận được ngắt vì nếu như không sao chép gói tin từ card mạng vào bộ nhớ chính ngay, bộ nhớ của card mạng vốn ít sẽ nhanh bị đầy, các gói tin tới sau sẽ bị drop loại bỏ. Hay nếu không cài đặt lại card mạng nhanh để card mạng sẵn sàng nhận các gói tin tiếp theo, các gói tin tới sau cũng sẽ bị drop loại bỏ. Những công việc như trên sẽ được thực hiện ở top half. Tuy nhiên, như chúng ta đã biết, kernel cần thực hiện thêm rất nhiều công việc khi nhận được một gói tin từ card mạng: xử lý gói tin, phân tích gói tin để gửi gói tin, định tuyến gói tin hay đẩy lên xử lý ở tầng ứng dụng Những công việc trong network stack của kernel tốn rất nhiều thời gian, nhưng không cấp thiết phải xử lý ngay lập tức. Những công việc như thế này thường được thực hiện ở bottom half. Trong bài hàm xử lý ngắt ví dụ viết hàm xử lý ngắt của bàn phím là một ví dụ về top half. Khi đăng kí hàm xử lý ngắt, hàm sẽ được gọi ở top half được thực thi ngay lập tức khi có ngắt. Thêm một vấn đề nữa, với bottom half, công việc sẽ không được thực hiện ngay lập tức, mà sẽ được thực hiện sau. Vậy thế nào, khi nào là sau. Thực tế, việc thực hiện bottom half không thể xác định được ở thời điểm nào trong tương lai, nhưng về cơ bản, bottom half delay công việc tới một thời điểm bất kì trong tương lai khi mà hệ thống bớt busy bận, và khi toàn bộ các ngắt được enable trở lại. Điểm quan trọng là ở đây: Toàn bộ các ngắt được enable trở lại. Công việc nào nên thực hiện ở top half, công việc nào ở bottom half?
Có một vài mẹo nhỏ để lập trình viên có thể chọn được, công việc nào nên đặt ở top half, công việc nào nên thực hiện ở bottom half như sau: Nếu công việc thuộc loại nhạy cảm về mặt thời gian, thực hiện ở top half Nếu công việc liên quan đến phần cứng, thực thi ở top half Nếu công việc cần chắc chắn không bị ngắt khác (đặc biệt là ngắt cùng loại) làm gián đoạn, thực thi nó ở top half Những công việc còn lại, cân nhắc thực thi ở bottom half. Vì nguyên tắc, top half càng thực hiện nhanh càng tốt, càng nhiều việc đẩy cho bottom half càng tốt. Để hỗ trợ việc thực hiện các công việc ở bottom half, linux kernel hiện tại đang có 3 phương thức: softirqs, tasklets, và workqueues. Chúng còn được gọi chung là hàm có thể trì hoãn deferrable functions. Softirqs Softirqs hiếm khi được sử dụng trực tiếp, trong khi Tasklets được sử dụng phổ biến hơn ở bottom half. Tuy nhiên chúng ta đề cập tới Softirqs trước vì Tasklets bản chất được tạo ra từ Softirqs. Một vài tính chất của Softirqs Softirqs được khởi tạo ở thời điểm biên dịch. Nghĩa là không thể khởi tạo Softirqs khi thiết bị đang chạy ví dụ về việc khởi tạo khi thiết bị đang chạy là việc install và uninstall các kernel modules. Softirqs có thể chạy đồng thời trên nhiều CPU cùng một lúc, kể cả là cùng một loại softirqs. Vì thế khi sử dụng softirqs việc bảo vệ dữ liệu (ví dụ dùng spin lock) là rất quan trọng. Tại sao lại nói softirqs được khởi tạo ở thời điểm biên dịch, và softirqs không thể tạo thêm, hay xóa bỏ trong khi thiết bị đang chạy? Mã nguồn softirqs: /*include/linux/interrupt.h*/ struct softirq_action { void( * action)(struct softirq_action * ); ; /*kernel/softirq.c*/ static struct softirq_action softirq_vec[nr_softirqs]; Softirqs được mô tả bằng kiểu dữ liệu softirq_action. Kiểu dữ liệu này chứa phần tử action là con trỏ hàm xử lý của softirqs. Khi kernel thực thi một softirqs, kernel sẽ thực hiện gọi hàm action. Trong linux kernel, số lượng tối đa softirqs được sử dụng là mảng softirq_action gồm NR_SOFTIRQS phần tử. Trong phiên bản kernel hiện tại, có tất cả 10 loại softirqs đang được sử dụng (NR_SOFTIRQS = 10). Những loại này được mô tả trong file interrupt.h như dưới đây enum {
; HI_SOFTIRQ = 0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, BLOCK_SOFTIRQ, IRQ_POLL_SOFTIRQ, TASKLET_SOFTIRQ, SCHED_SOFTIRQ, HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */ RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ NR_SOFTIRQS Thông tin về softirqs trong thiết bị thực tế chạy Linux: Hình 1: Softirqs trong thiết bị Linux Từ những mã nguồn trên, chúng ta có thể tóm tắt một vài ý chính về cách kernel quản lý và thực thi softirqs như sau: Kernel quản lý tối đa NR_SOFTIRQS loại softirqs, được thể hiện bằng mảng dữ liệu softirq_action softirq_vec[nr_softirqs]. Trong phiên bản kernel hiện tại, có tất cả 10 loại softirqs đang được sử dụng thể hiện bằng khai báo enum trong file interrupt.h. Như vậy ta có thể thấy, số lượng softirqs đã được cố định, không thể thêm mới cũng như xóa bỏ một softirqs khi thiết bị đang chạy => Softirqs được khởi tạo ở thời điểm biên dịch. Kernel thực thi softirqs như thế nào? Một softirqs phải được đánh dấu trước khi chúng được thực thi hay còn được gọi là raising the softirq. Như đã đề cập ở phần trước, softirqs được thực thi ở bottom haft, còn hàm xử lý ngắt được thực thi ở top half. Thông thường hàm xử lý ngắt sẽ là người đánh dấu softirqs, để sau đó, softirqs được thực thi. Softirqs có thể được thực thi tại một trong ba ngữ cảnh sau: Sau khi hàm xử lý ngắt phần cứng thực thi xong, và gọi hàm irq_exit(). Được thực thi bởi ksoftirqd kernel thread (giải thích ở phần tiếp theo). Được thực thi trong một vài điểm trong mã nguồn của kernel, ví dụ như trong hệ thống mạng networking subsystem. Ba ngữ cảnh này còn có thể gọi là điểm kiểm tra softirqs checkpoint. Tại bất kì một trong ba ngữ cảnh này, nếu một softirqs được phát hiện đang ở trạng thái đánh dấu, kernel sẽ gọi hàm
do_softirq() thực thi toàn bộ những softirqs đang ở trạng thái chờ được thực hiện. Các kiểm kiểm tra softirqs này có thể dễ dàng tìm được trong kernel source code bằng việc tìm hàm do_softirq(). ksoftirqd kernel thread? Trong những phiên bản kernel gần đây, mỗi CPU có một ksoftirqd/n kernel thread của riêng nó trong đó n là số thứ tự CPU. Mỗi một luồng ksoftirqd/n thực thi hàm run_ksoftirqd(): /*Linux kernel 2.6.26 file softirq.c*/ static int run_ksoftirqd(void * bind_cpu) { while (!kthread_should_stop()) { if (!local_softirq_pending()) { preempt_enable_no_resched(); schedule(); while (local_softirq_pending()) { /* Preempt disable stops cpu going offline.if already offline, we'll be on wrong CPU:don't process */ if (cpu_is_offline((long) bind_cpu)) goto wait_to_die; do_softirq(); preempt_enable_no_resched(); cond_resched(); rcu_note_context_switch((long) bind_cpu); preempt_enable(); return 0; wait_to_die: preempt_enable(); /* Wait for kthread_stop */ while (!kthread_should_stop()) { schedule(); return 0; oạn code trên thực hiện hai việc cơ bản như sau: Nếu có bất kì softirqs nào đang được đánh dấu, luồng sẽ gọi hàm do_softirq() để xử lý. Ở trong mỗi một vòng lặp, cond_resched() được gọi để thực hiện lập lịch chuyển sang thực thi tiến trình khác có độ ưu tiên cao hơn nếu cần. Vậy tại sao lại cần đến luồng ksoftirqd/n để thực thi các softirqs? Luồng ksoftirqd/n được sử dụng để giải quyết hai vấn đề sau: Vấn đề đầu tiên softirqs sẽ bị bỏ qua trong quá trình hàm do_softirq() được thực thi. Thật vậy, các điểm kiểm tra softirqs kiểm tra có những softirqs nào được đánh dấu, và gọi hàm do_softirq() để thực thi. Trong khi hàm do_softirq() đang chạy mà có một softirq được đánh dấu tại thời điểm đó, softirq này sẽ bị bỏ qua và chỉ được thực hiện tại lần kiểm tra
tiếp theo. Trường hợp này rất dễ xảy ra với các softirqs cho việc gửi nhận các gói tin trong mạng. Khi các gói tin đến liên tục, các softirqs sẽ được đánh dấu liên tục. Khi đó việc thực thi các softirqs được đánh dấu trong lúc gọi hàm do_softirq() sẽ bị dừng cho tới lần tiếp theo, ngay cả khi CPU đang rảnh rỗi. Điều này là khó chấp nhận, đặc biệt trong lĩnh vực network mạng. Vấn đề thứ 2, giả sử chúng ta có một vòng lặp tại mỗi điểm kiểm tra softirqs, và kiểm tra trạng thái các softirqs liên tục, thực thi softirqs liên tục, và chỉ thoát ra khỏi vòng lặp khi không còn softirqs nào đang được đánh dấu. Nếu làm như thế, cũng trong trường hợp các gói tin đến liên tục, vòng lặp gần như sẽ không bao giờ kết thúc, CPU liên tục bị chiếm dụng, các ứng dụng còn lại trong hệ thống sẽ bị tạm dừng. ksoftirqd/n giải quyết hai vấn đề trên. Luồng này đảm bảo sẽ cố gắng thực thi hết tất cả các softirqs bằng vòng lặp liên tục kiểm tra trạng trái các softirqs, tuy nhiên trong vòng lặp vẫn thực hiện việc lập lịch, để đảm bảo các tiến trình khác có độ ưu tiên cao hơn không bị dừng lại. Kết luận Bài viết trình bày khái niệm về top half, bottom half, cũng như giải thích thế nào là softirqs. Trong bài này không đi sâu quá nhiều vào việc hướng dẫn cách tạo ra softirqs mà cố gắng giải thích về khái niệm, cũng như ý nghĩa của softirqs. Về cách sử dụng, các bạn có thể tìm hiểu thêm ở hai đường dẫn dưới đây: https://notes.shichao.io/lkd/ch8/#ksoftirqd https://www.bookstack.cn/read/linux-insides/interrupts-linux-interrupts-9.md Trong bài tiếp, chúng ta sẽ tìm hiểu về tasklet và workqueue, cũng là hai loại hàm có thể trì hoãn được.