深入Linux设备驱动程序内核机制
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.1 应用程序与设备驱动程序互动实例

在深入讨论字符设备驱动程序之前,我们先用一个实际的例子来展示应用程序如何与字符设备驱动程序进行交互。这个例子中,我们首先给出一个简单的字符设备驱动程序的内核模块,接着通过insmod工具将这个内核模块加入到系统中,之后通过mknod来创建一个设备文件节点(在这个例子中我们将手动创建设备文件节点,不过后面会讨论设备节点的自动生成机制),最后再编写一个小的应用程序,用该应用程序来调用前面设备驱动程序所提供的服务。因为这里只是展示应用程序与设备驱动程序相互交互的环节,所以无论是设备驱动程序还是应用程序,都尽量保持简单且与具体硬件设备无关。在本章后续的小节中将仔细讨论这个例子中所有关键环节的幕后技术细节。

字符设备驱动程序源码

<demo_chr_dev.c>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
static struct cdev chr_dev;//定义一个字符设备对象
static dev_t ndev;//字符设备节点的设备号
static int chr_open(struct inode *nd, struct file *filp)
{
            int major = MAJOR(nd->i_rdev);
            int minor = MINOR(nd->i_rdev);
            printk("chr_open, major=%d, minor=%d\n", major, minor);
            return 0;
}
static ssize_t chr_read(struct file *f, char __user *u, size_t sz, loff_t *off)
{
    printk("In the chr_read() function!\n");
    return 0;
}
//字符设备驱动程序中非常关键的一个数据结构struct file_operations
struct file_operations chr_ops=
{
    .owner = THIS_MODULE,
    .open = chr_open,
    .read = chr_read,
};
//模块的初始化函数
static int demo_init(void)
{
    int ret;
    cdev_init(&chr_dev, &chr_ops); //初始化字符设备对象
    ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev"); //分配设备号
    if(ret < 0)
            return ret;
    printk("demo_init():major=%d, minor=%d\n", MAJOR(ndev), MINOR(ndev));
    ret = cdev_add(&chr_dev, ndev, 1);//将字符设备对象chr_dev注册进系统
    if(ret < 0)
            return ret;
    return 0;
}
static void demo_exit(void)
{
    printk("Removing chr_dev module...\n");
    cdev_del(&chr_dev);//将字符设备对象chr_dev从系统中注销掉
    unregister_chrdev_region(ndev,1);//释放分配的设备号
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Dennis @AMDLinuxFGL");
MODULE_DESCRIPTION("A char device driver as an example");

以上就是一个字符设备驱动程序的源码,虽然极其简单,以至于没有做任何有实质意义的事情,但是它展示了字符设备驱动程序的典型框架结构,字符设备驱动程序中绝大多数的关键元素都出现在了上面这个示例程序中,它们将成为本章后续讨论的核心。

读者可以参照下面这个简单的Makefile文件来编译上述的模块:

obj-m := demo_chr_dev.o
KERNELDIR:=/lib/modules/$(shell uname-r)/build
PWD := $(shell pwd)
default:
    $(MAKE)  -C$(KERNELDIR)M=$(PWD)  modules
clean:
    rm -f *.o *.ko *.mod.c

注:读者可能需要根据自己系统中实际的内核源码路径来修改这里的KERNELDIR值,以消除可能出现的编译错误。

如果一切顺利,将得到一个名为demo_chr_dev.ko的内核模块。

应用程序源码

<main.c>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define CHR_DEV_NAME "/dev/chr_dev"
int main()
{
    int ret;
    char buf[32];
    int fd = open(CHR_DEV_NAME, O_RDONLY|O_NDELAY);
    if(fd < 0)
    {
            printf("open file %s failed!\n", CHR_DEV_NAME);
            return -1;
    }
    read(fd, buf, 32);
    close(fd);
    return 0;
}

读者可以用gcc来生成该应用程序的可执行文件main:

root@AMDLinuxFGL:/home/dennis/book/chap2/app# gcc main.c -o main

应用程序主要是用open打开一个设备文件节点,然后在打开的设备文件描述符fd上调用read函数。调用read函数时,除了fd必须使用外,其他两个参数完全是为了满足read函数调用的需要,设备驱动程序中的chr_read不会用到这些参数。

这个简单的例子将展示应用程序如何通过文件系统调用,穿越到内核空间,呼叫到设备驱动程序实现的各种接口函数。本章稍后将和读者一道去探讨这个示例程序背后所包含的技术细节,等到对字符设备驱动程序的各种内核设施及文件系统的接口有了深入的理解,相信在实际的工作中一定可以自由地驾驭它们,即便遇到问题也可以快速定位和解决。

示例操作步骤

现在用insmod把demo_chr_dev.ko加入到系统:

root@AMDLinuxFGL:/home/dennis/book/chap2/gene-module# insmod demo_chr_dev.ko

dmesg针对这个insmod的输出信息为:

[19611.946440] demo_init():major=248, minor=0

通过上面dmesg的输出信息,我们知道alloc_chrdev_region函数给内核模块demo_chr_dev.ko分配的主设备号为248,次设备号为0。根据这个设备号信息用mknod命令在系统的/dev目录下为该模块生成一个新的设备文件节点:

root@AMDLinuxFGL:/home/dennis/book/chap2/app# mknod /dev/chr_dev c 248 0

如果一切正常,那么在/dev目录下就会产生一个新的设备文件节点“/dev/chr_dev”,可以用ls命令来仔细观察一下它:

root@AMDLinuxFGL:/home/dennis/book/chap2/app# ls -l /dev/chr_dev
crw-r--r-- 1 root root 248, 0 2011-05-11 21:41 /dev/chr_dev

上面ls命令的输出反映出了设备节点“/dev/chr_dev”的如下一些关键信息:

“crw-r--r--”中的字符“c”表明这是个字符设备文件,248是该设备节点的主设备号,次设备号则是0,这跟我们的预期是完全一致的。

有了对应的设备文件之后,现在可以运行我们的应用程序了:

root@AMDLinuxFGL:/home/dennis/book/chap2/app# ./main

查看dmesg对此的输出信息:

root@AMDLinuxFGL:/home/dennis/book/chap2/app# dmesg -c
[20340.589750] chr_open, major=248, minor=0
[20340.589760] In the chr_read() function!

对比前面内核模块demo_chr_dev.ko的源码,读者应该知道上述两行的输出分别来自内核模块中的chr_open和chr_read函数,虽然在这个示例程序中它们几乎没做任何事情,但是我们见证了应用程序成功调用到了设备驱动程序实现的函数,这正是我们所预期的目标。