歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

Linux 字符驅動開發心得

Linux字符驅動框架相比初學還是比較難記的,在學了一陣子字符驅動的開發後對於框架的搭建總結出了幾個字 。

對於框架來講主要要完成兩步。

申請設備號,注冊字符驅動

其關鍵代碼就兩句


int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);//動態申請設備號
 
int cdev_add(struct cdev *, dev_t, unsigned);                      //注冊字符驅動

執行完次就可以將我們的驅動程序加載到內核裡了

首先我們搭建主程序,字符驅動的名字就叫做"main"

首先先寫下將要用到的頭文件,以及一個宏定義,指明了我們驅動的名稱,當然名稱可以任意這裡就取"main" 作為名字 

#include <linux/fs.h>
#include <linux/module.h>
#include <linux/coda.h>
#include <linux/slab.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define MUDULE_NAME "main"

驅動由於需要加載到內核裡,所以我們需要聲明一下我們驅動所遵循的協議,如果沒有申明,那麼加載內核的時候系統會提示一段信息。我們按照內核的風格來,就加一個GPL協議吧

MODULE_LICENSE("GPL");

我們要想將我們的驅動注冊到內核裡,就必須將我們的驅動本身作為一個抽象,抽象成一個struct cdev的結構體。因為我們系統內部有許多中字符驅動,為了將這些不同種類的驅動都能使用同一個函數進行注冊,內核聲明了一個結構體,不同的驅動通過這個結構體--變成了一個抽象的驅動供系統調用。這段有點羅嗦,我們來看一下cdev這個結構體吧。

//這段不屬於主程序
struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;
    struct list_head list;
    dev_t dev;
    unsigned int count;
};

這個結構體就包含了一個驅動所應有的東西其中 kobj 不需要管它,我也沒有仔細研究,owner指向模塊的所有者,常常使用THIS_MODULE這個宏來賦值,ops是我們主要做的工作,其中定義了各種操作的接口。

下面我們定義了我們程序的抽象體mydev,以及他所需要的接口

struct cdev mydev;
struct file_operations ops;

struct file_operations這個結構有點龐大。

//不屬於本程序
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    int (*readdir) (struct file *, void *, filldir_t);
    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 (*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 **);
    long (*fallocate)(struct file *file, int mode, loff_t offset,
              loff_t len);
};

上面看到,這個結構內部都是一些函數指針,相當與這個結構本身就是一個接口,在c語言中沒有接口這個概念,使用這種方式來定義也是一種巧妙的用法。不過有所不同的是我們可以不完全實現其中的接口。

應用程序在使用驅動的時候常常需要open,write,read,close這幾種操作,也就對應了file_operations結構中的open,write,read,release這幾個函數指針。下面我們開始實現我們自己的函數體。注意:我們自己實現的函數必須滿足接口函數所定義的形式。

static int main_open(struct inode* inode,struct file* filp)
{
    return 0;
}

這個教程裡面的程序,我們就讓驅動只能往裡面寫一個字符為例,讀取也是只能讀取一個字符。

 我們定義一個靜態的字符類型的變量來當作我們的存儲空間,通過copy_from_user來將用戶空間的數據拷貝到我們驅動  所在的內核空間。原型是: 

static inline long copy_from_user(void *to, const void __user * from, unsigned long n)

 類似地,我們使用copy_to_user來完成內核空間到用戶空間的數據拷貝。

static inline long copy_to_user(void __user *to,const void *from, unsigned long n)

static char main_buffer;
static ssize_t main_write(struct file* filp,const char __user* buffer,size_t length,loff_t * l)
{
    if(length!=1)  return -1;
    copy_from_user(&main_buffer,buffer,length);
    return 1;
}

下面是讀的實現

static ssize_t main_read(struct file* filp,char __user * buffer,size_t length,loff_t*l)
{
    if(length!=1) return -1;
    copy_to_user(buffer,&main_buffer,length);
    return 1;
}

再稍稍實現一下close

static int main_close(struct inode* inode,struct file* filp)
{
    return 0;

我們所需要的內容都已經填寫完畢,我們在驅動初始化的時候調用cdev_add驅動注冊到系統就行了,不過在注冊之前我們要申請設備號。

static dev_t dev;
static int __init main_init(void)

首先我們使用動態申請的方式申請設備號

int result=alloc_chrdev_region(&dev,0,1,MODULE_NAME);

dev就是我們申請的設備號,其中dev_t其實就是一個無符號的long型,通過調用alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *) 將申請到的設備號寫入到dev中,第二個參數是子設備號從幾開始,第三個參數是申請幾個設備號,因為申請多個設備號是連續的,所以我們只需要知道第一個就行了。第四個參數代表我們驅動的名稱。

返回值如果小於0則表示我們申請失敗,通過printk打印錯誤信息。我測試的在動態加載的時候printk都不能打印其信息,如果在Ubuntu下可以查看/var/log/kern.log,如果是CentOS下可以查看/var/log/mssages來查看printk打印的信息,一般查看後10條就能足夠了。

        if(result<0)
    {
        printk(KERN_ALERT"device load error");
        return -1;
    }
 

然後我們再構造一下我們的接口結構體

    ops.owner=THIS_MODULE;
    ops.open=main_open;
    ops.release=main_close;
    ops.write=main_write;
    ops.read=main_read;

構造完之後,我們就只剩下我們最重要的一步了,就是向系統注冊我們的驅動。

不過,先別急,我們注冊前得先把我們的抽象驅動mydev給構造了,mydev的定義在最上面。

cdev_init(&mydev,&ops);
mydev.owner=THIS_MODULE;

這樣,我們就使用了我們的ops構造了我們的抽象驅動,當我們把這個驅動添加到我們的內核裡面的時候,假如內核想對這個驅動進行寫的操作,就會從mydev->ops->main_write這樣找到我們自己實現的寫函數了。

接下來就注冊我們的驅動。

        cdev_add(&mydev,dev,1);
        printk(KERN_ALERT"device load success\n");
        return 0;
}

至此,我們的驅動就算完成了,不過有一點,就是我們的驅動有了初始化函數了就一定還有一個清理的函數了,該函數主要在我們卸載驅動的時候會調用,module_init及module_exit主要用於聲明這個驅動的入口與出口,是必須要做的一步。

static void __exit main_exit(void)
{
        unregister_chrdev_region(dev,1);
    cdev_del(&mydev);
}
module_init(main_init);
module_exit(main_exit);
 


我的Makefile是下面這樣

ifeq ($(KERNELRELEASE),)
KERNELDIR?=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules -Wall
modules_install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install -Wall
clean:
    rm -rf *.0 *~ core .depend .*.cmd
    sudo rmmod main
    sudo rm /dev/main
install:
    sudo insmod main.ko
    sudo mknod /dev/main c 250 0
message:
    tail -n 5 /var/log/kern.log
.PHONY: modules modules_install clean
else
    obj-m :=main.o
endif

因為我的機器是使用ubuntu,所以在message標簽下是tail -n 5 /var/log/kern.log 如果的/var/log目錄下沒有kern.log,那麼就替換成/var/log/messages

編譯

$make

因為這個Makefile的install 都是針對我自己電腦而寫的,所以並不能在你電腦上保證執行make install 的正確性。還是在命令行中敲吧

sudo insmod main.ko

安裝模塊,然後查看/proc/devices裡面main這個模塊對應的設備號是多少,我的是250,所以創建設備節點時這樣創建

sudo mknod /dev/main c 250 0

這樣就成功把我們的驅動安裝到內核裡了

執行make message可以看到如下信息

tail -n 5 /var/log/kern.log
Sep 17 20:05:57 quanweiC kernel: [23536.688371] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=2558 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43
Sep 17 20:06:02 quanweiC kernel: [23541.691748] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=3335 DF PROTO=UDP SPT=11818 DPT=26724 LEN=43
Sep 17 20:06:51 quanweiC kernel: [23590.610275] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=10708 PROTO=UDP SPT=11818 DPT=10948 LEN=43
Sep 17 20:07:04 quanweiC kernel: [23603.815562] [UFW BLOCK] IN=wlan0 OUT= MAC=c0:18:85:73:a1:8e:b8:88:e3:eb:30:c3:08:00 SRC=192.168.1.103 DST=192.168.1.105 LEN=63 TOS=0x00 PREC=0x00 TTL=64 ID=12523 PROTO=UDP SPT=11818 DPT=10104 LEN=43
Sep 17 20:07:04 quanweiC kernel: [23603.930248] device load success

最後一行顯示我們加載驅動成功了。這樣一個簡單的字符驅動就寫成功了。

Linux字符驅動中動態分配設備號與動態生成設備節點 http://www.linuxidc.com/Linux/2014-03/97438.htm

字符驅動設計----mini2440 LED驅動設計之路 http://www.linuxidc.com/Linux/2012-08/68706.htm

Linux 設備驅動 ====> 字符驅動 http://www.linuxidc.com/Linux/2012-03/57581.htm

如何編寫Ubuntu字符驅動 http://www.linuxidc.com/Linux/2010-05/25887.htm

2.4下內核linux字符驅動模板 http://www.linuxidc.com/Linux/2007-06/5338.htm

Copyright © Linux教程網 All Rights Reserved