linux设备驱动归纳总结(三):2.字符型设备的操作open、close、read、write【转】

本文转载自:http://blog.chinaunix.net/uid-25014876-id-59417.html

linux设备驱动归纳总结(三):2.字符型设备的操作openclosereadwrite

 

一、文件操作结构体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,运行后发现,应用程序的openclose就会调用内核驱动中的

test_opentest_close

[root: 1st]# ./app

[test_open]open device

[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 filefile结构体,现在暂时不用,可以先不传参。

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 filefile结构体,现在暂时不用,可以先不传参。

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

size_t:写内核的人就是闲着喜欢编一些奇怪的类型,其实这只是unsigned int。对应用户层的write函数的第三个参数count

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

返回值:

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

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

 

 

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

 

先来个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

函数我只是修改了readwrite函数

 

.......

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 }

 

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

coyy_xx_user出错时,函数返回-EFUALT,内核一看是负数,就知道函数出错,此时应用层的readwrite函数就会返回-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 //错误信息

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

 

 

四、总结:

 

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

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

 

 

上面讲的东西很少:

1)file operations的用途

2)copy_to_usercopy_from_user的用法

 

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

1)struct file

2)struct inode

 

这些都将在下节讲。

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

posted @ 2017-01-03 16:44  请给我倒杯茶  阅读(446)  评论(0编辑  收藏  举报