【Linux开发】linux设备驱动归纳总结(三):2.字符型设备的操作open、close、read、write
一、文件操作结构体file_operations
继续上次没讲完的问题,文件操作结构体到底是什么东西,为什么我注册了设备之后什么现象都没有?可以验证文件操作结构体的内容。
file_operations是一个函数指针的集合,用于存放我们定义的用于操作设备的函数的指针,如果我们不定义,它默认保留为NULL。
来个文件操作结构体的定义:
/*include/linux/fs.h*/
1310 struct file_operations {
1311 struct module *owner;
1312 loff_t (*llseek) (struct file *, loff_t, int);
1313 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1314 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1315 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1316 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1317 int (*readdir) (struct file *, void *, filldir_t);
1318 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1319 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
1320 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1321 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1322 int (*mmap) (struct file *, struct vm_area_struct *);
1323 int (*open) (struct inode *, struct file *);
1324 int (*flush) (struct file *, fl_owner_t id);
1325 int (*release) (struct inode *, struct file *);
1326 int (*fsync) (struct file *, struct dentry *, int datasync);
1327 int (*aio_fsync) (struct kiocb *, int datasync);
1328 int (*fasync) (int, struct file *, int);
1329 int (*lock) (struct file *, int, struct file_lock *);
1330 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
1331 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
1332 int (*check_flags)(int);
1333 int (*flock) (struct file *, int, struct file_lock *);
1334 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
1335 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned i nt);
1336 int (*setlease)(struct file *, long, struct file_lock **);
1337 };
会发现,上面的函数很多都跟系统编程的函数很相似,因为这里的函数是跟系统编程的函数对应的,如在应用层调用函数open来操作设备文件,内核就会调用文件操作结构体中的成员open来进行相应的操作。
上面的函数我也只是用过一小部分,下面先写一下打开和关闭设备的函数
int (*open) (struct inode *, struct file *);
在操作设备前必须先调用open函数打开文件,可以干一些需要的初始化操作。当然,如果不实现这个函数的话,驱动会默认设备的打开永远成功。打开成功时open返回0。
int (*release) (struct inode *, struct file *);
当设备文件被关闭时内核会调用这个操作,当然这也可以不实现,函数默认为NULL。关闭设备永远成功。
上面的函数中的的两个参数现在还没需要用到,迟点用到了会解释这两个结构体的用途。所以,下面的程序的打开和关闭并没有做实质的操作,只是想验证一下,注册设备可以调用filr_opreations中定义的函数。
上程序 目录 1st/test.c
程序和上节的5th没什么修改,我贴上修改的部分:
18 int test_open(struct inode *node, struct file *filp)
19 {
20 P_DEBUG("open device\n");
21 return 0;
22 }
23
24 int test_close(struct inode *node, struct file *filp)
25 {
26 P_DEBUG("close device\n");
27 return 0;
28 }
29
30 struct file_operations test_fops = {
31 .open = test_open,
32 .release = test_close,
33 };
编译后加载模块:
[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
1 #include
2 #include
3 #include
4 #include
5
6 int main(void)
7 {
8 int fd;
9 fd = open("/dev/test", O_RDWR);
10 if(fd < 0)
11 {
12 perror("open");
13 return -1;
14 }
15
16 close(fd);
17 return 0;
18 }
编译程序生成cpp,运行后发现,应用程序的open和close就会调用内核驱动中的
test_open和test_close。
[root: 1st]# ./app
[test_open]open device
[test_close]close device
结果出来了,函数被调用了。这样就说明了结构体cdev和file_operations的作用了。
二、内核中的memcpy---copy_from_user和copy_to_user:
虽然说内核中不能使用C库提供的函数,但是内核也有一个memcpy的函数,用法跟C库中的一样。
下面用用file_operations中的read和write模拟两件事:
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。
当然和现实的read、write有点区别。我只是实现了参数在内核与用户态之间传递,偏移量、存进内存等我都没有实现。
先来个memcpy版的:目录2nd/test.c
我只上修改的部分:
.........
4 #include
5
6 #include //memcpy必须包含该头文件
7
8 #define DEBUG_SWITCH 1
............
26 int test_close(struct inode *node, struct file *filp)
27 {
28 P_DEBUG("close device\n");
29 return 0;
30 }
31
32 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
33 {
34 memcpy(buf, "test_data", count); //从内核复制"test_data"到用户态
35 return 0; //这里返回0是不对的,应该返回成功读取的字节数,
36 } //这里就先将就一下,下个程序就改进了。
37
38 ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)
39 {
40 char kbuf[20];
41
42 memcpy(kbuf, buf, count); //从用户态读取数据到内核
43 P_DEBUG("kbuf is [%s]\n", kbuf); //在内核中打印出来,一般是存入
44 //某个地方的。现在也先不做。
45 return 0; //同样,一般返回成功读取的字节数。
46 }
47
48 struct file_operations test_fops = {
49 .open = test_open,
50 .release = test_close,
51 .write = test_write,
52 .read = test_read,
53 };
................
上面的程序是很不完善的,其实就是想说明一下memcpy的效果。
下面是应用程序: 2nd/app.c
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, 20);
18 printf("[%s]\n", buf);
19
20 write(fd, buf, 20);
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_open]open device
[test_data] //这是调用read程序后读到内核中的数据。
[test_write]kbuf is [test_data] //这是应用层调用write的效果
[test_close]close device
但是,上面的memcpy是有缺陷的,譬如有些人像我这样比较喜欢捣乱的,在用户层调用函数时传入的不是字符串,而是一个不能访问或修改的地址,那样就会造成系统崩溃,下面修改应用程序捣乱一下:
就在2nd/app.c修改了一下:
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, 20);
18 printf("[%s]\n", buf);
19
20
21 write(fd, (const void*)(0), 20); //把参数改成非法地址
22 //write(fd, buf, 20);
23
24 close(fd);
25 return 0;
26 }
编译后尝试一下运行,函数就崩溃了。
[root: 2nd]# ./app
[test_open]open device
[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 : [] lr : [] 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
...........................
出于上面的原因,内核和用户态之间交互的数据时必须要先对数据进行检测,如果数据是安全的,才可以进行数据交互。因此,有了下面的两个函数:(包含头文件)
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
函数我只是修改了read和write函数
.......
6 #include
.......
33 ssize_t test_read(struct file *filp, char __user *buf, size_t count, loff_t *offset)
34 {
35 int ret;
36 // memcpy(buf, "test_data", count);
37 if (copy_to_user(buf, "test_data", count)){
38 ret = - EFAULT;
39 }else{
40 ret = count;
41 P_DEBUG("kbuf is [%s]\n", buf);
42 }
43
44 return ret; //返回实际读取的字节数或错误号
45 }
46
47 ssize_t test_write(struct file *filp, const char __user *buf, size_t count, loff_t *offset)
48 {
49 char kbuf[20];
50 int ret;
51
52 //memcpy(kbuf, buf, count);
53 if(copy_from_user(kbuf, buf, count)){
54 ret = - EFAULT;
55 }else{
56 ret = count;
57 P_DEBUG("kbuf is [%s]\n", kbuf);
58 }
59
60 return ret; //返回实际写入的字节数或错误号
61 }
这里要说一下read和write的返回值:
当coyy_xx_user出错时,函数返回-EFUALT,内核一看是负数,就知道函数出错,此时应用层的read、write函数就会返回-1。
如果执行正确,test_read返回成功读取的字节数,内核看到非负就会认为函数正确执行,应用层的函数同样返回相同的值。
应用程序:3rd/app.c
1 #include
2 #include
3 #include
4 #include
5
6 int main(void)
7 {
8 char buf[20];
9 int fd, count;
10 fd = open("/dev/test", O_RDWR);
11 if(fd < 0)
12 {
13 perror("open");
14 return -1;
15 }
16
17 count = read(fd, buf, 20);
18 printf("buf is [%s]\n", buf);
19
20
21 //write(fd, (const void*)(0), 20);
22 count = write(fd, buf, 20);
23
24 close(fd);
25 return 0;
26 }
如果用户程序传入的地址正确:
[root: 3rd]# insmod test.ko
major[253] minor[0]
hello kernel
[root: 3rd]# ./app
[test_open]open device
[test_read]kbuf is [test_data]
buf is [test_data][test_write]kbuf is [test_data]
[test_close]close device
因为用户态和内核抢着打印,所以会出现倒数第二行的情况,不过没什么关系。
如果传入非法的参数: 3rd/app_wrong.c
1 #include
2 #include
3 #include
4 #include
5
6 int main(void)
7 {
8 char buf[20];
9 int fd, count;
10 fd = open("/dev/test", O_RDWR);
11 if(fd < 0)
12 {
13 perror("open");
14 return -1;
15 }
16
17 count = read(fd, buf, 20);
18 printf("buf is [%s]\n", buf);
19
20
21 count = write(fd, (const void*)(0), 20);
22 if (count == -1)
23 {
24 perrnor("write");
25 }
26 //count = write(fd, buf, 20);
27
28 close(fd);
29 return 0;
30 }
运行时,程序会检测到错误:
[root: 3rd]# ./app_wrong
[test_open]open device
[test_read]kbuf is [test_data]
buf is [test_data][test_close]close device
write: Bad address //错误信息
因为内核和用户层抢着输出,所以难免有打印乱序,但错误是出来了。
四、总结:
根据上面open、close、read、write四个操作,下面来画一个拉风的时序图。上面的read、write函数的数据是我在函数里面瞎编的,根本不是从硬件(如寄存器)读取出来的。我就先想象一下这是硬件上的数据。(当然这是指一个基本的模型,内核的操作比这个复杂)
注:箭头方向是从调用的一方指向受作用的一方。
上面讲的东西很少:
1)file operations的用途
2)copy_to_user和copy_from_user的用法
还有两个问题还没有解决:
1)struct file
2)struct inode
这些都将在下节讲。
=========================================================
源代码: 3rd_char_2.rar
【Linux开发】linux设备驱动归纳总结(三):2.字符型设备的操作open、close、read、write的更多相关文章
- Linux内核驱动学习(三)字符型设备驱动之初体验
Linux字符型设备驱动之初体验 文章目录 Linux字符型设备驱动之初体验 前言 框架 字符型设备 程序实现 cdev kobj owner file_operations dev_t 设备注册过程 ...
- Part10-字符型设备驱动模型-part10.1-使用字符型设备
‘ ’
- 【Linux开发】linux设备驱动归纳总结(十):1.udev&misc
linux设备驱动归纳总结(十):1.udev&misc xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
- linux设备驱动归纳总结(十):1.udev&misc【转】
本文转载自:http://blog.chinaunix.net/uid-25014876-id-111839.html linux设备驱动归纳总结(十):1.udev&misc xxxxxxx ...
- 【Linux开发】linux设备驱动归纳总结(三):4.ioctl的实现
linux设备驱动归纳总结(三):4.ioctl的实现 一.ioctl的简介: 虽然在文件操作结构体"struct file_operations"中有很多对应的设备操作函数,但是 ...
- 【Linux开发】linux设备驱动归纳总结(三):7.异步通知fasync
linux设备驱动归纳总结(三):7.异步通知fasync xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
- 【Linux开发】linux设备驱动归纳总结(三):6.poll和sellct
linux设备驱动归纳总结(三):6.poll和sellct xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
- 【Linux开发】linux设备驱动归纳总结(三):5.阻塞型IO实现
linux设备驱动归纳总结(三):5.阻塞型IO实现 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
- 【Linux开发】linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现
linux设备驱动归纳总结(三):3.设备驱动面向对象思想和lseek的实现 一.结构体struct file和struct inode 在之前写的函数,全部是定义了一些零散的全局变量.有没有办法整合 ...
随机推荐
- JAVA之StringUtils工具类
StringUtils 方法的操作对象是 java.lang.String 类型的对象,是对 JDK 提供的 String 类型操作方法的补充,并且是 null 安全的(即如果输入参数 String ...
- 逻辑卷----LVM的基础和应用
逻辑卷管理器 Logical Volume Manager-------逻辑卷宗管理器.逻辑扇区管理器.逻辑磁盘管理器,是Linux核心所提供的逻辑卷管理(Logical volume managem ...
- 附:常见的Jdbc Type 与 Java Type之间的关系
附:常见的Jdbc Type 与 Java Type之间的关系 JDBC Type Java Type CHAR String VARCHAR String LONG ...
- php文件夹上传源码
1.使用PHP的创始人 Rasmus Lerdorf 写的APC扩展模块来实现(http://pecl.php.net/package/apc) APC实现方法: 安装APC,参照官方文档安装,可以使 ...
- LibreOffice/Calc:带条件判断的求和
本文适用于LibreOffice Calc 5.1.6.2 + Ubuntu 16.04,熊猫帮帮主@cnblogs 2018/3/7 以下图为例,假设要根据C列对D列中被选中单元进行求和,即对D列中 ...
- Java当中的集合框架Map
简书作者:达叔小生 Java当中的集合框架Map 01 Map提供了三个集合视图: 键集 值集 键-值 映射集 public String getWeek(int num){ if(num<0 ...
- [CSP-S模拟测试]:Weed(线段树)
题目描述 $duyege$的电脑上面已经长草了,经过辨认上面有金坷垃的痕迹.为了查出真相,$duyege$准备修好电脑之后再进行一次金坷垃的模拟实验.电脑上面有若干层金坷垃,每次只能在上面撒上一层高度 ...
- HTTP服务器(1)
import socket def service_client(new_socket): """为这个客户端返回数据""" # 1. 接收 ...
- Amaple.js框架详细介绍
Amaple · 体验优先的JavaScript单页框架 Amaple (点此查看Github仓库)是专为单页web应用而设计的基于页面模块化的JavaScript框架,它可使开发者快速开发单页web ...
- Riot.js——一个小而美的JS框架
Riot.js是什么? Riot 拥有创建现代客户端应用的所有必需的成分: "响应式" 视图层用来创建用户界面 用来在各独立模块之间进行通信的事件库 用来管理URL和浏览器回退按钮 ...