首页 > 代码库 > Linux 简单字符设备驱动程序 (自顶向下)

Linux 简单字符设备驱动程序 (自顶向下)

<style></style>

第零章:扯扯淡

  特此总结一下写的一个简单字符设备驱动程序的过程,我要强调一下“自顶向下”这个介绍方法,因为我觉得这样更容易让没有接触过设备驱动程序的童鞋更容易理解,“自顶向下”最初从《计算机网络 自顶向下方法》这本书学到的,我觉得有时候这是一种很好的方式。



第一章:测试程序

  咦?你怎么跟别人的思路不一样???自顶向下嘛,我就直接从测试程序来说啦,这样那个不是更熟悉吗?看看下面的测试程序的代码,是不是很熟悉?

 1 #include <stdio.h> 2 #include <unistd.h> 3 #include <fcntl.h>  4  5 #define MY_CDEV_NAME "/dev/mychardev" 6 #define BUF_LEN 16 7  8 int main(void) 9 {10     int fd;11     int ret,i;12     char buf[BUF_LEN];13     14     /*打开设备*/15     fd=open(MY_CDEV_NAME,O_RDWR | O_NONBLOCK);16     if(fd<0)17     {18         printf("open %s fail!\n",MY_CDEV_NAME);19         return -1;20     }21     printf("open %s success!\n",MY_CDEV_NAME);22     23     /*设置buf的数据,以后会写进设备*/24     for(i=0;i<BUF_LEN;i++)25     {26         buf[i]=i+65;27     }28     29         30     /*写设备*/31     if((ret=write(fd,buf,BUF_LEN))<0)32     {33         printf("write %s fail!\n",MY_CDEV_NAME);34     }35     else36     {37         printf("write %s success! Write totoal:%d\n",MY_CDEV_NAME,ret);38     }39     40     /*把文件偏移量设置为文件开始处*/41     if((ret=lseek(fd,0,SEEK_SET))<0)42     {43         printf("lseek %s fail!\n",MY_CDEV_NAME);44     }45     else46     {47         printf("lseek %s success! Now position:%d\n",MY_CDEV_NAME,ret);48     }49     50     /*读设备*/51     if((ret=read(fd,buf+BUF_LEN/2,BUF_LEN))<0)52     {53         printf("read %s fail!\n",MY_CDEV_NAME);54     }55     else56     {57         printf("read %s success! Read totoal:%d\n",MY_CDEV_NAME,ret);58     }59     60     for(i=0;i<BUF_LEN;i++)61     {62         printf("buf[%d]:%c\n",i,buf[i]);63     }64     65     close(fd);66     67     return 0;    68 }
最终测试代码

这里其实不就是Unix环境打开一个普通的文件嘛,我打开的是 /dev/mychardev ,虽然是一个字符设备文件,但是操作系统统一了接口,所以和打开一个普通文件没有区别,先open(),再向文件write(),再把当前文件偏移量设置为文件的开始处lseek(),再读读看read(),最后关闭close()。写写这些代码越发觉得Linux真是厉害,基本上把所有的设备都当做文件给处理掉了,系统提供统一的接口给上层,酷毙了!

  到这里基本上没有问题,唯一的问题是 /dev/mychardev 这个文件怎么来的???



第二章:设备文件怎么来的

0、Linux文件类型简单介绍:

  linux系统将设备基本分为3类:字符设备、块设备、网络设备。详情请搜搜,我不会告诉你我理解不够深刻……

1、先看看这个文件属性:

 看最前面是“c”,说明是一个字符设备文件。我不会告诉你这个文件是我自己创建的,O(∩_∩)O哈哈~

2、Linux创建一个字符设备文件很简单,只要mknod命令即可:

说得很清楚,创建字符或块特殊设备文件。所以要创建一个字符设备很简单啊:

 sudo mknod /dev/mychardev c 248 0

看,这不就创建了一个字符设备文件嘛,sudo要获得权限;/dev/mychardev 为文件名,其实也指定了文件路径;c表明创建的是字符设备文件,块设备文件就是b啦;最后重要的是 major 和 minor 这两个参数,我创建时用248和0替换的,这个不是小打小闹瞎搞的,有来头! 

3、为什么在/dev目录:

  linux的/dev目录里,存放的是系统里的各种设备文件,每个设备对应一个设备文件,而我们就是通过这个设备文件访问和使用这个设备的,即打开这个文件相当于打开设备了,向文件里面写数据相当于把数据写到设备了,读文件相当于从设备中读数据了。咱们不是要创建一个字符设备嘛,虽然不知道这个设备具体是什么鸟样,但是总有一个设备文件来对应这个设备。

4、设备文件主次设备号:

  从上一节可以看到,/dev目录下是各种杂七杂八的设备文件,这些设备文件是怎么样对应设备的呢?它们两个好基友肯定要一一对应嘛。为了管理这些设备,操作系统为设备编了号,每个设备号又分为主设备号和次设备号。

  从设备文件与设备来讲:主设备号用来区分不同种类的设备;而次设备号用来区分同一类型的多个设备。

  从设备文件与设备驱动来讲:主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型;次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。

  上面两个角度意思一样。举个例子:假如我的电脑连了2台打印机,打印机类型完全一样,那么按照前面讲的,在/dev目录下肯定会有2个设备文件来对应这2个打印机,比如有/dev/printer1和/dev/printer2,对这两个文件的读写其实就是对打印机1和2的读写操作。但是由于这两个打印机类型一样,所以它们的驱动程序不也是一样的吗?没必要把2份一样的驱动代码加载到操作系统的内核吧?所以就让这两个文件的主设备号一样就可以了,比如都是248。好了,我们加载了一个驱动程序模块到内核里面,下面这个驱动程序怎么知道是向哪个打印机发送数据呢?这时候就是次设备号起作用了,比如给它们分配1和2次设备号,这样不久区分了嘛。所以我就可以这样:sudo mknod /dev/printer1 c 248 0 和sudo mknod /dev/printer2 c 248 1。这样就创建了2设备文件以对应2设备。

  好了,到这里设备文件是怎么来的解决了:自己创建的呗。那么唯一的问题是主设备号与次设备号怎么来的???



第三章:向系统装载设备驱动模块

  这里没有介绍上一章主设备号与次设备号怎么来的问题,因为是程序执行过程中从系统获得的,具体要看代码,但是在把驱动程序装载到系统会执行一个函数,一般会在这个函数里面向系统要设备号,所以可以在这个函数里面打印获得设备号。所以在把驱动模块装载到系统是可以看到设备号的,前提是你一定写了一个打印函数。

  这一章讲的是如果你已经把具体的设备驱动程序写好了之后,你应该怎么把它装载到系统。看下面的文件夹:

我写的驱动程序的源文件只有一个:myCDev.c,就这个,还有就是Makefile,另外test.c和a是测试源文件和生成的测试程序。其余的都是执行make命令时生成的,也就是编译生成的文件。其中有个.ko的文件,向系统装载驱动和这个密切相关。

  既然我们是自顶向下的,假设下层已经提供了源文件,也已经编译好了,现在向系统装载驱动程序只要一个命令:

sudo insmod myCDev.ko

insmod就是干这个事情的。相对应:

sudo rmmod myCDev

是卸载模块的,模块不用了可以直接从内核去掉的,注意insmod有.ko后缀,这个没有。

  这里有两个命令;dmesg和cat /proc/kmsg 可以查看你在源代码的中printk()函数的输出,因为一般会在模块装载和卸载时调用特定的函数。

  好了,这一章解决了如果我们已经写好了驱动程序并编译好了,如何装载的问题。