linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现

转自:http://blog.chinaunix.net/uid-25014876-id-59418.html

一、结构体struct file和struct inode

在之前写的函数,全部是定义了一些零散的全局变量。有没有办法整合成到一个结构体当中?这样的话,看起来和用起来都比较方便。接下来就要说这方面的问题。

不过先要介绍一下除了fops以外的两个比较重要的结构体:

1)struct file

在内核中,file结构体是用来维护打开的文件的。每打开一次文件,内核空间里就

会多增加一个file来维护,当文件关闭是释放。

所以,在内核中可以存在同一个文件的多个file,因为该文件被应用程序打开被打

开。

在struct file中有几个重要的成员:

1)loff_t f_pos;

这是用来记录文件的偏移量。在应用程序中,打开文件时偏移量为0,每次的读写操作都会使偏移量增加。

从这个原因可以看出为什么每打开一次文件就新建一个file结构体了。不然的话,每个打开文件的读写操作都修改同一个偏移量,那读写岂不是乱套了吗?

2)void *private_data;

这是空类型的指针可以用于存放任何数据,我会用这个指针来存放待会要定义的结构体指针。

回想一下,文件操作结构体fops中所有的函数成员里面都有一个参数是file结构体,所以每个函数都可以在file->private_data中拿到我自己定义的结构体了。

3)struct file_operations *fops;

打开文件后,内核会把fops存放在这里,以后的操作就在这里在这里找函数了。

2)struct inode

这个结构体是用来保存一个文件的基本信息的结构体,即使打开多个相同的文件,也只会有一个对应的inode。

它也有两个常用的成员:

1)dev_t i_rdev;

这里存放着这个文件的设备号。

2)struct cdev *i_cdev;

这个结构体很熟悉吧,这就是注册设备时用的cdev就存在这。这个结构体的用处现在我还不好说,待会看程序就知道了。

二、面向对象的思想

接下来就封装一下之前程序的数据类型吧:

18 struct _test_t{

19 char kbuf[DEV_SIZE]; //这里存放数据

20 unsigned int major; //这里存放主设备号

21 unsigned int minor; //这里存放次设备号

22 unsigned int cur_size; //这里存放当前的kbuf的大小

23 dev_t devno; //这里存放设备号

24 struct cdev test_cdev; //这里存放cdev结构体

25 };

定义了这样的一个结构体后,在操作函数中怎么拿到这个结构体的指针呢?

先来个函数:

#define container_of(ptr, type, member) ({ \

const typeof( ((type *)0)->member ) *__mptr = (ptr); \

(type *)( (char *)__mptr - offsetof(type,member) );})

使用:

已知一个结构体里面一个成员的指针ptr,同时,这个成员也是另外一个结构体类型中的一个成员,这个结构体的类型是type,而这个成员以member这个名字命名。就可以通过这个函数找到指向类型是type的结构体的指针。

返回值:

返回值就是指向type结构体类型的数据的指针。

如:现在定义这样的两个结构体:

struct A {

int *xiaobai_a;

};

struct B {

int xiaobai_b;

};

struct A a;

在遥远的另一处有这样的定义:struct B b;

并且,a.xiaobai_a = &b.xiaobai_b;

这样,在不知道b只知道a的情况下也可以找到b的位置:

struct B *bb = container_of(a.xiaobai_a, struct B, xiaobai_b);

估计被上面的解释说晕了吧。我还是举个例比较方便:

虽然一个函数不值得说这么久,但是我觉得这种思想很不错,内核中很多时候都用到这个函数,如在内核链表中。

来个邪恶的例子名字——老板与小秘:

老板他请了个年轻的小秘,他就跟客户说:“我电话号码经常换,你记着我小秘的电话,想找我嘛,找我小秘就可以了!”

于是,客户想找老板了,就打通小秘的电话,说:“我知道你是秘书小红,我想找你老板小黑,麻烦给他的电话号码我。”

这样,客户就拿到了老板最新的电话号码了。

想象老板和客户是个结构体,秘书和他的电话号码是个各自成员,电话号码想象成指针:

老板的电话 = container_of(秘书的电话, 老板,小秘)

说了半天还没进入正题,这个函数用在哪里呢?谁当小秘呢?

就是那个说了半天都不知道能做什么还经常出现的struct cdev!

而我把cdev添加到了我自己建的结构体struct _test_t中,所哟struct _test_t就是老板!

而struct inode就是客户了,因为它的成员里面有小秘的电话号码:struct cdev *i_cdev;

所以,如果想得到_test_t,只要调用这个函数就行了。

下面看一下改良后的open函数

27 int test_open(struct inode *node, struct file *filp)

28 {

29 struct _test_t *dev;

30 dev = container_of(node->i_cdev, struct _test_t, test_cdev);

31 filp->private_data = dev;

32 return 0;

33 }

上面还有一句,将获得的结构体指针存放到filp的private_data中。

这是因为,struct file_operations中的每个函数的第一个参数就是struct file,只要有file,每个函数都可以从private_data中得到数据了。相反,struct inode这个参数并不是file_operations中所有的函数都有。

下面贴上部分代码:1st/test.c

18 struct _test_t{

19 char kbuf[DEV_SIZE];

20 unsigned int major;

21 unsigned int minor;

22 unsigned int cur_size;

23 dev_t devno;

24 struct cdev test_cdev;

25 };

26

27 int test_open(struct inode *node, struct file *filp)

28 {/*open操作需要给把拿到的结构体指针赋值给private_data*/

29 struct _test_t *dev;

30 dev = container_of(node->i_cdev, struct _test_t, test_cdev);

31 filp->private_data = dev;

32 return 0;

33 }

34

35 int test_close(struct inode *node, struct file *filp)

36 {

37 return 0;

38 }

39

40 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)

41 {

42 int ret;

43 struct _test_t *dev = filp->private_data;

44

45 if(!dev->cur_size){

46 return 0;

47 }

48

49 if (copy_to_user(buf, dev->kbuf, count)){

50 ret = - EFAULT;

51 }else{/*read函数成功读取后要修改cur_size*/

52 ret = count;

53 dev->cur_size -= count;

54 }

55 P_DEBUG("cur_size:[%d]\n", dev->cur_size);

56

57 return ret;

58 }

59

60 ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)

61 {

62 int ret;

63 struct _test_t *dev = filp->private_data;

64

65 if(copy_from_user(dev->kbuf, buf, count)){

66 ret = - EFAULT;

67 }else{/*write函数成功写入后也要修改cur_size*/

68 ret = count;

69 dev->cur_size += count;

70 P_DEBUG("kbuf is [%s]\n", dev->kbuf);

71 P_DEBUG("cur_size:[%d]\n", dev->cur_size);

72 }

73

74 return ret; //返回实际写入的字节数或错误号

75 }

上面的程序其实就多了比上一个程序多了三步:

1)封装了一个结构体。

2)open函数要获得结构体并存放到private_data中。

3)read和write函数成功后要更新cur_size这个值。

这样,一个像样点的程序出来了,写个应用程序验证一下:

1 #include

2 #include

3 #include

4 #include

5

6 int main(void)

7 {

8 char buf[20];

9 int fd;

10 fd = open("/dev/test", O_RDWR);

11 if(fd < 0)

12 {

13 perror("open");

14 return -1;

15 }

16

17 read(fd, buf, 10);

18 printf("buf is [%s]\n", buf);

19

20 write(fd, "xiao bai", 10);

21

22 read(fd, buf, 10);

23 printf("buf is [%s]\n", buf);

24

25 close(fd);

26 return 0;

27 }

运行一下:

[root: 1st]# insmod test.ko

major[253] minor[0]

hello kernel

[root: 1st]# mknod /dev/test c 253 0

[root: 1st]# ./app

buf is [] //第一次读取时cur_size==0,没数据就会返回

[test_write]kbuf is [xiao bai] //成功写入

[test_write]cur_size:[10] //更新cur_size

[test_read]cur_size:[0] //read读取成功,跟新cur_size

buf is [xiao bai] //应用程序返回读到的内容

[root: 1st]#

三、read、write的改进

上面的函数还是不完善的,想象一下,平时的read、write函数会增加偏移量,但上面的函数是不会的。这是因为还有一个参数我没用上,就是"loff_t offset"。

"loff_t offset"这个参数是内核在调用函数时,从"struct file"的成员"f_ops"拿到指针并当作参数传入。这样的做法让用户不用再从"struct file"提取成员,直接拿参数用就行了!

通过这个参数,我们就可以改进并且实现三个函数:

1test_read:当应用程序调用read时内核会调用test_read。读取数据的同时,偏移量会增加。

2test_write:当应用程序调用write时内核会调用test_write。写入数据的同时,偏移量也会增加。

3test_llseek:这是跟应用程序的lseek对应的,用来修改偏移量的位置。

有了上面的三个函数的功能,这样才算是个像样的函数!

先改进一下read、write函数

40 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)

41 {

42 int ret;

43 struct _test_t *dev = filp->private_data;

44

45 if(*offset >= DEV_SIZE){//如果偏移量已经超过了数组的容量

46 return count ? - ENXIO : 0; //count为0则返回0,表示读取0个数据成功

47 } //count不为0则分会错误号,地址越界

48 if(*offset + count > DEV_SIZE){ //如果读取字节数超过了最大偏移量

49 count = DEV_SIZE - *offset; //则减少读取字节数。

50 }

51 /*copy_to_user的参数也要改一下*/

52 if (copy_to_user(buf, dev->kbuf + *offset, count)){

53 ret = - EFAULT;

54 }else{

55 ret = count;

56 dev->cur_size -= count; //读取后数组的字节数减少

57 *offset += count; //偏移量增加

58 P_DEBUG("read %d bytes, cur_size:[%d]\n", count, dev->cur_size);

59 }

60

61 return ret; //返回实际写入的字节数或错误号

62 }

63

64 ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)

65 {

66 int ret;

67 struct _test_t *dev = filp->private_data;

68 /*copy_from_user的参数也要改一下*/

69 if(*offset >= DEV_SIZE){//如果偏移量已经超过了数组的容量

70 return count ? - ENXIO : 0; //count为0则返回0,表示读取0个数据成功

71 } //count不为0则分会错误号,地址越界

72 if(*offset + count > DEV_SIZE){ //如果读取字节数超过了最大偏移量

73 count = DEV_SIZE - *offset; //则减少读取字节数。

74 }

75

76 if(copy_from_user(dev->kbuf, buf, count)){

77 ret = - EFAULT;

78 }else{

79 ret = count;

80 dev->cur_size += count; //写入后数组的字节数增加

81 *offset += count; //偏移量增加

82 P_DEBUG("write %d bytes, cur_size:[%d]\n", count, dev->cur_size);

83 P_DEBUG("kbuf is [%s]\n", dev->kbuf);

84 }

85

86 return ret; //返回实际写入的字节数或错误号

87 }

话说得好,越是需要检测出错,代码就会几何级增加,如果不想看这么多代码,把这两个函数前面的两个if(45-50、69-74)都删掉!反正写应用程序的时候小心翼翼一点就好了。这个程序只是为了验证"offset"的作用。

再来个小心翼翼的应用程序:

1 #include

2 #include

3 #include

4 #include

5

6 int main(void)

7 {

8 char buf[20];

9 int fd;

10 fd = open("/dev/test", O_RDWR);

11 if(fd < 0)

12 {

13 perror("open");

14 return -1;

15 }

16

17 write(fd, "xiao bai", 10);

18

19 read(fd, buf, 10);

20 printf("buf is [%s]\n", buf);

21

22 close(fd);

23 return 0;

24 }

验证一下:

[root: 2nd]# insmod test.ko

major[253] minor[0]

hello kernel

[root: 2nd]# mknod /dev/test c 253 0

[root: 2nd]# ./app

[test_write]write 10 bytes, cur_size:[10]//写入

[test_write]kbuf is [xiao bai]

[test_read]read 10 bytes, cur_size:[0]//但读不出,因为偏移量增加

buf is []

上面的read函数根本读不出数据,这是因为偏移量增加了。这个时候需要一个函数来把偏移量移到开头,lseek函数就用上场了。下面就讲一下。

四、lseek函数的实现

应用层的函数lseek函数对应驱动的函数是llseek(为什么多了一个l我也想不懂)。

内核驱动:loff_t (*llseek) (struct file * filp, loff_t offset, int whence);

对应应用层:off_t lseek(int fd, off_t offset, int whence);

使用:

一看参数就知道,这两个函数的第二和第三个参数就是对应的,当应用层调用函数时,对应的参数就会让内核传给驱动的函数llseek。

参数:

offset:一看这个参数不是指针,就知道和read、write的参数不一样。这是应用层传来的参数,并不是"struct file"的偏移量"f_ops"。

whence:这个也跟应用层的参数一样,指定从哪个位置开始偏移。

从开头位置:#define SEEK_SET 0

从当前位置:#define SEEK_CUR 1

从文件末端:#define SEEK_END 2

返回值:成功返回当前的更新的偏移量,失败返回错误号,而应用层会返回-1。

下面来个程序:/3rd_char/3rd_char_3/3rd/test.c

/*test_llseek*/

89 loff_t test_llseek (struct file *filp, loff_t offset, int whence)

90 {

91 loff_t new_pos; //新偏移量

92 loff_t old_pos = filp->f_pos; //旧偏移量

93

94 switch(whence){

95 case SEEK_SET:

96 new_pos = offset;

97 break;

98 case SEEK_CUR:

99 new_pos = old_pos + offset;

100 break;

101 case SEEK_END:

102 new_pos = DEV_SIZE + offset;

103 break;

104 default:

105 P_DEBUG("unknow whence\n");

106 return - EINVAL;

107 }

108

109 if(new_pos < 0 || new_pos > DEV_SIZE){ //如果偏移量越界,返回错误号

110 P_DEBUG("f_pos failed\n");

111 return - EINVAL;

112 }

113

114 filp->f_pos = new_pos;

115 return new_pos; //正确返回新的偏移量

116 }

再来个应用程序:/3rd_char/3rd_char_3/3rd/app.c

1 #include

2 #include

3 #include

4 #include

5

6 int main(void)

7 {

8 char buf[20];

9 int fd;

10 int ret;

11

12 fd = open("/dev/test", O_RDWR);

13 if(fd < 0)

14 {

15 perror("open");

16 return -1;

17 }

18

19 write(fd, "xiao bai", 10);

20 /*让偏移量移至开头,这样才能读取数据*/

21 ret = lseek(fd, 0, SEEK_SET);

22

23 read(fd, buf, 10);

24 printf("buf is [%s]\n", buf);

25

26 close(fd);

27 return 0;

28 }

验证一下:

[root: 2nd]# ./app

[test_write]write 10 bytes, cur_size:[10]

[test_write]kbuf is [xiao bai]

[test_read]read 10 bytes, cur_size:[0] //读到数据了!

buf is [xiao bai] //读到数据了!

五、总结

拉风的时序图我就不画了。

上面讲的东西不多:

1)container_of的使用

2)怎么使用偏移量"filp->f_ops"。

3)llseek的编写。

=========================================================

源代码: 3rd_char_3.rar

linux设备驱动归纳总结(三):3面向对象思想和lseek、container_of、write、read 【转】的更多相关文章

  1. linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现【转】

    本文转自自:http://blog.chinaunix.net/uid-25014876-id-59418.html linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现 一. ...

  2. 【Linux开发】linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现

    linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现 一.结构体struct file和struct inode 在之前写的函数,全部是定义了一些零散的全局变量.有没有办法整合 ...

  3. linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-110738.html linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想 xxxxxx ...

  4. 【Linux开发】linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想

    linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  5. linux设备驱动归纳总结(三):7.异步通知fasync【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-62725.html linux设备驱动归纳总结(三):7.异步通知fasync xxxxxxxxxxx ...

  6. linux设备驱动归纳总结(三):6.poll和sellct【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-61749.html linux设备驱动归纳总结(三):6.poll和sellct xxxxxxxxxx ...

  7. linux设备驱动归纳总结(三):5.阻塞型IO实现【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-60025.html linux设备驱动归纳总结(三):5.阻塞型IO实现 xxxxxxxxxxxxxx ...

  8. linux设备驱动归纳总结(三):4.ioctl的实现【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-59419.html linux设备驱动归纳总结(三):4.ioctl的实现 一.ioctl的简介: 虽 ...

  9. linux设备驱动归纳总结(三):2.字符型设备的操作open、close、read、write【转】

    本文转载自:http://blog.chinaunix.net/uid-25014876-id-59417.html linux设备驱动归纳总结(三):2.字符型设备的操作open.close.rea ...

随机推荐

  1. PowerDesigner16工具学习笔记-建立CDM

    1.基本术语 1.1.实体和属性 实体(entity):指现实世界中客观存在,并可相互区别的事物或者事件. 属性(attribute):一组用来描述实体特征的属性. 实体集(entity set):具 ...

  2. 编码用命令执行的C语言词语统计程序

    需求介绍 程序处理用户需求的模式为: wc.exe [parameter][filename] 在[parameter]中,用户通过输入参数与程序交互,需实现的功能如下: 1.基本功能 支持 -c   ...

  3. 『编程题全队』Alpha 阶段冲刺博客Day7

    1.每日站立式会议 1.会议照片 2.昨天已完成的工作统计 孙志威: 1.添加了网络通信管理类 2.稍微修改了燃尽图模块ChartWidget 3.在主窗口中添加了用户信息框 4.重构了项目中的文件结 ...

  4. Parameter not found的出现的原因

      with dataM.Q_xfgl_dhjf do   begin     close;     sql.Clear;     sql.Add('insert into jfdh(jfdh_id, ...

  5. Django-项目配置

    目录 配置文件 数据库配置DATABASE - 指定MySQL数据库 数据库配置DATABASE - 指定SQL Server数据库 数据库配置DATABASE - 指定多个数据库 模版文件 静态文件 ...

  6. ionic npm安装报错 no such file ,解决办法

    Install the latest version of NodeJS from their website (e.g. 6.X.X). Open the Node.js command promp ...

  7. BCB将RichEdit光标移到最后一行

    int linecount=RichEdit1->Lines->Count; RichEdit1-> SelStart=SendMessage(RichEdit1-> Hand ...

  8. 【华为机试】—— 15.求int型正整数在内存中存储时1的个数

    题目 解法 import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner ...

  9. 无旋treap板子!

    #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> # ...

  10. luogu1081 [NOIp2012]开车旅行 (STL::multiset+倍增)

    先用不管什么方法求出来从每个点出发,A走到哪.B走到哪(我写了一个很沙雕的STL) 然后把每个点拆成两个点,分别表示A从这里出发和B从这里出发,然后连边是要A连到B.B连到A.边长就是这次走的路径长度 ...