哪些是驅動:
驅動就是將底層硬體設備的操作進行封裝,並向下層提供函數插口。
裝置分類:
linux系統將設備分為3類:字元設備、區塊設備、網路設備。
字元設備:指只能一個位元組一個位元組讀寫的設備,不能隨機讀取設備顯存中的某一數據,讀取資料須要根據先後次序。字元設備是面向流的裝置linux 驅動程式 開發,常見的字元裝置有鼠標、鍵盤、串列埠、控制台和LED裝置等。塊設備:指可以從設備的任意位置讀取一定厚度資料的設備。塊設備包括硬盤、磁碟、U盤和SD卡等。網路設備:網路設備可以是一個硬體設備,如網卡;但也可以是一個純粹的軟體設備,例如回環插口(lo).一個網路插口負責發送和接收資料封包。驅動認知:
先看一張圖,圖中描述了流程,有助於了解驅動。
#使用者狀態:
內核狀態:
##驅動數組:管理所有設備的驅動,添加或查找,添加是發生在我們編撰完驅動程序,加載到核心。查找是在呼叫驅動程序,由應用層用戶空間去尋找使用open函數。驅動插入數組的次序由設備號檢索,就是說主設備號和次設備號不僅能分辨不同種類的設備和不同類型的設備,能夠起到將驅動程序加載到數組的某個位置,在下邊介紹的驅動程式碼的開發無非就是新增驅動(新增裝置號、裝置名稱和裝置驅動函數)和呼叫驅動。
補充:
每位系統呼叫都對應一個系統呼叫號,而係統呼叫號就對應核心中的對應處理函數。所有系統呼叫都是透過中斷0x80來觸發的。使用系統呼叫時,透過eax暫存器將系統呼叫號碼傳遞到內核,系統呼叫的入參透過ebx、ecx…依序傳遞到內核和函數一樣,系統呼叫的回傳值保存在eax中,所有要從eax中取出字元裝置驅動程式工作原理
字元裝置驅動程式運作原理在linux的世界裡一切皆文件,所有的硬體裝置操作到應用層就會被具象成文件的操作。我們曉得倘若應用層要存取硬體設備,它必將要呼叫到硬體對應的驅動程式。 Linux核心有這麼多驅動程序,應用如何能夠精確的呼叫到底層的驅動程式?
補充:
(1)當open函數開啟裝置檔案時,可以依據裝置檔案對應的structinode結構體所描述的訊息,可以曉得接出來要操作的裝置類型(字元裝置還是區塊裝置),都會指派一個structfile結構體。
(2)依據structinode結構體上面記錄的設備號,可以找到對應的驅動程式。這兒以字元設備為例。在Linux作業系統中每位字元設備都有一個structcdev結構體。此結構體描述了字元設備所有訊息,其中最重要的一項是字元設備的操作函數插口。
#(3)找到structcdev結構體後,linux核心都會將structcdev結構體所在的顯存空間首位址記錄在structinode結構體i_cdev成員中,將structcdev結構體中的記錄的函數操作插口位址記錄在structfile結構體的f_ops成員中。
(4)任務完成,VFS層會為應用程式傳回一個檔案描述符(fd)。這個fd是和structfile結構體對應的。接出來下層應用程式就可以透過fd找到structfile,之後在structfile找到操作字元設備的函數插口file_operation了。
其中,cdev_init和cdev_add在驅動程式的入口函數中就早已被調用,分別完成字元裝置與file_operation函數操作插口的綁定,和將字元驅動註冊到核心的工作。
相關影片推薦
免費學習地址:LinuxC/C 開發(前端/音訊視訊/遊戲/嵌入式/高效能網路/儲存/基礎架構/安全)
須要C/C Linux伺服器架構師學習資料加qun579733396取得(資料包括C/C ,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,串流媒體,CDNlinux 輸入法,P2Redis,fastdfs,MongoDB,ZK,串流媒體,CDNlinux 輸入法,P2Redis,fastdfs,MongoDB,ZK,串流媒體,CDNlinux 輸入法,P2Redis,fastdfs,MongoDB,ZK,串流媒體,CDNlinux 輸入法,P2Redis,fastdfs,MongoDB,ZK,串流媒體,CDNlinux 輸入法,P2Redis ,K8S,Docker,TCP/IP,解釋器,DPDK,ffmpeg等),免費分享
#字元裝置、字元裝置驅動程式與使用者空間存取該裝置的程式兩者之間的關係
#如圖,在Linux核心中使用cdev結構體來描述字元設備,透過其成員dev_t來定義設備號(分為主、次設備號)以決定字元設備的惟一性。透過其成員file_operations來定義字元裝置驅動提供給VFS的插口函數,如常見的open()、read()、write()等。
在Linux字元裝置驅動程式中,模組載入函數透過register_chrdev_region()或alloc_chrdev_region()來靜態或則動態取得裝置號,透過cdev_init()建置cdev與file_operations之間的聯接,透過cdev_add()向系統新增一個cdev以完成註冊。模組卸載函數透過cdev_del()來註銷cdev,透過unregister_chrdev_region()來釋放裝置號。
用戶空間存取該裝置的程式透過Linux系統調用,如open()、read()、write(),來「調用」file_operations來定義字元裝置驅動提供給VFS的插口函數。
驅動程式開發步驟
Linux核心就是由各類驅動組成的,核心原始碼中有大概85%是各類驅動程式的程式碼。核心中驅動程式種類齊全,可以在同類驅動的基礎上進行更改以符合具體單板。
編撰驅動程式的困難並不是硬體的具體操作,而是弄清楚現有驅動程式的框架,在這個框架中加入這個硬體。
通常來說,編撰一個linux裝置驅動程式的大致流程如下:
下邊就以一個簡單的字元裝置驅動框架程式碼來進行驅動程式的開發、編譯等。
基於驅動框架的程式碼開發
下層呼叫程式碼
#include #include #include #include void main() { int fd,data; fd = open("/dev/pin4",O_RDWR); if(fd<0){ printf("open failn"); perror("reson:"); } else{ printf("open successn"); } fd=write(fd,'1',1); }
驅動框架程式碼
#include //file_operations声明 #include //module_initmodule_exit声明 #include //__init__exit 宏定义声明 #include //classdevise声明 #include//copy_from_user 的头文件 #include//设备号dev_t 类型声明 #include //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno;//设备号 static int major = 231; //主设备号 static int minor = 0; //次设备号 static char *module_name = "pin4"; //模块名 //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_openn");//内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_writen"); return 0; } static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open= pin4_open, .write = pin4_write, }; int __init pin4_drv_init(void) //真实驱动入口 { int ret; devno = MKDEV(major, minor);//创建设备号 ret = register_chrdev(major, module_name, &pin4_fops);//注册驱动告诉内核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE, "myfirstdemo"); //用代码在dev自动生成设备 pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name);//创建设备文件 return 0; } void __exit pin4_drv_exit(void) { device_destroy(pin4_class, devno); class_destroy(pin4_class); unregister_chrdev(major, module_name);//卸载驱动 } module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使用 module_exit(pin4_drv_exit); MODULE_LICENSE("GPL v2");
裡面說過驅動開發的重點困難在於看懂框架程式碼,在上面進行設備的添加和更改,下邊就來了解一下這個框架邏輯。
驅動框架設計流程
1.確定主設備號碼
2.定義結構體類型file_operations
3.實作對應的drv_open/drv_read/drv_write等函數,填入file_operations結構體
4.實作驅動入口:安裝驅動程式時,才會去呼叫這個入口函數,執行工作:
①把file_operations結構體告訴核心:註冊驅動程式register_chrdev.
②建立類別class_create.
③建立裝置device_create.
5.實作出口:卸載驅動程式時,才會去呼叫這個出口函數,執行工作:
①把file_operations結構體從核心註銷:unregister_chrdev.
②銷毀類別class_create.
③銷毀裝置結點device_destroy.
6.其他建立:GPL合約、入口載入
1、確定主設備、變數定義
static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno;//设备号 static int major = 231; //主设备号 static int minor = 0; //次设备号 static char *module_name = "pin4"; //模块名
2、定義file_operations結構體,載入到核心驅動數組中
這是Linux核心中的file_operations結構體
#依照下層呼叫函數定義結構體成員
static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open= pin4_open, .write = pin4_write, .read= pin4_read, };
3、實作結構體成員pin4_read等函數
static int pin4_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk("pin4_readn"); return 0; } //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_openn");//内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_writen"); return 0; }
4、驅動入口
int __init pin4_drv_init(void) //真实驱动入口 { int ret; devno = MKDEV(major, minor);//创建设备号 ret = register_chrdev(major, module_name, &pin4_fops);//注册驱动告诉内核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE, "myfirstdemo");//由代码在dev自动生成设备 pin4_class_dev =device_create(pin4_class, NULL, devno, NULL, module_name);//创建设备文件 return 0; }###
其中pin4_class=class_create(THIS_MODULE,"myfirstdemo");//由代码在dev手动生成设备,除此之外还可以自动生成设备,在dev目录下sudomknod+设备名子+设备类型(c表示字符设备驱动)+主设备号+次设备号。
5、出口
void __exit pin4_drv_exit(void) { device_destroy(pin4_class, devno); class_destroy(pin4_class); unregister_chrdev(major, module_name);//卸载驱动 }
6、GPI合同,入口加载,出口加载
module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使用 module_exit(pin4_drv_exit); MODULE_LICENSE("GPL v2");
驱动模块代码编译和测试
编译阶段
驱动模块代码编译(模块的编译须要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶楼的Makefile文件,之后再返回模块源码所在目录。)
#include //file_operations声明 #include //module_initmodule_exit声明 #include //__init__exit 宏定义声明 #include //classdevise声明 #include//copy_from_user 的头文件 #include//设备号dev_t 类型声明 #include //ioremap iounmap的头文件 static struct class *pin4_class; static struct device *pin4_class_dev; static dev_t devno;//设备号 static int major = 231;//主设备号 static int minor = 0;//次设备号 static char *module_name = "pin4"; //模块名 static int pin4_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { printk("pin4_readn"); return 0; } //led_open函数 static int pin4_open(struct inode *inode,struct file *file) { printk("pin4_openn");//内核的打印函数和printf类似 return 0; } //led_write函数 static ssize_t pin4_write(struct file *file,const char __user *buf,size_t count, loff_t *ppos) { printk("pin4_writen"); return 0; } static struct file_operations pin4_fops = { .owner = THIS_MODULE, .open= pin4_open, .write = pin4_write, .read= pin4_read, }; int __init pin4_drv_init(void) //真实驱动入口 { int ret; devno = MKDEV(major, minor);//创建设备号 ret = register_chrdev(major, module_name, &pin4_fops);//注册驱动告诉内 核,把这个驱动加入到内核驱动的链表中 pin4_class=class_create(THIS_MODULE, "myfirstdemo");//用代码 在dev自动生成设备 return 0; } void __exit pin4_drv_exit(void) { device_destroy(pin4_class, devno); class_destroy(pin4_class); unregister_chrdev(major, module_name);//卸载驱动 } module_init(pin4_drv_init);//入口,内核加载该驱动(insmod)的时候,这个宏被使>用 module_exit(pin4_drv_exit); MODULE_LICENSE("GPL v2");
将该驱动代码拷贝到linux-rpi-4.14.y/drivers/char目录下文件中(也可选择设备目录下其它文件)
更改该文件夹下Makefile(驱动代码放在那个目录,就更改该目录下的Makefile),将前面的代码编译生成模块,文件内容如右图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是按照config生成的),所以只须要将obj-m+=pin4drive.o添加到Makefile中即可。
回到linux-rpi-4.14.y/编译驱动文件
使用指令:ARCH=armCROSS_COMPILE=arm-linux-gnueabihf-KERNEL=kernel7makemodules进行编译生成驱动模块。
编译生成驱动模块会生成以下几个文件:
.o的文件是object文件,.ko是kernelobject,与.o的区别在于其多了一些sections,例如.modinfo。.modinfosection是由kernelsource里的modpost工具生成的,包括MODULE_AUTHOR,MODULE_DESCRIPTION,MODULE_LICENSE,deviceIDtable以及模块依赖关系等等。depmod工具按照.modinfosection生成modules.dep,modules.*map等文件,便于modprobe更便捷的加载模块。
编译过程中,经历了这样的步骤:先步入Linux内核所在的目录,并编译出pin4drive.o文件,运行MODPOST会生成临时的pin4drive.mod.c文件linux 驱动 开发,而后依据此文件编译出pin4drive.mod.o,然后联接pin4drive.o和pin4drive.mod.o文件得到模块目标文件pin4drive.ko,最后离开Linux内核所在的目录。
将生成的.ko文件发送给猕猴桃派:[email protected]:/home/pi
将pin4test.c(下层调用代码)进行交叉编译后发送给猕猴桃派,就可以看见pi目录下存在发送过来的.ko文件和pin4test这两个文件,
加载内核驱动
sudo insmod pin4drive.ko
加载内核驱动(相当于通过insmod调用了module_init这个宏,之后将整个结构体加载到驱动数组中)加载完成后就可以在dev下边见到名子为pin4的设备驱动(这个和驱动代码上面staticchar*module_name="pin4";//模块名这行代码有关),设备号也和代码上面相关。
lsmod查看系统的驱动模块,执行下层代码,赋于权限
查看内核复印的信息,
dmesg |grep pin4
如右图所示:表示驱动调用成功
在装完驱动后可以使用指令:sudormmod+驱动名(不须要写ko)将驱动卸载。
调用流程:
我们下层空间的open去查找dev下的驱动(文件名),文件名背后包含了驱动的主设备号和次设备号,此时用户open触发一个系统调用linux是什么系统,系统调用经过vfs(虚拟文件系统),vfs按照文件名背后的设备号去调用sys_open去判定,找到内核中驱动数组的驱动位置,再去调用驱动上面自己的dev_open函数
为何生成驱动模块须要在虚拟机上生成?猕猴桃派不行吗?
生成驱动模块须要编译环境(linux源码而且编译,须要下载和系统版本相同的Linux内核源代码),也可以在猕猴桃派里面编译,但在猕猴桃派里编译,效率会很低,要特别久。
以上是底層硬體設備驅動插入鍊錶的順序(驅動)的詳細內容。更多資訊請關注PHP中文網其他相關文章!