Character device driver

1. Tổng quan

Linux được thiết kế để chạy trong rất nhiều các nền tảng khác nhau và hỗ trợ nhiều loại device: TV, ô tô, smarthome, PC,… Vậy làm thế nào để 1 hệ điều hành có thể quản lý được nhiều thiết bị (device) đến như vậy?

Hệ điều hành cung cấp các file đặc biệt (device file) tương ứng với mỗi phần cứng được sử dụng trên hệ thống (nằm trong thư mục /dev). Các chương trình trên tầng user có thể truy cập vào thiết bị tương ứng với các file nằm trong thư mục /dev và thực hiện các system call thích hợp trên thiết bị, các lệnh này sau đó sẽ được chuyển tới driver đã được liên kết với device file của thiết bị đó.

Linux quản lý các thiết bị dựa trên cây phân cấp device gồm 3 loại chính:

  • Character device: dữ liệu được đọc tuần tự theo từng byte một (chuột, bàn phím, serial,…)
  • Block device: dữ liệu được đọc theo từng block một (ổ cứng, CD-ROM,…)
  • Network device: dữ liệu được được gán địa chỉ, chuyển ra bên ngoài và có thể bị mất mát dữ liệu (Ethernet, Wifi. 5G,…)

Ngoài ra, để thuận tiện cho việc quản lý các thiết bị, Linux xây dựng sẵn template driver cho từng loại device cụ thể. Bản chất của template driver sẽ chứa các con trỏ hàm (APIs) mà hệ điều hành gọi ra khi thao tác thiết bị. Nhiệm vụ của người lập trình là phải điền thông tin phần cứng theo template do hệ điều hành quy định.

Ở trong bài viết lần này, chúng ta sẽ tập trung đi tìm hiểu về character deviec và driver của chúng: các API và các struct dữ liệu phổ biến được sử dụng.

2. Character Device Structure

Character device driver là loại driver phổ biến nhất trong Linux kernel source. Các character device được mô tả trong kernel thông qua một struct data “cdev” – là một phần của API để đăng ký và quản lý thiết bị trong kernel. Struct này được khai báo trong include/linux/cdev.h

  • struct kobject kobj: Đây là một cấu trúc dữ liệu cơ bản trong kernel Linux để đại diện cho đối tượng kernel.
  • struct module *owner: Con trỏ đến module kernel mà trình điều khiển thuộc về. Nên để là THIS_MODULE
  • const struct file_operations *ops: Con trỏ đến một cấu trúc file_operations chứa các con trỏ đến các hàm thực hiện các thao tác trên thiết bị như open, read, write,…
  • dev_t dev: Kí tự nhận dạng character device.

Với struct trên, chúng ta cần quan tâm tới struct file_operations.

Struct file_operations

Con trỏ ops trong struct cdev trỏ tới file operations tương ứng với từng thiết bị nhất định trong hệ thống.

Ứng với mỗi phần tử trong struct trên là một system call cụ thể ,khi system call được gọi ra tại tầng user trên character device file, các system call đó sẽ được chuyển hướng trong kernel tới các hàm được khai báo tương ứng trong struct file_operations.

Để hiểu rõ hơn chúng ta cùng xem ví dụ sau:

Với ví dụ trên, người dùng thực hiện gọi các system call open(), read(), write(), release() tương tác với device file trên tầng user space, hệ điều hành sẽ tìm các tên hàm đã được gán trong trong struct file_operations (.open, .read, .write, .release) và thực thi các công việc đã được chỉ định trong hàm. Ở đây chúng ta có quan hệ sau (user space – driver): open() – misc_open(), read() – misc_read, write() – misc_write(), release() – misc_realease()

Struct miscdevice

Để có thể ví dụ về sự tương tác giữa character device và character device driver, chúng ta cần có thiết bị cụ thể đăng kí với hệ điều hành. Tuy nhiên, Linux kernel cung cấp một struct cho phép đơn giản hóa việc đăng kí character devices mà không phụ thuộc bất kỳ lớp thiết bị cụ thể nào: struct miscdevice.

Struct này chứa các thông tin của device và giúp đăng kí struct file_operations với hệ thống.

  • minor: thường đặt giá trị là MISC_DYNAMIC_MINOR để kernel tự động chọn một minor number không trùng lặp.
  • name: Tên của file device được tạo ra xuất hiện trong đường dẫn /dev.
  • fops: Con trỏ đến cấu trúc file_operations chứa các con trỏ đến các hàm xử lý sự kiện cho thiết bị (như open, read, write, ioctl, …)

3. Trao đổi dữ liệu giữa kernel space và user space

Chúng ta có thể trao đổi dữ liệu device và user thông qua các API đã được khai báo trong struct file_operations như:

  • static ssize_t misc_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
  • static ssize_t misc_write(struct file *filp, const char __user *buf,

                          size_t count, loff_t *f_pos)

Các buffer ở hàm read/write là địa chỉ virtual address thuộc về không gian bộ nhớ của process gọi ra các hàm trên. Trong khi driver lại chạy trong kernel process có không gian bộ nhớ riêng. Do vậy không được truy cập trực tiếp vào những địa chỉ này, thay vào đó kernel support 2 hàm để truy cập đó là:

  • to: Con trỏ đến vùng nhớ trong không gian bộ nhớ kernel nơi dữ liệu được sao chép lưu trữ.
  • from: Con trỏ đến vùng nhớ trong không gian bộ nhớ người dùng nơi chứa dữ liệu cần sao chép.
  • n: Số lượng byte cần sao chép.
  • to: Con trỏ đến vùng nhớ trong không gian bộ nhớ người dùng nơi dữ liệu sẽ được sao chép đến.
  • from: Con trỏ đến vùng nhớ trong không gian bộ nhớ kernel nơi dữ liệu cần sao chép.
  • n: Số lượng byte cần sao chép.

4.  Ví dụ sử dụng character device driver

Các driver cũng như kernel module đã tìm hiểu trước đó, chương trình của driver cũng không có hàm main, thay vào đó là 2 hàm init và exit được đăng kí thông qua các macro:

  • module_init(misc_init);      // đặt tên hàm init là misc_init
  • module_exit(misc_exit);     // đặt tên hàm exit là misc_exit

Driver cũng được load lên hệ thống bằng câu lệnh insmod và gỡ bỏ ra khỏi kernel bằng cậu lệnh rmmod, đồng thời 2 hàm init và exit cũng được gọi ra tương ứng. Khi insmod thành công, một device sẽ được tạo ra trong thư mục /dev với tên trùng với trường “name”được khai báo trong struct device.

Chúng ta cùng nhau xét ví dụ in ra dòng chữ “Hello World” tại kernel space từ user space:

Character Device driver: ( hello.c )

Application người dùng sử dụng:

Khi dùng câu lệnh insmod hello.ko ta thu được dòng log sau:

Ta cũng có thể kiểm tra xem device đã được đăng kí thành công hay chưa bằng cách dùng cmd ls/dev: ( tìm file misc_example )

Với application, khi run ta thu được dòng log sau:

Như vậy, ta đã in thành công “Hello world” từ user space trên kernel space

Tiếp theo, ta cùng ứng dụng bật tắt đèn LED ( tại GPIO31 ) bằng cách gửi kí tự “1” từ user để bật LED và “0” để tắt LED:

Chương trình driver như trên chỉ thay đổi ở hàm misc_open() với chức năng enable GPIO31:

Và hàm misc_write() thực hiện xác nhận dữ liệu từ user và bật tắt LED:

Khi đó chương trình app thay đổi như sau:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top