如何写一个Linux设备驱动程序

原文为 Writing device driver in Linux:A brief tutorial.

该文重点给出了三个实例来讲解Linux驱动,使新手快速、从代码层了解什么是Linux设备驱动。

本文算是笔记,大体上是翻译该文的前两部分,即前两个实例,这两个例子都可能正确成功运行。

| | 文件: | Writing device drivers in Linux.pdf |
| :-----: | ----------------------------------------------------------------- |
| 大小: | 216KB |
| 下载: | 下载 |

所需知识

C 语言编程

微处理器编程.对处理器的工作原理有一定的了解,如内存管理、中断等

用户空间和内核空间

==================

写设备驱动时,了解“用户空间”和“内核空间”之间的区别是非常重要的。

内核空间。Linux内核简单并高效地管理着机器的硬件,为用户提供简单并

规范的编程接口。同样地,内核,特别是内核中的驱动,是用户/程序员与硬

件之间的桥梁或接口。内核的任何例程或函数(比如模块、驱动)都属于内核

空间。用户空间。用户程序,比如unix shell或其他的gui应用程序(比如

kpresenter),都属于用户空间。显然,这些应用程序都要与硬件打交道。但是

它们不并直接操作硬件,而是通过内核提供的函数来实现。

用户空间与内核空间之间的接口函数

内核为用户空间提供了一系列的例程或函数,用户的应用程序利用这些接口来与硬件交互

。通常,在UNIX或Linux系统中,这种对话是通过函数或子程序来读写文件的。原因是从

用户的角度来看,UNIX设备就是文件。

另一方面,在内核空间中Linux也提供了一系列的函数或子程序来完成底层与硬件的交互

,并允许从内核向用户空间传递信息。

通常,每个用户空间的(设备或文件允许使用的)函数,都能在内核空间中找到一个类似

的函数,(允许信息从内核传递给用户空间,反之亦然)

内核空间与硬件设备之间的接口函数

内核空间中有许多函数用于控制硬件或在内核与硬件之间交互信息。

第一个驱动:在用户空间加载和移除驱动

现在将展示如何完成第一个驱动,在内核中将看作模块

新建一个文件nothing.c如下

#include

MODULE_LICENSE("Dual BSD/GPL");

2.6.x版本后的内核,编译模块会略微复杂一点。首先,需要有一份完整的、编译过的内

核源码树。在下面的文字中,将假设使用2。6。8版本的内核。

其次,需要一个makefile文件,本例中的makefile文件名为Makefile,内容如下:

obj-m := nothing.o

与之前版本的内核不同,现在编译模块时使用的内核需要与模块将要加载的内核相同。

编译上面的文件,可以使用命令:

make -C /usr/src/kernel-source-2.6.8 M=pwd modules

这个极其简单的模块就属于内核空间,一旦其被加载,它就是内核空间的一部分。

在用户空间,可以使用下面的命令加载它,需要root权限:

insmod nothing.ko

insmod 这个命令用于为内核加载模块。尽管现在我们已经加载了nothing.ko这个模块,

但是这个模块毕竟没有任何用处。

可以通过查看系统里已加载的模块来检查是否已经成功加载了nothing.ko

lsmod

最后,需要卸载该模块时使用下面的命令:

rmmod nothing

重新使用lsmod,可以发现nothing模块已经不在了。

"Hello world"驱动:在内核空间加载和移除驱动

当一个模块设备驱动加载到内核,将执行一些初始的工作,如重新设置设备,reserving

RAM, reserving interrupts, reserving input/output ports, etc.

这些工作得以在内核空间执行,必须要有两个函数存在:module_init 和

module_exit;它们对应于用户空间的insmod和rmmod命令。总之,用户命令insmod和

rmmod使用了内核空间的函数module_init和module_exit.

来看一个经典的程序 HELLO WORLD:

//hello.c
#include
#include
#inlucde

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
        printk("<1> Hello world!\n");
        return 0;
}

static void hello_exit(void)
{
        printk("<1> Bye, cruel world!\n");
}

module_init(hello_init);
module_exit(hello_exit);

其中hello_init 和 hello_exit 函数可以取任意名,但为了加载和移除功能是更容易识

别,它们作为参数传递给函数module_init 和 module_exit.

printk函数与printf函数非常类似,但printk只工作在内核中。<1>表示打印信息

为最高优先级(数字越低,优先级越高)。这样,不仅可以在内核系统日志中看到该

打印信息,还能在系统控制台接收到该打印信息。

可以用之前的命令来编译这个模块,此时只需要将模块名加入到Makefile文件中即可:

obj-m := nothing.o hello.o

本文的其他部分,将Makefile作为给读者的练习。一个完整的Makefile文件可以编译

本教程中的所有示例模块。

当模块被加载或卸载时,通过printk打印的信息将会出现在系统控制台。如果打印

信息没有出现在终端里,则可通过dmesg命令或查看系统日志文件(cat

var/log/syslog)看到打印信息。

一个完整的驱动“memory“:此驱动的初始部分

接着将介绍如何构建一个完整的设备驱动:memory.c。可以从该设备中读取一个字符,

也可向其写入一个字符。这个设备并没有实际意义,只是因为它是一个完整的

驱动程序,遂将其作为一个实例来说明。它很容易实现,因为它并不是一个正真的

硬件设备的接口(除了电脑本身)。

这个驱动中,要添加几个在设备驱动程序中频繁出现的头文件

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

int memory_open(struct inode *inode, struct file *filp);
int memory_release(struct inode *inode, struct file *filp);
ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t
        *f_pos);
ssize_t memory_write(struct file *filp, char *buf, size_t count, loff_t
        *f_pos);
void memory_exit(void);
int memory_init(void);

struct file_operations memory_fops = {
        read: memory_read,
        write: memory_write,
        open: memory_open,
        release: memory_release
};

module_init(memory_init);
module_exit(memory_exit);

int memory_major = 60;

char *memory_buffer;

在头文件之后,声明了几个后面要定义的函数。在file_operations结构的定义中声明了

几个通常用来操作文件的函数。这些在后面会详细介绍到。接着,向内核声明初始化和

退出函数-加载和卸载模块时使用的。最后,声明驱动的全局变量:memory_major表示驱

动的主驱动号,memory_buffer指向一块用于存储驱动数据的内存区域。

“memory”驱动:设备与其文件的连接

在UNIX和Linux中,从用户空间访问设备与访问文件相同。这些设备文件通常位于/dev目

录下。

将一个普通文件与设备文件关联起来需要使用两个数字:major number 和 minor

number。内核使用major number将一个文件链接到它的驱动。而minor number是供设备内

部使用。

要做到这一点,一个文件(将用于访问设备驱动程序)的创建必须使用root身份键入以下

命令:

mknod /dev/memory c 60 0

上面这句命令中,c表示创建一个字符设备,该设备的主驱动号major number为60,次驱

动号minor number为0。

对于这个驱动,为了在内核空间将其链接到对应的/dev下的文件,需要使用

register_chrdev函数。调用该函数使用到三个参数:major number,

一个字符串用于表示该模块的名字,一个file_operations结构。

在安装模块时它以下面的方式被调用:

int memory_init(void)
{
        int result;

        result = register_chrdev(memory_major, "memory", &memory_fops);
        if (result < 0) {
                printk("<1>memory: can't obtain major number %d\n",
                        memory_major);
                return result;
        }

        memory_buffer = kmalloc(1, GFP_KERNEL);
        if (!memroy_buffer) {
                result = -ENOMEM;
                goto fail;
        }
        memset(memory_buffer, 0, 1);

        printk("<1> Inserting memory module\n");
        return 0;

fail:
        memory_exit();
        return result;
}

注意kmalloc函数的使用。这个函数在内核空间中分配一块用于设备驱动缓冲区的内存。

它的使用方法与著名的malloc函数类似。最后,如果注册主驱动号失败或分配内存失败,

这个模块也将失败。

“memory”驱动:移除驱动

为了在memory_exit函数中移除模块,需要使用到unregsiter_chrdev函数。它将为内核

释放相应的主驱动号。

void memory_exit(void)
{
        unregister_chrdev(memory_major, "memory");

        if (memory_buffer) {
                kfree(memory_buffer);
        }

        printk("<1> Removing memory module\n");
}

为了在移除驱动时还原一个干净的内核,在这个函数中同时释放了驱动的缓冲区。

“memory”驱动:像打开文件一样打开设备

内核空间中与用户空间中打开文件(fopen)相对应的是open:调用register_chrdev时

使用到了一个file_operations结构,而open正是这个结构的成员。open函数的参数有:

一个inode结构,它向内核传递有关主驱动号major number和次驱动号minor number的相

关信息;一个file结构,该结构中包括操作文件的多个不同函数。但本文并不对这些函数

作详细介绍。

当一个文件被打开,通常需要初始化驱动变量或重新设置这个设备。但在这个例子中这

些没有做这些工作。

memory_open函数如下:

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

“memory”驱动:像关闭文件一样关闭设备

与用户空间中关闭文件(fclose)相对应的是release:调用register_chrdev时使用到一

个file_operations结构,release正是该结构的成员。在本例中,它对应

memory_release函数,与前面类似,它也有两个参数:inode结构和file结构。

当一个文件关闭,通常需要释放已使用的内存和任何打开文件时关链到的变量。但是,同

样的因为本例十分简单,这些工作这里都没有做。

memory_release函数如下:

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

“memory”驱动:读设备

同样,对应于用户空间读文件的fread,这里用到read:它也是file_operations结构的成

员。这里它对应memory_read函数。它的参数有:一个file结构;一个缓冲区buf,用户

空间从该缓冲区中读数据;一个计数器count记录传输的字节;最后,还有f_pos,用来

指示从文件的哪里开始读取。

在本例中,memory_read函数使用函数copy_to_user从驱动缓冲区中发送一个字节给用户

空间:

ssize_t memory_read(struct *file filp, char *buf,
        size_t count, loff_t *f_pos)
{
        copy_to_user(buf, memory_buffer, 1);

        if (*f_pos == 0) {
                *f_pos += 1;
                return 1;
        } else {
                return 0;
        }
}

读数位置f_pos同时也会改变。如果从文件开头读起,f_pos会以1递增,并且返回已正确

读到的字节数,即1。如果不是从文件开头读起,文件的结束标志0将被返回,因为文件中

没有数据。

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料;进群私聊群管理领取 内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

“memory”驱动:写设备

与fwrite类似,内核空间有write:它是file_operations结构的成员。本例中为

memory_write,有下面几个参数:一个file结构;buf缓冲区,供用户空间写入;count,

计数器记录写入数据的字节数;f_pos,写入的位置。

ssize_t memory_write(struct file *filp, char *buf,
        size_t count, loff_t *f_pos)
{
        char *tmp;

        tmp = buf + count - 1;
        copy_from_user(memory_buffer, tmp, 1);
        return 1;
}

此处,copy_from_user将数据从用户空间传送到内核空间。

完整的“memory“驱动

加入之前的所有代码后,便组成了完整的memory驱动memory.c:

在使用本驱动之前,当然需要先编译它,方法与前面类似。加载模块:

insmod memory.ko

很方便即可取消对设备的保护:

chmod 666 /dev/memory

如果一切顺利,将有一个设备/dev/memory存在,且可以将其中写字符串或字符,它将存

储字符串或多个字符中的最后一个。可以像这样来操作:

echo -n abcdef > /dev/memory

使用cat来检查这个设备的内容:

cat /dev/memory

已存的字符不会改变,除非再写入覆盖它或这个模块被卸载。

附:

实例2 memory 驱动实验:

代码 memory.c

#include <linux/init.h>
//#include

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/proc_fs.h>
#include <linux/fcntl.h>
//#include

#include <linux/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

int memory_open(struct inode *inode, struct file *filp);
int memory_release(struct inode *inode, struct file *filp);
ssize_t memory_read(struct file *filp, char *buf, size_t count, loff_t *f_pos);
ssize_t memory_write(struct file *filp, char *buf, size_t count, loff_t *f_pos);
void memory_exit(void);
int memory_init(void);

struct file_operations memory_fops = {
        read:memory_read,
        write:memory_write,
        open:memory_open,
        release:memory_release
};

module_init(memory_init);
module_exit(memory_exit);

int memory_major = 60;

char *memory_buffer;

int memory_init(void)
{
        int result;

        result = register_chrdev(memory_major, "memory", &memory_fops);
        if (result < 0) {
                printk("<1>memory: can't obtain major number %d\n", memory_major);
                return result;
        }

        memory_buffer = kmalloc(1, GFP_KERNEL);
        if (!memory_buffer) {
                result = - ENOMEM;
                goto fail;
        }
        memset(memory_buffer, 0, 1);

        printk("<1>Inserting memory module\n");
        return 0;

fail:
        memory_exit();
        return result;
}

void memory_exit(void)
{
        unregister_chrdev(memory_major, "memory");

        if (memory_buffer)
                kfree(memory_buffer);

        printk("<1>Removing memory module\n");
}

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

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

ssize_t memory_read(struct file *filp, char *buf,
        size_t count, loff_t *f_pos)
{
        copy_to_user(buf, memory_buffer, 1);

        if (*f_pos == 0) {
                *f_pos += 1;
                return 1;
        } else
                return 0;
}

ssize_t memory_write(struct file *filp, char *buf,
        size_t count, loff_t *f_pos)
{
        char *tmp;
    
        tmp = buf + count - 1;
        copy_from_user(memory_buffer, tmp, 1);

        return 1;
}

Makefile:

obj-m := memory.o

KERNELDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

编译:make

生成文件中有 memory.ko, 该文件即要使用的目标模块

加载:sudo insmod ./memory.ko

查看dmesg信息:dmesg | tail -n 1

[10911.945739] Inserting memory module

改变操作设备文件权限:sudo chmod 666 /dev/memory

向驱动中写入数据:echo -n abcdefg > /dev/memory

查看驱动中保存的数据:

[linux@ ~]$ cat /dev/memory

g[linux@ ~]$

可见其为最后写入的数据。

卸载驱动:

[linux@ ~]$ sudo rmmod memory

[linux@ ~]$ dmesg | tail -n 2

[10911.945739] Inserting memory module

[11155.809076] Removing memory module

----------------------------实验完毕

分析

上面代码中主要有五个函数重点注意下:

register_chrdev
unregister_chrdev
copy_to_user
copy_from_user
kmalloc

/*
 * 成功:返回0
 * 失败:-EINVAL表示申请的主设备号非法(可能是主设备号大于最大设备号)
 *       -EBUSY 表示所申请的主设备号已为其它设备使用
 * 如果动态分配成功,则此函数将返回主设备号
 *
 */
static inline int register_chrdev(
        unsigned int major, //设备驱动向内核申请主设备号,若为0则系统动态分配一个主设备号
        const char *name, //设备名
        const struct file_operations *fops //各调用的入口点
        );

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


arch/x86/lib/usercopy_32.c
/**
 * copy_to_user: - Copy a block of data into user space.
 * @to:   Destination address, in user space.
 * @from: Source address, in kernel space.
 * @n:    Number of bytes to copy.
 *
 * Context: User context only.  This function may sleep.
 *
 * Copy data from kernel space to user space.
 *
 * Returns number of bytes that could not be copied.
 * On success, this will be zero.
 */
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

/**
 * copy_from_user: - Copy a block of data from user space.
 * @to:   Destination address, in kernel space.
 * @from: Source address, in user space.
 * @n:    Number of bytes to copy.
 *
 * Context: User context only.  This function may sleep.
 *
 * Copy data from user space to kernel space.
 *
 * Returns number of bytes that could not be copied.
 * On success, this will be zero.
 *
 * If some data could not be copied, this function will pad the copied
 * data to the requested size using zero bytes.
 */
unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n);

在内核中动态开辟内存

void *kmalloc(size_t size, int flags);

size:要分配内存的大小

flags:分配标志,以几个方式控制kmalloc的行为

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个简单的字符设备驱动程序的示例代码: ```c #include <linux/module.h> #include <linux/fs.h> #include <linux/uaccess.h> #define DEVICE_NAME "my_char_dev" #define BUFFER_SIZE 1024 static int major; static char buffer[BUFFER_SIZE]; static ssize_t my_char_dev_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { ssize_t retval = 0; if (*f_pos >= BUFFER_SIZE) { goto out; } if (*f_pos + count > BUFFER_SIZE) { count = BUFFER_SIZE - *f_pos; } if (copy_to_user(buf, buffer + *f_pos, count)) { retval = -EFAULT; goto out; } *f_pos += count; retval = count; out: return retval; } static ssize_t my_char_dev_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { ssize_t retval = 0; if (*f_pos >= BUFFER_SIZE) { goto out; } if (*f_pos + count > BUFFER_SIZE) { count = BUFFER_SIZE - *f_pos; } if (copy_from_user(buffer + *f_pos, buf, count)) { retval = -EFAULT; goto out; } *f_pos += count; retval = count; out: return retval; } static struct file_operations my_char_dev_fops = { .owner = THIS_MODULE, .read = my_char_dev_read, .write = my_char_dev_write, }; static int __init my_char_dev_init(void) { int retval; memset(buffer, 0, BUFFER_SIZE); major = register_chrdev(0, DEVICE_NAME, &my_char_dev_fops); if (major < 0) { printk(KERN_ERR "Failed to register char device.\n"); return major; } printk(KERN_INFO "Registered char device with major number %d.\n", major); return 0; } static void __exit my_char_dev_exit(void) { unregister_chrdev(major, DEVICE_NAME); printk(KERN_INFO "Unregistered char device with major number %d.\n", major); } module_init(my_char_dev_init); module_exit(my_char_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your name"); MODULE_DESCRIPTION("A simple character device driver."); ``` 这个驱动程序只是一个非常简单的示例,它创建了一个名为 "my_char_dev" 的字符设备,具有读和功能。当应用程序读取设备时,它从缓冲区中读取数据,并将其复制到应用程序的缓冲区中。当应用程序设备时,它将数据从应用程序的缓冲区中复制到驱动程序的缓冲区中。 当你编译并加载这个模块后,在终端中执行命令: ``` $ sudo mknod /dev/my_char_dev c <major number> 0 ``` 其中,`<major number>` 是上面注册的字符设备的主设备号。 然后,你就可以像操作一个普通的文件一样进行读写操作了,例如: ``` $ echo "Hello, world!" > /dev/my_char_dev $ cat /dev/my_char_dev Hello, world! ``` 当然,这个示例只是一个非常简单的驱动程序,实际的驱动程序可能涉及到更复杂的操作,例如设备的初始化,中断处理等等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值