潍坊市论坛

首页 » 分类 » 问答 » 一文带你掌握Linux字符设备架构
TUhjnbcbe - 2021/4/1 2:15:00
一、Linux设备分类

Linux系统为了管理方便,将设备分成三种基本类型:

字符设备块设备网络设备字符设备:

字符(char)设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常至少要实现open、close、read和write的系统调用。

字符终端(/dev/console)和串口(/dev/ttyS0以及类似设备)就是两个字符设备,它们能很好的说明“流”这种抽象概念。

字符设备可以通过文件节点来访问,比如/dev/tty1和/dev/lp0等。这些设备文件和普通文件之间的唯一差别在于对普通文件的访问可以前后移动访问位置,而大多数字符设备是一个只能顺序访问的数据通道。然而,也存在具有数据区特性的字符设备,访问它们时可前后移动访问位置。例如framebuffer就是这样的一个设备,app可以用mmap或lseek访问抓取的整个图像。

在/dev下执行ls-l,可以看到很多创建好的设备节点:

字符设备文件(类型为c),设备文件是没有文件大小的,取而代之的是两个号码:主设备号5+次设备号1。

块设备:

和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备(例如磁盘)上能够容纳filesystem。在大多数的Unix系统中,进行I/O操作时块设备每次只能传输一个或多个完整的块,而每块包含字节(或2的更高次幂字节的数据)。

Linux可以让app像字符设备一样地读写块设备,允许一次传递任意多字节的数据。因此,块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不同对用户来讲是透明的。在内核中,和字符驱动程序相比,块驱动程序具有完全不同的接口。

块设备文件(类型为b):

网络设备:

任何网络事物都需要经过一个网络接口形成,网络接口是一个能够和其他主机交换数据的设备。接口通常是一个硬件设备,但也可能是个纯软件设备,比如回环(loopback)接口。

网络接口由内核中的网络子系统驱动,负责发送和接收数据包。许多网络连接(尤其是使用TCP协议的连接)是面向流的,但网络设备却围绕数据包的传送和接收而设计。网络驱动程序不需要知道各个连接的相关信息,它只要处理数据包即可。

由于不是面向流的设备,因此将网络接口映射到filesystem中的节点(比如/dev/tty1)比较困难。

Unix访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在filesystem中不存在对应的节点。内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包相关的函数socket,也叫套接字。

查看网络设备使用命令ifconfig:

二、字符设备架构是如何实现的?

在Linux的世界里面一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。我们知道如果应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核中有那么多驱动程序,应用层怎么才能精确的调用到底层的驱动程序呢?

在这里我们字符设备为例,来看一下应用程序是如何和底层驱动程序关联起来的。必须知道的基础知识:

1.在Linux文件系统中,每个文件都用一个structinode结构体来描述,这个结构体里面记录了这个文件的所有信息,例如:文件类型,访问权限等。

2.在Linux操作系统中,每个驱动程序在应用层的/dev目录下都会有一个设备文件和它对应,并且该文件会有对应的主设备号和次设备号。

3.在Linux操作系统中,每个驱动程序都要分配一个主设备号,字符设备的设备号保存在structcdev结构体中。

structcdev{structkobjectkobj;structmodule*owner;conststructfile_operations*ops;//接口函数集合structlist_headlist;//内核链表dev_tdev;//设备号unsignedintcount;//次设备号个数};4.在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个structfile结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。

注意:

常常我们认为structinode描述的是文件的静态信息,即这些信息很少会改变。而structfile描述的是动态信息,即在对文件的操作的时候,structfile里面的信息经常会发生变化。典型的是structfile结构体里面的f_pos(记录当前文件的位移量),每次读写一个普通文件时f_ops的值都会发生改变。

这几个结构体关系如下图所示:

通过上图我们可以知道,如果想访问底层设备,就必须打开对应的设备文件。也就是在这个打开的过程中,Linux内核将应用层和对应的驱动程序关联起来。

1.当open函数打开设备文件时,可以根据设备文件对应的structinode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备)。还会分配一个structfile结构体。

2.根据structinode结构体里面记录的设备号,可以找到对应的驱动程序。这里以字符设备为例。在Linux操作系统中每个字符设备有一个structcdev结构体。此结构体描述了字符设备所有的信息,其中最重要一项的就是字符设备的操作函数接口。

3.找到structcdev结构体后,Linux内核就会将structcdev结构体所在的内存空间首地记录在structinode结构体的i_cdev成员中。将structcdev结构体的中记录的函数操作接口地址记录在structfile结构体的f_op成员中。

4.任务完成,VFS层会给应用层返回一个文件描述符(fd)。这个fd是和structfile结构体对应的。接下来上层的应用程序就可以通过fd来找到strutfile,然后在由structfile找到操作字符设备的函数接口了。

三、字符驱动相关函数分析

/***cdev_init()-initializeacdevstructure*

cdev:thestructuretoinitialize*

fops:thefile_operationsforthisdevice**Initializes

cdev,remembering

fops,makingitreadytoaddtothe*systemwithcdev_add().*/voidcdev_init(structcdev*cdev,conststructfile_operations*fops)功能:初始化cdev结构体参数:

cdevcdev结构体地址

fops操作字符设备的函数接口地址返回值:无

/***register_chrdev_region()-registerarangeofdevicenumbers*

from:thefirstinthedesiredrangeofdevicenumbers;mustinclude*themajornumber.*

count:thenumberofconsecutivedevicenumbersrequired*

name:thenameofthedeviceordriver.**Returnvalueiszeroonsuccess,anegativeerrorcodeonfailure.*/intregister_chrdev_region(dev_tfrom,unsignedcount,constchar*name)功能:注册一个范围()的设备号参数:

from设备号

count注册的设备个数

name设备的名字返回值:成功返回0,失败返回错误码(负数)

/***cdev_add()-addachardevicetothesystem*

p:thecdevstructureforthedevice*

dev:thefirstdevicenumberforwhichthisdeviceisresponsible*

count:thenumberofconsecutiveminornumberscorrespondingtothis*device**cdev_add()addsthedevicerepresentedby

ptothesystem,makingit*liveimmediately.Anegativeerrorcodeisreturnedonfailure.*/intcdev_add(structcdev*p,dev_tdev,unsignedcount)功能:添加一个字符设备到操作系统参数:

pcdev结构体地址

dev设备号

count次设备号个数返回值:成功返回0,失败返回错误码(负数)

/***cdev_del()-removeacdevfromthesystem*

p:thecdevstructuretoberemoved**cdev_del()removes

pfromthesystem,possiblyfreeingthestructure*itself.*/voidcdev_del(structcdev*p)功能:从系统中删除一个字符设备参数:

pcdev结构体地址返回值:无

staticinlineintregister_chrdev(unsignedintmajor,constchar*name,conststructfile_operations*fops)功能:注册或者分配设备号,并注册fops到cdev结构体,如果major0,功能为注册该主设备号,如果major=0,功能为动态分配主设备号。参数:

major:主设备号

name:设备名称,执行cat/proc/devices显示的名称

fops:文件系统的接口指针返回值如果major0成功返回0,失败返回负的错误码如果major=0成功返回主设备号,失败返回负的错误码

该函数实现了对cdev的初始化和注册的封装,所以调用该函数之后就不需要自己操作cdev了。

相对的注销函数为unregister_chrdev

staticinlinevoidunregister_chrdev(unsignedintmajor,constchar*name)四、如何编写字符设备驱动

参考上图,编写字符设备驱动步骤如下:

1.实现模块加载和卸载入口函数

module_init(hello_init);module_exit(hello_exit);2.申请主设备号

申请主设备号(内核中用于区分和管理不同字符设备)

register_chrdev_region(devno,number_of_devices,"hello");3.创建设备节点

创建设备节点文件(为用户提供一个可操作到文件接口--open())创建设备节点有两种方式:手动方式创建,函数自动创建。手动创建:

mknod/dev/helloc

自动创建设备节点

除了使用mknod命令手动创建设备节点,还可以利用linux的udev、mdev机制,而我们的ARM开发板上移植的busybox有mdev机制,那么就使用mdev机制来自动创建设备节点。

在etc/init.d/rcS文件里有一句:

echo/sbin/mdev/proc/sys/kernel/hotplug

该名命令就是用来自动创建设备节点。

udev是一个工作在用户空间的工具,它能根据系统中硬件设备的状态动态的更新设备文件,包括设备文件的创建,删除,权限等。这些文件通常都定义在/dev目录下,但也可以在配置文件中指定。udev必须有内核中的sysfs和tmpfs支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。

udev运行在用户模式,而非内核中。udev的初始化脚本在系统启动时创建设备节点,并且当插入新设备——加入驱动模块——在sysfs上注册新的数据后,udev会创新新的设备节点。

注意,udev是通过对内核产生的设备文件修改,或增加别名的方式来达到自定义设备文件的目的。但是,udev是用户模式程序,其不会更改内核行为。也就是说,内核仍然会创建sda,sdb等设备文件,而udev可根据设备的唯一信息来区分不同的设备,并产生新的设备文件(或链接)。

例如:

如果驱动模块可以将自己的设备号作为内核参数导出,在sysfs文件中就有一个叫做uevent文件记录它的值。

由上图可知,uevent中包含了主设备号和次设备号的值以及设备名字。

在Linux应用层启动一个udev程序,这个程序的第一次运行的时候,会遍历/sys目录,寻找每个子目录的uevent文件,从这些uevent文件中获取创建设备节点的信息,然后调用mknod程序在/dev目录下创建设备节点。结束之后,udev就开始等待内核空间的event。这个设备模型的东西,我们在后面再详细说。这里大就可以这样理解,在Linux内核中提供了一些函数接口,通过这些函数接口,我们可在sysfs文件系统中导出我们的设备号的值,导出值之后,内核还会向应用层上报event。此时udev就知道有活可以干了,它收到这个event后,就读取event对应的信息,接下来就开始创建设备节点啦。

如何创建一个设备类?

第一步:通过宏class_create()创建一个class类型的对象;

/*Thisisa#definetokeepthe

1
查看完整版本: 一文带你掌握Linux字符设备架构