第一个Linux驱动程序(一)——aMsg的open()、read()和write()(驱动程序的基本结构)

从大二开始就一直想学Linux驱动开发,可是当时自己基础薄弱,不仅不太懂Linux,而且也没有什么实际的嵌入式硬件可以让我练手,为此我卧薪尝胆,依次学习了8051单片机、Arduino、STM32等MCU,又接触了uCOS、FreeRTOS等嵌入式操作系统,还学习了I2C、SPI、UART等外设常用的通信协议,更是接触了各种各样的传感器。现在终于有了足够的硬件基础以进入Linux驱动程序的开发了,充满了期待。一旦能够把各种嵌入式外设加入到Linux系统中,那么我的嵌入式也算成功入门了~

然而,这个第一个驱动程序Demo与硬件没有任何关系,我用它来熟悉Linux驱动程序的基本结构。这个驱动叫做aMsg,意思就是a message。它的功能非常简单——应用程序可以写入一个字符串,aMsg持有最近一次写入它的一个字符串,供应用程序读取。

每一个Linux设备驱动都需要有一个主设备号,不同的设备驱动就是通过主设备号区分的(虽然最新的Linux内核不要求每个驱动的主设备号不同,但是习惯上还是尽量不重复)。主设备号的分配有两种,一种是静态分配,另一种是动态分配。静态分配就是开发者自己指定一个数字作为主设备号,动态分配就是由系统分配一个空闲的主设备号。虽然静态分配有诸多弊端,但是其简单性很适合入门~所以aMsg的主设备号是我随便指定的常数——我的学号224。

关于读写策略,可以有很多种,为了演示各种读写策略,我将编写多个版本。

=================阶段一:全局aMsg================

这个版本的aMsg中保存的字符串是全局的,也就是所有应用程序都会访问同一个缓冲区。为了简单起见,存放字符串的缓冲区是静态分配的,最大容量是1024字节。

aMsg只需要实现2个对外方法:read和write。

所以代码就是这样的:

aMsg.c:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

//主设备号
#define AMSG_MAJOR 224
//缓冲区大小
#define AMSG_MAX_BUF_SZ 1024

//当前字符串长度
static int g_length=0;
//缓冲区
static char g_buffer[AMSG_MAX_BUF_SZ];

//read操作
static ssize_t amsg_read(struct file* p_file,char* p_buf,size_t p_count,loff_t* p_offset)
{
    //最多能够读取的字节数(p_count和g_length之间较小者)
    int t_size=(p_count<g_length?p_count:g_length);
    //没有成功拷贝的字节数
    int t_rest=copy_to_user(p_buf,g_buffer,t_size);
    //不管结果如何,都清空缓冲区
    g_length=0;
    //返回成功拷贝的字节数
    return t_size-t_rest;
}

//write操作
static ssize_t amsg_write(struct file* p_file,const char* p_buf,size_t p_count,loff_t* p_offset)
{
    //最多能够写入的字节数(p_count和sizeof(g_buffer)之间较小者)
    int t_size=(p_count<sizeof(g_buffer)?p_count:sizeof(g_buffer));
    //没有成功拷贝的字节数
    int t_rest=copy_from_user(g_buffer,p_buf,t_size);
    //成功拷贝的字节数,也就是字符串的长度
    g_length=t_size-t_rest;
    //返回成功拷贝的字节数
    return g_length;
}

//填充file_operations结构体
static struct file_operations amsg_fops=
{
    .owner=THIS_MODULE,
    .read=amsg_read,
    .write=amsg_write
};

//模块初始化代码
static int amsg_init_module(void)
{
    //注册字符设备(这是Old way)
    int t_ret=register_chrdev(AMSG_MAJOR,"aMsg",&amsg_fops);
    //注册失败的处理
    if(t_ret<0)
    {
        printk("Unable to register\n");
        return t_ret;
    }
    return 0;
}

//模块清理代码
static void amsg_cleanup_module(void)
{
    //注销字符设备
    unregister_chrdev(AMSG_MAJOR,"aMsg");
    printk("clean up!");
}

module_init(amsg_init_module);
module_exit(amsg_cleanup_module);

从代码中可以看出,模块使用了register_chrdev()函数来注册字符设备。该方式是一种Old way,虽然Linux支持,但可能将来会丢弃该函数。不过这种方法确实方便、容易理解,所以这里先将就着用。

read()的实现可以看出,不管读取多少字节,都会清空缓冲区。

编译代码用的Makefile(注意,一定得是“Makefile”,不能是“makefile”或者其他)内容如下:

obj-m := aMsg.o
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
	make -C $(KERNEL_DIR) SUBDIRS=$(PWD) modules
clean:
	rm *.o *.ko *.mod.c
.PHONY:clean

使用命令make之后,目录下产生aMsg.ko。接下来,以root身份执行

insmod aMsg.ko
mknod /dev/aMsg c 224 0

就能够加载驱动程序,并在/dev目录下创建aMsg设备文件。mknod命令中的224就是主设备号。

接着如图测试,可以证实:1、/dev/aMsg中的内容读了就没了;2、/dev/aMsg中的内容会被最新的内容覆盖。这两点都与代码逻辑一致。

接下来我们可以做这么一个实验:

rm /dev/aMsg
mknod /dev/aMsg0 c 224 0
mknod /dev/aMsg1 c 224 1

可以看到,/dev/aMsg0和/dev/aMsg1访问的是同一个缓冲区,而不会根据次设备号的不同而加以区分。所以,这里的“全局”就是“驱动程序相关的”。

===================阶段二:设备相关aMsg===============

之前那个aMsg中保存的字符串是全局的,而这个版本的aMsg中的字符串则是与特定的设备相关的。换句话说,当创建了两个aMsg的设备文件(比如一个aMsg0,次设备号是0,一个是aMsg1,次设备号是1),那么aMsg0和aMsg1的缓冲区是相互独立的,但所有访问aMsg0的应用程序都是访问的同一个缓冲区0,所有访问aMsg1的应用程序也都是访问同一个缓冲区1。这样更加符合实际情况。

从上面的代码中可以看出,要实现不同的读写策略,需要能够区分不同的调用来源,找到对应的数据进行操作。观察read()、write()的函数原型,

ssize_t read(struct file* p_file,char* p_buf,size_t p_count,loff_t* p_offset);
ssize_t write(struct file* p_file,const char* p_buf,size_t p_count,loff_t* p_offset);

要想区分不同的调用来源,只能从struct file结构体下手,好在Linux的struct file结构体考虑到了这个问题,为我们预留了一个private_data字段。它是一个指针,用来存放我们自定义的数据。《Linux device driver》中的介绍如下:

这个例子中,private_data应该指向一个由缓冲区和长度构成的结构体,以表示待操作的字符串缓冲区。而private_data在何时初始化呢?答案就是open()。open()的原型为:

int open(struct inode* p_inode,struct file* p_file);

其中struct inode中包含了很多与文件有关的信息。在驱动开发中,通常只需要使用两个字段,分别是

dev_t i_rdev;
struct cdev *i_cdev;

其中dev_t就是一个由主设备号与次设备号构成的32位整数,而cdev结构体中也包好了一个dev_t的字段。但是呢,inode结构体比较多变,可以见《Linux device driver》中的描述:

所以,我们就使用

unsigned int imajor(struct inode *inode);
unsigned int iminor(struct inode *inode);

来获取主次设备号。

代码如下:

aMsg.c:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

MODULE_LICENSE("Dual BSD/GPL");

//主设备号
#define AMSG_MAJOR 224
//缓冲区大小
#define AMSG_MAX_BUF_SZ 1024
//最大分配记录数
#define AMSG_MAX_RECORDS 16

//消息结构体
struct message
{
    //当前字符串长度
    int length;
    //缓冲区
    char buffer[AMSG_MAX_BUF_SZ];
};

//分配记录
struct alloc_record
{
    //次设备号
    int minor;
    //消息
    struct message* msg;
};

//所有已分配的记录缓冲区
static struct alloc_record g_records[AMSG_MAX_RECORDS];
//所有已分配记录的个数
static int g_record_count=0;

//open操作
static int amsg_open(struct inode* p_inode,struct file* p_file)
{
    //获取次设备号
    int t_minor=iminor(p_inode);
    //对应的分配记录
    struct alloc_record* t_record=NULL;
    //遍历所有已有的分配记录
    int t_i;
    for(t_i=0;t_i<g_record_count;t_i++)
    {
        //之前存在该次设备号的分配记录
        if(g_records[t_i].minor==t_minor)
        {
            t_record=&g_records[t_i];
            break;
        }
    }
    //如果不存在该次设备号的分配记录,则需要分配
    if(t_record==NULL)
    {
        //如果分配条数已经达到上限,则返回错误
        if(g_record_count>=AMSG_MAX_RECORDS)
            return ENOSPC;
        //分配一条记录
        t_record=&g_records[g_record_count];
        g_record_count++;
        //初始化记录的次设备号
        t_record->minor=t_minor;
        //初始化记录的消息
        t_record->msg=kmalloc(sizeof(struct message),GFP_KERNEL);
        t_record->msg->length=0;
    }
    //把private_data指向对应的消息
    p_file->private_data=t_record->msg;
    return 0;
}

//read操作
static ssize_t amsg_read(struct file* p_file,char* p_buf,size_t p_count,loff_t* p_offset)
{
    //获取对应的消息结构体
    struct message* t_msg=p_file->private_data;
    //最多能够读取的字节数(p_count和t_msg->length之间较小者)
    int t_size=(p_count<t_msg->length?p_count:t_msg->length);
    //没有成功拷贝的字节数
    int t_rest=copy_to_user(p_buf,t_msg->buffer,t_size);
    //不管结果如何,都清空缓冲区
    t_msg->length=0;
    //返回成功拷贝的字节数
    return t_size-t_rest;
}

//write操作
static ssize_t amsg_write(struct file* p_file,const char* p_buf,size_t p_count,loff_t* p_offset)
{
    //获取对应的消息结构体
    struct message* t_msg=p_file->private_data;
    //最多能够写入的字节数(p_count和sizeof(g_buffer)之间较小者)
    int t_size=(p_count<AMSG_MAX_BUF_SZ?p_count:AMSG_MAX_BUF_SZ);
    //没有成功拷贝的字节数
    int t_rest=copy_from_user(t_msg->buffer,p_buf,t_size);
    //成功拷贝的字节数,也就是字符串的长度
    t_msg->length=t_size-t_rest;
    //返回成功拷贝的字节数
    return t_msg->length;
}

//填充file_operations结构体
static struct file_operations amsg_fops=
{
    .owner=THIS_MODULE,
    .open=amsg_open,
    .read=amsg_read,
    .write=amsg_write
};

//模块初始化代码
static int amsg_init_module(void)
{
    //注册字符设备(这是Old way)
    int t_ret=register_chrdev(AMSG_MAJOR,"aMsg",&amsg_fops);
    //注册失败的处理
    if(t_ret<0)
    {
        printk("Unable to register\n");
        return t_ret;
    }
    return 0;
}

//模块清理代码
static void amsg_cleanup_module(void)
{
    //注销字符设备(这是Old way)
    unregister_chrdev(AMSG_MAJOR,"aMsg");
    printk("clean up!");
}

module_init(amsg_init_module);
module_exit(amsg_cleanup_module);

make之后产生aMsg.ko,以root身份执行:

rmmod aMsg
insmod aMsg.ko
mknod /dev/aMsg0 c 224 0
mknod /dev/aMsg1 c 224 1

以重新加载驱动。

然后如图进行测试:

可见,aMsg0和aMsg1相互独立,互不影响。

至此已经基本了解了不同读写策略需要怎么实现了。我还将不断学习更多的关于驱动程序的知识~