您的位置:首页 > 运维架构 > Linux

Linux设备驱动开发基础---字符设备驱动程序开发

2013-06-28 09:49 281 查看
一、Linux字符设备驱动结构

1、cdev结构体

在 Linux 2.6 内核中使用 cdev结构体描述字符设备, cdev 结构体的定义如下所示:

cdev 结构体

1 struct cdev

2 {

3 struct kobject kobj; /* 内嵌的kobject对象 */

4 struct module *owner; /*所属模块*/

5 struct file_operations *ops; /*文件操作结构体*/

6 struct list_head list;

7 dev_t dev; /*设备号*/

8 unsigned int count;

9 };

对字符设备的访问是通过文件系统内的设备名称来访问的,设备名称位于目录/dev下.为了便于系统管理,设置了和设备名称一一对应的设备号,它分为主设备号和次设备号.通常来说,主设备号标示了设备对应的驱动程序,次设备号则用来分辨拥有同一个主设备号的的各个不同设备。例如一个嵌入式系统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。

cdev结构体的 dev_t 成员定义了设备号,为 32 位,其中高 12 位为主设备号,低20位为次设备号。使用下列宏可以从dev_t获得主设备号和次设备号。

MAJOR(dev_t dev)

MINOR(dev_t dev)

而使用下列宏则可以通过主设备号和设备号生成 dev_t。

MKDEV(int major, int minor) 其中,major为主设备号,minor为次设备号。

cdev 结构体的另一个重要成员 file_operations 定义了字符设备驱动提供给虚拟文件系统的接口函数。

Linux 2.6 内核提供了一组函数用于操作 cdev 结构体,如下所示:

void cdev_init(struct cdev *, struct file_operations *);

struct cdev *cdev_alloc(void);

void cdev_put(struct cdev *p);

int cdev_add(struct cdev *, dev_t, unsigned);

void cdev_del(struct cdev *);

cdev_init() 函数用于初始化 cdev 的成员,并建立 cdev 和 file_operation 之间的连接。

cdev_alloc() 函数用于动态申请一个 cdev 内存

cdev_add() 函数和 cdev_del() 函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对cdev_add()的调用通常发生在字符设备驱动模块加载函数中,而 cdev_del() 函数的调用则通常发生在字符设备驱动模块卸载函数中。

2. 分配和释放设备号

在调用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region() 或 alloc_chrdev_region() 函数向系统申请设备号,这两个函数的原型如下:

int register_chrdev_region(dev_t from, unsigned count, const char *name);

其中,from是要分配的设备号范围的起始值。一般只提供from的主设备号,from的次设备号通常被设置成0。count是需要申请的连续设备号的个数。最后name是和该范围编号关联的设备名称,该名称不能超过64字节。

和大多数内核函数一样,register_chrdev_region()函数成功时返回0。错误时,返回一个负的错误码,并且不能为字符设备分配设备号。下面是一个例子代码,其申请了CS5535_GPIO_COUNT个设备号。

retval = register_chrdev_region(dev_id, CS5535_GPIO_COUNT,NAME);

在Linux中有非常多的字符设备,在人为的为字符设备分配设备号时,很可能发生冲突。Linux内核开发者一直在努力将设备号变为动态的。可以使用alloc_chrdev_region()函数达到这个目的。

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

在上面的函数中,dev作为输出参数,在函数成功返回后将保存已经分配的设备号。函数有可能申请一段连续的设备号,这是dev返回第一个设备号。baseminor表示要申请的第一个次设备号,其通常设为0。count和name与register_chrdev_region()函数的对应参数一样。count表示要申请的连续设备号个数,name表示设备的名字。下面是一个例子代码,其申请了CS5535_GPIO_COUNT个设备号。

retval = alloc_chrdev_region(&dev_id, 0, CS5535_GPIO_COUNT, NAME);

register_chrdev_region() 函数用于已知起始设备的设备号的情况;而 alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况。函数调用成功之后,会把得到的设备号放入第一参数 dev 中。alloc_chrdev_region() 与 register_chrdev_region() 对比的优点在于它会自动避开设备号重复的冲突。

相反地,在调用 cdev_del() 函数从系统注销字符设备之后, unregister_chrdev_region() 应该被调用以释放原先申请的设备号,这个函数的原型如下:

void unregister_chrdev_region(dev_t from, unsigned count);

3、file_operations结构体

file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容, 这些函数实际会在应用程序进行 Linux 的 open()、write()、read()、close()等系统调用时最终被调用。

file_operations结构体

1 struct file_operations

2 {

3 struct module *owner;

4 // 拥有该结构的模块的指针,一般为THIS_MODULES

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

6 // 用来修改文件当前的读写位置

7 ssize_t(*read)(struct file *, char _ _user *, size_t, loff_t*);

8 // 从设备中同步读取数据

9 ssize_t(*aio_read)(struct kiocb *, char _ _user *, size_t, loff_t);

10 // 初始化一个异步的读取操作

11 ssize_t(*write)(struct file *, const char _ _user *, size_t, loff_t*);

12 // 向设备发送数据

13 ssize_t(*aio_write)(struct kiocb *, const char _ _user *, size_t, loff_t);

14 // 初始化一个异步的写入操作

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

16 // 仅用于读取目录,对于设备文件,该字段为 NULL

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

18 // 轮询函数,判断目前是否可以进行非阻塞的读取或写入

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

20 // 执行设备I/O控制命令

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

22 // 不使用BLK文件系统,将使用此种函数指针代替ioctl

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

24 // 在64位系统上,32位的ioctl调用将使用此函数指针代替

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

26 // 用于请求将设备内存映射到进程地址空间

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

28 // 打开

29 int(*flush)(struct file*);

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

31 // 关闭

32 int(*synch)(struct file *, struct dentry *, int datasync);

33 // 刷新待处理的数据

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

35 // 异步fsync

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

37 // 通知设备FASYNC标志发生变化

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

39 ssize_t(*readv)(struct file *, const struct iovec *, unsigned long, loff_t*);

40 ssize_t(*writev)(struct file *, const struct iovec *, unsigned long, loff_t*);

41 // readv和writev:分散/聚集型的读写操作

42 ssize_t(*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void*);

43 // 通常为NULL

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

45 // 通常为NULL

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

47 unsigned long, unsigned long);

48 // 在进程地址空间找到一个将底层设备中的内存段映射的位置

49 int(*check_flags)(int);

50 // 允许模块检查传递给fcntl(F_SETEL...)调用的标志

51 int(*dir_notify)(struct file *filp, unsigned long arg);

52 // 仅对文件系统有效,驱动程序不必实现

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

54 };

下面对file_operations结构体中的主要成员进行讲解。

●owner成员根本不是一个函数;它是一个指向拥有这个结构模块的指针。这个成员用来维持模块的引用计数,当模块还在使用时,不能用rmmod卸载模块。几乎所有时刻,它被简单初始化为 THIS_MODULE,一个在<linux/module.h>中定义的宏。

●llseek()函数用来改变文件中的当前读/写位置,并将新位置返回。loff_t参数是一个"long long"类型,"long long"类型即使在32位机上也是64位宽。这是为了与64位机兼容而定的,因为64位机的文件大小完全可以突破4G。

●read()函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。

●write()函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行write()系统调用时,将得到-EINVAL返回值。

●readdir()函数仅用于目录,设备节点不需要实现它。

●ioctl()函数提供了一种执行设备特定命令的方法。例如使设备复位,这既不是读操作也不是写操作,不适合用read()和write()方法来实现。如果在应用程序中给ioctl传入没有定义的命令,那么将返回-ENOTTY的错误,表示该设备不支持这个命令。

●mmap()函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,用户进行 mmap()系统调用时将获得-ENODEV返回值。 这个函数对于帧缓冲等设备特别有意义。

●poll()函数一般用于询问设备是否可被非阻塞地立即读写。当询问的条件未触发时,用户空间进行select()和poll()系统调用将引起进程的阻塞。

●aio_read()和aio_write()函数分别对与文件描述符对应的设备进行异步读、写操作。设备实现这两个函数后,用户空间可以对该设备文件描述符调用aio_read()、aio_write()

等系统调用进行读写。

file_operations结构体中的成员参数与系统调用中的函数原型有很大一部分的相似之处。

二、Linux设备字符驱动的组成

在Linux系统中,字符设备驱动由如下几个部分组成。

1.字符设备驱动模块加载与卸载函数

在字符设备的加载函数中,应该实现字符设备号的申请和cdev的注册。相反,在字符设备的卸载函数中应该实现字符设备号的释放和cdev的注销。

cdev是内核开发者对字符设备的一个抽象。除了cdev中的信息外,特定的字符设备还需要特定的信息,常常将特定的信息放在cdev之后,形成一个设备结构体,如代码中的xxx_dev。

常见的设备结构体、加载函数和卸载函数如下面的代码:

1. struct xxx_dev /*自定义设备结构体*/
2. {
3. struct cdevcdev; /*cdev结构体*/
4. ... /*特定设备的特定数据*/
5. };
6. static int __init xxx_init(void) /*设备驱动模块加载函数*/
7. {
8. ...
9. /* 申请设备号,当xxx_major不为0时,表示静态指定;当为0时,表示动态申请*/
10. if(xxx_major)
11. result =register_chrdev_region(xxx_devno,1, "DEV_NAME");
12. /*静态申请设备号*/
13. else /*动态申请设备号*/
14. {
15. result =alloc_chrdev_region(&xxx_devno,0, 1, " DEV_NAME ");
16. xxx_major =MAJOR(xxx_devno); /*获得申请的主设备号*/
17. }
18. /*初始化cdev结构,并传递file_operations结构指针*/
19. cdev_init(&xxx_dev.cdev,&xxx_fops);
20. dev->cdev.owner =THIS_MODULE; /*指定所属模块*/
21. err =cdev_add(&xxx_dev .cdev,xxx_devno, 1); /*注册设备*/
22. }
23. static void __exit xxx_exit(void) /*模块卸载函数*/
24. {
25. cdev_del(&xxx_dev.cdev); /*注销cdev*/
26. unregister_chrdev_region(xxx_devno, 1); /*释放设备号*/
27. }
2.字符设备驱动的file_operations 结构体中成员函数

file_operations结构体中的成员函数都对应着驱动程序的接口,用户程序可以通过内核来调用这些接口,从而控制设备。大多数字符设备驱动都会实现read()、write()和ioctl()函数,这3个函数的常见写法如下面的代码所示。

1. /*文件操作结构体*/
2. static const struct file_operationsxxx_fops =
3. {
4. .owner =THIS_MODULE, /*模块引用,任何时候都赋值THIS_MODULE */
5. .read =xxx_read, /*指定设备的读函数 */
6. .write =xxx_write, /*指定设备的写函数 */
7. .ioctl =xxx_ioctl, /*指定设备的控制函数 */
8. };
9. /*读函数*/
10. static ssize_t xxx_read(struct file *filp, char __user*buf,

size_t size,loff_t *ppos)
11. {
12. ...
13. if(size>8)
14. copy_to_user(buf,...,...); /*当数据较大时,使用copy_to_user(),效率较高*/
15. esle
16. put_user(...,buf); /*当数据较小时,使用put_user(),效率较高*/
17. ...
18. }
19. /*写函数*/
20. static ssize_t xxx_write(struct file *filp, const

char __user *buf,size_t size, loff_t *ppos)
21. {
22. ...
23. if(size>8)
24. copy_from_user(..., buf,...); /*当数据较大时,使用copy_to_user(),效率较高*/
25. else
26. get_user(...,buf); /*当数据较小时,使用put_user(),效率较高*/
27. ...
28. }
29. /* ioctl设备控制函数 */
30. static long xxx_ioctl(struct file *file, unsigned intcmd,

unsigned long arg)
31. {
32. ...
33. switch (cmd)
34. {
35. casexxx_cmd1:
36. ... /*命令1执行的操作*/
37. break;
38. casexxx_cmd1:
39. ... /*命令2执行的操作*/
40. break;
41. default:
42. return - EINVAL; /*内核和驱动程序都不支持该命令时,

返回无效的命令*/
43. }
44. return 0;
45. }
文件操作结构体xxx_fops中保存了操作函数的指针。对于没有实现的函数,被赋值为NULL。xxx_fops结构体在字符设备加载函数中,作为cdev_init()的参数,与cdev建立了关联。

设备驱动的read()和write()函数有同样的参数。filp是文件结构体的指针,指向打开的文件。buf是来自用户空间的数据地址,该地址不能在驱动程序中直接读取。size是要读的字节。ppos是读写的位置,其相对于文件的开头。

xxx_ioctl控制函数的cmd参数是事先定义的I/O控制命令,arg对应该命令的参数。

3、驱动程序与应用程序的数据交换

驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。

安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);

上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为 0。

如果要复制的内存是简单类型, 如char、 int、 long等, 则可以使用简单的 put_user()和get_user(),如下所示:

int val; //内核空间整型变量

...

get_user(val, (int *) arg); //用户空间到内核空间,arg是用户空间的地址

...

put_user(val, (int *) arg); //内核空间到用户空间,arg是用户空间的地址

4、字符设备驱动程序组成小结

字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: