Linux内核模块编程-字符设备驱动

设备驱动简介

设备被大概的分为两类: 字符设备和块设备。

  • 字符设备
    提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,键盘、串口、调制解调器都是典型的字符设备。

  • 块设备
    应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘、软盘、CD-ROM驱动器和闪存都是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

这两种类型的设备的根本区别在于它们是否可以被随机访问。字符设备只能顺序读取,块设备可以随机读取。其它的一些区别:

  • 字符设备只能以字节为最小单位访问,而块设备以块为单位访问,例如512字节,1024字节等
  • 块设备可以随机访问,但是字符设备不可以
  • 字符和块没有访问量大小的限制,块也可以以字节为单位来访问

对于设备来说,linux抽象为一个个文件,放在/dev目录下,每个文件都有一定的属性其中比较重要的有主设备号和次设备号两个。主设备号标明了设备的类型,次设备号表示同类型设备的不同设备

[root@localhost blog]# ls -al /dev/sda
brw-rw----. 1 root disk 8, 0 Apr 22 22:49 /dev/sda
8是主设备号表示这是一个磁盘,0是次设备号区别相同类型设备的不同设备

开始编写字符设备驱动

注册和撤销设备

通过注册设备驱动模块,可以将一组操作设备的函数和某一设备进行关联起来。
注册一个设备:

linux/fs.h 中的函数 register_chrdev 
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
其中major是主设备号,name是设备的名字,fops则是该设备对应的一些列操作函数,成功返回主设备号
major如果为0的话,系统会给我们随机分配主设备号

问题是主设备号怎么去定义,难道可以自己随便填吗?如果大家都这样,那么设备好像就乱了要申请一个没有使用的设备号可以去查看文档:Documentation/devices.txt
撤销一个设备:

int unregister_chrdev(unsigned int major, const char *name);

在linux中把对设备的操作,抽象成对文件的操作,写设备驱动程序,其实本质是去填充file_operations这个结构体

设备操作

实现一个最基本的设备驱动程序的话,那么至少应该实现下面四个操作设备的函数打开一个设备,向设备写入数据,从设备读取数据,关闭设备等

static struct file_operations fops = {
    .read = device_read,
    .write = device_write,
    .open = device_open,
    .release = device_release
};

打开设备的实现

static int Device_Open = 0; //表明设备状态,是打开还是关闭
所有打开设备的函数原型都是这样,一个设备的inode,还有一个与设备文件对应的file结构体
static int device_open(struct inode *inode,struct file *file)
{
    static int counter = 0;  //记录设备打开的次数
    if(Device_Open) 
        return -EBUSY;
    Device_Open++; 
    sprintf(msg,"I already told you %d times Hello world\n",counter++);
    msg_Ptr = msg;
    try_module_get(THIS_MODULE);  //增加内核模块的引用的计数
    return SUCCESS;
}

释放设备的实现

//释放设备
static int device_release(struct inode *inode,struct file *file)
{
    Device_Open--;   //更改设备打开状态
    module_put(THIS_MODULE);  //减少内核模块的引用计数
    return 0;
}

读取设备的实现

内核空间和用户空间的数据拷贝使用copy_to_user和copy_from_user两个函数实现 
//读文件,把内核空间的数据拷贝到buffer上
static ssize_t device_read(struct file *filp,char *buffer,size_t length,loff_t *offset)
{
    if(*msg_Ptr == 0) 
        return 0;
    return copy_to_user(buffer,msg_Ptr,length); //把msg_Ptr指向的内容拷贝到buffer中
}

写入设备的实现

//写文件,这里占时还没有去实现这个功能
static ssize_t device_write(struct file *filp,const char *buff,size_t len,loff_t *off)
{
    printk("<1> Sorry this operation isn't supported\n");
    return -EINVAL;
}

到此为止,写一个设备驱动所具备的的要素都完成了。那么最后是写成一个内核模块的形式

内核模块的初始化和注销

#define SUCCESS 0
#define DEVICE_NAME "chardev"
#define BUF_LEN 80

static int Major;
static int Device_Open = 0;
static char msg[BUF_LEN];
static char *msg_Ptr;

//内核模块初始化
int init_module(void)
{
    //注册设备驱动,随机产生主设备号
    Major = register_chrdev(0,DEVICE_NAME,&fops);
    if(Major < 0) {
        printk("Registering the character device failed with %d\n",Major);
    return Major;
    }
    printk("<1> I was assigned major number %d ",Major);
    printk("<1> the drive,create a dev file");
    printk("<1> mknod /dev/hello c %d 0.\n",Major);
    printk("<1> I was assigned major number %d ",Major);
    printk("<1> the device file\n");
    printk("<1> Remove the file device and module when done\n");
    return 0;
}
void cleanup_module(void)
{
    unregister_chrdev(Major,DEVICE_NAME);
}

到此为止一个简单的设备驱动就完成了,剩下的时间是要测试了,

内核模块测试

插入内核模块,使用dmesg查看系统随机分配的主设备号
insmod chardev.ko

dmesg查看内核输出日志
 I was assigned major number 247 
 the drive,create a dev file
 mknod /dev/hello c 247 0.
 I was assigned major number 247 
 the device file
 Remove the file device and module when done

根据输出的主设备号,创建设备
mknod /dev/hello c 247 0

最后你就可以使用用户态程序去open这个设备和read/write这个设备了。

用户态程序进行测试

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
int main()
{
    char buf[4096] = {0};
    int fd = open("/dev/hello",O_RDWR);
    int ret = read(fd,buf,sizeof(buf));
    buf[ret] = '\0';
    printf("%s\n",buf);
}
输入结果:
I already told you 1 times Hello world

你可能感兴趣的:(编程,linux,kernel,驱动,字符设备)