linux设备驱动归纳总结(三):3面向对象思想和lseek、container_of、write、read 【转】
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 【转】的更多相关文章
- linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现【转】
本文转自自:http://blog.chinaunix.net/uid-25014876-id-59418.html linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现 一. ...
- 【Linux开发】linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现
linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现 一.结构体struct file和struct inode 在之前写的函数,全部是定义了一些零散的全局变量.有没有办法整合 ...
- linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-110738.html linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想 xxxxxx ...
- 【Linux开发】linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想
linux设备驱动归纳总结(八):3.设备管理的分层与面向对象思想 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
- linux设备驱动归纳总结(三):7.异步通知fasync【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-62725.html linux设备驱动归纳总结(三):7.异步通知fasync xxxxxxxxxxx ...
- linux设备驱动归纳总结(三):6.poll和sellct【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-61749.html linux设备驱动归纳总结(三):6.poll和sellct xxxxxxxxxx ...
- linux设备驱动归纳总结(三):5.阻塞型IO实现【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-60025.html linux设备驱动归纳总结(三):5.阻塞型IO实现 xxxxxxxxxxxxxx ...
- linux设备驱动归纳总结(三):4.ioctl的实现【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-59419.html linux设备驱动归纳总结(三):4.ioctl的实现 一.ioctl的简介: 虽 ...
- linux设备驱动归纳总结(三):2.字符型设备的操作open、close、read、write【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-59417.html linux设备驱动归纳总结(三):2.字符型设备的操作open.close.rea ...
随机推荐
- Alpha 冲刺八
团队成员 051601135 岳冠宇 051604103 陈思孝 031602629 刘意晗 031602248 郑智文 031602234 王淇 会议照片 项目燃尽图 项目进展 完善各自部分 项目描 ...
- C语言入门:04.数据类型、常量、变量
一.数据 1.什么是数据 生活中时时刻刻都在跟数据打交道,比如体重数据.血压数据.股价数据等.在我们使用计算机的过程中,会接触到各种各样的数据,有文档数据.图片数据.视频数据,还有聊QQ时产生的文字数 ...
- thnkphp框架面试问题
Thinkphp面试问题 1.如何理解TP中的单一入口文件? 答:ThinkPHP采用单一入口模式进行项目部署和访问,无论完成什么功能,一个项目都有一个统一(但不一定是唯一)的入口.应该说,所有项目都 ...
- JAVA的垃圾回收机制(GC)
1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制. 2.什么时候 ...
- Windows 10 中的存储空间
存储空间有助于保护你的数据免受驱动器故障的影响,并随着你向电脑添加驱动器而扩展存储.你可以使用存储空间将两个或多个驱动器一起分组到一个存储池中,然后使用该池的容量来创建称为存储空间的虚拟驱动器.这些存 ...
- Python day7之mysql
写在前面: 由于毕业论文撰写和答辩耽搁了几个月,但是在这几个月没有放弃学习Python,就是没有时间写博客.进行我们主要对数据库mysql的操作指令集的学习. 一.mysql术语 Mysql是最流行的 ...
- Day18-前端和后端怎么区分
前端 - 通常是针对浏览器而开发的,是在浏览器端运行的程序,而后端 - 针对的是服务器,准确的来说应该是服务器端开发.前端开发偏向于用户体验,比较直观,服务器端开发偏向于性能. 前端和后端指的是网站建 ...
- mysql列类型char,varchar,text,tinytext,mediumtext,longtext的比较与选择
储存不区分大小写的字符数据 TINYTEXT 最大长度是 255 (2^8 – 1) 个字符. TEXT 最大长度是 65535 (2^16 – 1) 个字符. MEDIUMTEXT 最大长度是 16 ...
- BZOJ 1565 [NOI2009]植物大战僵尸 | 网络流
传送门 BZOJ 1565 题解 这道题也是个经典的最大权闭合子图-- 复习一下最大权闭合子图是什么? 就是一个DAG上,每个点有个或正或负的点权,有的点依赖于另外一些点(如果选这个点,则被依赖点必选 ...
- 前端学习 -- Css -- 内联元素的盒模型
内联元素不能设置width和height: 设置水平内边距,内联元素可以设置水平方向的内边距:padding-left,padding-right: 垂直方向内边距,内联元素可以设置垂直方向内边距,但 ...