驱动学习第2天 之 最简单的字符设备驱动testchr

我不是说scull简单,即使是一个纯软件的scull,对于我这个菜鸟来说还是很有挑战意义的,毕竟基本上所有内核模块的特性它都具备,真正的“麻雀虽小,五脏俱全”啊!

第2天,我们要实现一个只能完成基本的读写操作的设备文件/dev/scull,要求是只在单进程访问的情况下能够正常地打开它,并进行读写操作。为了表示咱的设备不同于LDD实现的scull,咱取名叫testchr。

 

注:本文按照便于理解的思路来描述,实际代码工作请读完全文在自己理解的基础上去写。

 

1. 生成/dev/testchr

进入/dev,我们看到很多设备文件,包括常见的/dev/zero,/dev/null,/dev/tty等等。这些文件是什么?怎么生成的?首先,我们就来创建一个这个的文件。

    其实,很简单!只要一条命令就搞定:   

mknod /dev/testchr c $major $minor

    这里的$major和0分别是设备的主设备号和此设备号。设备号相当于设备的身份ID,并且分成两部分:前半部分为主设备号,相同类型的设备可能具有相同的设备号;后半部分为此设备号,用来标识此设备的唯一性。至于一个设备号是多少位,其中多少位用来表示主设备号,多少位用来表示此设备号,这个问题不用我们操心,内核已经为我们提供了三个宏:

MAJOR(devno); //获得主设备号
MINOR(devno); //获得次设备号
MKDEV(major, minor); //组合主设备号和次设备号,生成一个设备号

    继续回到那条命令,这里你要注意$major变量不能跟系统已经存在的设备文件重复(怎么看?打开/proc/devices文件,每一行就是一个设备文件,第一列的数字就是这里的$major)。

    当然了,这个文件还啥都干不了,因为这个设备的主设备号和此设备号只有我们知道,内核并不知道!怎么让内核和我们都知道呢?那只能让内核告诉我们一个设备号啦!

 

2. 分配设备号

    设备号相当于我们打开内核驱动的钥匙,当然首先要从内核处获得一个钥匙!

获得设备号,我们用下面这个函数(<linux/fs.h>): 

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

    dev_t是设备号的类型,一般是32位值,其中12位表示主设备号,20位表示次设备号。 

 dev_t dev;
 alloc_chrdev_region(&dev, 0, 0,“scull”);

这样,我们就获得了设备号,保存在了内核变量dev中。怎么让内核把这个变量的值告诉我们呢?还记得我们之前通过/proc/devices来查看系统已经存在的设备号了吗?怎样让我们的设备号也出现在这个文件里呢?注册它。

 

3. 注册字符设备

脑补一下,一个设备这么复杂(不复杂我们怎么学的这么纠结。。),在内核里肯定是一个很复杂很复杂的struct,占据了一大块内存。至于我们的设备号,应该只是它的一个成员变量而已。我们从内核得到设备号之后,相当于买了把锁,现在要把这把锁装到这个struct上,这样我们(指应用程序)才能使用“钥匙”进入内核驱动啊!

    没错!这个struct就是传说的中inode和cdev!(<linux/fs.h>)

    struct inode {
       …
       dev_t i_rdev;
       struct cdev *i_cdev;
       …
    };

每个文件在内核里均有一个inode结构,记录有关该文件的所有的静态的信息。针对不同的文件,例如,我们的字符设备文件,还有一个“分档案”,每个字符设备文件均有一个cdev结构,包含一些字符设备文件的特性。因此,我们的testchr要向内核申请一个cdev(LDD中的scull一下子“申请”了4个设备,scull0~scull3)。

接下来,向内核申请一个struct cdev:(linux/cdev.h): 

void cdev_init(struct cdev *cdev, struct file_operations *fops);

    struct file_operations包含一系列的函数指针,每个指针指向一个对文件的操作,例如open, read, write等等。这里的函数是内核空间的函数,跟系统调用不一样,一般同名的系统调用进入内核后最终都会调用这些内核函数。

    看来内核不仅给我们批了一块地,还附带初始化了我们cdev结构里的fops变量啊!(这里有个疑问,struct cdev有个struct file_operations *变量,而struct inode也有一个struct file_operations *变量,貌似重复了啊。。。有空要读一下cdev_init源码看看对struct file_operations *怎么处理的)

    好了,我们现在cdev有了(房子有了),设备号有了(钥匙跟锁有了),接下来只要把设备号和cdev绑定了(把锁装到房子上),就ok了:

 int cdev_add(struct cdev *dev, dev_t num, unsignedint count);   

    若调用成功,那么我们的设备应该就能够出现在/proc/devices里了!

 

4.设计testchr内存模型

    在/proc/devices里出现了我们的设备,相当于我们已经成功地在内核里开辟了一块属于我们字符设备testchr的内存区域。当然,现在我们的testchr还是属于“毛坯房”,什么功能都没有,在用户层还不能使用它。接下来,就要实现几个主要的功能(简单装修)。

如何实现testchr,这是一个策略问题。我们要实现的testchr能够对某块内存区域进行读和写操作,这是我们的目的。怎么申请内存,申请多大的内存,以及怎么对申请到的内存进行管理,这都是策略问题。LDD中的scull采用的策略是,以量子(4kb)为单位,动态分配内存,在需要的时候申请,不需要的时候回收。这里,我们采用的设计方案是:testchr只能操纵一块连续的4kb内存(即一个4kb的字符数组),且是静态分配的。毕竟我们的目的是熟悉驱动架构,而非实现高性能的字符设备驱动。

    testchr的数据结构如下:    

struct testchr_dev {
    unsigned char data[MAXLINE]; //存放数据的内存区域
    unsigned int size; //当前已使用大小
    struct cdev cdev; //字符设备cdev
};

    其中,data指向内存区域,size表明当前已用字节数。cdev是内核字符设备结构。这就相当于,我家里有个cdev,我就叫字符设备。 

好了,现在我们可以实现我们的初始化部分的代码了

 

5. 实现open,read和write

看了我们的testchr的内存模型,就知道我们的read和write实现很简单啦!

(1)testchr_open

    LDD说,如果没有实现open,那么默认设备打开总是成功。至于打开是什么样子,那就不知道了。所以,我们不是一定要实现open函数,但实现open函数确实能给我们带来一些便利。比如,可以设置一些东西。

    所有打开的文件在内核里都有一个结构体file(<linux/fs.h>),它包含一些文件的动态信息(inode是静态信息),包括文件的访问权限(f_mode)、当前偏移(f_pos)、文件打开标志(f_flags)、文件关联操作f_op和一个private_data。   

int testchr_open(struct inode *inode, structfile *filp)
{
    struct testchr_dev *dev;
 
    dev = container_of(inode->i_cdev,struct testchr_dev, dev);
    filp->private_data = dev;
 
    if ((filp->f_flags & O_ACCMODE) ==O_WRONLY) {
        dev->size = 0;
        memset(dev->data, 0,sizeof(MAXLINE));
    }
    printk(KERN_NOTICE “testchr: opensuccess\n”);
    return 0;
}

    首先看open的形参列表,一个inode指针和一个file指针。inode表示文件的静态信息,主要是文件的内容;file则表示动态信息。我们的testchr的数据是保存在testchr_dev->data里的,因此需要首先获得testchr_dev指针。应对这种情况,struct A 包含有struct B,在知道指向B的指针的情况下,内核又为我们提供了一个函数,来获得指向A的指针。这里我们根据inode->i_cdev指针来获得指向testchr_dev的指针:

dev = container_of(inode->i_cdev, structtestchr_dev, dev);

观察struct file_operations里的操作函数,不难发现,几乎所有的函数的形参都是指向inode的指针。这不奇怪,因为所有的文件在内核里都是用inode来表示。针对我们的字符设备,我们想要的是指向testchr_dev的指针,所以,这里,我们可以把好不容易获得的指向testchr_dev的指针保存在private_data里。这样,在其它的操作函数里,我们直接可以引用了:

filp->private_data = dev;

最后,若我们的设备打开是为了只写的。我们期望从头写,并覆盖掉原来的内容。所以,我们将size设为0,并初始化内存为0。

if ((filp->f_flags &O_ACCMODE) == O_WRONLY) {
    dev->size = 0;
    memset(dev->data, 0,sizeof(MAXLINE));
}

这里有个疑惑的地方,file结构里关于文件权限有两个字段:f_flags和f_mode,这两个有什么区别?什么时候用f_flags,什么时候用f_mode?f_flags只是单纯的一些标志,比如O_WRONLY, O_RDONLY, O_RDWR, O_SYNC,O_NONBLOCK,通常是在系统调用open的时候指定;而f_mode则包含更多的信息,比如什么用户具备什么权限(读/写)。若要检查对文件是否有读或写的权限,检查f_mode;若判断文件本次打开的时候指定了什么标志,检查f_flags。

最后,若成功打开,则printk一下。刚开始写驱动,printk便于调试,定位错误出现在哪。

 

(2)testchr_read和testchr_write

    很easy,因为我们的testchr只包含一个字符数组,所以读写操作就是简单的内存复制,只不过用内核特有的复制函数:

unsigned long copy_to_user(void *to, constvoid *from, unsigned long count);
unsigned long copy_from_user(void *to, constvoid *from, unsigned long count);

    这里有个问题需要注意,read和write的最后一个参数是loff_t *offp。在read/write执行前,这个参数由内核传递来(取自file结构),表示该文件读/写开始位置;当read/write执行完后,内核会根据offp再更新file的偏移指针。因此在read或write的实现中,记得更新*offp。

 

6. 测试设备

    走到这一步,就快要大功告成了!

    首先,来写个Makefile文件:

 # Makefile
 obj-m := testchr.o

    执行make:   

 $ make –C ~/src/linux-2.6.18.8 M=`pwd`

    装载设备:    

# insmod testchr.ko

    查看输出的信息:    

$ cat /var/log/messages | grep testchr

    创建设备节点/dev/testchr:    

$ major = `cat /proc/devices | grep testchr| awk ‘{print $1}’`
$ mknod /dev/testchr c $major 0

    测试设备:   

 $ cat > /dev/testchr
 Hello, world!
 ^D
 $ cat /dev/testchr

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值