Linux驱动入门学习(二):字符设备和块设备区别

自己网上整理的一些概念供大家学习用,如果有侵请告诉我,立刻删除。

一、概念区分

Linux中I/O设备分为两类:块设备和字符设备。两种设备本身没有严格限制,但是,基于不同的功能进行了分类。

(1) 字符设备:提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。相反,此类设备支持按字节/字符来读写数据。举例来说,调制解调器是典型的字符设备。

(2) 块设备:应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。硬盘是典型的块设备,应用程序可以寻址磁盘上的任何位置,并由此读取数据。此外,数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

两种设备本身并没用严格的区分,主要是字符设备和块设备驱动程序提供的访问接口(file I/O API)是不一样的。本文主要就数据接口、访问接口和设备注册方法对两种设备进行比较。

系统中能够随机(不需要按顺序)访问固定大小数据片(chunks)的设备被称作块设备,这些数据片就称作块。最常见的块设备是硬盘,除此以外,还有软盘驱动器、CD-ROM驱动器和闪存等等许多其他块设备。注意,它们都是以安装文件系统的方式使用的——这也是块设备的一般访问方式。

  另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就都属于字符设备。如果一个硬件设备是以字符流的方式被访问的话,那就应该将它归于字符设备;反过来,如果一个设备是随机(无序的)访问的,那么它就属于块设备。

     这两种类型的设备的根本区别在于它们是否可以被随机访问——换句话说就是,能否在访问设备时随意地从一个位置跳转到另一个位置。举个例子,键盘这种设备提供的就是一个数据流,当你敲入“fox” 这个字符串时,键盘驱动程序会按照和输入完全相同的顺序返回这个由三个字符组成的数据流。如果让键盘驱动程序打乱顺序来读字符串,或读取其他字符,都是没有意义的。所以键盘就是一种典型的字符设备,它提供的就是用户从键盘输入的字符流。对键盘进行读操作会得到一个字符流,首先是“f”,然后是“o”,最后是“x”,最终是文件的结束(EOF)。当没人敲键盘时,字符流就是空的。硬盘设备的情况就不大一样了。硬盘设备的驱动可能要求读取磁盘上任意块的内容,然后又转去读取别的块的内容,而被读取的块在磁盘上位置不一定要连续,所以说硬盘可以被随机访问,而不是以流的方式被访问,显然它是一个块设备。

  内核管理块设备要比管理字符设备细致得多,需要考虑的问题和完成的工作相比字符设备来说要复杂许多。这是因为字符设备仅仅需要控制一个位置—当前位置—而块设备访问的位置必须能够在介质的不同区间前后移动。所以事实上内核不必提供一个专门的子系统来管理字符设备,但是对块设备的管理却必须要有一个专门的提供服务的子系统。不仅仅是因为块设备的复杂性远远高于字符设备,更重要的原因是块设备对执行性能的要求很高;对硬盘每多一分利用都会对整个系统的性能带来提升,其效果要远远比键盘吞吐速度成倍的提高大得多。另外,我们将会看到,块设备的复杂性会为这种优化留下很大的施展空间。

  简单来讲,块设备可以随机存取,而字符设备不能随机存取,那裸设备又该如何解释呢?

  难道裸设备,如磁盘裸设备也不能随机读取吗?那在数据库中用裸设备建一个2g的数据文件,为了存取最后一个数据块,难道ORACLE还要把前面的所有数据块都读一遍,显然不符合事实,如果这样解释呢,操作系统不能随机读取,并不意味着数据库也不能随机读取。

  块设备通过系统缓存进行读取,不是直接和物理磁盘读取。字符设备可以直接物理磁盘读取,不经过系统缓存。(如键盘,直接相应中断)

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/xgjianstart/archive/2009/09/19/4569043.aspx

作者:唐浩然
链接:https://www.zhihu.com/question/40472366/answer/86821313

1.在操作系统的角度,块设备/字符设备/网络设备 都是io的一种,可以从这些设备读写数据。
2.区别在于
a: 字符设备最小的读写单位是一个字符的长度-- 即两个字节(16位),这样对我们造成的印象是当我们通过键盘输入的时候或者是读终端数据的时候,如果不考虑缓冲,数据可以每次以16个bit被送给字符设备。 ( 原答案中我是这样写的,经知友 in nek指正这样说法存在错误: 字符设备不能随机读写,只能按顺序读写,比如你依次在键盘输入的内容,只会按你的输入顺序被系统接受。在比如操作系统向终端写入数据,也是顺序的在终端显示。就像拿着碗喝豆浆,你只能从最上面的喝起,如果碗不漏的话。)
b:对于块设备, 最小的读写单位是一个物理块,对于磁盘来说是一个扇区(一般来说是512字节),会给人造成“512字节,好大一堆字符组成的一块啊”的印象,猜想由于这样的特性被操作系统设计时定义成块设备 。(我的原答案:块设备可以随机读写,比如你可以读写系统下挂载的硬盘的任意位置的数据,就像吃一块豆腐,你拿着刀可以吃这块豆腐的任意部分。并且由于块设备的访问速度太慢,读写一次(硬盘)的cpu等待时间太长,系统会为块设备在内存设立缓冲区,先把要写入硬盘的数据写入缓冲区,等满足一定条件时一并写入硬盘。)
c: 总线设备是连接各种设备的桥梁,比如lspci -t可以看到pci总线的设备连接树,总线设备负责把块设备/字符设备/网络设备 /cpu /内存 在硬件上是连通的,这样当cpu发出指令“从硬盘的某个地方读多少数据并把这些数据写到终端”,“把键盘输入的内容写入到网卡”的时候,这些数据会通过总线被传输。
d:驱动是打开设备的方式和方法,比如吃豆腐,你可以拿刀切,这里用刀切就是一种方法,喝豆浆可以用吸管。但刀切和吸管这两种方式都要向系统注册,系统将刀切和吃豆腐关联,将吸管和和豆浆关联。之后,当你需要吃豆腐时,系统会调用“刀切这个方法”。

二、驱动程序

https://www.zhihu.com/question/40472366/answer/86756011
代码是这样的:我们为一个存在于系统中设备写一个驱动程序。系统是识别到了的,但是就是无法使用,为什么系统是识别了的但是无法使用?因为对于用户来讲,是无法直接使用一个系统中的硬件的,要使用这个添加的硬件,我们就要安装这个设备的驱动程序。通过驱动程序来操作这个设备。
#include "linux/kernel.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/init.h"
#include "linux/types.h"
#include "linux/errno.h"
#include "linux/uaccess.h"
#include "linux/kdev_t.h"
#define MAX_SIZE 1024

static int my_open(struct inode *inode, struct file *file);
static int my_release(struct inode *inode, struct file *file);
static ssize_t my_read(struct file *file, char __user *user, size_t t, loff_t *f);
static ssize_t my_write(struct file *file, const char __user *user, size_t t, loff_t *f);

static char message[MAX_SIZE] = "-------This is dmesg info--------!";
static int device_num = 0;
static int counter = 0;
static int mutex = 0;
static char* devName = "NewDev";//设备名

struct file_operations pStruct =
{ open:my_open, release:my_release, read:my_read, write:my_write, };

/* 初始化模块,向系统注册这个设备,*/
int init_module()
{
	int ret;
	/********************************************************
         * 第一个参数是0,告诉系统此时的设备号是由系统自己选一个可用的来分配,
	 * 第二个参数是新设备注册时的设备名字,
	 * 第三个参数是指向file_operations的指针,
	 *******************************************************/
	ret = register_chrdev(0, devName, &pStruct);
	if (ret < 0)
	{
		printk("regist failure!\n");
		return -1;
	}
	else
	{
		printk("the device has been registered!\n");
		device_num = ret;
		printk("<1>Device's major number %d.\n", device_num);
		return 0;
	}
}
/* 注销 */
void cleanup_module()
{
	unregister_chrdev(device_num, devName);
	printk("unregister it success!\n");
}

static int my_open(struct inode *inode, struct file *file)
{
	if (mutex)
		return -EBUSY;
	mutex = 1;
	printk("<1>main  device : %d\n", MAJOR(inode->i_rdev));
	printk("<1>slave device : %d\n", MINOR(inode->i_rdev));
	printk("<1>%d times to call the device\n", ++counter);
	try_module_get(THIS_MODULE);
	return 0;
}

static int my_release(struct inode *inode, struct file *file)
{
	printk("Device released!\n");
	module_put(THIS_MODULE);
	mutex = 0;//开锁
	return 0;
}

static ssize_t my_read(struct file *file, char __user *user, size_t t, loff_t *f)
{
        /*从内核空间拷贝到用户空间*/
	if(copy_to_user(user,message,sizeof(message)))
	{
		return -EFAULT;
	}
	return sizeof(message);
}

static ssize_t my_write(struct file *file, const char __user *user, size_t t, loff_t *f)
{
         /*从用户空间到内核空间*/
	if(copy_from_user(message,user,sizeof(message)))
	{
		return -EFAULT;
	}
	return sizeof(message);
}
有了这个驱动程序后,结合下面的Makefile来编译:
# If KERNELRELEASE is defined, we've been invoked from the
# kernel build system and can use its language.
ifeq ($(KERNELRELEASE),)
    # Assume the source tree is where the running kernel was built
    # You should set KERNELDIR in the environment if it's elsewhere
    KERNELDIR ?= /lib/modules/$(shell uname -r)/build
    # The current directory is passed to sub-makes as argument
    PWD := $(shell pwd)
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY: modules modules_install clean
else
    # called from kernel build system: just declare what our modules are
    obj-m := devDrv.o
endif

使用make命令来编译。
编译好了后,就要将这个驱动安装到系统中去,Linux中就是把这个驱动程序装载到系统中。
装载:
&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/ea39f2ef4b7c6def1579c67ab7300735.png&quot; data-rawwidth=&quot;308&quot; data-rawheight=&quot;39&quot; class=&quot;content_image&quot; width=&quot;308&quot;&amp;gt;到系统中看看 到系统中看看
&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/fefd6d4768ad47477b748e34c2b6c32d.png&quot; data-rawwidth=&quot;311&quot; data-rawheight=&quot;60&quot; class=&quot;content_image&quot; width=&quot;311&quot;&amp;gt;成功了。 成功了。
通过dmesg命令可以看到驱动程序已经向系统注册了这个设备。
设备号都看的出来了。
我这里设备号是248。

接下来要做的就是添加一个设备。
给系统添加一个设备,在Linux中,“一切都是文件”,那么,设备也是文件,所以,添加的这个设备自然也就以一个文件的形式呈现出来----设备文件。
关于主设备号和从设备号。简单的说主设备号就是标识这一类的设备,驱动程序只管这一类的设备。而从设备号才是真正标识单个设备的,即就是某一类设备中的个体。
现在开始建立这个设备,也就是给这个设备分配从设备号:
&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/8020aa28d4776ad8ba1dd2a091b2cb03.png&quot; data-rawwidth=&quot;365&quot; data-rawheight=&quot;52&quot; class=&quot;content_image&quot; width=&quot;365&quot;&amp;gt;是的,已经创建好了: 是的,已经创建好了:
--------------------------------------------------------------------
到现在,来看看我们做了什么。
首先是写了一个驱动程序,这个驱动程序向系统注册了这个设备。
然后,向系统添加了这个设备。
&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/7a9f0eb5a8764ba324b1b56a0ea420b9.png&quot; data-rawwidth=&quot;319&quot; data-rawheight=&quot;60&quot; class=&quot;content_image&quot; width=&quot;319&quot;&amp;gt;
接下来就差个应用程序了,毕竟这样才完整。
用户通过应用程序操作这个设备。现在,这个设备说是一个设备,但是在Linux下就是一个文件,对这个文件进行读写操作也就是对这个设备进行读写操作了。

        fd = open("/dev/NewDev", O_RDWR | O_NONBLOCK);
这样就可以打开了。
尽绵薄之力,希望可以帮到你。
以上。

三、设备驱动的模式

作者:in nek
链接:https://www.zhihu.com/question/40472366/answer/86739224


用户态的device,我们一般就是指/dev目录下的哪些设备文件,那些文件的作用是建立一个用户程序到内核驱动的门户。这些门户背后,是驱动程序。

内核中,很早很早以前(我也懒得查是什么时候),我们的驱动是这样写的:你写一个动态模块,插入到内核中,这个动态模块就开始和要驱动的硬件进行通讯,然后注册一个字符设备或者一个块设备,你就可以把你的信息传递给用户程序了。我印象中LDD2好像就是这样的例子来教大家写程序的。你们看看后面那位 @0x07c00 写的程序,就是这种模型。

但这种模型太旧太旧了,Linux早就不是这个样子的了。这个模型有个严重问题,我一个驱动注册一个字符设备,我就知道我的硬件地址是什么,那如果我有两个一样的设备,我要不要写两个驱动啊?

为了解决这个问题,应该是从2.5开始,Linux就引入了一个新的模型,就是所谓的device-bus-driver模型。简单说,你有多少个硬件,初始化程序就建立多少个device,device就是个数据结构,里面描述了硬件的信息,比如中断号啦,IO地址啦什么的。driver呢,才是原来那个动态模块,它负责读device提供过来的信息,然后用这个信息来初始化,并且调用register_chrdev()一类的函数,把设备的用户态接口暴露到用户态去。所以呢,内核并没有取消字符设备,块设备的支持,而是把这些东西分离成了device和driver,靠probe()过程去动态注册了。

那么bus是个什么东西呢?bus在内核中称为bus_type,它是用来匹配device和driver的,x86这个部分搞得比较ugly,我用arm来举例。ARM很多设备是焊死在AMBA总线上的,或者说设备和CPU是直接连在一起的,没有什么总线发现功能,这种设备,Linux就在启动的时候建立一个称为Platform_bus的虚拟总线类型,BIOS是知道自己这个硬件有什么设备的,它就在DTS里,或者通过ACPI接口,把这些设备一个个描述出来,这样Linux的初始化代码,通过of_xxx函数(针对DTS)或者acpi函数,读到这些设备描述,然后创建成device,注册给platform_bus。之后,内核初始化,一个个动态模块就可以插入来,这些动态模块也把自己注册在platform_bus上,platform_bus就可以启动它的匹配算法(platform_bus的匹配算法是用名称),把device作为参数传递给驱动(的probe())函数,probe函数就可以注册到各个子系统(字符设备,网络设备,块设备,都行)了。

真正的总线控制器,比如pci-c,一开始也可以是个平台设备,这个总线控制器作为device被probe,probe中就可以对下属的硬件进行枚举,就可以创建更多的device,那些device和pci_bus的driver匹配,就可以实现对这些设备的驱动,这样就会形成一个树状的结构,完成整个设备树的构建。


最后链接一个姊妹问题:
linu x input设备模型问题? - in nek 的回答



  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值