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

linux设备驱动归纳总结(三):2open.close.read.write

2015年03月06日 驱动开发 ⁄ 共 9283字 ⁄ 字号 linux设备驱动归纳总结(三):2open.close.read.write已关闭评论 ⁄ 阅读 1,898 次

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

 

一、文件操作结构体file_operations

继续上次没讲完的问题,文件操作结构体到底是什么东西,为什么我注册了设备之后什么现象都没有?可以验证文件操作结构体的内容。

file_operations是一个函数指针的集合,用于存放我们定义的用于操作设备的函数的指针,如果我们不定义,它默认保留为NULL

来个文件操作结构体的定义:


/*include/linux/fs.h*/

struct file_operations {

struct module *owner;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

int (*readdir) (struct file *, void *, filldir_t);

unsigned int (*poll) (struct file *, struct poll_table_struct *);

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, struct dentry *, int datasync);

int (*aio_fsync) (struct kiocb *, int datasync);

int (*fasync) (int, struct file *, int);

int (*lock) (struct file *, int, struct file_lock *);

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

int (*check_flags)(int);

int (*flock) (struct file *, int, struct file_lock *);

ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned i nt);

int (*setlease)(struct file *, long, struct file_lock **);

};

会发现,上面的函数很多都跟系统编程的函数很相似,因为这里的函数是跟系统编程的函数对应的,如在应用层调用函数open来操作设备文件,内核就会调用文件操作结构体中的成员open来进行相应的操作。

上面的函数我也只是用过一小部分,下面先写一下打开和关闭设备的函数


int (*open) (struct inode *, struct file *);

在操作设备前必须先调用open函数打开文件,可以干一些需要的初始化操作。当然,如果不实现这个函数的话,驱动会默认设备的打开永远成功。打开成功时open返回0


int (*release) (struct inode *, struct file *);

当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL。关闭设备永远成功。

 

上面的函数中的的两个参数现在还没需要用到,迟点用到了会解释这两个结构体的用途。所以,下面的程序的打开和关闭并没有做实质的操作,只是想验证一下,注册设备可以调用filr_opreations中定义的函数。

 

上程序 目录 1st/test.c

程序和上节的5th没什么修改,我贴上修改的部分:


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

{

P_DEBUG("open device\n");

return 0;

}

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

{

P_DEBUG("close device\n");

return 0;

}

struct file_operations test_fops = {

.open = test_open,

.release = test_close,

};

编译后加载模块:


[root: 1st]# insmod test.ko

major[253] minor[0]

hello kernel

现在确实是有个设备号和操作设备的函数了,但是需要操作哪个文件来操作设备?

 

所以先要创建一个设备文件,使用命令mknod:


用法:mknod filename type major minor

filename:设备文件名

type:设备文件类型

major:主设备号

minor:次设备号

 

在开发板上使用命令:


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

这样,应用程序就能通过文件/dev/test来操作对应设备号的设备了。

写个应用程序来操作这个设备1st/app.c


#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(void)

{

int fd;

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

if(fd < 0)

{

perror("open");

return -1;

}

close(fd);

return 0;

}

编译程序生成cpp,运行后发现,应用程序的openclose就会调用内核驱动中的

test_opentest_close


[root: 1st]# ./app

<kernel>[test_open]open device

<kernel>[test_close]close device

结果出来了,函数被调用了。这样就说明了结构体cdevfile_operations的作用了。

 

二、内核中的memcpy---copy_from_usercopy_to_user

虽然说内核中不能使用C库提供的函数,但是内核也有一个memcpy的函数,用法跟C库中的一样。

下面用用file_operations中的readwrite模拟两件事:

1)从内核态通过read函数读取数据到用户态。

2)从用户态通过write函数读取数据到内核态。


驱动函数:ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

与用户层的read对应:ssize_t read(int fd, void *buf, size_t count);

用法:

从设备中读取数据,当用户层调用函数read时,对应的,内核驱动就会调用这个函数。

参数:

struct file:file结构体,现在暂时不用,可以先不传参。

char __user:只看到__user就知道这是从用户态的指针,通过这个指针往用户态传数据。这是对应用户层的read函数的第二个参数void *buf。

size_t:其实这只是unsigned int。对应应用层的read函数的第三个参数。

loff_t:这是用于存放文件的偏移量的,回想一下系统编程时,读写文件的操作都会使偏移量往后移。不过待会的代码先不实现,迟点会说。

返回值:

当返回正数时,内核会把值传给应用程序的返回值。一般的,调用成功会返回成功读取的字节数。

如果返回负数,内核就会认为这是错误,应用程序返回-1。


ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

与用户层的write对应:ssize_t write(int fd, const void *buf, size_t count);

用法:往设备写入数据,当用户层调用函数write时,对应的,内核驱动就会调用这个函数。

参数:

struct file:file结构体,现在暂时不用,可以先不传参。

char __user:只看到__user就知道这是从用户态的指针,通过这个指针读取用户态的数据。这是对应用户层的write函数的第二个参数const void *buf。

size_t:其实这只是unsigned int。对应用户层的write函数的第三个参数count。

loff_t:这是用于存放文件的偏移量的,回想一下系统编程时,读写文件的操作都会使偏移量往后移。不过待会的代码先不实现,迟点会说。

返回值:

当返回正数时,内核会把值传给应用程序的返回值。一般的,调用成功会返回成功读取的字节数。

如果返回负数,内核就会认为这是错误,应用程序返回-1。

当然和现实的readwrite有点区别。我只是实现了参数在内核与用户态之间传递,偏移量、存进内存等都没有实现。

先来个memcpy版的:目录2nd/test.c

只上修改的部分:


.........

#include <linux/cdev.h>

#include <linux/string.h> //memcpy必须包含该头文件

#define DEBUG_SWITCH 1

...........

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

{

P_DEBUG("close device\n");

return 0;

}

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

{

memcpy(buf, "test_data", count); //从内核复制"test_data"到用户态

return 0; //这里返回0是不对的,应该返回成功读取的字节数,

} //这里就先将就一下,下个程序就改进了。

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

{

char kbuf[20];

memcpy(kbuf, buf, count); //从用户态读取数据到内核

P_DEBUG("kbuf is [%s]\n", kbuf); //在内核中打印出来,一般是存入

//某个地方的。现在也先不做。

return 0; //同样,一般返回成功读取的字节数。

}

struct file_operations test_fops = {

.open = test_open,

.release = test_close,

.write = test_write,

.read = test_read,

};

................

下面运行一下看效果:


[root: 2nd]# insmod test.ko

major[253] minor[0]

hello kernel

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

[root: 2nd]# ./app

<kernel>[test_open]open device

<app>[test_data] //这是调用read程序后读到内核中的数据。

<kernel>[test_write]kbuf is [test_data] //这是应用层调用write的效果

<kernel>[test_close]close device

但是,上面的memcpy是有缺陷的,譬如有些人比较喜欢捣乱的,在用户层调用函数时传入的不是字符串,而是一个不能访问或修改的地址,那样就会造成系统崩溃,下面修改应用程序捣乱一下:

就在2nd/app.c修改了一下:


#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, 20);

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

write(fd, (const void*)(0), 20); //把参数改成非法地址

//write(fd, buf, 20);

close(fd);

return 0;

}

编译后尝试一下运行,函数就崩溃了。


[root: 2nd]# ./app

<kernel>[test_open]open device

<app>[test_data]

Unable to handle kernel NULL pointer dereference at virtual address 00000000

pgd = c3968000

[00000000] *pgd=33a1a031, *pte=00000000, *ppte=00000000

Internal error: Oops: 17 [#1]

Modules linked in: test

CPU: 0 Not tainted (2.6.29.4uplooking #1)

PC is at memcpy+0x54/0x29c

LR is at test_write+0x1c/0x40 [test]

pc : [<c01188d4>] lr : [<bf00006c>] psr: 00000013

sp : c39d5f0c ip : 0000000c fp : c39d5f54

r10: 40025000 r9 : c39d4000 r8 : c0025fe4

r7 : 00000004 r6 : c39d5f78 r5 : 00000000 r4 : c39d5f2c

r3 : c39d5f78 r2 : fffffff4 r1 : 00000000 r0 : c39d5f2c

Flags: nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Seg

...........................

 

出于上面的原因,内核和用户态之间交互的数据时必须要先对数据进行检测,如果数据是安全的,才可以进行数据交互。因此,有了下面的两个函数:(包含头文件<asm/uaccess.h>)


static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)

static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)

用法:

和memcpy的参数一样,但它根据传参方向的不同分开了两个函数。

"to"是相对于内核态来说的。所以,to函数的意思是从from指针指向的数据将n个字节的数据传到to指针指向的数据。

"from"也是相对于内核来说的。所以,from函数的意思是从from指针指向的数据将n个字节的数据传到to指针指向的数据。

返回值:

函数的返回值是指定要读取的n个字节中还剩下多少字节还没有被拷贝。

注意:

一般的,如果返回值不为0时,调用copy_to_user的函数会返回错误号-EFAULT表示操作出错。当然也可以自己决定。

上面的函数就是memcpy的改进版,在memcpy功能的基础上加上的检查传入参数的功能,防止有些人有意或者无意的传入无效的参数。

这样。下面就可以改进一下之前的函数了。

函数路径:3rd/test.c

函数只是修改了readwrite函数


.......

#include <asm/uaccess.h>

.......

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

{

int ret;

// memcpy(buf, "test_data", count);

if (copy_to_user(buf, "test_data", count)){

ret = - EFAULT;

}else{

ret = count;

P_DEBUG("kbuf is [%s]\n", buf);

}

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

}

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

{

char kbuf[20];

int ret;

//memcpy(kbuf, buf, count);

if(copy_from_user(kbuf, buf, count)){

ret = - EFAULT;

}else{

ret = count;

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

}

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

}

这里要说一下readwrite的返回值:

coyy_xx_user出错时,函数返回-EFUALT,内核一看是负数,就知道函数出错,此时应用层的readwrite函数就会返回-1

如果执行正确,test_read返回成功读取的字节数,内核看到非负就会认为函数正确执行,应用层的函数同样返回相同的值。

应用程序: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, count;

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

if(fd < 0)

{

perror("open");

return -1;

}

count = read(fd, buf, 20);

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

//write(fd, (const void*)(0), 20);

count = write(fd, buf, 20);

close(fd);

return 0;

}

如果用户程序传入的地址正确:


[root: 3rd]# insmod test.ko

major[253] minor[0]

hello kernel

[root: 3rd]# ./app

<kernel>[test_open]open device

<kernel>[test_read]kbuf is [test_data]

<app>buf is [test_data]<kernel>[test_write]kbuf is [test_data]

<kernel>[test_close]close device

因为用户态和内核抢着打印,所以会出现倒数第二行的情况,不过没什么关系。

如果传入非法的参数:3rd/app_wrong.c


#include <stdio.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int main(void)

{

char buf[20];

int fd, count;

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

if(fd < 0)

{

perror("open");

return -1;

}

count = read(fd, buf, 20);

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

count = write(fd, (const void*)(0), 20);

if (count == -1)

{

perrnor("write");

}

//count = write(fd, buf, 20);

close(fd);

return 0;

}

运行时,程序会检测到错误:


[root: 3rd]# ./app_wrong

<kernel>[test_open]open device

<kernel>[test_read]kbuf is [test_data]

<app>buf is [test_data]<kernel>[test_close]close device

write: Bad address //错误信息

因为内核和用户层抢着输出,所以难免有打印乱序,但错误是出来了。

三、总结:

根据上面openclosereadwrite四个操作,下面来画一个拉风的时序图。上面的readwrite函数的数据是我在函数里面瞎编的,根本不是从硬件(如寄存器)读取出来的。我就先想象一下这是硬件上的数据。(当然这是指一个基本的模型,内核的操作比这个复杂)

注:箭头方向是从调用的一方指向受作用的一方。

数据操作流程

数据操作流程

上面讲的东西很少:

1)file operations的用途

2)copy_to_usercopy_from_user的用法

还有两个问题还没有解决:

1)struct file

2)struct inode

这些都将在下节讲。

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

×