现在的位置: 首页 > 技术文章 > 驱动开发 > 正文

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

2015年03月07日 驱动开发 ⁄ 共 8327字 ⁄ 字号 linux设备驱动归纳总结(三):3面向对象思想和lseek已关闭评论 ⁄ 阅读 1,521 次

在网上看到的,讲的不错,遂转载过来。原文地址:http://blog.chinaunix.net/uid-25014876-id-59418.html

 

一、结构体struct filestruct 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就存在这。这个结构体的用处现在我还不好说,待会看程序就知道了。

二、面向对象的思想

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


struct _test_t{

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

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

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

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

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

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

};

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

先来个函数:


#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 *techbulo_a;

};

struct B {

int techbulo_b;

};

struct A a;

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

并且,a.techbulo_a = &b.techbulo_b;

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

struct B *bb = container_of(a.techbulo_a, struct B, techbulo_b);

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

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

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

container_of模拟

container_of模拟

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

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

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

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

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

 

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

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

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

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

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

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


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

{

struct _test_t *dev;

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

filp->private_data = dev;

return 0;

}

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

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

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


struct _test_t{

char kbuf[DEV_SIZE];

unsigned int major;

unsigned int minor;

unsigned int cur_size;

dev_t devno;

struct cdev test_cdev;

};

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

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

struct _test_t *dev;

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

filp->private_data = dev;

return 0;

}

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

{

return 0;

}

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

{

int ret;

struct _test_t *dev = filp->private_data;

if(!dev->cur_size){

return 0;

}

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

ret = - EFAULT;

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

ret = count;

dev->cur_size -= count;

}

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

return ret;

}

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

{

int ret;

struct _test_t *dev = filp->private_data;

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

ret = - EFAULT;

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

ret = count;

dev->cur_size += count;

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

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

}

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

}

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

1)封装了一个结构体。

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

3)readwrite函数成功后要更新cur_size这个值。

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


#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(void)

{

char buf[20];

int fd;

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

if(fd < 0)

{

perror("open");

return -1;

}

read(fd, buf, 10);

printf("<app>buf is [%s]\n", buf);

write(fd, "techbulo", 8);

read(fd, buf, 8);

printf("<app>buf is [%s]\n", buf);

close(fd);

return 0;

}

运行一下:


[root: 1st]# insmod test.ko

major[253] minor[0]

hello kernel

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

[root: 1st]# ./app

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

<kernel>[test_write]kbuf is [techbulo] //成功写入

<kernel>[test_write]cur_size:[8] //更新cur_size

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

<app>buf is [techbulo] //应用程序返回读到的内容

[root: 1st]#

三、readwrite的改进

上面的函数还是不完善的,想象一下,平时的readwrite函数会增加偏移量,但上面的函数是不会的。这是因为还有一个参数我没用上,就是"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对应的,用来修改偏移量的位置。

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

先改进一下readwrite函数


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

{

int ret;

struct _test_t *dev = filp->private_data;

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

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

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

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

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

}

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

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

ret = - EFAULT;

}else{

ret = count;

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

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

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

}

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

}

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

{

int ret;

struct _test_t *dev = filp->private_data;

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

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

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

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

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

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

}

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

ret = - EFAULT;

}else{

ret = count;

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

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

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

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

}

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

}

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

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


#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(void)

{

char buf[20];

int fd;

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

if(fd < 0)

{

perror("open");

return -1;

}

write(fd, "techbulo", 8);

read(fd, buf, 8);

printf("<app>buf is [%s]\n", buf);

close(fd);

return 0;

}

验证一下:


[root: 2nd]# insmod test.ko

major[253] minor[0]

hello kernel

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

[root: 2nd]# ./app

<kernel>[test_write]write 8 bytes, cur_size:[8]//写入

<kernel>[test_write]kbuf is [techbulo]

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

<app>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*/

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

{

loff_t new_pos; //新偏移量

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

switch(whence){

case SEEK_SET:

new_pos = offset;

break;

case SEEK_CUR:

new_pos = old_pos + offset;

break;

case SEEK_END:

new_pos = DEV_SIZE + offset;

break;

default:

P_DEBUG("unknow whence\n");

return - EINVAL;

}

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

P_DEBUG("f_pos failed\n");

return - EINVAL;

}

filp->f_pos = new_pos;

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

}

&nbsp;

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


#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(void)

{

char buf[20];

int fd;

int ret;

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

if(fd < 0)

{

perror("open");

return -1;

}

write(fd, "techbulo", 8);

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

ret = lseek(fd, 0, SEEK_SET);

read(fd, buf, 10);

printf("<app>buf is [%s]\n", buf);

close(fd);

return 0;

}

验证一下:


[root: 2nd]# ./app

<kernel>[test_write]write 8 bytes, cur_size:[8]

<kernel>[test_write]kbuf is [techbulo]

<kernel>[test_read]read 8 bytes, cur_size:[0] //读到数据了!

<app>buf is [techbulo] //读到数据了!

五、总结

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

上面讲的东西不多:

1)container_of的使用

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

3)llseek的编写。

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

×