上一篇文章學習了如何編寫linux驅動,通過能否正常加載模塊進行驗證是否成功,有做過liunx應用開發的小伙伴都知道驅動會在‘/dev’目錄下以文件的形式展現出來,所以只是能加載驅動模塊不能算是完成驅動的開發,而linux驅動分為三類,現在開始學習字符設備的注冊。
一、准備材料
因為我主要是學習arm開發板的驅動編寫,所以以后的測試中我都是以開發板測試為主,如果有想了解ubuntu下的測試或驅動編寫的小伙伴,請閱讀上一篇文章linux設備驅動編寫入門
開發環境:VMware
操作系統:ubuntu
開發版:湃兔i2S-6UB
庫文件:linux開發板或ubuntu的內核源碼
二、注冊字符設備
經過我的了解注冊字符設備主要有兩種方法,分為指定設備號注冊和自動分配設備號注冊兩種方式。
1.通過指定字符設備號進行注冊
通過指定設備號注冊通常稱為靜態注冊,主要注冊函數有兩個
a.linux2.4版本之前的注冊方式是通過register_chrdev函數
int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
major:主設備號
name:字符設備名稱
fops:file_operations結構體
使用這個函數注冊是會有很大的缺點,因為linux的設備號分為主設備號和次設備號,而這個函數注冊時會將主設備號的次設備號全部進行注冊,所以2.4版本后引入了新的靜態函數進行注冊。
b.register_chrdev_region()函數
int register_chrdev_region(dev_t from, unsigned count, const char *name);
from:注冊的指定起始設備編號,比如:MKDEV(100, 0),表示起始主設備號100, 起始次設備號為0
count:需要連續注冊的次設備編號個數
*name:字符設備名稱
細心的小伙伴會發現注冊的函數中缺少了file_operations結構體,沒錯2.4版本后確實有所改變,具體的注冊方式見后續步驟。
c.設備號獲取
因為靜態注冊是通過指定設備號進行注冊的,那么設備號應該設備為多少才不會和設備已有的沖突了,為此我們可以通過一個命令查看設備已經在使用的主設備號,我們只需要選擇一個沒有使用的即可,命令如下
cat /proc/devices
2.通過自動分配設備號進行注冊
采用動態的方式獲取主設備號,就不需要通過指令查看后在指定具體的設備號,為編寫程序提供了便捷,可以通過alloc_chrdev_region函數獲取設備號
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
*dev: 存放起始設備編號的指針,當注冊成功, *dev就會等於分配到的起始設備編號,可以通過MAJOR()和MINNOR()函數來提取主次設備號
baseminor:次設備號基地址,也就是起始次設備號
count:需要連續注冊的次設備編號個數
*name:字符設備名稱
3.注銷字符設備
因為linux的函數基本是成對存在餓,所以有注冊函數便有注銷函數,以下是注銷函數
int unregister_chrdev(unsigned int major,const char *name)
int register_chrdev_region(dev_t from, unsigned count, const char *name)
從函數名既可以看出,unregister_chrdev注銷函數對應注冊函數是register_chrdev,而register_chrdev_region和alloc_chrdev_region注冊函數都是通過register_chrdev_region函數來注銷的。
4.cdev使用
通過以上介紹的三個字符設備的注冊函數可知、register_chrdev_region和alloc_chrdev_region函數注冊時缺少file_operations這個結構體的參數,而使用這兩個函數進行注冊時需要使用cdev_init和cdev_add函數添加file_operations結構體到系統中,卸載時通過cdev_del將file_operations從系統中卸載。
通過include/linux/cdev.h文件可知cdev結構體的成員,如下所示:
struct cdev {
struct kobject kobj; // 內嵌的kobject對象
struct module *owner; //所屬模塊
const struct file_operations *ops; //操作方法結構體
struct list_head list; //與 cdev 對應的字符設備文件的 inode->i_devices 的鏈表頭
dev_t dev; //起始設備編號,可以通過MAJOR(),MINOR()來提取主次設備號
unsigned int count; //連續注冊次設備號的個數
};
初始化cdev結構體,並將file_operations結構體放入cdev-> ops 中
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
添加cdev結構體到系統中
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
最后在卸載驅動之前別忘記將cdev結構體從系統中移除
void cdev_del(struct cdev *p)
到此注冊字符設備的函數已經介紹完了
三、file_operations結構體
在介紹字符設備時會有一個file_operations結構體的參數,現在開始了接一下file_operations結構體,在include/linux/fs.h文件中我們可以看到結構體的定義,原型如下所示
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 *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
通過file_operations結構體可知,字符設備的實現方法都有哪些,實現方式如下所示
static int test_open(struct inode *inode, struct file *filp)
{
return 0;
}
static int test_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t test_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
return 0;
}
static ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
return 0;
}
/*
*字符設備操作集合
*/
static const struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = test_open,
.release = test_release,
.read = test_read,
.write = test_write,
};
現在字符設備注冊已經完成了,結果上一節設備驅動的源碼進行實現。
四、字符設備注冊源碼
1.使用register_chrdev方式注冊的源碼如下所示
hell_demo1.c文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#define HELLO1_NAME "hello1"
#define HELLO1_MAJOR 300
static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"hello This is the kernel data"};
static int hello1_open(struct inode *inode, struct file *filp)
{
return 0;
}
static int hello1_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t hello1_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
int ret = 0;
memcpy(readbuf, kerneldata, sizeof(kerneldata));
ret = copy_to_user(buf, readbuf, count);
if(ret == 0) {
} else {
}
return 0;
}
static ssize_t hello1_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
int ret = 0;
ret = copy_from_user(writebuf, buf, count);
if(ret == 0) {
printk("kernel recevdata:%s\r\n", writebuf);
} else {
}
return 0;
}
/*
*字符設備操作集合
*/
static const struct file_operations hello1_fops = {
.owner = THIS_MODULE,
.open = hello1_open,
.release = hello1_release,
.read = hello1_read,
.write = hello1_write,
};
static int __init hello1_init(void)
{
int ret = 0;
printk("hello1_init\r\n");
/*注冊字符設備*/
register_chrdev(HELLO1_MAJOR, HELLO1_NAME, &hello1_fops);
if(ret < 0) {
printk("hell01 init failed!\r\n");
} else {
printk("hello1 init ok");
}
return 0;
}
static void __exit hello1_exit(void)
{
printk("hello1_exit\r\n");
/*注銷字符設備*/
unregister_chrdev(HELLO1_MAJOR, HELLO1_NAME);
}
/*
*
*模塊入口與出口函數
*
*/
module_init(hello1_init);
module_exit(hello1_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiaozhu");
2.使用register_chrdev_region和alloc_chrdev_region方式注冊的源碼如下所示
hell_demo2.c文件
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#define HELLO2_NAME "hello2"
#define HELLO2_COUNT 1
/*設備結構體*/
struct hello2_dev{
struct cdev cdev; /*字符設備*/
dev_t devid; /*設備號*/
int major; /*主設備號*/
int minor; /*次設備號*/
};
struct hello2_dev hello2;
static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"hello This is the kernel data"};
static int hello2_open(struct inode *inode, struct file *filp)
{
return 0;
}
static int hello2_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t hello2_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos)
{
int ret = 0;
memcpy(readbuf, kerneldata, sizeof(kerneldata));
ret = copy_to_user(buf, readbuf, count);
if(ret == 0) {
} else {
}
return 0;
}
static ssize_t hello2_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
int ret = 0;
ret = copy_from_user(writebuf, buf, count);
if(ret == 0) {
printk("kernel recevdata:%s\r\n", writebuf);
} else {
}
return 0;
}
/*
*字符設備操作集合
*/
static const struct file_operations hello2_fops = {
.owner = THIS_MODULE,
.open = hello2_open,
.release = hello2_release,
.read = hello2_read,
.write = hello2_write,
};
static int __init hello2_init(void)
{
int ret = 0;
printk("hello2_init\r\n");
/*設置設備號*/
if(hello2.major){
hello2.devid = MKDEV(hello2.major, 0);
ret = register_chrdev_region(hello2.devid, HELLO2_COUNT, HELLO2_NAME);
} else {
ret = alloc_chrdev_region(&hello2.devid, 0, HELLO2_COUNT, HELLO2_NAME);
hello2.major = MAJOR(hello2.devid);
hello2.minor = MINOR(hello2.devid);
}
if(ret < 0) {
printk("hello2 chrdev_region err!\r\n");
return -1;
}
printk("hello2 major = %d, minor = %d\r\n",hello2.major, hello2.minor);
/*注冊字符設備*/
hello2.cdev.owner = hello2_fops.owner;
cdev_init(&hello2.cdev, &hello2_fops);
ret = cdev_add(&hello2.cdev, hello2.devid, HELLO2_COUNT);
return 0;
}
static void __exit hello2_exit(void)
{
printk("hello2_exit\r\n");
/*刪除字符設備*/
cdev_del(&hello2.cdev);
/*注銷字符設備*/
unregister_chrdev_region(hello2.devid, HELLO2_COUNT);
}
/*
*
*模塊入口與出口函數
*
*/
module_init(hello2_init);
module_exit(hello2_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiaozhu");
3.不論使用哪種方式Makefile文件的內容基本一樣,只需要更改一下‘obj-m’對應的驅動文件即可,hell_demo2項目的工程如下所示
Makefile文件
KERNELDIR := /home/xfg/linux/imx_6ull/i2x_6ub/system_file/i2SOM-iMX-Linux
CURRENT_PATH := $(shell pwd)
obj-m := hello_demo2.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
為了方便測試,在內核中使用了copy_to_user和copy_from_user函數,因為內核態和用戶態之間的空間不能直接訪問,所以需要使用這兩個函數進行數據的拷貝。
內核空間-->用戶空間
unsigned long copy_to_user(void *to, const void *from, unsigned long n)
to:目標地址(用戶空間)
from:源地址(內核空間)
n:將要拷貝數據的字節數
用戶空間-->內核空間
返回:成功返回0,失敗返回沒有拷貝成功的數據字節數
unsigned long copy_from_user(void *to, const void *from, unsigned long n)
to:目標地址(內核空間)
from:源地址(用戶空間)
n:將要拷貝數據的字節數
返回:成功返回0,失敗返回沒有拷貝成功的數據字節數
五、測試
將編譯好的.ko文件拷貝到arm開發版的/lib/modules/4.1.43+目錄下
make
sudo cp hello_demo2.ko /home/rootfs/lib/modules/4.1.43+ -f
啟動開發板加載驅動模塊
lsmod
modprobe hello_demo2
lsmod
加載結果如下圖所示:
成功加載驅動后,使用cat /proc/devices
命令查看設備號並創建設備屬性節點
cat /proc/devices
mknod /dev/hello2 c 248 0
創建設備屬性節點后會在/dev目錄下存在hello2的文件,如下圖所示
到此我們的驅動已經編寫完成,在下一篇文章中將會編寫一個程序對驅動進行驗證,有需要的小伙伴下一篇文章見。
六、參考文獻
使用register_chrdev_region()系列來注冊字符設備:https://www.cnblogs.com/lifexy/p/7827559.html
linux驅動開發--copy_to_user 、copy_from_user函數實現內核空間數據與用戶空間數據的相互訪問https://blog.csdn.net/xiaodingqq/article/details/80150347