Linux 简单字符设备驱动程序 (自顶向下)
第零章:扯扯淡
特此总结一下写的一个简单字符设备驱动程序的过程,我要强调一下“自顶向下”这个介绍方法,因为我觉得这样更容易让没有接触过设备驱动程序的童鞋更容易理解,“自顶向下”最初从《计算机网络 自顶向下方法》这本书学到的,我觉得有时候这是一种很好的方式。
第一章:测试程序
咦?你怎么跟别人的思路不一样???自顶向下嘛,我就直接从测试程序来说啦,这样那个不是更熟悉吗?看看下面的测试程序的代码,是不是很熟悉?
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h> #define MY_CDEV_NAME "/dev/mychardev"
#define BUF_LEN 16 int main(void)
{
int fd;
int ret,i;
char buf[BUF_LEN]; /*打开设备*/
fd=open(MY_CDEV_NAME,O_RDWR | O_NONBLOCK);
if(fd<)
{
printf("open %s fail!\n",MY_CDEV_NAME);
return -;
}
printf("open %s success!\n",MY_CDEV_NAME); /*设置buf的数据,以后会写进设备*/
for(i=;i<BUF_LEN;i++)
{
buf[i]=i+;
} /*写设备*/
if((ret=write(fd,buf,BUF_LEN))<)
{
printf("write %s fail!\n",MY_CDEV_NAME);
}
else
{
printf("write %s success! Write totoal:%d\n",MY_CDEV_NAME,ret);
} /*把文件偏移量设置为文件开始处*/
if((ret=lseek(fd,,SEEK_SET))<)
{
printf("lseek %s fail!\n",MY_CDEV_NAME);
}
else
{
printf("lseek %s success! Now position:%d\n",MY_CDEV_NAME,ret);
} /*读设备*/
if((ret=read(fd,buf+BUF_LEN/,BUF_LEN))<)
{
printf("read %s fail!\n",MY_CDEV_NAME);
}
else
{
printf("read %s success! Read totoal:%d\n",MY_CDEV_NAME,ret);
} for(i=;i<BUF_LEN;i++)
{
printf("buf[%d]:%c\n",i,buf[i]);
} close(fd); return ;
}
最终测试代码
这里其实不就是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的。但是程序是在内核态运行的而非用户态,所以需要包含的头文件不一样了。看:
/*包含我的电脑中已有的linux内核源代码,注意版本,我的路径:/usr/src/linux-headers-3.13.0-30/include*/
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
头文件
这些头文件按需要包含,我的系统自己就有这个操作系统的源代码,上面说明了。其实在/usr/src文件夹下还有其他版本的内核源代码,因为我的ubuntu总是更新到最新。
第2节、模块装载与卸载时会调用的函数:
一个模块装载到内核时会执行一个函数的,这个函数是你自己写的,然后告诉系统一下就行了,就是说这样:嗨,系统兄弟,把我这个模块加载到内核时执行的函数是这个哦,记住哈。同样把模块从内核卸载也会调用一个特定函数,你只要写好告诉它一下就好了。函数原型如下:
int my_init(void);
void my_exit(void);
所以你可以自己写自己希望驱动程序被加载到内核和从中卸载时执行的函数,我的如下:
/*初始化函数,当模块装载时被调用,如果装载成功返回0,否则返回非0值*/
static int myCDevInit(void) //int my_init(void);
{
int res; //初始化函数的返回值 printk(KERN_EMERG "\n\n\nmyCDevInit() process...\n"); /*动态分配设备号*/
res=alloc_chrdev_region(&myDev,,,"MyCharDev");
if(res<) //表示分配设备号失败
{
return res;
}
printk(KERN_EMERG "myCDevInit(): alloc_chrdev_region() success! major:%d,minor:%d\n", MAJOR(myDev), MINOR(myDev)); //打印获得的主次设备号 /*为设备描述结构分配内存*/
pMyCharDev= kmalloc(sizeof(struct MyCharDev), GFP_KERNEL);
if(!pMyCharDev)
{
res=-ENOMEM; //系统定义的内存不足
goto failMalloc;
}
memset(pMyCharDev,,sizeof(struct MyCharDev));
printk(KERN_EMERG "myCDevInit(): kmalloc() success!\n"); /*下面初始化及注册字符设备到系统中*/
cdev_init(&(pMyCharDev->myCDev),&myCDevOps); //初始化struct cdev结构
cdev_add(&(pMyCharDev->myCDev),myDev,); //注册字符设备
printk(KERN_EMERG "myCDevInit(): cdev_init() and cdev_add() success!\nmyCDevInit() process success!\n"); return ; //一切正常返回0 failMalloc: //内存分配不足时跳到这里
unregister_chrdev_region(myDev,);
return res;
}
模块初始化函数
/*退出函数,当模块从内存卸载时被调用*/
static void myCDevExit(void) //void my_exit(void);
{
printk(KERN_EMERG "myCDevExit() process...\n"); cdev_del(&(pMyCharDev->myCDev)); //注销设备
kfree(pMyCharDev); //释放设备结构体内存
unregister_chrdev_region(myDev,); //释放设备号 printk(KERN_EMERG "myCDevExit() process success!\n");
}
模块卸载时调用
具体在函数里面干啥了再说,看不懂没关系。怎么告诉系统我的这模块装载和卸载时是调用这两个呢?通过下面的宏:
module_init(myCDevInit); //通过module_init例程把模块入口点myCDevInit注册到系统中
module_exit(myCDevExit); //由module_exit例程把模块出口函数注册到系统
这样就告诉操作系统了,这两个宏也是在某个源代码文件定义的,我还没找到……
下面的一些宏也是告诉系统一些信息的:
/*下面是指定模块版权、模块作者、模块简要描述信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiayith");
MODULE_DESCRIPTION("jiayith->A simple virtual char device.");
第3节、一些数据类型
首先先不看初始化及卸载函数都干了啥,先看看定义了什么数据类型。看:
/*下面是我的字符设备驱动程序的一些定义*/
#ifndef MY_DATA_LEN
#define MY_DATA_LEN 8 //自定义设备描述符中数据的长度
#endif /*自定义的设备描述结构体*/
struct MyCharDev
{
struct cdev myCDev; //struct cdev在<linux/cdev.h>中定义,描述一个字符设备
char myData[MY_DATA_LEN]; //以后对设备的读写啊是对这块内存的操作,因为这是一个虚拟的设备
}; /*下面是几个全局变量*/
struct MyCharDev * pMyCharDev; //设备结构体指针,因为是动态分配
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节、模块装载初始化函数干了啥:
static int myCDevInit(void) //int my_init(void);
{
int res; //初始化函数的返回值 printk(KERN_EMERG "\n\n\nmyCDevInit() process...\n"); /*动态分配设备号*/
res=alloc_chrdev_region(&myDev,,,"MyCharDev");
if(res<) //表示分配设备号失败
{
return res;
}
printk(KERN_EMERG "myCDevInit(): alloc_chrdev_region() success! major:%d,minor:%d\n", MAJOR(myDev), MINOR(myDev)); //打印获得的主次设备号 /*为设备描述结构分配内存*/
pMyCharDev= kmalloc(sizeof(struct MyCharDev), GFP_KERNEL);
if(!pMyCharDev)
{
res=-ENOMEM; //系统定义的内存不足
goto failMalloc;
}
memset(pMyCharDev,,sizeof(struct MyCharDev));
printk(KERN_EMERG "myCDevInit(): kmalloc() success!\n"); /*下面初始化及注册字符设备到系统中*/
cdev_init(&(pMyCharDev->myCDev),&myCDevOps); //初始化struct cdev结构
cdev_add(&(pMyCharDev->myCDev),myDev,); //注册字符设备
printk(KERN_EMERG "myCDevInit(): cdev_init() and cdev_add() success!\nmyCDevInit() process success!\n"); return ; //一切正常返回0 failMalloc: //内存分配不足时跳到这里
unregister_chrdev_region(myDev,);
return res;
}
模块初始化函数
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()就是干这个事情,初始化字符设备是也绑定了对这个设备进行读写操作时内核要调用什么函数,以后对这个设备的操作就直接调用这些函数就可以了,这个结构体里面的赋值右侧是自己写的函数。这就是告诉系统一个绑定嘛。
/*自定义字符设备的操作函数的结构体*/
static const struct file_operations myCDevOps =
{
.owner = THIS_MODULE,
.llseek = myCDevLlseek,
.read = myCDevRead,
.write = myCDevWrite,
.open = myCDevOpen,
.release = myCDevRelease
};
字符设备的操作函数
然后把这个字符设备添加到系统:cdev_add()。函数具体使用请搜搜。
第5节、模块卸载函数干了啥:
/*退出函数,当模块从内存卸载时被调用*/
static void myCDevExit(void) //void my_exit(void);
{
printk(KERN_EMERG "myCDevExit() process...\n"); cdev_del(&(pMyCharDev->myCDev)); //注销设备
kfree(pMyCharDev); //释放设备结构体内存
unregister_chrdev_region(myDev,); //释放设备号 printk(KERN_EMERG "myCDevExit() process success!\n");
}
其实就是与装载时相反的事情啦,分配的内存要回收吧?注册了要注销吧等。
第6节、其他的具体操作:
上面说了我们在内存开辟了一个地方代表这是虚拟的字符设备的,对这个字符设备的读写啊什么的都是针对这个内存空间的,我的就是一个字符数组啦,所以write()、read()什么的就好写了嘛,直接装到这个数组、从这个数组读不久可以了嘛?具体看代码的注释吧,很详细了,聪明的你一定会懂。
里面涉及到用户态和内核态的数据的交换,系统也给我们了,什么copy_from_user()之类的。
还有那些函数的参数,这个系统规定了。
第7节、全部代码:
/*包含我的电脑中已有的linux内核源代码,注意版本,我的路径:/usr/src/linux-headers-3.13.0-30/include*/
#include <linux/cdev.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/slab.h> /*下面是我的字符设备驱动程序的一些定义*/
#ifndef MY_DATA_LEN
#define MY_DATA_LEN 8 //自定义设备描述符中数据的长度
#endif /*自定义的设备描述结构体*/
struct MyCharDev
{
struct cdev myCDev; //struct cdev在<linux/cdev.h>中定义,描述一个字符设备
char myData[MY_DATA_LEN]; //以后对设备的读写啊是对这块内存的操作,因为这是一个虚拟的设备
}; /*下面是几个全局变量*/
struct MyCharDev * pMyCharDev; //设备结构体指针,因为是动态分配
static dev_t myDev; //装获得的设备号,因为在多个函数里面都用到 /*文件打开函数,无论一个进程何时试图去打开这个设备都会调用这个函数*/
int myCDevOpen(struct inode* inode, struct file* filp)
{
} /*文件释放函数,当一个进程试图关闭这个设备特殊文件的时候调用这个函数*/
int myCDevRelease(struct inode* inode, struct file* filp)
{
} /*读函数,当一个进程已经打开次设备文件以后并且试图去读它的时候调用这个函数。从filp的ppos位置读size个到用户空间的buf中*/
static ssize_t myCDevRead(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
unsigned long pos = *ppos; /*记录文件指针偏移位置*/
unsigned int count = size; /*记录需要读取的字节数*/
int ret = ; /*返回值*/
struct MyCharDev * pDev=filp->private_data; //获得这个文件对应的相当于私有的数据 /*判断读位置是否有效*/
if(pos>=MY_DATA_LEN) //要读取的偏移大于设备的内存空间,也就是我自己定义的数组
{
return ;
}
if(pos+count>MY_DATA_LEN) //无法满足读取这么多个字节
{
count=MY_DATA_LEN-pos; //尽量多地读取
} /*读数据到用户空间:内核空间->用户空间交换数据*/
if(copy_to_user(buf,(void*)(pDev->myData+pos),count))
{
ret= -EFAULT;
}
else
{
*ppos+=count; //把记录文件读取的位置移动到正确的位置,注意,这里可以看出*ppos范围是[0,MY_DATA_LEN]
ret=count; //返回正确读到的字符个数 printk(KERN_EMERG "myCDevRead():read %d byte(s) from %d position\n",count,pos);
} return ret;
} /*写函数,当试图将数据写入这个设备文件的时候,这个函数被调用。把用户空间buf开始的size个写入filp对应的文件ppos位置*/
static ssize_t myCDevWrite(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
unsigned long pos = *ppos;
unsigned int count = size;
int ret = ;
struct MyCharDev * pDev = filp->private_data; /*获得设备结构体指针*/ /*分析和获取有效的写长度*/
if(pos>=MY_DATA_LEN)
{
return ;
}
if(pos+count>MY_DATA_LEN)
{
count=MY_DATA_LEN-pos;
} /*从用户空间写入数据*/
if(copy_from_user(pDev->myData+pos,buf,count))
{
ret = -EFAULT;
}
else
{
*ppos+=count;
ret=count; printk(KERN_EMERG "myCDevWrite():write %d byte(s) from %d\n",count,pos);
} return ret;
} /*seek文件定位函数,为已经打开的设备文件设置其偏移。把filp的偏移设置成从whence开始的加上offset*/
static loff_t myCDevLlseek(struct file *filp, loff_t offset, int whence)
{
} /*自定义字符设备的操作函数的结构体*/
static const struct file_operations myCDevOps =
{
.owner = THIS_MODULE,
.llseek = myCDevLlseek,
.read = myCDevRead,
.write = myCDevWrite,
.open = myCDevOpen,
.release = myCDevRelease
}; /*初始化函数,当模块装载时被调用,如果装载成功返回0,否则返回非0值*/
static int myCDevInit(void) //int my_init(void);
{
int res; //初始化函数的返回值 printk(KERN_EMERG "\n\n\nmyCDevInit() process...\n"); /*动态分配设备号*/
res=alloc_chrdev_region(&myDev,,,"MyCharDev");
if(res<) //表示分配设备号失败
{
return res;
}
printk(KERN_EMERG "myCDevInit(): alloc_chrdev_region() success! major:%d,minor:%d\n", MAJOR(myDev), MINOR(myDev)); //打印获得的主次设备号 /*为设备描述结构分配内存*/
pMyCharDev= kmalloc(sizeof(struct MyCharDev), GFP_KERNEL);
if(!pMyCharDev)
{
res=-ENOMEM; //系统定义的内存不足
goto failMalloc;
}
memset(pMyCharDev,,sizeof(struct MyCharDev));
printk(KERN_EMERG "myCDevInit(): kmalloc() success!\n"); /*下面初始化及注册字符设备到系统中*/
cdev_init(&(pMyCharDev->myCDev),&myCDevOps); //初始化struct cdev结构
cdev_add(&(pMyCharDev->myCDev),myDev,); //注册字符设备
printk(KERN_EMERG "myCDevInit(): cdev_init() and cdev_add() success!\nmyCDevInit() process success!\n"); return ; //一切正常返回0 failMalloc: //内存分配不足时跳到这里
unregister_chrdev_region(myDev,);
return res;
} /*退出函数,当模块从内存卸载时被调用*/
static void myCDevExit(void) //void my_exit(void);
{
printk(KERN_EMERG "myCDevExit() process...\n"); cdev_del(&(pMyCharDev->myCDev)); //注销设备
kfree(pMyCharDev); //释放设备结构体内存
unregister_chrdev_region(myDev,); //释放设备号 printk(KERN_EMERG "myCDevExit() process success!\n");
} module_init(myCDevInit); //通过module_init例程把模块入口点myCDevInit注册到系统中
module_exit(myCDevExit); //由module_exit例程把模块出口函数注册到系统 /*下面是指定模块版权、模块作者、模块简要描述信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiayith");
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
抄袭了一大堆,希望大家不要介意哈。
Linux 简单字符设备驱动程序 (自顶向下)的更多相关文章
- Linux 简单字符设备驱动
1.hello_drv.c (1) 初始化和卸载函数的格式是固定的,函数名自定义 (2) printk是内核的打印函数,用法与printf一致 (3) MODULE_LICENSE:模块代码支持开源协 ...
- 【转】linux设备驱动程序之简单字符设备驱动
原文网址:http://www.cnblogs.com/geneil/archive/2011/12/03/2272869.html 一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用 ...
- 一个简单的演示用的Linux字符设备驱动程序
实现如下的功能:--字符设备驱动程序的结构及驱动程序需要实现的系统调用--可以使用cat命令或者自编的readtest命令读出"设备"里的内容--以8139网卡为例,演示了I/O端 ...
- 简单linux字符设备驱动程序
本文代码参考<LINUX设备驱动程序>第三章 字符设备驱动程序 本文中的“字符设备”是一段大小为PAGE_SIZE的内存空间 功能:向字符设备写入字符串:从字符设备读出字符串 代码: 1. ...
- ARM Linux字符设备驱动程序
1.主设备号和次设备号(二者一起为设备号): 一个字符设备或块设备都有一个主设备号和一个次设备号.主设备号用来标识与设备文件相连的驱动程序,用来反 映设备类型.次设备号被驱动程序用来辨别操作的是哪个 ...
- LINUX设备驱动程序笔记(三)字符设备驱动程序
<一>.主设备号和次设备号 对字符设备的訪问时通过文件系统内的设备名称进行的.那些设备名称简单称之为文件系统树的节点,它们通常位于/dev文件夹. 字符设备驱动程 ...
- Linux驱动实践:你知道【字符设备驱动程序】的两种写法吗?
作 者:道哥,10+年嵌入式开发老兵,专注于:C/C++.嵌入式.Linux. 关注下方公众号,回复[书籍],获取 Linux.嵌入式领域经典书籍:回复[PDF],获取所有原创文章( PDF 格式). ...
- 嵌入式Linux驱动学习之路(二十一)字符设备驱动程序总结和块设备驱动程序的引入
字符设备驱动程序 应用程序是调用C库中的open read write等函数.而为了操作硬件,所以引入了驱动模块. 构建一个简单的驱动,有一下步骤. 1. 创建file_operations 2. 申 ...
- 浅析Linux字符设备驱动程序内核机制
前段时间在学习linux设备驱动的时候,看了陈学松著的<深入Linux设备驱动程序内核机制>一书. 说实话.这是一本非常好的书,作者不但给出了在设备驱动程序开发过程中的所须要的知识点(如对 ...
随机推荐
- Android 浅谈相机研发
在android中应用相机功能,一般有两种:一种是直接调用系统相机,一种自己写的相机. 我将分别演示两种方式的使用: 第一种:是使用Intent跳转到系统相机,action为:andro ...
- iOS开发——高级UI&带你玩转UITableView
带你玩装UITableView 在实际iOS开发中UITableView是使用最多,也是最重要的一个控件,如果你不会用它,那别说什么大神了,菜鸟都不如. 其实关于UItableView事非常简单的,实 ...
- MYSQL 备份工具
backup of a database is a very important thing. If no backup, meet the following situation goes craz ...
- python 2.5源代码编绎
VS C++项目中,选择工程项名称,右菜单中选择--->仅适用于项目---->仅生成(项目) 1.make_buildinfo,make_versioninfo make_buildinf ...
- mysql索引需要了解的几个注意
板子之前做过2年web开发培训(入门?),获得挺多学生好评,这是蛮有成就感的一件事,准备花点时间根据当时的一些备课内容整理出一系列文章出来,希望能给更多人带来帮助,这是系列文章的第一篇 注:科普文章一 ...
- Helpers\Tags
Helpers\Tags The tags helper is a collection of useful methods: Tags::clean($data) Clean function to ...
- 你真的会用UITableView嘛
UITableView是工程开发中最经常使用到的UI控件,但是你真的了解它嘛,这里记录几点有用的但你可能并不知道的. 当我们的数据未能显示满一屏幕的时候,UITableView会显示多余的横线,这个时 ...
- SSIS 学习(4):变量和表达式【转】
变量,作为程序员的我们,是一个多么熟悉的概念,从开始学习编程的第一天起,就要了解什么是变量?如何定义一个变量?变量的使用范围等.现在还有必 要在这里大费口舌来讨论“变量”吗?不错,我们今天谈的变量,也 ...
- [改善Java代码]使用静态内部类提高封装性
建议38: 使用静态内部类提高封装性 Java中的嵌套类(Nested Class)分为两种:静态内部类(也叫静态嵌套类,Static Nested Class)和内部类(Inner Class).内 ...
- 关于HashMap根据Value获取Key
关于我对java中集合的总结有如下三篇: 关于JDK中的集合总结(一) 关于JDK中的集合总结(二) 关于JDK中的集合总结(三) 关于数组集合之间的转换 Map中是一个key有且只有一个value. ...