ldd3-3 字符驱动程序

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ldd3-3 字符驱动程序相关的知识,希望对你有一定的参考价值。

【scull的设计】

【主设备号和次设备号】

ls -l /dev/

技术分享

主设备号:标识设备对应的驱动程序。

次设备号:用于确定设备文件所指的设备。

【设备编号的内部表达】

dev_t类型 /linux/types.h

技术分享

获取主设备号和次设备号 /linux/kdev_t.h

MAJOR(dev_t dev);

MAJOR(dev_t dev);

在source insight的符号列表窗口中搜索MAJOR可以找到这个宏的定义:

技术分享

在source insight的引用查找中,可以搜到使用MAJOR宏的范例程序,可以用来学习如何使用:

技术分享

将主设备号和次设备号转换成dev_t类型,见上面的截图

MKDEV(int major, int minor);

【分配和释放设备编号】也就是先要腾个地方

linux/fs.h

fs/char_dev.c

register_chrdev_region 相当于排队取号占个座位

技术分享

name 将出现在 /proc/devices(从中读取到设备号)和 sysfs(通常挂载在/sys上,可以更好地获取设备信息)

? ?

动态分配主设备号alloc_chrdev_region

技术分享

释放设备编号unregister_chrdev_region

技术分享

? ?

大部分常见设备清单Documentation/devices.txt

技术分享

? ?

【动态分配主设备号】

/proc目录

Linux系统上的/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息(这个怎么查到的,没看出来有进程信息啊?),甚至可以通过更改其中某些文件来改变内核的运行状态。

/dev目录下创建设备文件

cat /proc/devices 一旦分配了设备号,可以从这个操作读取到注册的设备(模块名???)和前面的主设备号:

技术分享

2016年9月10日15:04:07

被老师提醒了下,感觉复习这种事情和兴趣不同,你得抓住关键点,正如读懂新闻抓关键词一样,这是正确的方法。

自从上断开第三章的学习,居然过去8天了,就是因为中途去学习awk用法了,实在不应该这么个学法了。

现有 /proc/devices 下的 主设备号 模块名,然后才创建对应的设备文件到/dev/****

判断是否存在某个变量在文件中:

技术分享

模块装载基本scull_load.sh

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

#!/bin/sh

#?=两边不能有空格shell

#?字符要加上双引号

module="scull"

device="scull"

mode="664"

? ?

#?这个脚本必须由超级用户执行

#?使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定模块位置,

#?这是因为新的modutils默认不会在当前目录中查找模块

/sbin/insmod?./$module.ko?$*?||?exit?1

? ?

#?删除原有节点

rm?-f?/dev/$${device}[0-3]

? ?

#?在调用insmod后读取/proc/devices以获得新分配的主设备号,然后创建对应的设备文件

major=$(awk?"\$2==\"$module\"{print?\$1}"?/proc/devices)

? ?

#?/proc/devices中获取信息,并在/dev目录中创建对应的设备文件

#?也就是创建了4个设备

mknod?/dev/${device}0?c?$major?0

mknod?/dev/${device}1?c?$major?1

mknod?/dev/${device}2?c?$major?2

mknod?/dev/${device}3?c?$major?3

? ?

#?给定适当的组属性及许可,并修改属组

#?并非所有的发行版都具有staff组,有些组有wheel

group="staff"

#?书的写法好像没用

#?grep?-q?‘^staff:‘?/etc/group?||?group="wheel"

if?[?!?-n?"$(grep?‘staff:‘?/etc/group)"?];?then

????group="wheel"

fi

? ?

#?赋予一个用户组特定的权限来访问设备文件

#?脚本默认地把访问权限赋予一个用户组,而读者需求可能不同

chgrp?$group?/dev/${device}[0-3]

chmod?$mode?/dev/${device}[0-3]?

接着写scull.c里面的获取主设备号的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

#include?<linux/init.h>

#include?<linux/module.h>

#include?<linux/fs.h>

#include?"scull.h"

? ?

MODULE_LICENSE("Dual?BSD/GPL");

? ?

dev_t?dev;

int?scull_major?=?SCULL_MAJOR;

int?scull_minor;

int?scull_nr_devs?=?4;

int?result;

? ?

static?int?scull_init(void)

{

????printk(KERN_ALERT?"Hello,?world!\n");

????//?获取主设备号的方法,SCULL_MAJOR

????//?如果scull_major不为0,说明不是动态分配的,则手动分配

????if?(scull_major)?{

????????//?将主设备号和次设备号转换成专用的dev_t类型

????????dev?=?MKDEV(scull_major,?scull_minor);

????????result?=?register_chrdev_region(dev,?scull_nr_devs,?"scull");

????}?else?{

????????result?=?alloc_chrdev_region(&dev,?scull_minor,?scull_nr_devs,

????????????????"scull");

????????scull_major?=?MAJOR(dev);

????????if?(result?<?0)?{

????????????printk(KERN_WARNING?"scull:?can‘t?get?major?%d\n",?scull_major);

????????????return?result;

????????}

????}

????return?0;

}

? ?

static?void?scull_exit(void)

{

????printk(KERN_ALERT?"Goodbye,?cruel?world!\n");

????unregister_chrdev_region(dev,?scull_nr_devs);

}

? ?

module_init(scull_init);

module_exit(scull_exit);?//?不要把module写错了

把必要的scull.h写一下:

1

2

3

4

5

6

#ifndef?__SCULL_H__

#define?__SCULL_H__

? ?

#define?SCULL_MAJOR?0

? ?

#endif

Makefile好像又忘记了,再写一遍:

这里再复习一下吧,如何把C语言格式的驱动源文件编译成能够装载到内核的可执行模块。

【编译模块】

编译工具:Documentation/Changes

编译工具版本:内核源代码对编译器做了大量假定,因此新的编译器版本可能导致问题的出现。

编译前提条件:配置并构造了内核树(最好运行和模块对应的内核)

内核构造系统(Documentation/kbuild)处理了编译模块的特殊方式

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

ifeq?($(KERNELRELEASE),)

????KERNELDIR??=?/lib/modules/$(shell?uname?-r)/build

????PWD?:=?$(shell?pwd)

? ?

modules:

????$(MAKE)?-C?$(KERNELDIR)?M=$(PWD)?modules

? ?

modules_install:

????$(MAKE)?-C?$(KERNELDIR)?M=$(PWD)?modules_install

? ?

clean:

????rm?-rf?*.o?*~?core?.depend?.*.cmd?*.ko?*.mod.c?.tmp_versions/

? ?

.PHONY:?modules?modules_install?clean

? ?

else

????obj-m?:=?scull.o

endif

? ?

【装载和卸载模块】

先写个自己能用的版本吧,不要把程序改的一样了。

运行出错了:

技术分享

意思是说脚本文件不能识别,How can I resolve the error "cannot execute binary file"?,因为我是在win7下写的然后拷贝过去的,估计是.sh格式文件被win7动过手脚了,所以Linux无法识别,于是我重建了在Linux下,然后运行:

技术分享

结果又提示没有权限,看了直接运行.sh脚本需要文件有可执行的属性:

技术分享

OK,成功了!因为刚才已经加载过了,所以提示不能重复安装,下面把这个模块卸载了,因为虽然安装了模块,但是没有创建设备文件:

技术分享

查看下系统给驱动程序分的位置/proc/devices主设备号:

技术分享

设备文件其实是可以任意创建的,但是如果没有驱动模块,创建时没有意义的:

技术分享

把自己胡乱创建的多余设备文件删除:

技术分享

把卸载脚本也写一下,不然不能删除设备文件了,scull_unload.sh不知道为什么多了那么多文件,把没用的先删了吧

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#!/bin/sh

module="scull"

device="scull"

? ?

#?invoke?rmmod?with?all?arguments?we?got

/sbin/rmmod?$module?$*?||?exit?1

? ?

#?Remove?stale?nodes

? ?

rm?-f?/dev/${device}?/dev/${device}[0-3]?

rm?-f?/dev/${device}priv

rm?-f?/dev/${device}pipe?/dev/${device}pipe[0-3]

rm?-f?/dev/${device}single

rm?-f?/dev/${device}uid

rm?-f?/dev/${device}wuid

卸载驱动:

技术分享

dmesg可以查看到printk的打印信息!

【一些重要的内核数据结构】

? ?

【文件操作】

驱动程序组件

连接

设备编号

file_operations

层层指针就好比公司里老板管经理,经理管主管,主管管员工,员工就是函数体,而上层的管理是负责对外的联系。

如果领导的下属都辞职了,那么领导管理的就是NULL

标记化的结构体初始化方法

重要文件:fs.h 这个文件存放了关于文件系统的重要数据结构

技术分享

? ?

【file_operations数据结构,存放文件操作的管理者(函数指针)】大当家

技术分享

下面这是4.0.9版本的内核,可以发现ioctl被分成了两个类型的ioctl,暂时不知道为什么?

技术分享

对比下2.0.10版本的内核:

技术分享

被初始化如下的形式,把代码添加到scull.c文件中,截图如下:

? ?

技术分享

? ?

【file结构】设备驱动程序所使用的第二个最重要的数据结构

技术分享

本来看不懂的,看到后面看完全书就懂了,但是有时就是忍不住停下来,结果就放弃了。

技术分享

【iNode结构】驱动设备驱动程序中要用到的数据结构的三当家

技术分享

这是struct inode的部分截图:

技术分享

inode结构中包含了大量有关文件的信息,inode可以指向任意类型的文件包含了特殊的设备文件会有特殊的数据域为其服务。

dev_t i_rdev;对于表示设备文件的inode结构,该字段包含了真正的设备编号。

宏函数某种程度上就是对象的方法封装,用于保护对象的数据成员不被破坏(可移植性准确地讲)。

struct cdev *i_cdev; 是表示字符设备的内核的内部结构。

? ?

【字符设备的注册】

每抽象出一层,就要把机制套用一遍,然后上层管理下层???说的莫名其妙

? ?

【字符设备的注册】

从系统中移除一个字符设备,在将cdev结构传递到cdev_del函数后,就不应该再访问cdev结构了:

技术分享

? ?

【Scull中的设备注册】

struct cdev:内核和设备间的接口。

什么是注册?

注册对应分配,比如房产商现在开发了一堆空房子就等于分配了一个对象,但是对象必须卖出去才是目的,卖出去就是把房子注册个社会上的某个人,然后这个人才能用这个房子来安家来工作。

开始编码:

在scull.h中添加设备结构体来表示scull设备???和字符设备什么关系???

技术分享

接着:

技术分享

接着:

技术分享

【早期的办法】

? ?

【open和release】

【open方法】

long long 是GCC类型?

int (*open)(struct inode *inode, struct file *filp); /* 因为设备就是文件inode,打开设备就是对应一个file对象 */

上面参数里的inode是什么东西?要自己定义吗还是系统自动创建的结构?

inode可以理解为对应具体的物理设备(也可以是模拟的),而当inode指向一个字符设备的时候,i_cdev字段就包含了指向struct cdev结构的指针。

那么之前的scull_dev设备又是什么鬼?可以把它理解为scull模块(设备的保姆),底下由多个不同类型设备组成。而cdev就是其中一个,现在我们不需要底层cdev,而要稍微上一层的scull_dev。cdev结构被嵌入到自己的设备特定结构scull_dev中。

技术分享

【release方法】

技术分享

【scull的内存使用】

内存管理的两个核心函数

<linux/slab.h>

void *kmalloc(size_t size, int flags);

void kfree(void *ptr);

kmalloc不适用于分配大的内存区域

? ?

为量子quantum和量子集qset选择合适的数值是一个策略而非机制问题,而且最优值依赖于如何使用设备。

策略:

  1. 编译时直接修改头文件;
  2. 模块加载时传递参数;
  3. 运行时,调用ioctrl函数;

技术分享

定义全局变量scull.c:

技术分享

内部表示scull设备的scull_dev结构,该结构的quantum和qset字段分别保存设备的量子和量子集大小:scull.h

技术分享

? ?

/* scull_trim函数负责释放整个数据区域,并且在文件以写入方式打开时由scull_open调用

它简单地遍历链表,并释放所有找到的量子和量子集

*/

scull.c

技术分享

? ?

【read和write函数】

可重入是什么鬼?

<asm/uaccess.h>

unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);

unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);

PCI、SPI以及I2C接口其实都是硬件接口总线的一种,可以简单的认为类似软件协议,实现两种芯片之间的数据能够相互承认,接口的区别主要在于接口速率、管脚数量等,和USB、RS232/422接口等是类似的;

PCI总线一般是用FPGA或专用的PCI桥实现,SPI/I2C接口其实可以用CPU的通用IO管教(CPU内部可灵活配置其功能的管脚)采用标准时序来模拟的。

【read方法】

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

/*?read方法?*/

ssize_t?scull_read(struct?file?*filp,?char?__user?*buf,?size_t?count)

{

????struct?scull_dev?*dev?=?filp->privata_data;? /*?通过file结构获取设备结构指针?*/

????struct?scull_qset?*dptr;? /*?第一个链表项?*/

????int?quantum?=?dev->quantum,?qset?=?dev->qset;

????int?itemsize?=?quantum?*?qset; /*?链表项中有多少个字节,也就是每个链表元素对应的量子总数?*/

????int?item,?s_pos,?q_pos,?rest;

????ssize_t?retval?=?0;

????? ?

????if?(down_interruptible(&dev->sem))

????????return?-ERESTARTSYS;

????? ?

????/*?这两个if相当于有人敲门,你通过猫眼判断一下?*/

????if?(*f_pos?>=?dev->size)

????????got?out;

????if?(*f_pos?+?count?>?dev_size)

????????count?=?dev->size?-?*f_pos;

????? ?

????/*?在量子集中寻找链表项、qset索引以及偏移量?*/

????item?=?(long)*f_pos?/?itemsize; /*?链表项编号,从0开始?*/

????rest?=?(long)*f_pos?%?itemsize;

????s_pos?=?rest?/?quantum;

????q_pos?=?rest?%?quantum;

????? ?

????/*?沿该链表前行,直到正确的位置(在其他地方定义)?*/

????dptr?=?scull_follow(dev,?item);? /*?获取链表项指针?*/

????? ?

????if?(dptr?==?NULL?||?!dptr->data?||?!dptr->data[s_pos])

????????goto?out;

????? ?

????/*?读取该量子的数据直到结尾?*/

????if?(count?>?quantum?-?q_pos)

????????count?=?quantum?-?q_pos;??? /*?多了的不读?*/

????if?(copy_to_user(buf,?dptr-data[s_pos]?+?q_pos,?count))?{

????????retval?=?-EFAULT; /*?无效地址?*/

????????goto?out;

????}

????*f_pos?+=?count;

????retval?=?count;

????? ?

????out:

????????up(&dev->sem);

????????return?retval;

}

? ?

【write方法】

照着书上的程序添加到了scull.c里面去了,但是感觉好多没实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

/*?write方法?*/

ssize_t?scull_write(struct?file?*filp,?const?char?__user?*buf,?size_t?count,?loff_t?*f_ops)

{

????struct?scull_dev?*dev?=?filp->private_data;? /*?从文件结构获取设备指针,好怪的方法?*/

????struct?scull_qset?*dptr;

????int?quantum?=?dev->quantum,?qset?=?dev->qset;

????int?itemsize?=?quantumn?*?qset;?/*?量子集合的大小?*/

????int?item?s_pos,?q_pos,?rest;

????ssize_t?retval?=?-ENOMEM;??? /*?"goto?out"?语句使用的值?*/

????? ?

????if?(down_interruptible(&dev->sem))

????????return?-ERESTARTSYS;

????? ?

????/*?在量子集中寻找链表项、qset索引以及偏移量?*/

????item?=?(long)*f_pos?/?itemsize;

????rest?=?(long)*f_pos?%?itemsize;

????s_pos?=?rest?/?quantum; /*?第几个量子?*/

????q_pos?=?rest?%?quantum; /*?第几个字节?*/

????? ?

????/*?沿该链表前行,直到正确的位置(在其他地方定义)?*/

????dptr?=?scull_follow(dev,?item);

????if?(dptr?==?NULL)

????????goto?out;

????/*?分配链表项指向的指针数组,然后清空?*/

????if?(!dptr->data)?{ /*?如果不存在数据?*/

????????dptr->data?=?kmalloc(qset?*?sizeof(char?*),?GFP_KERNEL);

????????if?(!dptr->data)

????????????goto?out;

????????memset(dptr->data,?0,?qset?*?sizeof(char?*));

????}

????/*?分配量子集?*/

????if?(!dptr->data[s_pos])?{

????????/*?分配一个量子的空间?*/

????????dptr->data[s_pos]?=?kmalloc(quantum,?GFP_KERNEL);

????????if?(!dptr->data[s_pos])

????????????goto?out

????}

????? ?

????/*?将数据写入该量子,直到结尾,相当于块写入?*/

????if?(count?>?quantum?-?q_pos)

????????count?=?quantum?-?q_pos;??? /*?最多写一个量子数据的大小?*/

????? ?

????if?(copy_from_user(dptr->data[s_pos]+q_pos,?buf,?count))?{

????????retval?=?-EFAULT;

????????goto?out;

????}

????*f_pos?+=?count;

????retval?=?count;

????? ?

????/*?更新设备文件的大小?*/

????if?(dev->size?<?*f_pos)

????????dev->size?=?*f_pos;

????? ?

????out:

????????up(&dev->sem);

????????return?retval;

}

【readv和writev方法】

? ?

? ?

【试试新设备】

代码抄了一遍,感觉根本无法编译和运行,因为缺了很多。再照着示例代码模仿一遍就好了

技术分享

这是ldd3提供的参考代码的目录,把文件拖到source insight便于找出代码逻辑,然后把自己的代码照着修改成能用的,因为参考代码实现了一些额外的功能,暂时不管了。

下面是source insight显示main.c的情况,可以定位到符号的定义和声明,很方便:

技术分享

----------------------------------

不知道为什么,这里的设备结构是动态分配的,不能直接定义吗?we can‘t have them static, as the number can be specified at load time.

在scull_init()里面动态分配了scull_devices结构

struct scull_dev *scull_devices;????????/* 是这样定义设备结构吗?当然不是,在scull_init()里面用kmalloc分配 */

技术分享

??

添加了裸设备的个数 numbers of bare scull devices

int scull_nr_devs = SCULL_NR_DEVS;

技术分享

技术分享

技术分享

在scull_init_module里面的fail里面也调用了scull_cleanup_module函数

定义struct scull_qset *scull_follow(struct scull_dev *dev, int n)

scull_dev的struct scull_qset *data;也是在这里分配的

技术分享

??

make发现的错误:

  1. 头文件中函数声明复制时忘记分号;
  2. 声明或者函数参数里的struct少写一个字母,写成了stuct;

    每出现一次此类错误全文件查找一次,因为肯定不止出现一次;

  3. GFP_KERNEL 写成 GPF_KERNEL
  4. f_pos写成f_ops
  5. loff_t写成lofft_t
  6. dev->size写成dev_size或者dev-size
  7. 标记化初始化结构体不能初始化没有定义过的函数;

    技术分享

  8. 循环变量,在函数的开头定义;
  9. 变量定义在一行里容易丢了逗号:

    技术分享

  10. goto out; 后面是有分号的;
  11. goto的标识后面必须有处理语句;

逐项修改make的warning和error后终于make通过了。

用输入重定向来测试这个驱动程序:

技术分享

为了进一步证实每次是否读写一个量子单位(4000B),可以在驱动程序的适当位置加入printk,从而可以了解到程序读写大数据块时会发生什么事情。

写的时候,只能写小于一个量子单位的数据:

技术分享

调试看一下:

技术分享

dmesg结果:

技术分享

看了这个打印结果,一切都一目了然了,然后再回国头来看书上对write方法的功能说明就十分清楚了:

关键函数在于内核提供的copy_from_user()

  1. 从用户空间往内核写的时候是按4096填充缓冲区的?程序会确保所有数据的完整;
  2. 写入时按量子为单位的4000,多出来的再写一次;
  3. 写完4096后,写下一个4096,直到需要写的写完;

? ?

技术分享

? ?

? ?

【快速参考】

很有用,代码涉案的符号和头文件。

? ?

【代码后续优化】

感觉和参考代码差距很大,无论是:

  1. 错误检查;
  2. 调试功能;
  3. 代码组织;
  4. ...

? ?

【小结】

感觉又忘了

2016年9月11日22:57:39

以上是关于ldd3-3 字符驱动程序的主要内容,如果未能解决你的问题,请参考以下文章

使用字符驱动程序

字符设备驱动体验,字符设备驱动学习

虚拟字符设备驱动开发

Linux驱动开发:字符设备驱动开发

Linux驱动开发:字符设备驱动开发

Linux驱动开发:字符设备驱动开发