0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

关于Linux字符设备基本类型

FPGA之家 来源:FPGA之家 作者:FPGA之家 2022-08-17 09:04 次阅读

一、Linux设备分类

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

字符设备

块设备

网络设备

字符设备:

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

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

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

在/dev下执行ls -l ,可以看到很多创建好的设备节点:2ec155ec-1dc1-11ed-ba43-dac502259ad0.png

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

块设备:

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

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

块设备文件(类型为b):2ed031f2-1dc1-11ed-ba43-dac502259ad0.png

网络设备:

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

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

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

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

查看网络设备使用命令ifconfig:2ee23262-1dc1-11ed-ba43-dac502259ad0.png

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

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

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

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

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

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

structcdev{
structkobjectkobj;
structmodule*owner;
conststructfile_operations*ops;//接口函数集合
structlist_headlist;//内核链表
dev_tdev;//设备号
unsignedintcount;//次设备号个数
};

4.在Linux操作系统中,每打开一次文件,Linux操作系统在VFS层都会分配一个struct file结构体来描述打开的这个文件。该结构体用于维护文件打开权限、文件指针偏移值、私有内存地址等信息。

注意:

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

这几个结构体关系如下图所示:2efcbb78-1dc1-11ed-ba43-dac502259ad0.png

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

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

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

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

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

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

/**
*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结构体,
如果major>0,功能为注册该主设备号,
如果major=0,功能为动态分配主设备号。
参数:
@major:主设备号
@name:设备名称,执行cat/proc/devices显示的名称
@fops:文件系统的接口指针
返回值
如果major>0成功返回0,失败返回负的错误码
如果major=0成功返回主设备号,失败返回负的错误码

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

相对的注销函数为unregister_chrdev

staticinlinevoidunregister_chrdev(unsignedintmajor,constchar*name)

四、如何编写字符设备驱动

2f1b29dc-1dc1-11ed-ba43-dac502259ad0.png

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

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

module_init(hello_init);
module_exit(hello_exit);

2. 申请主设备号

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

register_chrdev_region(devno,number_of_devices,"hello");

3. 创建设备节点

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

mknod/dev/helloc2500

自动创建设备节点

除了使用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文件记录它的值。2f2cb85a-1dc1-11ed-ba43-dac502259ad0.png

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

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

如何创建一个设备类?

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

/*Thisisa#definetokeepthecompilerfrommergingdifferent
*instancesofthe__keyvariable*/
#defineclass_create(owner,name)
({
staticstructlock_class_key__key;
__class_create(owner,name,&__key);
})

参数:
@ownerTHIS_MODULE
@name类名字
返回值
可以定义一个structclass的指针变量cls接受返回值,然后通过IS_ERR(cls)判断
是否失败,如果成功这个宏返回0,失败返回非9值(可以通过PTR_ERR(cls)来获得
失败返回的错误码)

在Linux内核中,把设备进行了分类,同一类设备可以放在同一个目录下,该函数启示就是创建了一个类,例如:2f421948-1dc1-11ed-ba43-dac502259ad0.png

第二步:导出我们的设备信息到用户空间

/**
*device_create-createsadeviceandregistersitwithsysfs
*@class:pointertothestructclassthatthisdeviceshouldberegisteredto
*@parent:pointertotheparentstructdeviceofthisnewdevice,ifany
*@devt:thedev_tforthechardevicetobeadded
*@drvdata:thedatatobeaddedtothedeviceforcallbacks
*@fmt:stringforthedevice'sname
*
*Thisfunctioncanbeusedbychardeviceclasses.Astructdevice
*willbecreatedinsysfs,registeredtothespecifiedclass.
*
*A"dev"filewillbecreated,showingthedev_tforthedevice,if
*thedev_tisnot0,0.
*Ifapointertoaparentstructdeviceispassedin,thenewlycreated
*structdevicewillbeachildofthatdeviceinsysfs.
*Thepointertothestructdevicewillbereturnedfromthecall.
*Anyfurthersysfsfilesthatmightberequiredcanbecreatedusingthis
*pointer.
*
*Returns&structdevicepointeronsuccess,orERR_PTR()onerror.
*
*Note:thestructclasspassedtothisfunctionmusthavepreviously
*beencreatedwithacalltoclass_create().
*/
structdevice*device_create(structclass*class,structdevice*parent,
dev_tdevt,void*drvdata,constchar*fmt,...)

自动创建设备节点使用实例:

staticstructclass*cls;
staticstructdevice*test_device;

devno=MKDEV(major,minor);
cls=class_create(THIS_MODULE,"helloclass");
if(IS_ERR(cls))
{
unregister_chrdev(major,"hello");
returnresult;
}
test_device=device_create(cls,NULL,devno,NULL,"hellodevice");
if(IS_ERR(test_device))
{
class_destroy(cls);
unregister_chrdev(major,"hello");
returnresult;
}

4 实现file_operations

staticconststructfile_operationsfifo_operations={
.owner=THIS_MODULE,
.open=dev_fifo_open,
.read=dev_fifo_read,
.write=dev_fifo_write,
.unlocked_ioctl=dev_fifo_unlocked_ioctl,
};

open、release对应应用层的open()、close()函数。实现比较简单,

直接返回0即可。其中read、write、unloched_ioctrl 函数的实现需要涉及到用户空间和内存空间的数据拷贝。

在Linux操作系统中,用户空间和内核空间是相互独立的。也就是说内核空间是不能直接访问用户空间内存地址,同理用户空间也不能直接访问内核空间内存地址。

如果想实现,将用户空间的数据拷贝到内核空间或将内核空间数据拷贝到用户空间,就必须借助内核给我们提供的接口来完成。

1. read接口实现

用户空间-->内核空间

字符设备的write接口定义如下:

ssize_t(*write)(structfile*filp,constchar__user*buf,size_tcount,loff_t*f_pos);
参数:
filp:待操作的设备文件file结构体指针
buf:待写入所读取数据的用户空间缓冲区指针
count:待读取数据字节数
f_pos:待读取数据文件位置,写入完成后根据实际写入字节数重新定位
返回:
成功实际写入的字节数,失败返回负值

如果该操作为空,将使得write系统调用返回负EINVAL失败,正常返回实际写入的字节数。

用户空间向内核空间拷贝数据需要使用copy_from_user函数,该函数定义在arch/arm/include/asm/uaccess.h中。

staticinlineintcopy_from_user(void*to,constvoid__uservolatile*from,unsignedlongn)
参数:
to:目标地址(内核空间)
from:源地址(用户空间)
n:将要拷贝数据的字节数
返回:
成功返回0,失败返回没有拷贝成功的数据字节数
2f517aa0-1dc1-11ed-ba43-dac502259ad0.png

还可以使用get_user宏:

intget_user(data,ptr);
参数:
data:可以是字节、半字、字、双字类型的内核变量
ptr:用户空间内存指针
返回:
成功返回0,失败返回非0

2. write接口实现

内核空间-->用户空间

字符设备的read接口定义如下:

ssize_t(*read)(structfile*filp,char__user*buf,size_tcount,lofft*f_pos);
参数:
filp:待操作的设备文件file结构体指针
buf:待写入所读取数据的用户空间缓冲区指针
count:待读取数据字节数
f_pos:待读取数据文件位置,读取完成后根据实际读取字节数重新定位
__user:是一个空的宏,主要用来显示的告诉程序员它修饰的指针变量存放的是用户空间的地址。

返回值:
成功实际读取的字节数,失败返回负值

注意:如果该操作为空,将使得read系统调用返回负EINVAL失败,正常返回实际读取的字节数。

用户空间从内核空间读取数据需要使用copy_to_user函数:

staticinlineintcopy_to_user(void__uservolatile*to,constvoid*from,unsignedlongn)
参数:
to:目标地址(用户空间)
from:源地址(内核空间)
n:将要拷贝数据的字节数
返回:
成功返回0,失败返回没有拷贝成功的数据字节数
2f823dfc-1dc1-11ed-ba43-dac502259ad0.png在这里插入图片描述

还可以使用put_user宏:

intput_user(data,prt)
参数:
data:可以是字节、半字、字、双字类型的内核变量
ptr:用户空间内存指针
返回:
成功返回0,失败返回非0

这样我们就可以实现read、write函数了,实例如下:

ssize_thello_read(structfile*filp,char*buff,size_tcount,loff_t*offp)
{
ssize_tresult=0;

if(count>127)
count=127;

if(copy_to_user(buff,data,count))
{
result=-EFAULT;
}
else
{
printk(KERN_INFO"wrote%dbytes
",count);
result=count;
}
returnresult;
}
ssize_thello_write(structfile*filp,constchar*buf,size_tcount,loff_t*f_pos)
{
ssize_tret=0;
//printk(KERN_INFO"Writing%dbytes
",count);
if(count>127)return-ENOMEM;

if(copy_from_user(data,buf,count)){
ret=-EFAULT;
}
else{
data[count]='�';
printk(KERN_INFO"Received:%s
",data);
ret=count;
}
returnret;
}

3. unlocked_ioctl接口实现

(1)为什么要实现xxx_ioctl ?

前面我们在驱动中已经实现了读写接口,通过这些接口我们可以完成对设备的读写。但是很多时候我们的应用层工程师除了要对设备进行读写数据之外,还希望可以对设备进行控制。例如:针对串口设备,驱动层除了需要提供对串口的读写之外,还需提供对串口波特率、奇偶校验位、终止位的设置,这些配置信息需要从应用层传递一些基本数据,仅仅是数据类型不同。

通过xxx_ioctl函数接口,可以提供对设备的控制能力,增加驱动程序的灵活性。

(2)如何实现xxx_ioctl函数接口?

增加xxx_ioctl函数接口,应用层可以通过ioctl系统调用,根据不同的命令来操作dev_fifo。

kernel 2.6.35 及之前的版本中struct file_operations 一共有3个ioctl :ioctl,unlocked_ioctl和compat_ioctl 现在只有unlocked_ioctl和compat_ioctl 了

在kernel 2.6.36 中已经完全删除了struct file_operations 中的ioctl 函数指针,取而代之的是unlocked_ioctl 。

· 2.6.36 之前的内核

long(ioctl)(structinodenode,structfile*filp,unsignedintcmd,unsignedlongarg)

· 2.6.36之后的内核

long(*unlocked_ioctl)(structfile*filp,unsignedintcmd,unsignedlongarg)

参数cmd: 通过应用函数ioctl传递下来的命令

先来看看应用层的ioctl和驱动层的xxx_ioctl对应关系:2f936bae-1dc1-11ed-ba43-dac502259ad0.png<1>应用层ioctl参数分析

intioctl(intfd,intcmd,...);
参数:
@fd:打开设备文件的时候获得文件描述符
@cmd:第二个参数:给驱动层传递的命令,需要注意的时候,驱动层的命令和应用层的命令一定要统一
@第三个参数:"..."在C语言中,很多时候都被理解成可变参数。
返回值
成功:0
失败:-1,同时设置errno

小贴士:

当我们通过ioctl调用驱动层xxx_ioctl的时候,有三种情况可供选择:

1:不传递数据给xxx_ioctl
2:传递数据给xxx_ioctl,希望它最终能把数据写入设备(例如:设置串口的波特率)
3:调用xxxx_ioctl希望获取设备的硬件参数(例如:获取当前串口设备的波特率)
这三种情况中,有些时候需要传递数据,有些时候不需要传递数据。在C语言中,是
无法实现函数重载的。那怎么办?用"..."来欺骗编译器了,"..."本来的意思是传
递多参数。在这里的意思是带一个参数还是不带参数。

参数可以传递整型值,也可以传递某块内存的地址,内核接口函数必须根据实际情况
提取对应的信息。

<2>驱动层xxx_ioctl参数分析

long(*unlocked_ioctl)(structfile*file,unsignedintcmd,unsignedlongarg);
参数:
@file:vfs层为打开字符设备文件的进程创建的结构体,用于存放文件的动态信息
@cmd:用户空间传递的命令,可以根据不同的命令做不同的事情
@第三个参数:用户空间的数据,主要这个数据可能是一个地址值(用户空间传递的是一个地址),也可能是一个数值,也可能没值
返回值
成功:0
失败:带错误码的负值

<3>如何确定cmd 的值。

该值主要用于区分命令的类型,虽然我只需要传递任意一个整型值即可,但是我们尽量按照内核规范要求,充分利用这32bite的空间,如果大家都没有规矩,又如何能成方圆?

现在我就来看看,在Linux 内核中这个cmd是如何设计的吧!2fa53960-1dc1-11ed-ba43-dac502259ad0.png

具体含义如下:

设备类型 类型或叫幻数,代表一类设备,一般用一个字母或者1个8bit的数字
序列号 代表这个设备的第几个命令
方 向 表示是由内核空间到用户空间,或是用户空间到内核空间,入:只读,只写,读写,其他
数据尺寸 表示需要读写的参数大小

由上可以一个命令由4个部分组成,每个部分需要的bite都不完全一样,制作一个命令需要在不同的位域写不同的数字,Linux 系统已经给我们封装好了宏,我们只需要直接调用宏来设计命令即可。

2fb47e3e-1dc1-11ed-ba43-dac502259ad0.png在这里插入图片描述

通过Linux 系统给我们提供的宏,我们在设计命令的时候,只需要指定设备类型、命令序号,数据类型三个字段就可以了。

2fd29400-1dc1-11ed-ba43-dac502259ad0.pngLinux 系统中已经设计了一场用的命令,可以通过查阅Linux 源码中的Documentation/ioctl/ioctl-number.txt文件,看哪些命令已经被使用过了。

<4> 如何检查命令?

可以通过宏_IOC_TYPE(nr)来判断应用程序传下来的命令type是否正确;

可以通过宏_IOC_DIR(nr)来得到命令是读还是写,然后再通过宏access_ok(type,addr,size)来判断用户层传递的内存地址是否合法。

使用方法如下:

if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
pr_err("cmd%u,badmagic0x%x/0x%x.
",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
return-ENOTTY;
}
if(_IOC_DIR(cmd)&_IOC_READ)
ret=!access_ok(VERIFY_WRITE,(void__user*)arg,_IOC_SIZE(cmd));
elseif(_IOC_DIR(cmd)&_IOC_WRITE)
ret=!access_ok(VERIFY_READ,(void__user*)arg,_IOC_SIZE(cmd));
if(ret){
pr_err("badaccess%ld.
",ret);
return-EFAULT;
}

5 注册cdev

定义好file_operations结构体,就可以通过函数cdev_init()、cdev_add()注册字符设备驱动了。

实例如下:

staticstructcdevcdev;

cdev_init(&cdev,&hello_ops);
error=cdev_add(&cdev,devno,1);

注意如果使用了函数register_chrdev(),就不用了执行上述操作,因为该函数已经实现了对cdev的封装。

五、实例

千言万语,全部汇总在这一个图里,大家可以对照相应的层次来学习。2fe8ff60-1dc1-11ed-ba43-dac502259ad0.png

六、实例

好了,现在我们可以来实现一个完整的字符设备框架的实例,包括打开、关闭、读写、ioctrl、自动创建设备节点等功能。

#include
#include
#include
#include
#include
#include
#include
#include"dev_fifo_head.h"

//指定的主设备号
#defineMAJOR_NUM250

//自己的字符设备
structmycdev
{
intlen;
unsignedcharbuffer[50];
structcdevcdev;
};

MODULE_LICENSE("GPL");
//设备号
staticdev_tdev_num={0};
//全局gcd
structmycdev*gcd;
//设备类
structclass*cls;
//获得用户传递的数据,根据它来决定注册的设备个数
staticintndevices=1;
module_param(ndevices,int,0644);
MODULE_PARM_DESC(ndevices,"Thenumberofdevicesforregister.
");

//打开设备
staticintdev_fifo_open(structinode*inode,structfile*file)
{
structmycdev*cd;

printk("dev_fifo_opensuccess!
");
//用structfile的文件私有数据指针保存structmycdev结构体指针
cd=container_of(inode->i_cdev,structmycdev,cdev);
file->private_data=cd;
return0;
}

//读设备
staticssize_tdev_fifo_read(structfile*file,char__user*ubuf,size_t
size,loff_t*ppos)
{
intn;
intret;
char*kbuf;
structmycdev*mycd=file->private_data;

printk("read*ppos:%lld
",*ppos);

if(*ppos==mycd->len)
return0;

//请求大大小>buffer剩余的字节数:读取实际记得字节数
if(size>mycd->len-*ppos)
n=mycd->len-*ppos;
else
n=size;

printk("n=%d
",n);
//从上一次文件位置指针的位置开始读取数据
kbuf=mycd->buffer+*ppos;
//拷贝数据到用户空间
ret=copy_to_user(ubuf,kbuf,n);
if(ret!=0)
return-EFAULT;

//更新文件位置指针的值
*ppos+=n;
printk("dev_fifo_readsuccess!
");
returnn;
}
//写设备
staticssize_tdev_fifo_write(structfile*file,constchar__user*ubuf,size_tsize,loff_t*ppos)
{
intn;
intret;
char*kbuf;
structmycdev*mycd=file->private_data;

printk("write*ppos:%lld
",*ppos);
//已经到达buffer尾部了
if(*ppos==sizeof(mycd->buffer))
return-1;
//请求大大小>buffer剩余的字节数(有多少空间就写多少数据)
if(size>sizeof(mycd->buffer)-*ppos)
n=sizeof(mycd->buffer)-*ppos;
else
n=size;
//从上一次文件位置指针的位置开始写入数据

kbuf=mycd->buffer+*ppos;
//拷贝数据到内核空间
ret=copy_from_user(kbuf,ubuf,n);
if(ret!=0)
return-EFAULT;

//更新文件位置指针的值
*ppos+=n;
//更新dev_fifo.len
mycd->len+=n;
printk("dev_fifo_writesuccess!
");
returnn;
}

//linux内核在2.6以后,已经废弃了ioctl函数指针结构,取而代之的是

longdev_fifo_unlocked_ioctl(structfile*file,unsignedintcmd,
unsignedlongarg)
{
intret=0;
structmycdev*mycd=file->private_data;

if(_IOC_TYPE(cmd)!=DEV_FIFO_TYPE){
pr_err("cmd%u,badmagic0x%x/0x%x.
",cmd,_IOC_TYPE(cmd),DEV_FIFO_TYPE);
return-ENOTTY;
}
if(_IOC_DIR(cmd)&_IOC_READ)
ret=!access_ok(VERIFY_WRITE,(void__user*)arg,_IOC_SIZE(cmd));
elseif(_IOC_DIR(cmd)&_IOC_WRITE)
ret=!access_ok(VERIFY_READ,(void__user*)arg,_IOC_SIZE(cmd));
if(ret){
pr_err("badaccess%ld.
",ret);
return-EFAULT;
}
switch(cmd)
{
caseDEV_FIFO_CLEAN:
printk("CMD:CLEAN
");
memset(mycd->buffer,0,sizeof(mycd->buffer));
break;
caseDEV_FIFO_SETVALUE:
printk("CMD:SETVALUE
");
mycd->len=arg;
break;
caseDEV_FIFO_GETVALUE:
printk("CMD:GETVALUE
");
ret=put_user(mycd->len,(int*)arg);
break;
default:
return-EFAULT;
}
returnret;
}

//设备操作函数接口

staticconststructfile_operationsfifo_operations={
.owner=THIS_MODULE,
.open=dev_fifo_open,
.read=dev_fifo_read,
.write=dev_fifo_write,
.unlocked_ioctl=dev_fifo_unlocked_ioctl,
};
//模块入口
int__initdev_fifo_init(void)
{
inti=0;
intn=0;
intret;

structdevice*device;
gcd=kzalloc(ndevices*sizeof(structmycdev),GFP_KERNEL);

if(!gcd){
return-ENOMEM;
}

//设备号:主设备号(12bit)|次设备号(20bit)
dev_num=MKDEV(MAJOR_NUM,0);
//静态注册设备号
ret=register_chrdev_region(dev_num,ndevices,"dev_fifo");
if(ret< 0){
    //静态注册失败,进行动态注册设备号
     ret   =alloc_chrdev_region(&dev_num,0,ndevices,"dev_fifo");
      if(ret < 0){
        printk("Fail to register_chrdev_region
");
        goto   err_register_chrdev_region;
      }
    }
    //创建设备类
    cls   = class_create(THIS_MODULE, "dev_fifo");
    if(IS_ERR(cls)){
        ret   = PTR_ERR(cls);
        goto   err_class_create;
    }
    printk("ndevices :   %d
",ndevices);
    for(n = 0;n < ndevices;n   ++)
    {
      //初始化字符设备
      cdev_init(&gcd[n].cdev,&fifo_operations);
      //添加设备到操作系统
      ret   = cdev_add(&gcd[n].cdev,dev_num + n,1);
      if (ret < 0)
      {
         goto   err_cdev_add;
      }
     //导出设备信息到用户空间(/sys/class/类名/设备名)
      device   = device_create(cls,NULL,dev_num +n,NULL,"dev_fifo%d",n);
      if(IS_ERR(device)){
         ret   = PTR_ERR(device);
         printk("Fail to device_create
");
         goto   err_device_create;    
      }
    }
    printk("Register   dev_fito to system,ok!
");
    return   0;
err_device_create:

    //将已经导出的设备信息除去
    for(i = 0;i < n;i ++)
    {
       device_destroy(cls,dev_num + i);    
    }
err_cdev_add:
    //将已经添加的全部除去
    for(i = 0;i < n;i ++)
    {
       cdev_del(&gcd[i].cdev);
    }
err_class_create:
    unregister_chrdev_region(dev_num,   ndevices);
err_register_chrdev_region:
    return   ret;
}
void __exit dev_fifo_exit(void)
{
    int i;
    //删除sysfs文件系统中的设备
    for(i = 0;i < ndevices;i   ++)
    {
        device_destroy(cls,dev_num + i);    
    }
    //删除系统中的设备类
    class_destroy(cls);
    //从系统中删除添加的字符设备
    for(i = 0;i < ndevices;i   ++)
    {
       cdev_del(&gcd[i].cdev);
    } 
    //释放申请的设备号
    unregister_chrdev_region(dev_num,   ndevices);
    return;
}
module_init(dev_fifo_init);
module_exit(dev_fifo_exit);   

头文件内容:

dev_fifo_head.h

#ifndef_DEV_FIFO_HEAD_H
#define_DEV_FIFO_HEAD_H
#defineDEV_FIFO_TYPE'k'
#defineDEV_FIFO_CLEAN_IO(DEV_FIFO_TYPE,0x10)
#defineDEV_FIFO_GETVALUE_IOR(DEV_FIFO_TYPE,0x11,int)
#defineDEV_FIFO_SETVALUE_IOW(DEV_FIFO_TYPE,0x12,int)
#endif

Makefile :

ifeq($(KERNELRELEASE),)
KERNEL_DIR?=/lib/modules/$(shelluname-r)/build
PWD:=$(shellpwd)
modules:
$(MAKE)-C$(KERNEL_DIR)M=$(PWD)modules
.PHONY:modulesclean
clean:
$(MAKE)-C$(KERNEL_DIR)M=$(PWD)clean
else
obj-m:=dev_fifo.o
endif

应用程序:

#include
#include
#include
#include
#include
#include

intmain(intargc,constchar*argv[])
{
intfd;
intn;
charbuf[1024]="helloword";

fd=open("/dev/dev_fifo0",O_RDWR);
if(fd< 0){
        perror("Fail   ot open");
        return   -1;
    }
    printf("open   successful ,fd = %d
",fd);
    n = write(fd,buf,strlen(buf));
    if(n < 0){
        perror("Fail   to write");
        return   -1;
    }
    printf("write   %d bytes!
",n);
    n = write(fd,buf,strlen(buf));
    if(n < 0){
        perror("Fail   to write");
        return   -1;
    }
    printf("write   %d bytes!
",n);
    return 0;
}

测试步骤:

(1) 加载模块

sudoinsmodhello.ko

(2) 创建设备节点

sudomknod/dev/helloc2500

如果代码中增加了自动创建设备节点的功能,这个步骤不要执行。

(3) 测试字符设备

gcctest.c-orun
sudo./run
审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 数据
    +关注

    关注

    8

    文章

    6800

    浏览量

    88733
  • Linux
    +关注

    关注

    87

    文章

    11203

    浏览量

    208697
  • 驱动程序
    +关注

    关注

    19

    文章

    817

    浏览量

    47902

原文标题:一文带你掌握Linux字符设备架构

文章出处:【微信号:zhuyandz,微信公众号:FPGA之家】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Linux字符设备驱动开发框架介绍

    字符设备Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作
    发表于 04-15 11:52 1311次阅读
    <b class='flag-5'>Linux</b><b class='flag-5'>字符</b><b class='flag-5'>设备</b>驱动开发框架介绍

    C语言中数据类型介绍(基本类型、构造类型、指针类型、空类型

    C语言数据类型包括:基本类型、构造类型、指针类型、空类型; ==》空类型就一个:void; =
    发表于 07-19 09:09

    Vulkan的基本类型

    Vulkan基本类型
    发表于 02-19 07:08

    C语言的数据基本类型分为哪几种

    今天闲着无聊把书翻看一遍,收获颇丰。目录1、数据类型本类型数据长度强制类型转换bit强制类型转换2、变量类型局部变量全局变量变量的存储类别
    发表于 02-25 06:35

    嵌入式Linux字符设备驱动的设计与应用

    描述了基于嵌入式Linux字符设备驱动程序的设计方法和实现过程。以电机、数码管、串口和mini键盘的驱动设计为例,详细阐述了嵌入式linux字符
    发表于 02-23 15:45 24次下载

    嵌入式Linux字符设备驱动的设计与应用

    描述了基于嵌入式Linux字符设备驱动程序的设计方法和实现过程。以电机、数码管、串口和mini键盘的驱动设计为例,详细阐述了嵌入式linux字符
    发表于 07-14 17:31 31次下载

    基于linux系统的字符设备驱动研究与设计_王森

    基于linux系统的字符设备驱动研究与设计_王森
    发表于 03-18 09:23 1次下载

    C语言数据的基本类型

    现在大家只需要学习一下基本类型。其他三种类型更适合在后续分享相关知识点时详细介绍。
    的头像 发表于 05-05 20:08 3441次阅读

    一文带你掌握Linux字符设备架构

    一、Linux设备分类 Linux系统为了管理方便,将设备分成三种基本类型字符
    的头像 发表于 12-22 16:14 2247次阅读
    一文带你掌握<b class='flag-5'>Linux</b><b class='flag-5'>字符</b><b class='flag-5'>设备</b>架构

    Linux字符设备架构是如何实现的

    一、Linux设备分类Linux系统为了管理方便,将设备分成三种基本类型字符
    的头像 发表于 12-24 18:12 669次阅读

    Vulkan API 基本类型介绍

    Vulkan 基本类型,Vulkan 开发需要设计的类型非常多,整理其基本类型如下,主要包含设备、队列、命令缓冲、队列家族、渲染通,管线等……
    的头像 发表于 02-12 16:19 1795次阅读

    Vulkan API 基本类型 小结

    Vulkan 基本类型,Vulkan 开发需要设计的类型非常多,整理其基本类型如下,主要包含设备、队列、命令缓冲、队列家族、渲染通,管线等……
    发表于 02-23 06:02 5次下载
    Vulkan API 基<b class='flag-5'>本类型</b> 小结

    Linux字符设备的创建过程

    Linux中很多设备都是字符设备,使用ls -l查看/dev下的设备,前面带c的都是字符
    的头像 发表于 03-24 11:29 2205次阅读
    <b class='flag-5'>Linux</b>中<b class='flag-5'>字符</b><b class='flag-5'>设备</b>的创建过程

    运动误差基本类型原因与对策

    运动误差基本类型原因与对策
    发表于 06-20 15:58 0次下载

    javascript基本类型有哪些

    JavaScript中的基本类型包括: 数字类型(Number):表示数字,包括整数和浮点数。 字符类型(String):表示文本数据。 布尔类型
    的头像 发表于 11-16 10:32 465次阅读