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

最新文章

  1. 微信App支付通知验签
  2. 读书笔记:Sheldon Ross:概率论基础教程:随机变量
  3. Jsp中的pageContext对象
  4. 使用PowerShell读、写、删除注册表键值
  5. LeetCode 561. Array Partition I (数组分隔之一)
  6. SpringBoot集成security
  7. 02_HTML5+CSS详解第三天
  8. 08策略模式Strategy
  9. youtube-dl 使用小记
  10. Git之远程仓库
  11. MySql社区版和企业版的区别
  12. PAT 1054 求平均值
  13. go语言从零学起(一) -- 文档教程篇
  14. Python168的学习笔记5
  15. (三)MySQL学习笔记
  16. mac远程桌面Microsoft Remote Desktop for Mac的安装与使用
  17. Linux数据备份与恢复
  18. win8 商店应用 概观
  19. 卷积神经网络之ResNet网络模型学习
  20. Redis数据类型的常用API以及使用场景

热门文章

  1. Python之asyncio模块的使用
  2. join on 和group
  3. 2g 大文件上传
  4. CF1146F Leaf Partition 树形DP
  5. matlab函数 bsxfun浅谈(转载)
  6. Android中关于回调概念的笔记
  7. node链接mongoDB及封装
  8. lcx用法
  9. Android学习_7/25
  10. Go 结构体与初始化