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

第零章:扯扯淡

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



第一章:测试程序

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

 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!
",MY_CDEV_NAME);
19         return -1;
20     }
21     printf("open %s success!
",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!
",MY_CDEV_NAME);
34     }
35     else
36     {
37         printf("write %s success! Write totoal:%d
",MY_CDEV_NAME,ret);
38     }
39     
40     /*把文件偏移量设置为文件开始处*/
41     if((ret=lseek(fd,0,SEEK_SET))<0)
42     {
43         printf("lseek %s fail!
",MY_CDEV_NAME);
44     }
45     else
46     {
47         printf("lseek %s success! Now position:%d
",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!
",MY_CDEV_NAME);
54     }
55     else
56     {
57         printf("read %s success! Read totoal:%d
",MY_CDEV_NAME,ret);
58     }
59     
60     for(i=0;i<BUF_LEN;i++)
61     {
62         printf("buf[%d]:%c
",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()函数的输出,因为一般会在模块装载和卸载时调用特定的函数。

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



 第四章:正式编写驱动程序源代码文件

 第0节、虚拟字符驱动设备大概原理:

  如果这个设备不是虚拟的话,我向它写数据之类的是会写到这个设备的,如打印机,向打印机写数据直接通过连线写到了打印机,打印机再看着办。但是是虚拟的设备,我向它写数据写到哪里呢?要从这个设备哪里读数据呢?总不能写到空气中吧?于是只要在内存开辟一块存储空间,向这个空间写的相当于写到了具体设备,要从设备读数据也就只要从这个内存读就可以了。

  弄清了这个就好办了。

第1节、需要包含的头文件位置:

  写程序嘛,少不了包含头文件,也就是别人写好的东西。linux内核基本是用c写的,所以也是用include的。但是程序是在内核态运行的而非用户态,所以需要包含的头文件不一样了。看:

 1 /*包含我的电脑中已有的linux内核源代码,注意版本,我的路径:/usr/src/linux-headers-3.13.0-30/include*/
 2 #include <linux/cdev.h>
 3 #include <linux/module.h>
 4 #include <linux/types.h>
 5 #include <linux/init.h>
 6 #include <linux/errno.h>
 7 #include <linux/mm.h>
 8 #include <linux/sched.h>
 9 #include <linux/init.h>
10 #include <linux/fs.h>
11 #include <linux/kdev_t.h>
12 #include <linux/slab.h>
头文件

这些头文件按需要包含,我的系统自己就有这个操作系统的源代码,上面说明了。其实在/usr/src文件夹下还有其他版本的内核源代码,因为我的ubuntu总是更新到最新。

 

第2节、模块装载与卸载时会调用的函数:

  一个模块装载到内核时会执行一个函数的,这个函数是你自己写的,然后告诉系统一下就行了,就是说这样:嗨,系统兄弟,把我这个模块加载到内核时执行的函数是这个哦,记住哈。同样把模块从内核卸载也会调用一个特定函数,你只要写好告诉它一下就好了。函数原型如下:

int my_init(void);
void my_exit(void);

所以你可以自己写自己希望驱动程序被加载到内核和从中卸载时执行的函数,我的如下:

 1 /*初始化函数,当模块装载时被调用,如果装载成功返回0,否则返回非0值*/
 2 static int myCDevInit(void)    //int my_init(void);
 3 {
 4     int res;    //初始化函数的返回值
 5 
 6     printk(KERN_EMERG "


myCDevInit() process...
");      
 7             
 8     /*动态分配设备号*/
 9     res=alloc_chrdev_region(&myDev,0,1,"MyCharDev");
10     if(res<0)    //表示分配设备号失败
11     {
12         return res;
13     }
14     printk(KERN_EMERG "myCDevInit(): alloc_chrdev_region() success! major:%d,minor:%d
", MAJOR(myDev), MINOR(myDev));    //打印获得的主次设备号
15     
16     /*为设备描述结构分配内存*/
17     pMyCharDev= kmalloc(sizeof(struct MyCharDev), GFP_KERNEL);  
18     if(!pMyCharDev)
19     {
20         res=-ENOMEM;    //系统定义的内存不足
21         goto failMalloc;
22     }    
23     memset(pMyCharDev,0,sizeof(struct MyCharDev));
24     printk(KERN_EMERG "myCDevInit(): kmalloc() success!
");
25     
26     /*下面初始化及注册字符设备到系统中*/
27     cdev_init(&(pMyCharDev->myCDev),&myCDevOps);    //初始化struct cdev结构
28     cdev_add(&(pMyCharDev->myCDev),myDev,1);    //注册字符设备
29     printk(KERN_EMERG "myCDevInit(): cdev_init() and cdev_add() success!
myCDevInit() process success!
");
30     
31     return 0;    //一切正常返回0
32     
33 failMalloc:        //内存分配不足时跳到这里
34     unregister_chrdev_region(myDev,1);
35     return res;
36 }
模块初始化函数
/*退出函数,当模块从内存卸载时被调用*/
static void myCDevExit(void)    //void my_exit(void);
{
    printk(KERN_EMERG "myCDevExit() process...
");  
    
    cdev_del(&(pMyCharDev->myCDev));    //注销设备
    kfree(pMyCharDev);    //释放设备结构体内存
    unregister_chrdev_region(myDev,1);    //释放设备号
    
    printk(KERN_EMERG "myCDevExit() process success!
");  
}
模块卸载时调用

具体在函数里面干啥了再说,看不懂没关系。怎么告诉系统我的这模块装载和卸载时是调用这两个呢?通过下面的宏:

1 module_init(myCDevInit);    //通过module_init例程把模块入口点myCDevInit注册到系统中
2 module_exit(myCDevExit);    //由module_exit例程把模块出口函数注册到系统

这样就告诉操作系统了,这两个宏也是在某个源代码文件定义的,我还没找到……

  下面的一些宏也是告诉系统一些信息的:

1 /*下面是指定模块版权、模块作者、模块简要描述信息*/
2 MODULE_LICENSE("GPL");
3 MODULE_AUTHOR("jiayith");  
4 MODULE_DESCRIPTION("jiayith->A simple virtual char device.");  

第3节、一些数据类型

  首先先不看初始化及卸载函数都干了啥,先看看定义了什么数据类型。看:

 1 /*下面是我的字符设备驱动程序的一些定义*/
 2 #ifndef MY_DATA_LEN
 3 #define MY_DATA_LEN 8    //自定义设备描述符中数据的长度
 4 #endif
 5 
 6 /*自定义的设备描述结构体*/
 7 struct MyCharDev
 8 {
 9     struct cdev myCDev;    //struct cdev在<linux/cdev.h>中定义,描述一个字符设备
10     char myData[MY_DATA_LEN];    //以后对设备的读写啊是对这块内存的操作,因为这是一个虚拟的设备
11 };
12 
13 /*下面是几个全局变量*/
14 struct MyCharDev * pMyCharDev;    //设备结构体指针,因为是动态分配
15 static dev_t myDev;    //装获得的设备号,因为在多个函数里面都用到

  我定义了一个结构体struct MyCharDev,第9行struct cdev结构体是内核描述字符设备的,因为内核基本用c写得嘛,没有类,就用结构体了。第10行我把它与一个字符数组放一起了,表示这个设备的存储数据的空间,第0节不是说了嘛,这是个虚拟的设备,就用一块内存放设备的数据。这里有一个隐藏的问题的,struct cdev跟设备驱动程序关联,一个设备驱动程序只需要一个struct cdev就可以了,如果有多个设备怎么办呢?多个设备读写的空间不是一样嘛?先不管这个了,目前阶段就当作只有一个设备。

  dev_t,这个数据类型表示设备号的,是个typedef,typedef一个简单类型。不是说有主次设备号嘛?怎么就一个变量就可以了?那是因为是按位操作的:32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。所以:

  MAJOR(dev_t dev) 这个宏可以获得主设备号;

  MINOR(dev_t dev) 这个宏可以获得次设备号;

  MKDEV(int major,int minor) 又给定的参数获得一个dev_t的类型的设备号。

第4节、模块装载初始化函数干了啥:

 1 static int myCDevInit(void)    //int my_init(void);
 2 {
 3     int res;    //初始化函数的返回值
 4 
 5     printk(KERN_EMERG "


myCDevInit() process...
");      
 6             
 7     /*动态分配设备号*/
 8     res=alloc_chrdev_region(&myDev,0,1,"MyCharDev");
 9     if(res<0)    //表示分配设备号失败
10     {
11         return res;
12     }
13     printk(KERN_EMERG "myCDevInit(): alloc_chrdev_region() success! major:%d,minor:%d
", MAJOR(myDev), MINOR(myDev));    //打印获得的主次设备号
14     
15     /*为设备描述结构分配内存*/
16     pMyCharDev= kmalloc(sizeof(struct MyCharDev), GFP_KERNEL);  
17     if(!pMyCharDev)
18     {
19         res=-ENOMEM;    //系统定义的内存不足
20         goto failMalloc;
21     }    
22     memset(pMyCharDev,0,sizeof(struct MyCharDev));
23     printk(KERN_EMERG "myCDevInit(): kmalloc() success!
");
24     
25     /*下面初始化及注册字符设备到系统中*/
26     cdev_init(&(pMyCharDev->myCDev),&myCDevOps);    //初始化struct cdev结构
27     cdev_add(&(pMyCharDev->myCDev),myDev,1);    //注册字符设备
28     printk(KERN_EMERG "myCDevInit(): cdev_init() and cdev_add() success!
myCDevInit() process success!
");
29     
30     return 0;    //一切正常返回0
31     
32 failMalloc:        //内存分配不足时跳到这里
33     unregister_chrdev_region(myDev,1);
34     return res;
35 }
模块初始化函数

a:分配设备号

  int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
     /**
 * alloc_chrdev_region() - register a range of char device numbers
 * @dev: output parameter for first assigned number
 * @baseminor: first of the requested range of minor numbers
 * @count: the number of minor numbers required
 * @name: the name of the associated device or driver
 *
 * Allocates a range of char device numbers.  The major number will be
 * chosen dynamically, and returned (along with the first minor number)
 * in @dev.  Returns zero or a negative error code.
 */
alloc_chrdev_region

这个函数就是动态分配设备号的,设备号结果就在第一个参数,其它参数含义见上。

b:为设备描述结构分配内存

  我不是自己定义了struct MyCharDev嘛,其中的struct cdev要分配内存存放,存储区域也要分配内存,kmallo()就是内核态分配内存的调用。

c:初始化及注册字符设备到系统中

  给struct MyCharDev分配内存了,也就是也给系统描述字符设备的struct cdev分配内存了,但是光分配不行啊,空当当的没数据啊,所以要初始化struct cdev,cdev_init()就是干这个事情,初始化字符设备是也绑定了对这个设备进行读写操作时内核要调用什么函数,以后对这个设备的操作就直接调用这些函数就可以了,这个结构体里面的赋值右侧是自己写的函数。这就是告诉系统一个绑定嘛。

 1 /*自定义字符设备的操作函数的结构体*/
 2 static const struct file_operations myCDevOps =   
 3 {
 4     .owner    = THIS_MODULE,  
 5     .llseek   = myCDevLlseek,
 6     .read     = myCDevRead,
 7     .write    = myCDevWrite,
 8     .open     = myCDevOpen,
 9     .release  = myCDevRelease
10 };
字符设备的操作函数

  然后把这个字符设备添加到系统:cdev_add()。函数具体使用请搜搜。

第5节、模块卸载函数干了啥:

 1 /*退出函数,当模块从内存卸载时被调用*/
 2 static void myCDevExit(void)    //void my_exit(void);
 3 {
 4     printk(KERN_EMERG "myCDevExit() process...
");  
 5     
 6     cdev_del(&(pMyCharDev->myCDev));    //注销设备
 7     kfree(pMyCharDev);    //释放设备结构体内存
 8     unregister_chrdev_region(myDev,1);    //释放设备号
 9     
10     printk(KERN_EMERG "myCDevExit() process success!
");  
11 }

其实就是与装载时相反的事情啦,分配的内存要回收吧?注册了要注销吧等。

第6节、其他的具体操作:

  上面说了我们在内存开辟了一个地方代表这是虚拟的字符设备的,对这个字符设备的读写啊什么的都是针对这个内存空间的,我的就是一个字符数组啦,所以write()、read()什么的就好写了嘛,直接装到这个数组、从这个数组读不久可以了嘛?具体看代码的注释吧,很详细了,聪明的你一定会懂。

  里面涉及到用户态和内核态的数据的交换,系统也给我们了,什么copy_from_user()之类的。

  还有那些函数的参数,这个系统规定了。

第7节、全部代码:

  1 /*包含我的电脑中已有的linux内核源代码,注意版本,我的路径:/usr/src/linux-headers-3.13.0-30/include*/
  2 #include <linux/cdev.h>
  3 #include <linux/module.h>
  4 #include <linux/types.h>
  5 #include <linux/init.h>
  6 #include <linux/errno.h>
  7 #include <linux/mm.h>
  8 #include <linux/sched.h>
  9 #include <linux/init.h>
 10 #include <linux/fs.h>
 11 #include <linux/kdev_t.h>
 12 #include <linux/slab.h>
 13 
 14 /*下面是我的字符设备驱动程序的一些定义*/
 15 #ifndef MY_DATA_LEN
 16 #define MY_DATA_LEN 8    //自定义设备描述符中数据的长度
 17 #endif
 18 
 19 /*自定义的设备描述结构体*/
 20 struct MyCharDev
 21 {
 22     struct cdev myCDev;    //struct cdev在<linux/cdev.h>中定义,描述一个字符设备
 23     char myData[MY_DATA_LEN];    //以后对设备的读写啊是对这块内存的操作,因为这是一个虚拟的设备
 24 };
 25 
 26 /*下面是几个全局变量*/
 27 struct MyCharDev * pMyCharDev;    //设备结构体指针,因为是动态分配
 28 static dev_t myDev;    //装获得的设备号,因为在多个函数里面都用到
 29 
 30 
 31 /*文件打开函数,无论一个进程何时试图去打开这个设备都会调用这个函数*/
 32 int myCDevOpen(struct inode* inode, struct file* filp)
 33 {
 34 }
 35 
 36 /*文件释放函数,当一个进程试图关闭这个设备特殊文件的时候调用这个函数*/
 37 int myCDevRelease(struct inode* inode, struct file* filp)
 38 {
 39 }
 40 
 41 
 42 /*读函数,当一个进程已经打开次设备文件以后并且试图去读它的时候调用这个函数。从filp的ppos位置读size个到用户空间的buf中*/
 43 static ssize_t myCDevRead(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
 44 {
 45     unsigned long pos =  *ppos;    /*记录文件指针偏移位置*/  
 46     unsigned int count = size;    /*记录需要读取的字节数*/ 
 47     int ret = 0;    /*返回值*/
 48     struct MyCharDev * pDev=filp->private_data;    //获得这个文件对应的相当于私有的数据
 49     
 50     /*判断读位置是否有效*/
 51     if(pos>=MY_DATA_LEN)    //要读取的偏移大于设备的内存空间,也就是我自己定义的数组
 52     {
 53         return 0;
 54     }
 55     if(pos+count>MY_DATA_LEN)    //无法满足读取这么多个字节
 56     {
 57         count=MY_DATA_LEN-pos;    //尽量多地读取
 58     }
 59     
 60     /*读数据到用户空间:内核空间->用户空间交换数据*/
 61     if(copy_to_user(buf,(void*)(pDev->myData+pos),count))
 62     {
 63         ret= -EFAULT;
 64     }
 65     else
 66     {
 67         *ppos+=count;    //把记录文件读取的位置移动到正确的位置,注意,这里可以看出*ppos范围是[0,MY_DATA_LEN]
 68         ret=count;    //返回正确读到的字符个数
 69         
 70         printk(KERN_EMERG "myCDevRead():read %d byte(s) from %d position
",count,pos);      
 71     }
 72     
 73     return ret;
 74 }
 75 
 76 
 77 /*写函数,当试图将数据写入这个设备文件的时候,这个函数被调用。把用户空间buf开始的size个写入filp对应的文件ppos位置*/
 78 static ssize_t myCDevWrite(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
 79 {
 80     unsigned long pos =  *ppos;
 81     unsigned int count = size;
 82     int ret = 0;
 83     struct MyCharDev * pDev = filp->private_data; /*获得设备结构体指针*/
 84     
 85     /*分析和获取有效的写长度*/
 86     if(pos>=MY_DATA_LEN)
 87     {
 88         return 0;
 89     }
 90     if(pos+count>MY_DATA_LEN)
 91     {
 92         count=MY_DATA_LEN-pos;
 93     }
 94     
 95     /*从用户空间写入数据*/
 96     if(copy_from_user(pDev->myData+pos,buf,count))
 97     {
 98         ret = -EFAULT;
 99     }
100     else
101     {
102         *ppos+=count;
103         ret=count;
104         
105         printk(KERN_EMERG "myCDevWrite():write %d byte(s) from %d
",count,pos);      
106     }
107     
108     return ret;
109 }
110 
111 /*seek文件定位函数,为已经打开的设备文件设置其偏移。把filp的偏移设置成从whence开始的加上offset*/
112 static loff_t myCDevLlseek(struct file *filp, loff_t offset, int whence)
113 {
114 }
115 
116 
117 /*自定义字符设备的操作函数的结构体*/
118 static const struct file_operations myCDevOps =   
119 {
120     .owner    = THIS_MODULE,  
121     .llseek   = myCDevLlseek,
122     .read     = myCDevRead,
123     .write    = myCDevWrite,
124     .open     = myCDevOpen,
125     .release  = myCDevRelease
126 };
127 
128 /*初始化函数,当模块装载时被调用,如果装载成功返回0,否则返回非0值*/
129 static int myCDevInit(void)    //int my_init(void);
130 {
131     int res;    //初始化函数的返回值
132 
133     printk(KERN_EMERG "


myCDevInit() process...
");      
134             
135     /*动态分配设备号*/
136     res=alloc_chrdev_region(&myDev,0,1,"MyCharDev");
137     if(res<0)    //表示分配设备号失败
138     {
139         return res;
140     }
141     printk(KERN_EMERG "myCDevInit(): alloc_chrdev_region() success! major:%d,minor:%d
", MAJOR(myDev), MINOR(myDev));    //打印获得的主次设备号
142     
143     /*为设备描述结构分配内存*/
144     pMyCharDev= kmalloc(sizeof(struct MyCharDev), GFP_KERNEL);  
145     if(!pMyCharDev)
146     {
147         res=-ENOMEM;    //系统定义的内存不足
148         goto failMalloc;
149     }    
150     memset(pMyCharDev,0,sizeof(struct MyCharDev));
151     printk(KERN_EMERG "myCDevInit(): kmalloc() success!
");
152     
153     /*下面初始化及注册字符设备到系统中*/
154     cdev_init(&(pMyCharDev->myCDev),&myCDevOps);    //初始化struct cdev结构
155     cdev_add(&(pMyCharDev->myCDev),myDev,1);    //注册字符设备
156     printk(KERN_EMERG "myCDevInit(): cdev_init() and cdev_add() success!
myCDevInit() process success!
");
157     
158     return 0;    //一切正常返回0
159     
160 failMalloc:        //内存分配不足时跳到这里
161     unregister_chrdev_region(myDev,1);
162     return res;
163 }
164 
165 /*退出函数,当模块从内存卸载时被调用*/
166 static void myCDevExit(void)    //void my_exit(void);
167 {
168     printk(KERN_EMERG "myCDevExit() process...
");  
169     
170     cdev_del(&(pMyCharDev->myCDev));    //注销设备
171     kfree(pMyCharDev);    //释放设备结构体内存
172     unregister_chrdev_region(myDev,1);    //释放设备号
173     
174     printk(KERN_EMERG "myCDevExit() process success!
");  
175 }
176 
177 
178 module_init(myCDevInit);    //通过module_init例程把模块入口点myCDevInit注册到系统中
179 module_exit(myCDevExit);    //由module_exit例程把模块出口函数注册到系统
180 
181 /*下面是指定模块版权、模块作者、模块简要描述信息*/
182 MODULE_LICENSE("GPL");
183 MODULE_AUTHOR("jiayith");  
184 MODULE_DESCRIPTION("jiayith->A simple virtual char device.");  
源代码
ifneq ($(KERNELRELEASE),)
obj-m := myCDev.o

else
PWD  := $(shell pwd)
KVER := $(shell uname -r)
KDIR := /lib/modules/$(KVER)/build
all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions modules.*  Module.*
endif
makefile



第五章:啰嗦与参考资历

  看完了如果要写的话,就倒着看这个博客。相信我解释清楚了。

  抄袭了:http://www.cnblogs.com/geneil/archive/2011/12/03/2272869.html

  抄袭了:http://blog.chinaunix.net/uid-254237-id-2458604.html

  抄袭了:http://blog.chinaunix.net/uid-11829250-id-337300.html

  抄袭了一大堆,希望大家不要介意哈。

原文地址:https://www.cnblogs.com/jiayith/p/3794123.html