liunx驅動之字元裝置的註冊

澆築菜鳥發表於2021-07-09

上一篇文章學習瞭如何編寫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

相關文章