內核空間與用戶控件數據交換
前面了解的字符設備中對 file_operations 結構體的進行了填充, 該
結構體的每一個成員都對應著一個系統調用, 例如 read、 write 等, 在字符設備相關的文章中有實驗過對
調用函數進行了標志打印, 并沒有真正實現設備的讀寫功能。 這里需要實現的功能其實就是內核空間和用戶控件之間的數據交換
文章目錄
- 理解概念:內核空間與用戶空間
- 參考資料
- 用戶空間和內核空間數據交換
- 用戶空間數據復制到內核空間 copy_from_user
- 數據從內核空間拷貝到用戶空間copy_to_user
- 實驗
- 源碼程序 file.c
- 方法 copy_to_user
- 方法 copy_from_user
- 編譯文件 Makefile
- 測試程序
- 源碼程序分析
- 加載驅動 insmod file.ko
- 執行程序 ./appfile
- 總結
理解概念:內核空間與用戶空間
Linux 系統將可訪問的內存空間分為了兩個部分, 一部分是內核空間, 一部分是用戶空間。
操作系統和驅動程序運行在內核空間(內核態) , 應用程序運行在用戶空間(用戶態) 。
那么為什么要區分用戶空間和內核空間呢?
(1) 內核空間中的代碼控制了硬件資源, 用戶空間中的代碼只能通過內核暴露的系統調
用接口來使用系統中的硬件資源, 這樣的設計可以保證操作系統自身的安全性和穩定性。
(2) 從另一方面來說, 內核空間的代碼更偏向于系統管理, 而用戶空間中的代碼更偏重
業務邏輯實現, 倆者的分工不同。
硬件資源管理都是在內核空間完成的, 應用程序無法直接對硬件進行操作, 只能通過調用
相應的內核接口來完成相應的操作。 比如應用程序要對磁盤上的一個文件進行讀取, 應用程序
可以向內核發起一個“系統調用” 申請——我要讀取磁盤上的文件。 這個過程其實是通過一個
特殊的指令讓進程從用戶態進入到了內核態。 在內核空間中, CPU 可以執行任何命令, 包括
從磁盤上讀取數據, 具體過程是先把數據讀取到內核空間中, 然后再把數據拷貝到用戶空間并
從內核態切換到用戶態。 此時應用程序已經從系統調用中返回并拿到了想要的數據, 可以繼續
往下執行了。
進程只有從用戶空間切換到內核空間才可以使用系統的硬件資源, 切換的方式有三種: 系
統調用, 軟中斷, 硬中斷, 如下圖:
參考資料
字符設備的基礎知識一定要了解,都是關聯的知識點
申請字符設備號
注冊字符設備
創建字符設備節點
字符設備驅動框架
雜項設備
用戶空間和內核空間數據交換
內核空間和用戶空間的內存是不能互相訪問的。 但是很多應用程序都需要和內核進行數據
的交換, 例如應用程序使用 read 函數從驅動中讀取數據, 使用 write 函數向驅動中寫數據, 上
述功能就需要使用 copy_from_user 和 copy_to_user 倆個函數來完成。 copy_from_user 函數是將
用戶空間的數據拷貝到內核空間。 copy_to_user 函數是將內核空間的數據拷貝到用戶空間。
這倆個函數定義在了 kernel/include/linux/uaccess.h 文件下
用戶空間數據復制到內核空間 copy_from_user
copy_from_user 用于將數據從用戶空間復制到內核空間。
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
參數說明
- to: 內核空間的目標地址
- from: 用戶空間的源地址
- n: 要復制的字節
使用示例
char kernel_buf[BUFSIZE];
if (copy_from_user(kernel_buf, user_buf, count)) {// 處理錯誤return -EFAULT;
}
數據從內核空間拷貝到用戶空間copy_to_user
copy_to_user 用于將數據從內核空間復制到用戶空間。 函數原型如下:
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
- to: 用戶空間的目標地址
- from: 內核空間的源地址
- n: 要復制的字節數
實驗
源碼程序 file.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>static dev_t dev_num;//定義dev_t類型(32位大小)的變量dev_num
static int major; //定義int類型的主設備號major
static int minor;//定義int類型的 次設備號minor
static struct cdev cdev_test; //定義struct cdev 類型結構體變量cdev_test,表示要注冊的字符設備
struct class *class_test;//定于struct class *類型結構體變量class_test,表示要創建的類
struct device *device; //設備/*打開設備函數*/
static int chrdev_open(struct inode *inode, struct file *file)
{printk("This is chrdev_open \n");return 0;
}static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{/*本章實驗重點******/char kbuf[32] = "This is read from kernel";//定義內核空間數據// copy_to_user:內核空間向用戶空間傳數據if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) {printk("copy_to_user error\r\n"); //打印copy_to_user函數執行失敗return -1;}printk("This is cdev_test_read\r\n");return 0;
}/*向設備寫入數據函數*/
static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{/*本章實驗重點******/char kbuf[32] = {0}; //定義寫入緩存區kbufif (copy_from_user(kbuf, buf, size) != 0) // copy_from_user:用戶空間向內核空間傳數據{printk("copy_from_user error\r\n");//打印copy_from_user函數執行失敗return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", kbuf);return 0;
}
static int chrdev_release(struct inode *inode, struct file *file)
{return 0;
}static struct file_operations cdev_test_ops = {.owner=THIS_MODULE,//將owner字段指向本模塊,可以避免在模塊的操作正在被使用時卸載該模塊.open = chrdev_open,//將open字段指向chrdev_open(...)函數.read = chrdev_read,//將open字段指向chrdev_read(...)函數.write = chrdev_write,//將open字段指向chrdev_write(...)函數.release = chrdev_release,//將open字段指向chrdev_release(...)函數
};//定義file_operations結構體類型的變量cdev_test_opsstatic int __init chrdev_fops_init(void)//驅動入口函數
{int ret;//定義int類型的變量ret,用來判斷函數返回值ret=alloc_chrdev_region(&dev_num,0,1,"chardev_num"); //通過動態方式進行設備號注冊if(ret < 0){printk("alloc_chrdev_region is error\n");} printk("alloc_chrdev_region is ok\n");major=MAJOR(dev_num);//通過MAJOR()函數進行主設備號獲取minor=MINOR(dev_num);//通過MINOR()函數進行次設備號獲取printk("major is %d\n",major);printk("minor is %d\n",minor);使用cdev_init()函數初始化cdev_test結構體,并鏈接到cdev_test_ops結構體cdev_init(&cdev_test,&cdev_test_ops);cdev_test.owner = THIS_MODULE;//將owner字段指向本模塊,可以避免在模塊的操作正在被使用時卸載該模塊 ret= cdev_add(&cdev_test,dev_num,1);if(ret < 0 ){printk("cdev_add is error\n");}printk("cdev_add is ok\n");class_test = class_create(THIS_MODULE,"class_test");//使用class_create進行類的創建,類名稱為class_testdevice_create(class_test,NULL,dev_num,NULL,"device_test");//使用device_create進行設備的創建,設備名稱為device_testreturn 0;
}
static void __exit chrdev_fops_exit(void)//驅動出口函數
{cdev_del(&cdev_test);//使用cdev_del()函數進行字符設備的刪除unregister_chrdev_region(dev_num,1);//釋放字符驅動設備號 device_destroy(class_test,dev_num);//刪除創建的設備class_destroy(class_test);//刪除創建的類printk("module exit \n");}
module_init(chrdev_fops_init);//注冊入口函數
module_exit(chrdev_fops_exit);//注冊出口函數
MODULE_LICENSE("GPL v2");//同意GPL開源協議
MODULE_AUTHOR("wang fang chen "); //作者信息
源碼分析:
這里省略之前內容關聯知識的內容,重點看 內核空間和用戶控件數據交換部分內容。
方法 copy_to_user
讀取內核空間數據到用戶空間,定義了內核空間數據,其實就是驅動程序里面定義了字節數組
char kbuf[32] = “This is read from kernel”;//定義內核空間數據
copy_to_user 方法調用后, copy kbuf 數據到 buf里面去,buf 在方法里面的參數是一個用戶空間態指針,這樣就傳遞到用戶空間了。
static ssize_t chrdev_read(struct file *file,char __user *buf, size_t size, loff_t *off)
{/*本章實驗重點******/char kbuf[32] = "This is read from kernel";//定義內核空間數據// copy_to_user:內核空間向用戶空間傳數據if (copy_to_user(buf, kbuf, strlen(kbuf)) != 0) {printk("copy_to_user error\r\n"); //打印copy_to_user函數執行失敗return -1;}printk("This is cdev_test_read\r\n");return 0;
}
方法 copy_from_user
從用戶空間數據copy 數據到內核空間
buf 數據是用戶態指針類型,調用 copy_from_user 方法后 kbuf 就有新的數據了,其實其實驅動程序里面的數據就是內核態數據,然后將其打印出來。
/*向設備寫入數據函數*/
static ssize_t chrdev_write(struct file *file,const char __user *buf,size_t size,loff_t *off)
{/*本章實驗重點******/char kbuf[32] = {0}; //定義寫入緩存區kbufif (copy_from_user(kbuf, buf, size) != 0) // copy_from_user:用戶空間向內核空間傳數據{printk("copy_from_user error\r\n");//打印copy_from_user函數執行失敗return -1;}printk("This is cdev_test_write\r\n");printk("kbuf is %s\r\n", kbuf);return 0;
}
編譯文件 Makefile
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += file.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:make -C $(KDIR) M=$(PWD) modulesclean:make -C $(KDIR) M=$(PWD) clean
測試程序
appfile.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) // 主函數
{int fd; // 定義 int 類型的文件描述符char buf1[32] = {0}; // 定義讀取緩存區 buf1char buf2[32] = "nihao"; // 定義寫入緩存區 buf2fd = open("/dev/test", O_RDWR); // 打開字符設備驅動if (fd < 0){perror("open error \n");return fd;}read(fd, buf1, sizeof(buf1)); // 從/dev/test 文件讀取數據printf("buf1 is %s \r\n", buf1); // 打印讀取的數據write(fd, buf2, sizeof(buf2)); // 向/dev/test 文件寫入數據close(fd);return 0;
}
源碼程序分析
這里關注read 方法,buf1 用戶態數據,讀取后 經過系統調用
copy_to_user(buf, kbuf, strlen(kbuf)),內核態數據copy 了一份到用戶態 buf1 中,然后打印出來。
需要 aarch64-linux-gnu-gcc 來編譯, 輸入以下命令, 編譯完成以后會生成一個可執行程序
aarch64-linux-gnu-gcc appfile.c -o appfile
生成可執行程序 appfile
加載驅動 insmod file.ko
這里都是以前的基本知識,暫不擴展
執行程序 ./appfile
結果如下: 不正是內核和用戶態數據交互結果嗎?
總結
- copy_from_user 和 copy_to_user 是 Linux 內核中用于在用戶空間和內核空間之間安全傳輸數據的兩個重要函數。
- 內核和用戶態數據傳遞就是通過兩個方法調用來實現,回調到用戶態其實就是指針傳遞。