曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 5817|回复: 1
打印 上一主题 下一主题

简单字符设备创建实例

[复制链接]

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
跳转到指定楼层
楼主
发表于 2009-8-27 17:11:36 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
下面包括两个程序代码,一个是 call_app.c 构成的应用程序;一个是 call_dev.c 构成的字符设备驱动程序。

call_dev.c 完整代码如下(程序中使用了老式字符设备注册与注销函数)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/types.h>
#include <linux/fcntl.h>

#define CALL_DEV_NAME "call_dev"
#define CALL_DEV_MAJOR 240

int call_open (struct inode *inode, struct file *filp)
{
    int num = MINOR (inode->i_rdev);
    printk ("call open -> minor : %d\n", num);
    return 0;
}

loff_t call_llseek (struct file *filp, loff_t off, int whence)
{
    printk ("call llseek -> off : %08X, whence : %08X\n", off, whence);
    return 0x23;
}

ssize_t call_read (struct file *filp, char *buf, size_t count, loff_t *f_pos)
{
    printk ("call read -> buf : %08X, count : %08X\n", buf ,count);
    return 0x33;
}

ssize_t call_write (struct file *filp, const char *buf, size_t count, loff_t *f_pos)
{
    printk ("call write -> buf : %08X, count : %08X\n", buf, count);
    return 0x43;
}

int call_ioctl (struct inode *inode, struct file *filp, unsigned int cmd, unsigned int arg)
{
    printk ("call ioctl -> cmd : %08X, arg : %08X\n", cmd, arg);
    return 0x53;
}

int call_release (struct inode *inode, struct file *filp)
{
    printk ("call release \n");
    return 0;
}

struct file_operations call_fops = {
    .owner = THIS_MODULE,
    .llseek = call_llseek,
    .read = call_read,
    .write = call_write,
    .ioctl = call_ioctl,
    .open = call_open,
    .release = call_release,
};

int call_init (void)
{
    int result;
    
    printk ("call call_init \n");

    result = register_chrdev (CALL_DEV_MAJOR, CALL_DEV_NAME, &call_fops);
    
    if (result < 0)
        return result;

    return 0;
}

void call_exit (void)
{
    printk ("call call_exit \n");
    unregister_chrdev (CALL_DEV_MAJOR, CALL_DEV_NAME);
}

module_init (call_init);
module_exit (call_exit);

MODULE_LICENSE ("Dual BSD/GPL");

call_app.c 用户程序代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define DEVICE_FILENAME "/dev/call_dev"

int main()
{
    int  dev;
    char buff [128];
    int  ret;

    printf ("1) device file open\n");

    dev = open (DEVICE_FILENAME, O_RDWR | O_NDELAY);
    if (dev < 0) {
        perror ("errno");
        exit(1);
    }

    if (dev >= 0) {
        printf ("2) seek function call\n");
        
        ret = lseek (dev, 0x20, SEEK_SET);
        printf ("ret = %08X\n", ret);
        
        printf ("3) read function call \n");

        ret = read (dev, 0x30, 0x31);
        printf ("ret = %08X\n", ret);

        printf ("4) write function call\n");
        
        ret = write (dev, 0x40, 0x41);
        printf ("ret = %08X\n", ret);

        printf ("5) ioctl function call\n");
        
        ret = ioctl (dev, 0x51, 0x52);
        printf ("ret = %08X\n", ret);

        printf ("6) device file close\n");

        ret = close (dev);
        printf ("ret = %08X\n", ret);
    
    }

        return 0;
}

Makefile 文件

obj-m := call_dev.o

KDIR := /lib/modules/$(shell uname -r)/build

PWD := $(shell pwd)

default:
    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

clean:
    rm -rf *.ko
    rm -rf *.mod.*
    rm -rf .*.cmd
    rm -rf *.o
[/pre]

make 过程
beyes@linux-beyes:~/Drivers/my> make
make -C /lib/modules/2.6.27.29-0.1-pae/build SUBDIRS=/home/beyes/Drivers/my modules
make[1]: Entering directory `/usr/src/linux-2.6.27.29-0.1-obj/i386/pae'
make -C ../../../linux-2.6.27.29-0.1 O=/usr/src/linux-2.6.27.29-0.1-obj/i386/pae/. modules
  CC [M]  /home/beyes/Drivers/my/call_dev.o
/home/beyes/Drivers/my/call_dev.c: In function ‘call_llseek’:
/home/beyes/Drivers/my/call_dev.c:21: warning: format ‘%08X’ expects type ‘unsigned int’, but argument 2 has type ‘loff_t’
/home/beyes/Drivers/my/call_dev.c: In function ‘call_read’:
/home/beyes/Drivers/my/call_dev.c:27: warning: format ‘%08X’ expects type ‘unsigned int’, but argument 2 has type ‘char *’
/home/beyes/Drivers/my/call_dev.c: In function ‘call_write’:
/home/beyes/Drivers/my/call_dev.c:33: warning: format ‘%08X’ expects type ‘unsigned int’, but argument 2 has type ‘const char *’
/home/beyes/Drivers/my/call_dev.c: At top level:
/home/beyes/Drivers/my/call_dev.c:54: warning: initialization from incompatible pointer type
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/beyes/Drivers/my/call_dev.mod.o
  LD [M]  /home/beyes/Drivers/my/call_dev.ko
make[1]: Leaving directory `/usr/src/linux-2.6.27.29-0.1-obj/i386/pae'
因为在 call_app.c 用户程序中,为了体现低级输入输出函数和字符设备驱动程序的 file_operations 结构体函数之间的函数传递方式,在其中函数里直接传入了常数,而在驱动程序中也直接将这些传入的常数以十六进制方式打印出来,所以会有警告信息,但这并不影响应用,仍然可以生成驱动模块。

要想运行程序,首先要在应用程序上创建连接设备驱动程序的设备文件。上面的设备驱动程序的主设备号直接设置为 240。由于驱动程序中没有定义次设备号的使用方法,只是用 prink 文件加以说明,因此次设备号可以指定为任意值。

应用程序的文件名为 call_dev ,执行以下命令,创建必要的设备文件:
beyes@linux-beyes:~/Drivers/my> sudo mknod /dev/call_dev c 240 32

为了把 make 后生成的模块包含在内核中,使用 insmod 命令:
 sudo /sbin/insmod call_dev.ko

实际上,也可以先创建设备文件,再把设备驱动程序的模块插入到内核中;反过来也可以先把设备驱动程序的模块插入到内核中,然后再创建设备文件。打开应用程序之前,设备文件对内核和设备驱动程序不起作用。

编译 call_app.c 后生成 call_app.exe 文件,编译过程中会有警告,但同样也不影响使用。

运行及输出
beyes@linux-beyes:~/Drivers/my> sudo ./call_app.exe
1) device file open
2) seek function call
ret = 00000023
3) read function call
ret = 00000033
4) write function call
ret = 00000043
5) ioctl function call
ret = 00000053
6) device file close
ret = 00000000

call_app.exe 应用程序里使用了open(), write() 等系统调用函数,call_dev 模块利用 printk() 函数把传送的变量值输出到内核 message 上,执行 dmesg  可以看到相关输出:
call call_exit
call call_init
call open -> minor : 32
call llseek -> off : 00000020, whence : 00000000
call read -> buf : 00000030, count : 00000031
call write -> buf : 00000040, count : 00000041
call ioctl -> cmd : 00000051, arg : 00000052
call release

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34395
沙发
 楼主| 发表于 2009-8-28 11:46:52 | 只看该作者
当使用 insmod 命令将 call_dev.ko 插入到内核时,此时 call_dev.c 中声明的 module_init 所定义的 call_init() 函数将被调用( 2.4 内核调用 init_module() 函数)进行初始化工作。

在 call_init() 函数中,使用了 register_chrdev() 函数把字符设备驱动程序注册到内核上。register_chrdev() 函数把 CALL_DEV_MAJOR 声明的 240 作为主设备号,CALL_DEV_NAME DE 的字符注册为设备驱动程序。应用程序想要把低级文件输入输出函数应用到设备文件上,必须调用设备驱动程序对应的函数。具体的方法为,在 register_chrdev() 的第三个参数里填入 file_operations 结构体变量 call_fops 的地址 &call_fops 。若能正常注册,register_chrdev() 函数返回小于 0 的值,然后直接把该值作为 call_init() 函数的返回值,结束程序。正常注册,则返回 0 。

在用户应用程序中,调用了 open() 函数打开 /dev/call_dev 设备文件。 open() 函数中的参数 pathname 和 flags 不能直接传递到 call_open() 上。而是通过其它的系统调用(sys_open)事先完成了多种处理后才会调用到驱动程序了的 xxx_open() ,这从 open() 成功调用返回的文件描述符可知道 -- 这是内核的行为,而自定义的 xxx_open() 的返回值不是文件描述符

open() 函数运行 file_operations 结构体 call_fops 里的 call_open() 函数。这里,2.4 内核和 2.6 内核有所差异。在 2.4 内核中,由设备驱动程序管理模块的调用次数,而 2.6 内核中则由内核直接管理该次数。因此,2.4 内核的 call_open() 函数必须运行 MOD_INC_USE_COUNT ,从而增加调用次数,而 2.6 内核则没有必要使用此函数。

call_open() 函数通过利用了 MINOR 宏获取了次设备号。函数正常时返回 0 值,运行失败返回小于 0 的错误代码,此时可以参考 open() 函数的返回值,适当的处理错误代码。


在用户程序里,使用了 lseek() 函数。lseek() 函数中参数 offset 的值被直接设为 0x20 ,参数 whence 的值为 SEEK_SET (实际为 0)。

lseek() 函数将调用设备驱动程序中的 call_llseek() 函数。call_llseeK() 函数根据 offset 和 whence 做出适当的判断,不同的设备驱动程序具有不同的文件定义点。

发生错误时,call_llseek() 函数返回小于 0 的值,否则返回修改后的文件偏移值。在上面的程序中,自定义返回了 0x23 ,该值被传送为 call_app.c 的 lseek() 函数返回值。


call_app.c 运行向设备文件读取的 read 函数。为了测试 read() 函数,把 0x30 和 0x31 分别指定为缓存地址和大小。由于设备不使用缓存内容,所以 0x30 这样的地址不会产生错误。在用户程序里使用了 read() 调用,最终还是会调用到驱动程序中的 call_read() 函数。

call_read() 函数根据 f_ops 参数传送的文件 position 值,从此处开始读取 count 大小的数据存放在 buf 所指向的用户地址空间里。在 call_read() 中为了确认传送内容,利用 printk 函数输出了两个值。程序正常运行后,buf 为 read() 函数传送的值 0x30 ,count 上记录的是 read() 函数传送的值为 0x31 。对于 call_read() 函数,发生错误时返回小于 0 的值,否则返回处理后的参数,在上面的程序中自定义返回了 0x33 。


call_app.c 执行设备文件上写入数据的 write() 函数。为了测试,write() 函数把 0x40 和 0x41 分别指定为缓存地址和大小。同样由于设备不使用缓存内容,所以随意的传送这样的缓存地址不会产生错误。write() 函数最终调用驱动程序中的 call_write() 函数。write() 和 call_write() 的对应情况和 read() 和 call_read() 的对应情况类似。


call_app.c 中把设备文件中 read() 和 write() 函数无法处理的内容运行为 ioctl() 。ioctl() 函数的参数数目是可以改变的 。但是,设备文件能够接受的参数最大数量为 3 ,因此参数多为 2 个以上 3 个以下。在上面的程序中使用了 3 个变量。同样的, ioctl 函数为了测试,把 0x51 和 0x52 分别指定为命令和参数值。

ioctl() 函数最终通过内核调用设备驱动程序的 call_ioctl() 函数来完成相关工作。call_ioctl() 函数把 cmd 上传来的值解释为命令,并把 arg 值处理为命令所需的数据,从而运行命令的相应功能。cmd 命令不是标准化的命令,二十设备驱动程序固有的命令( 集中类型可在内核预处理,但是实际很少使用该命令 )。为了查看传过来的值,仍然使用了 printk 函数。程序正常运行后,cmd 为 ioctl 函数传送来的值 0x51 ,arg 上记录 ioctl() 函数传送的值为 0x52 。对于 call_ioctl() ,发生错误时返回小于 0 的值,否则返回 0 或者命令相应的数值,上面程序返回了 0x53 ,该值作为 call_app.c 中的 ioctl() 的返回值。


当设置完毕后,应用程序应关闭设备文件。为了关闭设备文件,使用了 close 命令。close 命令没有特别要传送的要素。close() 函数是内核调用设备驱动程序的 call_release() 函数。call_release() 函数用于关闭设备。2.4 内核运行 MOD_DEC_USE_COUNT 减少设备驱动程序的设备使用次数。2.6 内核没有必要使用该函数。call_release() 函数,当发生错误时返回小于0的值,否则返回 0 。由于多数应用程序不参考 close() 的返回值,因此返回值没有实际的利用价值。


最后,不在使用设备驱动程序时,利用模块实用程序 rmmod ,从内核中清除 call_dev ,此时内核调用定义在 call_dev.c 的 module_exit 宏上的 call_exit 函数。2.4 内核调用 cleanup_module() ,这两个函数具有相同的作用。

call_exit() 函数利用 unregister_chrdev() 函数删除注册在内核上的字符设备驱动程序。unregister_chrdev() 函数删除定义为 CALL_DEV_MAJOR 的主设备号 240 ,定义在 CALL_DEV_NAME 上的设备驱动程序明为 call_dev 的注册内容。call_exit() 函数没有返回值。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2025-5-4 01:58 , Processed in 0.098434 second(s), 21 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表