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ị đó.
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-40.png)
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
1 2 3 4 5 6 7 8 |
struct cdev { struct kobject kobj; struct module *owner; const struct file_operations *ops; dev_t dev; [...] }; |
- 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t,int datasync); int (*flock) (struct file *, int, struct file_lock *); [...] }; |
Ứ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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
struct file_operations misc_fops = { .owner = THIS_MODULE, .open = misc_open, .release = misc_release, .read = misc_read, .write = misc_write, }; int misc_open(struct inode *node, struct file *filep) { // enable phần cứng } static ssize_t misc_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { // thực hiện đọc dữ liệu từ phần cứng, lưu vào buffer của kernel } static ssize_t misc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { // thực hiện ghi dữ liệu từ buffer của kernel xuống phần cứng } int misc_release(struct inode *node, struct file *filep) { // disable phần cứng và synchronize data xuống phần cứng } |
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.
1 2 3 4 5 6 7 8 |
#include <linux/miscdevice.h> struct miscdevice { int minor; const char *name; const struct file_operations *fops; [....] }; |
- 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à:
1 2 3 |
#include <linux/uaccess.h> unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) |
- 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.
1 |
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) |
- 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 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
#include <linux/fs.h> #include <linux/init.h> #include <linux/miscdevice.h> #include <linux/module.h> #include <linux/uaccess.h> int misc_open(struct inode *node, struct file *filep) { // in log hiển thị tên hàm được gọi và số dòng nơi mã lệnh đang sử dụng pr_info("%s, %d\n", __func__, __LINE__); return 0; } int misc_release(struct inode *node, struct file *filep) { // in log hiển thị tên hàm được gọi và số dòng nơi mã lệnh đang sử dụng pr_info("%s, %d\n", __func__, __LINE__); return 0; } static ssize_t misc_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { // in log hiển thị tên hàm được gọi và số dòng nơi mã lệnh đang sử dụng pr_info("%s, %d\n", __func__, __LINE__); return 0; } static ssize_t misc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { int ret = 0; char local_data[128]; memset(local_data, 0, sizeof(local_data)); // thực hiện lấy dữ liệu từ user space ret = copy_from_user(local_data, buf, count < 128 ? count : 128); // in log hiển thị tên hàm được gọi và số dòng nơi mã lệnh đang sử dụng, dữ liệu từ user space pr_info("%s, %d, buf: %s\n", __func__, __LINE__, local_data); return count; } struct file_operations misc_fops = { .owner = THIS_MODULE, .open = misc_open, //Enable hardware .release = misc_release, //disable hardware, synchronize data xuong hardware .read = misc_read, //Doc du lieu tu hardware, luu vao buffer cua kernel .write = misc_write, //Ghi du lieu tu buffer cua kernel xuong hardware }; static struct miscdevice misc_example = { .minor = MISC_DYNAMIC_MINOR, .name = "misc_example", .fops = &misc_fops, }; // tạo ra node device file // ham init va exit cho driver static int misc_init(void) { pr_info("misc module init\n"); // đăng kí device với hệ thống misc_register(&misc_example); return 0; } static void misc_exit(void) { pr_info("misc module exit\n"); // gỡ bỏ device khỏi hệ thống misc_deregister(&misc_example); } module_init(misc_init); module_exit(misc_exit); MODULE_AUTHOR("Phu Luu An"); MODULE_DESCRIPTION("Example misc driver."); MODULE_LICENSE("GPL"); |
Application người dùng sử dụng:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int main() { int fd = 0; // mở device file: misc_example fd = open("/dev/misc_example", O_RDWR); if (fd > 0) { // ghi dữ liệu vào device file write(fd, "Hello world\n", strlen("Hello world\n")); close(fd); } return 0; } |
Khi dùng câu lệnh insmod hello.ko ta thu được dòng log sau:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-41.png)
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 )
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-42.png)
Với application, khi run ta thu được dòng log sau:
![](https://vinalinux.com.vn/wp-content/uploads/2023/11/image-43.png)
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int misc_open(struct inode *node, struct file *filep) { // Khởi tạo chân GPIO31 với mode output if (gpio_is_valid(31) == false) { pr_err("GPIO %d is not valid\n", 31); return 0; } // set chuc nang cho chan do la GPIO if(gpio_request(31 ,"GPIO_31") < 0) { pr_err("ERROR: GPIO %d request\n", 31); return 0; } // set output với đầu ra ban đầu là 0 gpio_direction_output(31, 0); // khong cho ứng dụng trong user thay đổi mode in or output gpio_export(31, false); pr_info("%s, %d\n", __func__, __LINE__); return 0; } |
Và hàm misc_write() thực hiện xác nhận dữ liệu từ user và bật tắt LED:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static ssize_t misc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { int ret = 0; char local_data[128]; memset(local_data, 0, sizeof(local_data)); ret = copy_from_user(local_data, buf, count < 128 ? count : 128); pr_info("%s, %d, buf: %s\n", __func__, __LINE__, local_data); switch (local_data[0]) { case '1': //bat led gpio_set_value(31, 1); break; case '0': //tat led gpio_set_value(31, 0); break; default: pr_info("send invalid command\n"); break; } return count; } |
Khi đó chương trình app thay đổi như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { int fd = 0; // mở device file: misc_example fd = open("/dev/misc_example", O_RDWR); if (fd > 0) { // Khi người dùng muốn bật LED write(fd, "1", 1); // Khi người dùng muốn tắt LED write(fd, "0", 1); close(fd); } return 0; } |