Giới thiệu
Một trong những nhiệm vụ của driver là điều khiển quá trình trao đổi dữ liệu giữa RAM và I/O module. Quá trình này có thể cần tới sự có mặt của CPU hoặc không.
- Đối với trường hợp không cần tới CPU, driver chứa các mã lệnh hướng dẫn CPU thiết lập quá trình DMA (Direct Memory Access). Sau khi thiết lập xong, việc trao đổi dữ liệu giữa RAM và I/O module diễn ra một cách trực tiếp mà không cần thông qua một thanh ghi nào của CPU.
- Đối với trường hợp cần tới CPU, driver chứa các mã lệnh hướng dẫn CPU đọc dữ liệu từ I/O module vào RAM (hoặc ngược lại) thông qua một thanh ghi của CPU. Do tốc độ làm việc của hầu hết các I/O module thấp hơn nhiều so với CPU, nên không phải lúc nào I/O module cũng sẵn sàng tương tác với CPU. Để giải quyết vấn đề này, driver có thể áp dụng cơ chế hỏi vòng (polling) hoặc cơ chế ngắt (interrupting).
Đối với cơ chế hỏi vòng, CPU liên tục thực thi một đoạn mã lệnh để kiểm tra xem I/O module đã sẵn sàng chưa, nếu sẵn sàng rồi thì bắt đầu tương tác.
- Ưu điểm: CPU nhanh chóng tương tác với I/O module ngay khi I/O module đã sẵn sàng. Cơ chế này phù hợp với các I/O module tốc độ cao, ví dụ I/O module trong các bộ chuyển mạch/định tuyến của mạng lõi.
- Nhược điểm: Đối với các I/O module làm việc ở tốc độ thấp, nếu áp dụng cơ chế này thì CPU phải đợi mất nhiều thời gian trước khi I/O module sẵn sàng, nên không hiệu quả.
Đối với cơ chế ngắt, CPU cứ việc thực thi các công việc khác. Khi nào I/O module muốn tương tác với CPU, nó sẽ gửi một tín hiệu đến CPU. CPU sẽ tạm dừng công việc đang làm, lưu lại trạng thái của công việc đó, rồi chuyển sang tương tác với I/O module. Sau khi tương tác xong, CPU sẽ khôi phục lại công việc đang làm dở.
- Ưu điểm: Đối với các I/O module làm việc ở tốc độ thấp, nếu áp dụng cơ chế này thì thời gian của CPU được sử dụng hiệu quả. CPU có thể chuyển qua thực hiện công việc khác trong lúc I/O module chưa sẵn sàng.
- Nhược điểm: Do mỗi lần ngắt, CPU mất một khoảng thời gian để lưu lại công việc đang làm dở, nên không thể tương tác với I/O module ngay được. Với các I/O module tốc độ cao, khoảng thời gian giữa những lần ngắt có thể còn nhỏ hơn cả khoảng thời gian lưu/khôi phục công việc đang làm dở. Khi đó, cơ chế này không còn hiệu quả nữa.
Trong chương này, chúng ta sẽ cùng tìm hiểu về ngắt, cơ chế xử lý ngắt và cách viết các hàm phục vụ ngắt trong device driver. Bài học hôm nay sẽ tập trung vào các nội dung sau:
- Ngắt là gì? Có những loại ngắt nào?
- Hệ thống hỗ trợ tiếp nhận và xử lý tín hiệu ngắt như thế nào?
- Quá trình tiếp nhận và xử lý ngắt diễn ra như thế nào?
Ngắt và các loại ngắt
Ngắt là một sự kiện làm gián đoạn hoạt động bình thường của CPU, buộc CPU phải chuyển sang thực thi một đoạn mã lệnh đặc biệt để xử lý sự kiện đó. Thực chất, sự kiện này là một tín hiệu điện, do một mạch điện tử nằm bên trong hoặc bên ngoài CPU phát ra. Dựa vào điều này, ta chia ngắt thành 2 loại:
- Interrupt:
- Interrupt được phát ra bởi các I/O module nằm bên ngoài CPU, do đó nó còn được gọi là hardware interrupt.
- Interrupt được phát ra ở bất kì thời điểm nào, không phụ thuộc vào xung nhịp của CPU, nên ta còn gọi là asynchronous interrupt.
- Dựa vào tính chất của Interrupt, ta chia Interrupt làm hai loại:
- Non-maskable Interrupt là các tín hiệu khẩn cấp. CPU luôn tiếp nhận Interrupt thuộc loại này.
- Maskable Interrupt là các tín hiệu không thật sự khẩn cấp. CPU có thể tiếp nhận các Interrupt thuộc loại này hoặc không.
- Exception:
- Exception được phát ra bởi khối CU (Control Unit) nằm bên trong CPU. Cứ mỗi khi thực hiện xong một lệnh của phần mềm, nếu khối CU phát hiện ra lỗi (ví dụ như lệnh đó là một phép chia cho 0), hoặc phát hiện ra một điều kiện đặc biệt (ví dụ bộ đồng xử lý toán học bị lỗi), hoặc thực thi một lệnh đặc biệt (ví dụ int, sysenter, syscall,…), thì khối CU sẽ tạm ngừng thực thi phần mềm đó, rồi chuyển qua thực thi một đoạn mã lệnh đặc biệt để xử lý các vấn đề nói trên. Do Exception được phát ra bởi quá trình thực thi phần mềm, nên nó còn được gọi là software interrupt.
- Exception chỉ được phát ra sau khi kết thúc mỗi lệnh, đồng bộ với xung nhịp của CPU, nên ta còn gọi là synchronous interrupt.
- Dựa vào giá trị của thanh ghi EIP khi xảy ra Exception, ta chia Exception làm ba loại là Fault, Abort và Trap.
Do phạm vi của khóa học, nên chương này chỉ tập trung vào Interrupt. Tuy nhiên, cơ chế tiếp nhận và xử lý Exception cũng tương tự như Interrupt.
Hệ thống hỗ trợ tiếp nhận và xử lý Interrupt như thế nào?
Hỗ trợ từ CPU
CPU thường có hai chân tiếp nhận tín hiệu Interrupt là:
- INTR (INTerrupt Request): tiếp nhận các tín hiệu Maskable Interrupt. Nếu xóa cờ IF trên thanh ghi EFLAGS, thì CPU sẽ ngừng tiếp nhận tín hiệu Interrupt trên chân này.
- NMI (Non-Mask Interrupt): tiếp nhận các tín hiệu Non-maskable Interrupt. CPU luôn tiếp nhận Interrupt trên chân này.
Ngoài ra, CPU còn có một chân gọi là INTA (INTerrupt Acknowledge). Chân này giúp CPU thông báo tới I/O module rằng CPU đã sẵn sàng tiếp nhận Interrupt.
Hỗ trợ từ PIC
Do CPU chỉ có 2 chân INTR và NMI để tiếp nhận Interrupt, trong khi lại có rất nhiều I/O module khác nhau, nên người ta dùng một con chip gọi là PIC (ProgrammableInterrupt Controller) đứng ra tiếp nhận tất cả các tín hiệu Interrupt từ các I/O module đó, rồi gửi đến chân INTR của CPU (Hình 1). Một chip PIC điển hình trong hệ thống x86 là chip 8259A.
Hình 1. Chip PIC được sử dụng trong hệ thống máy tính
Nhìn từ bên ngoài, PIC gồm nhiều chân tiếp nhận Interrupt, mỗi chân được gọi là một IRQ line (Interrupt ReQuest line). Tuy nhiên, ngay cả khi ta sử dụng PIC, thì số lượng I/O module trong hệ thống vẫn có thể lớn hơn số IRQ line. Do vậy, nhiều I/O module có thể phải chia sẻ chung một IRQ line.
Nhìn từ bên trong, dưới góc độ lập trình, PIC gồm 2 loại thanh ghi quan trọng:
- Thanh ghi điều khiển: cho phép từ chối (còn gọi là che) hoặc chấp nhận (còn gọi là không che) tín hiệu Interrupt trên một IRQ line nào đó. Ngoài ra, nó có thể thiết lập độ ưu tiên cho từng IRQ line.
- Thanh ghi trạng thái: cho biết Interrupt xảy ra trên IRQ line nào.
Chú ý rằng, trong các hệ thống sử dụng CPU đa lõi, người ta sử dụng APIC (Advance PIC) thay cho PIC. Một chip APIC điển hình trong hệ thống x86 là chip 82489DX. Nguyên tắc làm việc của PIC và APIC gần tương tự nhau. Để cho đơn giản, trong bài học này, ta chỉ xét hệ thống sử dụng CPU đơn lõi và chip PIC.
Hỗ trợ từ kernel
Kernel thực hiện 3 công việc để hỗ trợ tiếp nhận và xử lý ngắt:
- Một là khởi tạo chip PIC (hoặc khởi tạo APIC).
- Hai là khởi tạo bảng IDT (Interrupt Descriptor Table).
- Bảng IDT nằm trên RAM. Vị trí và kích thước của bảng IDT được lưu trong thanh ghi IDTR của CPU.
- Bảng IDT gồm 256 dòng, kích thước của mỗi dòng là 8 byte.
- Trong 256 dòng đó, có:
- 21 dòng được dùng để mô tả các ngắt của hệ thống (system-define), bao gồm Non-maskable Interrupt và các Exception.
- 223 dòng để mô tả các Maskable Interrupt do người dùng định nghĩa (user-define).
- Số còn lại để trống.
- Mỗi dòng được gọi là descriptor và liên kết với một số định danh, số đó được gọi là vector (bảng 1). Mỗi descriptor chứa thông tin về địa chỉ của một hàm phục vụ ngắt sơ cấp.
- Nếu ngắt thuộc loại Interrupt, thì hàm phục vụ ngắt được gọi là Interrupt Handler, và descriptor đó được gọi là interrupt gate.
- Nếu ngắt thuộc loại Exception thì hàm phục vụ ngắt được gọi là Exception Handler, và descriptor đó được gọi là trap gate.
- Trong 256 dòng đó, có:
- Ba là kích hoạt hàm phục vụ ngắt thứ cấp nằm trong một driver nào đó. Hàm phục vụ ngắt của driver còn được gọi là ISR (Interrupt Service Routine).
Vector |
Hàm phục vụ sơ cấp |
Loại ngắt |
Ý nghĩa |
0 |
divide_error | Fault | Được kích hoạt khi CPU vừa thực thi lệnh chia cho 0. |
1 |
debug | Trap | Được kích hoạt để phục vụ mục đích gỡ lỗi (debug). |
2 |
nmi | Non-maskable Interrupt | Được kích hoạt khi có tín hiệu trên chân NMI của CPU. |
3 |
int3 | Trap | Khi chương trình debugger gặp một breakpoint, debugger sẽ chèn lệnh INT3. Hàm này được kích hoạt để xử lý tình huống này. |
… |
|||
14 |
page_fault | Fault | Đươc kích hoạt khi page của chương trình không nằm trong RAM. |
15 |
N/A | N/A | Intel đã sử dụng vector này cho mục đích riêng. |
… |
|||
21-31 |
N/A | N/A | Intel đã sử dụng các vector này cho mục đích riêng. |
32-127 129 – 255 |
irq_entries_start | Maskable Interrupt | Được kích hoạt khi có tín hiệu trên chân INTR của CPU, hoặc khi CPU thực hiện lệnh INT n. |
128 | entry_SYSENTER_32 | Trap | Được kích hoạt khi CPU thực hiện lệnh SYSENTER.
Được dùng để triển khai system call. |
Bảng 1. Ý nghĩa của một số vector ngắt
Tiếp nhận và xử lý Interrupt
Giả sử, một I/O module gửi tín hiệu Interrupt tới CPU. Quá trình tiếp nhận và xử lý được chia làm 4 giai đoạn:
- Giai đoạn 1: CPU tiếp nhận tín hiệu Interrupt từ PIC.
- Giai đoạn 2: CPU xử lý Interrupt giai đoạn 1 (một cách tự động).
- Giai đoạn 3: CPU xử lý Interrupt giai đoạn 2 (theo hướng dẫn của hàm phục vụ ngắt sơ cấp).
- Giai đoạn 4: CPU xử lý Interrupt giai đoạn 3 (theo hướng dẫn của hàm phục vụ ngắt thứ cấp).
Giai đoạn 1
- Khi một I/O module muốn tương tác với CPU, nó sẽ nâng (hoặc hạ) điện áp trên đường dây nối với PIC.
- Nếu IRQ line nối với I/O module đó không che Interrupt, thì PIC sẽ ánh xạ IRQ line này thành một con số tương ứng (chính là vector) và lưu nó vào thanh ghi trạng thái. Ví dụ, IRQ line thứ n sẽ được PIC ánh xạ thành vector m = n + 32.
- PIC nâng (hoặc hạ) điện áp trên đường dây nối với chân INTR của CPU để thông báo với CPU rằng có một tín hiệu Interrupt. Giả sử lúc này, CPU đang trong quá trình thực thi lệnh thứ i.
- CPU tiếp tục thực hiện lệnh thứ i cho tới khi hoàn thành. Sau đó, CPU kiểm tra xem có Interrupt hay không. Nếu CPU không che Interrupt trên chân INTR và phát hiện có tín hiệu Interrupt, CPU sẽ xuất tín hiệu ra chân INTA (INTerrupt Acknowledge), để thông báo cho PIC biết rằng CPU đã sẵn sàng tiếp nhận Interrupt.
- Sau khi nhận được tín hiệu trả lời của CPU, PIC gửi vector lên bus dữ liệu. CPU đọc vector này vào một thanh ghi.
Giai đoạn 2
Ở giai đoạn này, CPU sẽ tự động lưu lại trạng thái của tác vụ bị ngắt. Tùy vào chế độ thực thi của CPU khi nhận được tín hiệu Interrupt, hoạt động sẽ hơi khác nhau một chút.
Giả sử, trong lúc thực hiện lệnh thứ i nằm trong kernel, CPU nhận được tín hiệu Interrupt. Điều này có nghĩa là CPU nhận được Interrupt khi đang ở chế độ kernel mode. Lúc đó, CPU sẽ thực hiện các bước sau:
- Lưu các thanh ghi EFLAGS, CS, EIP vào trong stack hiện tại (vì CPU đang ở chế độ kernel mode nên ta gọi là kernel stack).
- Dựa vào vector có giá trị là m, CPU sẽ truy cập đến dòng thứ m của bảng IDT. Từ đây, CPU sẽ nạp địa chỉ của hàm phục vụ ngắt sơ cấp vào tổ hợp thanh ghi CS:EIP.
- Xóa cờ IF trong thanh ghi EFLAG để ngừng tiếp nhận Interrupt trên chân INTR.
- Bắt đầu thực thi hàm phục vụ ngắt sơ cấp.
Còn nếu CPU nhận được tín hiệu Interrupt khi đang ở chế độ user mode, thì CPU sẽ thực hiện các bước sau:
- Lưu tạm thời các thanh ghi SS, ESP, EFLAG, CS:EIP vào các thanh ghi đặc biệt bên trong CPU.
- Nạp giá trị mới cho các thanh ghi SS và ESP để chuyển sang sử dụng kernel stack. Hành động này được gọi là stack switch.
- Lưu các giá trị SS, ESP, EFLAG, CS, EIP ở bước 1 vào trong kernel stack.
- Dựa vào vector có giá trị là m, CPU sẽ truy cập đến dòng thứ m của bảng IDT. Từ đây, CPU sẽ nạp địa chỉ của hàm phục vụ ngắt sơ cấp vào tổ hợp thanh ghi CS:EIP.
- Xóa cờ IF trong thanh ghi EFLAG để ngừng tiếp nhận Interrupt trên chân INTR.
- Bắt đầu thực thi hàm phục vụ ngắt sơ cấp.
Giai đoạn 3
Ở giai đoạn này, CPU thực thi các lệnh của hàm phục vụ ngắt sơ cấp.
- CPU bắt đầu thực hiện hàm irq_entries_start. Hàm irq_entries_start lưu vector ngắt vào trong kernel stack rồi gọi hàm common_interrupt.
- Hàm common_interrupt sẽ lưu tất cả các thanh ghi còn lại của CPU vào trong kernel stack (bao gồm GS, FS, ES, DS, EAX, EBP, EDI, ESI, EDX, ECX, EBX). Sau đó, hàm này gọi hàm do_IRQ.
- Hàm do_IRQ lấy được vector từ trong kernel stack. Dựa vào đó, nó sẽ tìm được hàm phục vụ ngắt thứ cấp (ISR) phù hợp.
Giai đoạn 4
Ở giai đoạn này, CPU thực thi các lệnh của ISR nằm trong device driver tương ứng. Vì mỗi ISR dùng để xử lý Interrupt của một thiết bị, nên người lập trình driver cần viết mã cho ISR. Khi viết ISR, bạn cần tuân thủ 2 nguyên tắc sau:
- Không được sử dụng hàm sleep trong ISR, dù trực tiếp hay gián tiếp. Ví dụ:
- Khi bạn muốn cấp phát bộ nhớ với hàm kmalloc, bạn không được sử dụng cờ GFP_KERNEL. Bởi vì nếu bộ nhớ không đủ, CPU sẽ ngừng thực thi ISR (tức là ISR sẽ đi ngủ). Trong trường hợp này, bạn nên sử dụng cờ GFP_ATOMIC.
- Bạn không được sử dụng hàm liên quan tới lập lịch, ví dụ hàm schedule_timeout.
- Bạn không được sử dụng hàm request_module để đưa một module nào đó vào trong kernel.
- Bạn không được sử dụng các hàm truyền/nhận dữ liệu giữa kernel space và user space (ví dụ như hàm copy_to_user, copy_from_user,…).
- Bạn nên viết mã của ISR sao cho xử lý thật nhanh để nhanh chóng đưa CPU thoát khỏi interrupt context. Lý do là vì, các Interrupt khác có thể xảy ra bất kì lúc nào. Do đó, ta gọi ISR là một hàm nhạy cảm với thời gian (timing-critical).
Đến đây, các bạn có thể đặt câu hỏi rằng: công việc cần làm có thể sẽ rất nhiều, vậy thì làm sao có thể viết được ISR để nó xử lý Interrupt thật nhanh. Đúng là có thể có rất nhiều việc cần xử lý, nhưng chỉ có một vài việc là khẩn cấp. Những việc cần xử lý khẩn cấp sẽ được đặt trong ISR. Còn những việc không khẩn cấp sẽ được lập lịch để xử lý sau. Do vậy, quá trình xử lý Interrupt trong giai đoạn thứ tư được chia làm 2 phần:
- Phần top-half: chỉ xử lý các công việc khẩn cấp. ISR sẽ xử lý các công việc thuộc phần top-half. Ví dụ về các công việc khẩn cấp như:
- Che ngắt trên các IRQ line của PIC nếu cần thiết.
- Nếu nhiều I/O module cùng chia sẻ một IRQ line, thì cần kiểm tra xem I/O module nào đã phát đi tín hiệu Interrupt.
- Đọc/ghi dữ liệu từ một vài thanh ghi của I/O module vào bộ đệm đã được cấp phát sẵn trên RAM.
- Kích hoạt phần bottom-half (nếu có).
- Phần bottom-half: thực hiện các công việc cần làm nhưng lại không khẩn cấp. Nghĩa là, các công việc đó sẽ được làm, nhưng sẽ làm sau, không phải bây giờ. Tùy vào tín hiệu Interrupt mà phần này có thể có hoặc không. Một số phương pháp lập lịch thực thi các công việc thuộc phần bottom-half:
- Tasklets.
- Softirqs.
- Workqueues.
Kết luận
Ngắt là một sự kiện làm gián đoạn hoạt động bình thường của CPU, buộc CPU phải chuyển sang thực thi một đoạn mã lệnh đặc biệt để xử lý sự kiện đó. Có hai loại ngắt là:
- Interrupt:
- Còn gọi là hardware interrupt hay asynchronization interrupt.
- Được chia làm 2 loại nhỏ là maskable interrupt và non-maskable interrupt.
- Exception:
- Còn gọi là software interrupt hay synchronization interrupt.
- Được chia làm 3 loại nhỏ là fault, trap và abort.
Quá trình tiếp nhận và xử lý Interrupt gồm 4 giai đoạn. Hệ thống đã hỗ trợ chúng ta trong 3 giai đoạn đầu. Riêng giai đoạn thứ tư, Interrupt được xử lý bởi device driver. Do đó, khi lập trình device driver, ta cần viết một đoạn mã gọi là ISR (hay hàm phục vụ ngắt thứ cấp). Quá trình xử lý một Interrupt ở giai đoạn thứ tư được chia làm 2 phần: top-half và bottom-half.
- Phần top-half chỉ xử lý một số công việc khẩn cấp để nhanh chóng đưa CPU thoát khỏi interrupt context. Ta không được sử dụng hàm sleep một cách trực tiếp hay gián tiếp.
- Phần bottom-half xử lý các công việc cần thiết nhưng không khẩn cấp. Tùy vào Interrupt mà phần này có thể có hoặc không.