Việc điều khiển các thiết bị đọc ghi ngoại vi thông thường sẽ bao gồm việc đọc và ghi vào các thanh ghi của các thiết bị đó. Các thanh ghi này thường được gọi là I/O ports
.
1 Memory barriers.
Việc đọc ghi các bộ nhớ thông thường đều khá đơn giản: Ghi có nghĩa là lưu một giá trị vào một địa chỉ trong bộ nhớ, còn đọc là lấy ra giá trị gần đây nhất được ghi vào địa chỉ đó, nó không tạo ra side efect náo. Tuy nhiên, đối với I/O Register thì mọi việc lại khác, bởi vì việc đọc/ghi một I/O register có thể sẽ khiến device thực hiện một số hành động khác nhau nào đó. Do vậy việc caching hay reordering các instruction (thường được thực hiện bởi quá trình tối ưu hóa của Compiler) là không thể sử dụng được.
Kernel cung cấp giải pháp sử dụng các memory barrier
để tránh các rủi ro này bằng các hàm sau:
barrier
: Hàm này sẽ khiến cho compiler lưu các giá trị đang được thay đổi vào một thanh ghi CPU, nhưng không tạo ra bất kỳ ảnh hưởng nào đến hardware.rmb()
: Chèn một barrier vào mem và đảm bảo rằng các lệnh đọc trước barrier phải được hoàn thành trước khi bất kỳ lệnh đọc nào sau barrier được thực thi.wmb()
: Tương tựrmb()
, nhưng là đối với lệnh ghimb()
: Tương tựrmb()
, nhưng áp dụng cho cả đọc và ghi.
Ngoài ra còn có các hàm smp_rmb()
, smp_read_barrier_depends()
, smp_wmb()
, smp_mb()
được sử dụng cho các hệ thống SMP.
Bởi vì các barrier sẽ ảnh hưởng đến hiệu năng chung, nên chỉ sử dụng chúng khi thực sự cần thiết.
2. Đăng ký I/O ports.
Trước khi sử dụng I/O ports, chúng ta cần đăng ký với kernel việc sử dụng các port muốn dùng thông qua hàm request_region()
và giải phóng vùng nhớ đã đăng ký bằng release_region
sau khi sử dụng xong.
Trong các hàm này thì from chính là base address của I/O region, còn extent là số lượng port mà chúng ta muốn đăng ký/ giải phóng. Ngoài ra, chúng ta có thể kiểm tra xem các I/O port region nào đã được sử dụng bằng command cat /proc/ioports
, nhớ phải chạy lệnh này bằng quyền sudo, ngược lại thì nó in ra toàn số 0.
Lưu ý là việc trên là không bắt buộc, bạn thậm chí có thể truy cập vào một I/O region kể cả khi việc đăng ký thất bại, tuy nhiên nguy cơ tiềm ẩn các BUG không đoán trước được và khó debug có thể xảy ra.
Một ví dụ đơn giản, module sample_ioport sẽ đăng ký một số I/O port sau đó sẽ giải phóng nó khi module exit.
Ở đây mình request 5 port bắt đầu từ vị trí 0x380, đây là một region mà mình thấy còn trống khi kiểm tra bằng cách cat nội dung của file proc/ioports. Sau khi insert module vừa viết vào hệ thống, thì dmesg sẽ có log như sau:
Hơn nữa, trong file ioports cũng có một entry mới được thêm vào:
Hàm request_region
sẽ trả về một con trỏ tới cấu trúc struct resource
, cấu trúc này đã được đề cập trong bài Platform device. Thực tế thì các hàm *_region()
thật ra là wrapper của các hàm request_resource
và release_resource
, do đó bạn cũng có thể sử dụng các hàm *_resource
để quản lý các I/O port.
3. Đọc và Ghi Dữ liệu với các I/O register.
Trong file asm/io.h, kernel định nghĩa các hàm đọc và ghi cho 8-bit, 16-bit và 32-bit ports.
Các hàm Đọc I/O Register
Các hàm Ghi I/O Register
Nếu muốn test các hàm đọc ghi và bạn đang sử dụng một con PC intel, thì bạn có thể thử dùng I/O port từ 0x378 đến 0x37a của parallel port, nhưng đừng request_region mà cứ dùng thẳng các hàm outb()
và inb()
.
4. Cấp phát, Mapping, và sử dụng I/O Memory.
Mặc dù phổ biến trong các thiết bị intel x86, nhưng I/O port không phải là kỹ thuật chính được sử dụng để Processor kết nối với các thiết bị ngoại vi, mà kỹ thuật đó chính là I/O Memory. I/O memory đơn giản là một vùng nhớ của thiết bị ngoại vi khả dụng để Processor có thể truy cập thông qua Bus. Vùng nhớ này có thể được sử dụng cho nhiều mục đích, chẳng hạn như để giữ các Gói dữ liệu, hoặc được sử dụng như các register tương tự I/O ports. Cách truy cập tới I/O memory phụ thuộc vào platform, nhưng việc này được implement bởi Kernel và nó là Transperent với Device driver.
Trước khi sử dụng I/O memory chúng ta cần cấp phát một vùng nhớ để sử dụng, và tương tự như đối với I/O ports, thì sau khi sử dụng chúng ta cần giải phsong nó:
Tuy nhiên, với I/O memory thì việc cấp phát bộ nhớ là chưa đủ, bạn phải đảm bảo rằng kernel có thể truy cập vùng nhớ I/O memory của device, thông qua việc sử dụng ioremap()
, hàm này sẽ map địa chỉ bộ nhớ ảo tới vùng nhớ I/O memory.
Việc đọc/ghi từ I/O memory được thực hiện bằng các hàm sau:
Địa chỉ sử dụng trong các hàm này là địa chỉ được trả về bởi ioremap()
+ offset
Nếu bạn muốn đọc hay ghi nhiều giá trị thì có thể dùng các hàm thuộc họ io*_rep
, tương tự như I/O port.
Các thao tác đối với một block I/O Memory được thực hiện bởi các hàm:
5. Ví dụ I/O memory.
Sau khi đã trình bày đầy đủ các lý thuyết rườm rà buồn ngủ, thì việc đưa ra một ví dụ về việc sử dụng các API trên để hiểu rõ hơn về chúng là điều cần thiết. Tốt nhất nếu có device thật thì nên thử viết các module tương tác với I/O mem của device đó, để xe các side effect của nó, tuy nhiên nếu không có device thật thì cũng đừng lo, vẫn có thể làm quen với việc sử dụng các API này bình thường.
Sau đây mình sẽ tạo một module tương tác với một vùng nhớ ảo của hệ thống, sử dụng các API về I/O memory. Chương trình này sẽ là một character device driver, trong đó các hàm đọc ghi thay vì truy cập vào một vùng nhớ được tạo ra bằng kmalloc
thì mình sẽ request và map một I/O memory region vào device này, và các hàm đọc ghi sẽ sử dụng các hàm ioread*
và iowrite*
để đọc và ghi dữ liệu vào vùng nhớ.
Chương trình này sẽ tương đối giống với chương trình ví dụ trong bài Character device, chỉ thêm bớt một số điểm nho nhỏ :v.
Mình sẽ chọn ghi vào I/O Memory region của VRAM, đầu tiên cần xác định xem region của nó là từ đâu đến đâu đã. Đầu tiên là dùng lspci
để in ra danh sách các PCI device, và trên máy của mình thì entry của VGA là:
Tức là chúng ta tìm device có id là 00:02.0 trong proc/iomem.
Tức là base address của nó là 0xe0000000. Bây giờ define hai biến sau:
Lưu ý là cái địa chỉ này nó phụ thuộc vào máy của bạn, và các side effect xảy ra khi bạn đọc ghi trên vùng nhớ đó cũng phụ thuộc vào máy của bạn.
Trong hàm init của module, chúng ta sẽ request và remap cho memory region này:
Ở đây biến oni_res là biến có kiểu strut resource * (code full sẽ có cuối bài).
Tiếp theo, thêm các hàm release và unmap vào hàm exit của module, để trả lại các địa chỉ này khi chúng ta không cần đến nó nữa.
Tiếp theo, trong hàm read và writecủa file_operations mình sẽ dụng các hàm ioread8()
và iowrite8
để đọc và ghi các giá trị vào vùng nhớ I/O memory đã yêu cầu.
Sau đây là source code đầy đủ của chương trình.
Sau khi đã hoàn thành source code, mình sẽ compile nó, và insert nó vào hệ thống
Sau đấy mình sẽ ghi nội dung linh tinh vào device file (tức là sẽ gọi đến hàm write), lệnh này phải chạy bằng quyền root.
Bây giờ đọc giá trị đã ghi vào bằng lệnh sau:
Kết quả là trên màn hình sẽ hiện ra giá trị mà mình đã ghi vào cộng với một lô một lốc các ký tự đặc biệt theo sau vì cái vùng nhớ này giá trị mặc định của các ô nhớ là cái gì đấy không thể hiện lên dưới dạng ascii.
Leave a Comment