当前位置:首页 > 嵌入式 > 嵌入式教程
[导读]设备驱动程序可以使用模块的方式动态加载到内核中去。加载模块的方式与以往的应用程序开发有很大的不同。以往在开发应用程序时都有一个main()函数作为程序的入口点,而在驱动开发时却没有main()函数,模块在调用insmod命令时被加载,此时的入口点是init_module()函数,通常在该函数中完成设备的注册。

11.2字符设备驱动编程

1.字符设备驱动编写流程

设备驱动程序可以使用模块的方式动态加载到内核中去。加载模块的方式与以往的应用程序开发有很大的不同。以往在开发应用程序时都有一个main()函数作为程序的入口点,而在驱动开发时却没有main()函数,模块在调用insmod命令时被加载,此时的入口点是init_module()函数,通常在该函数中完成设备的注册。同样,模块在调用rmmod命令时被卸载,此时的入口点是cleanup_module()函数,在该函数中完成设备的卸载。在设备完成注册加载之后,用户的应用程序就可以对该设备进行一定的操作,如open()、read()、write()等,而驱动程序就是用于实现这些操作,在用户应用程序调用相应入口函数时执行相关的操作,init_module()入口点函数则不需要完成其他如read()、write()之类功能。

上述函数之间的关系如图11.3所示。

图11.3设备驱动程序流程图

2.重要数据结构

用户应用程序调用设备的一些功能是在设备驱动程序中定义的,也就是设备驱动程序的入口点,它是一个在<linux/fs.h>中定义的structfile_operations结构,这是一个内核结构,不会出现在用户空间的程序中,它定义了常见文件I/O函数的入口,如下所示:

structfile_operations

{

loff_t(*llseek)(structfile*,loff_t,int);

ssize_t(*read)(structfile*filp,

char*buff,size_tcount,loff_t*offp);

ssize_t(*write)(structfile*filp,

constchar*buff,size_tcount,loff_t*offp);

int(*readdir)(structfile*,void*,filldir_t);

unsignedint(*poll)(structfile*,structpoll_table_struct*);

int(*ioctl)(structinode*,

structfile*,unsignedint,unsignedlong);

int(*mmap)(structfile*,structvm_area_struct*);

int(*open)(structinode*,structfile*);

int(*flush)(structfile*);

int(*release)(structinode*,structfile*);

int(*fsync)(structfile*,structdentry*);

int(*fasync)(int,structfile*,int);

int(*check_media_change)(kdev_tdev);

int(*revalidate)(kdev_tdev);

int(*lock)(structfile*,int,structfile_lock*);

};

这里定义的很多函数是否跟第6章中的文件I/O系统调用类似?其实当时的系统调用函数通过内核,最终调用对应的structfile_operations结构的接口函数(例如,open()文件操作是通过调用对应文件的file_operations结构的open函数接口而被实现)。当然,每个设备的驱动程序不一定要实现其中所有的函数操作,若不需要定义实现时,则只需将其设为NULL即可。

structinode结构提供了关于设备文件/dev/driver(假设此设备名为driver)的信息,structfile结构提供关于被打开的文件信息,主要用于与文件系统对应的设备驱动程序使用。structfile结构较为重要,这里列出了它的定义:

structfile

{

mode_tf_mode;/*标识文件是否可读或可写,FMODE_READ或FMODE_WRITE*/

dev_tf_rdev;/*用于/dev/tty*/

off_tf_pos;/*当前文件位移*/

unsignedshortf_flags;/*文件标志,如O_RDONLY、O_NONBLOCK和O_SYNC*/

unsignedshortf_count;/*打开的文件数目*/

unsignedshortf_reada;

structinode*f_inode;/*指向inode的结构指针*/

structfile_operations*f_op;/*文件索引指针*/

};

3.设备驱动程序主要组成

(1)早期版本的字符设备注册。

早期版本的设备注册使用函数register_chrdev(),调用该函数后就可以向系统申请主设备号,如果register_chrdev()操作成功,设备名就会出现在/proc/devices文件里。在关闭设备时,通常需要解除原先的设备注册,此时可使用函数unregister_chrdev(),此后该设备就会从/proc/devices里消失。其中主设备号和次设备号不能大于255。

当前不少的字符设备驱动代码仍然使用这些早期版本的函数接口,但在未来内核的代码中,将不会出现这种编程接口机制。因此应该尽量使用后面讲述的编程机制。

register_chrdev()函数格式如表11.1所示。

表11.1 register_chrdev()函数语法要点

所需头文件

#include<linux/fs.h>

函数原型

intregister_chrdev(unsignedintmajor,constchar*name,structfile_operations*fops)

函数传入值

major:设备驱动程序向系统申请的主设备号,如果为0则系统为此驱动程序动态地分配一个主设备号

name:设备名

fops:对各个调用的入口点

函数返回值

成功:如果是动态分配主设备号,此返回所分配的主设备号。且设备名就会出现在/proc/devices文件里

出错:-1

unregister_chrdev()函数格式如下表11.2所示:

表11.2 unregister_chrdev()函数语法要点

所需头文件

#include<linux/fs.h>

函数原型

intunregister_chrdev(unsignedintmajor,constchar*name)

函数传入值

major:设备的主设备号,必须和注册时的主设备号相同

name:设备名

函数返回值

成功:0,且设备名从/proc/devices文件里消失

出错:-1

(2)设备号相关函数。

在前面已经提到设备号有主设备号和次设备号,其中主设备号表示设备类型,对应于确定的驱动程序,具备相同主设备号的设备之间共用同一个驱动程序,而用次设备号来标识具体物理设备。因此在创建字符设备之前,必须先获得设备的编号(可能需要分配多个设备号)。

在Linux2.6的版本中,用dev_t类型来描述设备号(dev_t是32位数值类型,其中高12位表示主设备号,低20位表示次设备号)。用两个宏MAJOR和MINOR分别获得dev_t设备号的主设备号和次设备号,而且用MKDEV宏来实现逆过程,即组合主设备号和次设备号而获得dev_t类型设备号。

分配设备号有静态和动态的两种方法。静态分配(register_chrdev_region()函数)是指在事先知道设备主设备号的情况下,通过参数函数指定第一个设备号(它的次设备号通常为0)而向系统申请分配一定数目的设备号。动态分配(alloc_chrdev_region())是指通过参数仅设置第一个次设备号(通常为0,事先不会知道主设备号)和要分配的设备数目而系统动态分配所需的设备号。

通过unregister_chrdev_region()函数释放已分配的(无论是静态的还是动态的)设备号。

它们的函数格式如表11.3所示。

表11.3 设备号分配与释放函数语法要点

所需头文件

#include<linux/fs.h>

函数原型

intregister_chrdev_region(dev_tfirst,unsignedintcount,char*name)

intalloc_chrdev_region(dev_t*dev,unsignedintfirstminor,unsignedintcount,char*name)

voidunregister_chrdev_region(dev_tfirst,unsignedintcount)

函数传入值

first:要分配的设备号的初始值

count:要分配(释放)的设备号数目

name:要申请设备号的设备名称(在/proc/devices和sysfs中显示)

dev:动态分配的第一个设备号

函数返回值

成功:0(只限于两种注册函数)

出错:-1(只限于两种注册函数)

(3)最新版本的字符设备注册。

在获得了系统分配的设备号之后,通过注册设备才能实现设备号和驱动程序之间的关联。这里讲解2.6内核中的字符设备的注册和注销过程。

在Linux内核中使用structcdev结构来描述字符设备,我们在驱动程序中必须将已分配到的设备号以及设备操作接口(即为structfile_operations结构)赋予structcdev结构变量。首先使用cdev_alloc()函数向系统申请分配structcdev结构,再用cdev_init()函数初始化已分配到的结构并与file_operations结构关联起来。最后调用cdev_add()函数将设备号与structcdev结构进行关联并向内核正式报告新设备的注册,这样新设备可以被用起来了。

如果要从系统中删除一个设备,则要调用cdev_del()函数。具体函数格式如表11.4所示。

表11.4 最新版本的字符设备注册

所需头文件

#include<linux/cdev.h>

函数原型

sturctcdev*cdev_alloc(void)

voidcdev_init(structcdev*cdev,structfile_operations*fops)

intcdev_add(structcdev*cdev,dev_tnum,unsignedintcount)

voidcdev_del(structcdev*dev)

函数传入值

cdev:需要初始化/注册/删除的structcdev结构

fops:该字符设备的file_operations结构

num:系统给该设备分配的第一个设备号

count:该设备对应的设备号数量

函数返回值

成功:

cdev_alloc:返回分配到的structcdev结构指针

cdev_add:返回0

出错:

cdev_alloc:返回NULL

cdev_add:返回-1

2.6内核仍然保留早期版本的register_chrdev()等字符设备相关函数,其实从内核代码中可以发现,在register_chrdev()函数的实现中用到cdev_alloc()和cdev_add()函数,而在unregister_chrdev()函数的实现中调用cdev_del()函数。因此很多代码仍然使用早期版本接口,但这种机制将来会从内核中消失。

前面已经提到字符设备的实际操作在structfile_operations结构的一组函数中定义,并在驱动程序中需要与字符设备结构关联起来。下面讨论structfile_operations结构中最主要的成员函数和它们的用法。

(4)打开设备。

打开设备的函数接口是open,根据设备的不同,open函数接口完成的功能也有所不同,但通常情况下在open函数接口中要完成如下工作。

n 递增计数器,检查错误。

n 如果未初始化,则进行初始化。

n 识别次设备号,如果必要,更新f_op指针。

n 分配并填写被置于filp->private_data的数据结构。

其中递增计数器是用于设备计数的。由于设备在使用时通常会打开多次,也可以由不同的进程所使用,所以若有一进程想要删除该设备,则必须保证其他设备没有使用该设备。因此使用计数器就可以很好地完成这项功能。

这里,实现计数器操作的是在2.6内核早期版本的<linux/module.h>中定义的3个宏,它们在最新版本里早就消失了,在下面列出只是为了帮读者理解老版本中的驱动代码。

n MOD_INC_USE_COUNT:计数器加1。

n MOD_DEC_USE_COUNT:计数器减1。

n MOD_IN_USE:计数器非零时返回真。

另外,当有多个物理设备时,就需要识别次设备号来对各个不同的设备进行不同的操作,在有些驱动程序中并不需要用到。

注意

虽然这是对设备文件执行的第一个操作,但却不是驱动程序一定要声明的操作。若这个函数的入口为NULL,那么设备的打开操作将永远成功,但系统不会通知驱动程序。

(5)释放设备。

释放设备的函数接口是release()。要注意释放设备和关闭设备是完全不同的。当一个进程释放设备时,其他进程还能继续使用该设备,只是该进程暂时停止对该设备的使用;而当一个进程关闭设备时,其他进程必须重新打开此设备才能使用它。

释放设备时要完成的工作如下。

n 递减计数器MOD_DEC_USE_COUNT(最新版本已经不再使用)。

n 释放打开设备时系统所分配的内存空间(包括filp->private_data指向的内存空间)。

n 在最后一次释放设备操作时关闭设备。

(6)读写设备。

读写设备的主要任务就是把内核空间的数据复制到用户空间,或者从用户空间复制到内核空间,也就是将内核空间缓冲区里的数据复制到用户空间的缓冲区中或者相反。这里首先解释一个read()和write()函数的入口函数,如表11.5所示。

表11.5 read、write函数接口语法要点

所需头文件

#include<linux/fs.h>

函数原型

ssize_t(*read)(structfile*filp,char*buff,size_tcount,loff_t*offp)
ssize_t(*write)(structfile*filp,constchar*buff,size_tcount,loff_t*offp)

函数传入值

filp:文件指针

buff:指向用户缓冲区

count:传入的数据长度

offp:用户在文件中的位置

函数返回值

成功:写入的数据长度

虽然这个过程看起来很简单,但是内核空间地址和应用空间地址是有很大区别的,其中一个区别是用户空间的内存是可以被换出的,因此可能会出现页面失效等情况。所以不能使用诸如memcpy()之类的函数来完成这样的操作。在这里要使用copy_to_user()或copy_from_user()等函数,它们是用来实现用户空间和内核空间的数据交换的。

copy_to_user()和copy_from_user()的格式如表11.6所示。

表11.6 copy_to_user()/copy_from_user()函数语法要点

所需头文件

#include<asm/uaccess.h>

函数原型

unsignedlongcopy_to_user(void*to,constvoid*from,unsignedlongcount)
unsignedlongcopy_from_user(void*to,constvoid*from,unsignedlongcount)

函数传入值

to:数据目的缓冲区

from:数据源缓冲区

count:数据长度

函数返回值

成功:写入的数据长度
失败:-EFAULT

要注意,这两个函数不仅实现了用户空间和内核空间的数据转换,而且还会检查用户空间指针的有效性。如果指针无效,那么就不进行复制。

(7)ioctl。

大部分设备除了读写操作,还需要硬件配置和控制(例如,设置串口设备的波特率)等很多其他操作。在字符设备驱动中ioctl函数接口给用户提供对设备的非读写操作机制。

ioctl函数接口的具体格式如表11.7所示。

表11.7 ioctl函数接口语法要点

所需头文件

#include<linux/fs.h>

函数原型

int(*ioctl)(structinode*inode,structfile*filp,unsignedintcmd,unsignedlongarg)

函数传入值

inode:文件的内核内部结构指针

filp:被打开的文件描述符

cmd:命令类型

arg:命令相关参数

下面列出其他在驱动程序中常用的内核函数。

(8)获取内存。

在应用程序中获取内存通常使用函数malloc(),但在设备驱动程序中动态开辟内存可以以字节或页面为单位。其中,以字节为单位分配内存的函数有kmalloc(),注意的是,kmalloc()函数返回的是物理地址,而malloc()等返回的是线性虚拟地址,因此在驱动程序中不能使用malloc()函数。与malloc()不同,kmalloc()申请空间有大小限制。长度是2的整次方,并且不会对所获取的内存空间清零。

以页为单位分配内存的函数如下所示。

n get_zeroed_page():获得一个已清零页面。

n get_free_page():获得一个或几个连续页面。

n get_dma_pages():获得用于DMA传输的页面。

与之相对应的释放内存用也有kfree()或free_page函数族。

表11.8给出了kmalloc()函数的语法格式。

表11.8 kmalloc()函数语法要点

所需头文件

#include<linux/malloc.h>

函数原型

void*kmalloc(unsignedintlen,intflags)

函数传入值

len:希望申请的字节数

flags

GFP_KERNEL:内核内存的通常分配方法,可能引起睡眠

GFP_BUFFER:用于管理缓冲区高速缓存

GFP_ATOMIC:为中断处理程序或其他运行于进程上下文之外的代码分配内存,且不会引起睡眠

GFP_USER:用户分配内存,可能引起睡眠

GFP_HIGHUSER:优先高端内存分配

__GFP_DMA:DMA数据传输请求内存

__GFP_HIGHMEN:请求高端内存

函数返回值

成功:写入的数据长度
失败:-EFAULT

表11.9给出了kfree()函数的语法格式。

表11.9 kfree()函数语法要点

所需头文件

#include<linux/malloc.h>

函数原型

voidkfree(void*obj)

函数传入值

obj:要释放的内存指针

函数返回值

成功:写入的数据长度
失败:-EFAULT

表11.10给出了以页为单位的分配函数get_free_page类函数的语法格式。

表11.10 get_free_page类函数语法要点

所需头文件

#include<linux/malloc.h>

函数原型

unsignedlongget_zeroed_page(intflags)
unsignedlong__get_free_page(intflags)
unsignedlong__get_free_page(intflags,unsignedlongorder)
unsignedlong__get_dma_page(intflags,unsignedlongorder)

函数传入值

flags:同kmalloc()

order:要请求的页面数,以2为底的对数

函数返回值

成功:返回指向新分配的页面的指针
失败:-EFAULT

表11.11给出了基于页的内存释放函数free_page族函数的语法格式。

表11.11 free_page类函数语法要点

所需头文件

#include<linux/malloc.h>

函数原型

unsignedlongfree_page(unsignedlongaddr)
unsignedlongfree_pages(unsignedlongaddr,unsignedlongorder)

函数传入值

addr:要释放的内存起始地址

order:要请求的页面数,以2为底的对数

函数返回值

成功:写入的数据长度
失败:-EFAULT

(9)打印信息。

就如同在编写用户空间的应用程序,打印信息有时是很好的调试手段,也是在代码中很常用的组成部分。但是与用户空间不同,在内核空间要用函数printk()而不能用平常的函数printf()。printk()和printf()很类似,都可以按照一定的格式打印消息,所不同的是,printk()还可以定义打印消息的优先级。

表11.12给出了printk()函数的语法格式。

表11.12 printk类函数语法要点

所需头文件

#include<linux/kernel>

函数原型

intprintk(constchar*fmt,…)

函数传入值

fmt:
日志级别

KERN_EMERG:紧急时间消息

KERN_ALERT:需要立即采取动作的情况

KERN_CRIT:临界状态,通常涉及严重的硬件或软件操作失败

KERN_ERR:错误报告

KERN_WARNING:对可能出现的问题提出警告

KERN_NOTICE:有必要进行提示的正常情况

KERN_INFO:提示性信息

KERN_DEBUG:调试信息

…:与printf()相同

函数返回值

成功:0
失败:-1

这些不同优先级的信息输出到系统日志文件(例如:“/var/log/messages”),有时也可以输出到虚拟控制台上。其中,对输出给控制台的信息有一个特定的优先级console_loglevel。只有打印信息的优先级小于这个整数值,信息才能被输出到虚拟控制台上,否则,信息仅仅被写入到系统日志文件中。若不加任何优先级选项,则消息默认输出到系统日志文件中。

注意

要开启klogd和syslogd服务,消息才能正常输出。

4.proc文件系统

/proc文件系统是一个伪文件系统,它是一种内核和内核模块用来向进程发送信息的机制。这个伪文件系统让用户可以和内核内部数据结构进行交互,获取有关系统和进程的有用信息,在运行时通过改变内核参数来改变设置。与其他文件系统不同,/proc存在于内存之中而不是在硬盘上。读者可以通过“ls”查看/proc文件系统的内容。

表11.13列出了/proc文件系统的主要目录内容。

表11.13 /proc文件系统主要目录内容

目录名称

目录内容

目录名称

目录内容

apm

高级电源管理信息

locks

内核锁

cmdline

内核命令行

meminfo

内存信息

cpuinfo

CPU相关信息

misc

杂项

devices

设备信息(块设备/字符设备)

modules

加载模块列表

dma

使用的DMA通道信息

mounts

加载的文件系统

filesystems

支持的文件系统信息

partitions

系统识别的分区表

interrupts

中断的使用信息

rtc

实时时钟

ioports

I/O端口的使用信息

stat

全面统计状态表

kcore

内核映像

swaps

对换空间的利用情况

kmsg

内核消息

version

内核版本

ksyms

内核符号表

uptime

系统正常运行时间

loadavg

负载均衡

除此之外,还有一些是以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的PID号为目录名,它们是读取进程信息的接口。进程目录的结构如表11.14所示。

表11.14 /proc中进程目录结构

目录名称

目录内容

目录名称

目录内容

cmdline

命令行参数

cwd

当前工作目录的链接

environ

环境变量值

exe

指向该进程的执行命令文件

fd

一个包含所有文件描述符的目录

maps

内存映像

mem

进程的内存被利用情况

statm

进程内存状态信息

stat

进程状态

root

链接此进程的root目录

status

进程当前状态,以可读的方式显示出来

用户可以使用cat命令来查看其中的内容。

可以看到,/proc文件系统体现了内核及进程运行的内容,在加载模块成功后,读者可以通过查看/proc/device文件获得相关设备的主设备号。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

9月2日消息,不造车的华为或将催生出更大的独角兽公司,随着阿维塔和赛力斯的入局,华为引望愈发显得引人瞩目。

关键字: 阿维塔 塞力斯 华为

加利福尼亚州圣克拉拉县2024年8月30日 /美通社/ -- 数字化转型技术解决方案公司Trianz今天宣布,该公司与Amazon Web Services (AWS)签订了...

关键字: AWS AN BSP 数字化

伦敦2024年8月29日 /美通社/ -- 英国汽车技术公司SODA.Auto推出其旗舰产品SODA V,这是全球首款涵盖汽车工程师从创意到认证的所有需求的工具,可用于创建软件定义汽车。 SODA V工具的开发耗时1.5...

关键字: 汽车 人工智能 智能驱动 BSP

北京2024年8月28日 /美通社/ -- 越来越多用户希望企业业务能7×24不间断运行,同时企业却面临越来越多业务中断的风险,如企业系统复杂性的增加,频繁的功能更新和发布等。如何确保业务连续性,提升韧性,成...

关键字: 亚马逊 解密 控制平面 BSP

8月30日消息,据媒体报道,腾讯和网易近期正在缩减他们对日本游戏市场的投资。

关键字: 腾讯 编码器 CPU

8月28日消息,今天上午,2024中国国际大数据产业博览会开幕式在贵阳举行,华为董事、质量流程IT总裁陶景文发表了演讲。

关键字: 华为 12nm EDA 半导体

8月28日消息,在2024中国国际大数据产业博览会上,华为常务董事、华为云CEO张平安发表演讲称,数字世界的话语权最终是由生态的繁荣决定的。

关键字: 华为 12nm 手机 卫星通信

要点: 有效应对环境变化,经营业绩稳中有升 落实提质增效举措,毛利润率延续升势 战略布局成效显著,战新业务引领增长 以科技创新为引领,提升企业核心竞争力 坚持高质量发展策略,塑强核心竞争优势...

关键字: 通信 BSP 电信运营商 数字经济

北京2024年8月27日 /美通社/ -- 8月21日,由中央广播电视总台与中国电影电视技术学会联合牵头组建的NVI技术创新联盟在BIRTV2024超高清全产业链发展研讨会上宣布正式成立。 活动现场 NVI技术创新联...

关键字: VI 传输协议 音频 BSP

北京2024年8月27日 /美通社/ -- 在8月23日举办的2024年长三角生态绿色一体化发展示范区联合招商会上,软通动力信息技术(集团)股份有限公司(以下简称"软通动力")与长三角投资(上海)有限...

关键字: BSP 信息技术
关闭
关闭