6 Linux设备管理

设备管理概述 #

最复杂、最多样化的操作系统资源管理模块

设备并行操作是提高设备利用率关键所在

  • 依赖于通道和中断机制、设备物理特性的支持

功能范畴

  • 对计算机系统中(除处理器和内存之外的)外部设备的管理(选择与分配输入输出设备)
  • 实现多任务、多进程对多种设备的共享
  • 对设备相关数据传输过程的控制
  • 实现数据的高效传输(设备与处理器/设备并行)

设备分类

  • 存储设备(块设备)
    • 计算机用来存储文件信息的设备,如磁/光盘/带
  • 输入输出设备(字符设备)
    • 输入设备,如键盘、鼠标、扫描仪
    • 输出设备,如显示器、打印机、绘图仪
    • 输入输出设备,如电传打字机
  • 网络设备(通过套接字进行数据交换)
    • 在 TCP/IP 协议网络通信中,套接字使用 IP 地址(32 位)和端口号(16 位)共计 48 位来唯一标识具有某个 IP 地址的计算机及其某个端口

设备管理模块功能

  • 提供进程使用设备的接口
    • 一般为文件系统及文件操作相关系统调用
  • 实施设备的分配与回收
    • 实现设备与设备、设备与处理器之间的并行
  • 依赖于硬件提供的输入输出控制方式
  • 采取有效措施解决处理器与设备的速度差异矛盾、平衡计算机系统的输入输出负荷、提高设备的输入输出效率
    • 缓冲技术、提前读、延迟写、异步写

设备独立性

  • 指用户在程序中所使用的设备与实际使用的设备无关
    • 操作系统为同类设备设置一个逻辑设备名
    • 当用户以逻辑设备名提出设备请求时,由操作系统的设备管理模块将逻辑设备名转化为具体的物理设备、实现物理设备的分配
  • 优点
    • 方便了用户
    • 提高了系统效率

缓冲管理 #

缓冲及目的

  • 缓冲是在不同速度的部件之间传输信息时常用来平滑传输过程的技术手段
    • 通常为内存缓冲区,用于暂存输入/输出数据
  • 目的
    • 解决设备与处理器之间以及设备与设备之间的速度不匹配的问题
    • 解决系统中输入输出负荷的不均衡问题
    • 有效减少输入输出(中断)次数,从而提高输入输出速度
  • 分为单缓冲、双缓冲、循环缓冲和缓冲池

缓冲池

  • 三类缓冲区队列:空缓冲区队列、输入缓冲区队列、输出缓冲区队列
  • 四类工作缓冲区:【收容/提取】【输入/输出】的工作缓冲区
  • 缓冲池管理要旨
    • 空缓冲区的分配与回收
    • 输入/输出设备的输入/输出缓冲区队列管理
    • 缓冲区与进程数据块之间的信息交换
      • 采用内存块拷贝实现机制
    • 缓冲区与设备(内存数据块与设备物理块)之间的信息交换(即设备输入输出)
      • 基于中断的实现方式
    • 中断服务程序实现设备的输入请求处理
      • 对于设备输入过程而言,若还有数据输入请求,则申请空闲缓冲区、启动设备执行输入操作并将所输入的数据暂存到空闲缓冲区中,然后把该装满输入数据的缓冲区挂到设备的输入缓冲区队列上供用户进程拷贝和访问读取;若没有数据输入请求则通知设备结束输入操作,并将该设备重新分配给其他进程
    • 中断服务程序实现设备的输出请求处理
      • 对于设备输出过程而言,循环:若还有数据输出请求,则申请空闲缓冲区把所输出的数据拷贝和暂存其中并挂到设备的输出缓冲区队列上
      • 待设备空闲时,系统从输出缓冲区队列队首缓冲区提取数据输出到设备上,然后把该缓冲区放回空缓冲区队列,循环执行这一过程直到输出缓冲区队列为空

设备分配 #

  • 计算机系统中进程数总是多于设备数及由此导致的设备竞争的客观现实
  • 设备分配须方便用户并有效规避死锁问题
  • 设备分配方式
    • 独占式分配及独占式设备(譬如打印机、绘图仪)
    • 共享式分配及共享式设备(譬如磁盘)
    • 虚拟式分配及假脱机技术(在共享式设备上实现独占式设备的虚拟和共享使用),可以提高输入输出速度、改善设备利用率和系统吞吐量,提高了进程的并发执行程度和执行效率

假脱机技术

  • 虚拟设备分配
    • 采用共享式分配为进程分配一个共享式设备“井”,并将“井”与指定的独占式设备相关联

输入/输出控制方式及模块 #

程序查询方式

  • 实现简单、无需硬件支持
  • 处理器和设备串行工作、处理器利用率低
  • 多台设备之间也只能串行工作
  • 依靠测试设备状态来控制数据传输过程,故而无法发现和处理由设备或其他硬件发生的错误
  • 仅适用于处理器执行速度较慢且外设数量较少的系统

中断控制方式

内存直接存取方式(DMA)

通道方式

输入输出模块:

Unix 设备管理 #

Unix 设备管理概要 #

  • Unix 设备管理特点

    • 将设备作为特殊文件,由文件系统统一管理
    • 支持系统中设备灵活、方便的配置
    • 高效的缓冲区管理算法
  • Unix 设备管理数据结构

    • 设备控制表
    • 设备开关表
    • 控制器控制表(DMA 方式无此表)
    • 通道控制表
  • Unix 设备驱动程序管控方式

    • 输入输出模块调用方式

Unix 设备管理数据结构 #

  • 设备控制表 (DCT / 设备控制块 DCB)

    • 抽象的 I/O 调用函数 ⬄ 实际的 I/O 设备驱动
    • 包含抽象设备描述、对应实际物理设备接口地址及相应的设备驱动函数模块入口地址
    • 实现用户进程与物理设备无关性 ⬄ 设备独立性
  • 设备标识包括主设备号和次设备号

    • 主设备号为设备类型的标识
    • 次设备号为对应设备类型的不同设备的编号
  • 设备开关表(函数指针结构体数组)

    • 同一型号物理设备完成各种操作的驱动函数入口
    • 主设备号相同的所有设备共享一张设备开关表
struct devtab
{
 int dvid;			/*设备标识符*/
 int dvadd; 		/*设备地址*/
 int *dvec; 		/*设备中断向量*/
 int *dvbuf; 		/*设备缓冲区指针*/
 int *dvque; 		/*设备等待队列首址*/
 int (*dvinit)(); 		/*设备驱动初始化函数*/
 int(*dvopen)(); 		/*设备驱动打开函数*/
 int(*dvclose)(); 		/*设备驱动关闭函数*/
 int(*dvread)(); 		/*设备驱动读函数*/
 int(*dvwrite)(); 		/*设备驱动写函数*/
 int(*dvseek)(); 		/*设备驱动查询函数*/
 int(*dvcntl)(); 		/*设备驱动控制操作函数*/
 int(*dvgetc)(); 		/*设备驱动取字符函数*/
 int(*dvputc)(); 		/*设备驱动送字符函数*/
}dct[];

Unix 设备请求及处理

  • 用户进程通过系统调用发出读访问设备请求
int read(int dvcrp, char *buf, int size)
{
 struct devtab *devptr;  /*定义指向设备表的指针*/
 if (isbaddev(dvcrp)))	 /*确认设备标识符是否有效*/
  return(SYSERR);		
 devptr = &dct[dvcrp]; /*有效则找到对应设备表项*/
 return((*devptr->dvread)(devptr, buf, size));
 /*将参数传递给设备驱动程序*/
}

内核与设备驱动程序接口

Unix 块设备控制器控制表 struct iobuf

struct  iobuf
{
 int  b_flags;		/*设备的状态标志 */
 dev_t  b_dev;		/*设备名,即逻辑设备号*/
 char  b_active;		/*设备正在执行一个I/O请求的标志*/
 char  b_errcnt;		/*出错计数*/
 struct  buf  *b_forw;	/*指向本设备缓冲区散列队列下一个buf*/
 struct  buf  *b_back;	/*指向本设备缓冲区散列队列上一个buf*/
 struct  buf  *b_actf;	/*指向本设备I/O请求队列中第一个buf*/
 struct  buf  *b_actl;	/*指向本设备I/O请求队列中最后一个buf*/
 struct  eblock  *io_erec;	/*指向块设备错误记录块*/
 int  io_nreg;		/*设备寄存器的个数*/
 physadr  io_addr;		/*设备控制状态寄存器地址*/
 physadr  io_mba; 		/*MBA配置结构寄存器地址*/
 struct  iostart  *io_stp; 	/*指向部件I/O统计块*/
 time_t  io_start;		/*输入输出启动时间*/
 int  io_s1;		/*驱动程序留用位置*/
 int  io_s2;		/*驱动程序留用位置*/
};

Unix 块设备缓冲区管理 #

块设备缓冲池及其组成

  • 数据缓冲区与缓冲控制块物理上分离

Unix 缓冲区及块设备 I/O 管理

  • 三种队列及队列头
    • 设备缓冲区散列队列(队列头 hbuf[i])
    • 空闲缓冲区队列(队列头 bfreelist)
    • 每个物理设备的 I/O 请求队列(队列头 iobuf)
  • 设备缓冲区散列队列
    • 64 个双向队列:i = (b_dev + b_blkno) mod 64
    • 各队列挂接了所有曾经分配给各类块设备使用过且被释放了的空闲的缓冲区(av_forw 和 av_back)
    • b_dev 和 b_blkno 取值由进程在 I/O 请求中给出、并在 buf 结构中描述,可唯一确定一个物理数据块

盘块缓冲区的分配与回收

  • 盘块缓冲池互斥操作要求
  • 盘块缓冲区获取过程 getblk()
    • 返回任一空闲缓冲区,并设置 b-busy 标志
    • 若有延迟写标志,调用 bdwrite 过程
  • 盘块缓冲区获取过程 getblk(dev, blkno)
    • 为指定设备和盘块号的盘块申请缓冲区
  • 盘块缓冲区回收过程 brelse()
    • 进程唤醒
    • 视数据有效与否链入空闲链表末尾或首部

Unix 块设备缓冲管理要旨归纳

  • 基于 LRU 算法的缓冲区队列的管理
    • 使已读到内存缓冲区的数据尽可能长时间地保留在缓冲区中,从而为数据的复用创造机会和条件
    • 有效减少读写磁盘的次数,并提高磁盘 I/O 速度
  • 数据复用及数据一致性和完整性的保证
    • 每一个文件数据块在内存只有一个缓冲区与之对应,从而为实现数据的一致性和完整性提供保证
  • 缓冲区由系统统一管理控制
    • 防止了某进程因永远得不到缓冲区而无限期睡眠
    • 防止任何进程无法永远霸占某个缓冲区,也即所有缓冲区都有可能被重新分配利用

Unix 设备驱动程序 #

基本功能任务

  • 设备初始化
  • 设备<=数据=>内存/系统
  • 检测和处理设备错误

管控对象为设备控制器(含三种寄存器)

  • 数据缓冲寄存器用于存放需要传输的数据
  • 状态寄存器用于设备状态如就绪、忙、操作出错等不同状态的编程
  • 控制寄存器用于控制设备的各种操作或指定设备的 I/O 地址

磁盘驱动程序

  • 磁盘驱动器打开过程 gdopen
    • 输入参数为设备号
    • 打开后设置 b-flag 为 B-ONCE
    • 调用 gdtimer 启动对应控制器与设备闹钟
  • 磁盘控制器启动过程
    • gdstart 设置磁盘控制器各寄存器值并启动
    • gdstrategy 把指定缓冲首部放至 I/O 队列末尾
  • 磁盘中断处理过程 gdintr
    • 输入参数为磁盘控制器号

磁盘读写程序

  • 一般读过程 bread
    • 将盘块信息读入到缓冲区
  • 提前读过程 breada
    • 提前将下一盘块中的信息读入到缓冲区
  • 一般写(同步写)过程 bwrite
    • 真正把缓冲区中的数据写到磁盘上且须等待完成
  • 异步写过程 bawrite
    • 进程无需等待写操作完成便可返回
  • 延迟写过程 bdwrite
    • 缓冲首部设置延迟写标志,将缓冲区链至空缓冲尾

基于设备文件的管理方式

  • 底层物理特性视角(设备/普通文件不同)
    • 设备文件在磁盘上无数据盘块,但在文件系统中却拥有一个永久的索引结点
  • 设备文件的索引结点
    • 无数据盘块号指针数组(=>混合索引物理结构)
    • 有用来存放设备的主/次设备号的 i_dev 域
  • 输入输出进程功能任务
    • 根据用户命令中的设备文件名找到其索引结点,进而得到设备标识 i_dev,再由对应主设备号及用户提供的操作命令在系统设备开关表数组中查找映射到相应的设备驱动函数,并加以执行处理

Linux 内核模块及设备驱动 #

Linux 内核架构及动态扩展机制 #

Linux 采用了宏内核架构

  • 操作系统的大部分功能(包括进程管理、内存管理、进程调度、设备管理)在内核空间实现

Linux 内核动态扩展机制

  • Linux 内核运行时加载一组目标代码来实现特定功能,从而使得 Linux 实际使用和运行过程中无需重新编译 Linux 内核(即核心模块)代码就能够实现内核的动态扩充
  • 具体基于 LKM(Loadable Kernel Module)机制来实现特定内核功能的增加或删减
  • 相对的 - 微内核架构:仅把最基本的功能放入内核,其他大部分功能(包括设备驱动)都放到非特权模式下,故而拥有天生的动态扩展性

Linux 是一个借鉴了微内核精髓的宏内核结构,Linux 支持模块化的设计、抢占式内核、对内核线程的支持以及动态加载内核模块的能力。不仅如此,Linux 还避免了其微内核设计的性能损失,允许一切运行在内核模式下,直接调用函数,无需消息传递。

所以综合一点来讲,Linux 是一个模块化、多线程和内核可调度的操作系统。

聊了聊宏内核和微内核,并吹了一波 Linux

Linux用户抢占和内核抢占详解(概念, 实现和触发时机)–Linux进程的管理与调度(二十)-腾讯云开发者社区-腾讯云

Linux 最简内核模块 #

#include <linux/init.h>
#include <linux/module.h>

static int __init init_lkm1st_zgs(void)
{
 printk("Welcome to init() for 1st LKM of BJTU_ZGS\n");
 return 0;
}

static void __exit exit_lkm1st_zgs(void)
{
 printk("Goodbye from exit() for 1st LKM of BJTU_ZGS\n");
}

module_init(init_lkm1st_zgs);
module_exit(exit_lkm1st_zgs);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("GaoshouZhai@BJTU");
MODULE_DESCRIPTION("First LKM of BJTU_ZGS");
MODULE_ALIAS("LKM1ST_ZGS");

//模块参数声明(可选)
//模块导出符号声明(可选
BASEINCLUDE ?= /lib/modules/`uname -r`/build/

obj-m := FirstZgsLKM.o

all:
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) modules
clean:
	$(MAKE) -C $(BASEINCLUDE) M=$(PWD) clean
  • retpoline
    • 现代计算机系统 CPU 往往会提前执行 jmp 或 call 等指令的下一条指令,也即将其存到所谓 RSB(Return stack Buffer)的栈中,如果相关指令存在安全风险,就有可能被利用。为此,通常在 call 和 jmp 指令后面添加一段无用死循环代码,从而使 CPU 预测分支执行该无用代码。称为 Return trampoline
  • 执行 sudo insmoddmesglsmod 以及 tree 命令
    • sudo dmesg -c 清理内核输出显示内容

可执行与可链接格式 - 维基百科,自由的百科全书

ELF 文件格式 · Linux Inside 中文版

Linux 传参内核模块 #

Linux 内核模块参数传递

  • Linux 内核提供了用以实现模块参数传递的宏
    • #define module_param(name, type, perm) module_param_named(name, name, type, perm)
    • #define MODULE_PARM_DESC(_parm, desc) __MODULE_INFO(parm, _parm, #_parm ":" desc)
    • /include/linux/moduleparam.h
    • 指定 sysfs 中相应文件的访问权限:
      • 0 文件不会出现在 sysfs 文件系统中
      • S_IRUGO 可被所有人读不可写 (0444)
      • S_IRUGO| S_IWUSR 可被所有 (0644) 人读且可被 root 用户写
    • byte、short、ushort、int、uint、long、ulong、char、bool、……
#include <linux/init.h>
#include <linux/module.h>

static int zDebug = 1;
module_param(zDebug, int, 0644);
MODULE_PARM_DESC(zDebug, "Enable to output debugging information");

#define printk_debug(args...) \
 if (zDebug) { \
  printk(KERN_DEBUG args); \
 }

static int __init init_lkm2nd_zgs(void)
{
 printk("Welcome to init() for 2nd LKM of BJTU_ZGS\n");
 printk_debug("Module parameter zDebug = %d\n", zDebug);
 return 0;
}

static void __exit exit_lkm2nd_zgs(void)
{
 printk("Goodbye from exit() for 2nd LKM of BJTU_ZGS\n");
}


module_init(init_lkm2nd_zgs);
module_exit(exit_lkm2nd_zgs);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("GaoshouZhai@BJTU");
MODULE_DESCRIPTION("Second LKM of BJTU_ZGS");
MODULE_ALIAS("LKM2ND_ZGS");

Linux 内核符号共享机制 #

Linux 主体内核以及内核模块间符号联系

  • 譬如,设备驱动往往按功能划分为若干内核模块,这些模块间如何实现接口函数的相互调用,包括它们如何调用主体内核提供的内存管理等相关支撑接口函数

Linux 内核符号共享机制

  • EXPORT_SYMBOL()
  • EXPORT_SYMBOL_GPL()

Linux 内核所导出的符号表查看方法

  • cat /proc/kallsyms | grep sound

Linux 字符设备驱动 #

Linux 字符设备节点创建

  • Linux 系统环境中访问设备的前提条件
    • 对应于设备的特殊文件已经创建
    • ls -l /dev/ 可以查看系统已经生成的设备文件
  • Linux 创建设备特殊文件(节点)的两种方法
    • 设备驱动中利用 device_create() 内核函数
    • Shell 命令:mknod
      • sudo mknod /dev/FakeCharDev c 252 0
/* FakeCharDevDrv.h */

#ifndef FAKE_CDEV_DRV_H_INCLUDED
#define FAKE_CDEV_DRV_H_INCLUDED

/* **可创建的伪字符设备最多台数,缺省情况下为2台** */
#ifndef NUM_OF_FAKE_CDEVS
#define NUM_OF_FAKE_CDEVS 2    
#endif

/* **用于数据存储的缓冲区大小** */
#ifndef SIZE_OF_BUFFER
#define SIZE_OF_BUFFER 4096
#endif

/* **每次操作可以读写的块大小** */
#ifndef SIZE_OF_BLOCK
#define SIZE_OF_BLOCK 512
#endif

/* **伪字符设备结构体类型** */
struct fake_cdev {
 unsigned char *data;
 unsigned long buffer_size;
 unsigned long block_size;
 struct mutex fake_cdev_mutex;
 struct cdev cdev;
};

#endif
/* FAKE_CDEV_DRV_H_INCLUDED */

Linux设备管理(二)_从cdev_add说起 - Abnor - 博客园

char_traits struct 初步学习-CSDN博客

Linux内核驱动:cdev、misc以及device三者之间的联系和区别 - schips - 博客园

Linux设备驱动程序 之 主次设备号 - AlexAlex - 博客园

从/dev 目录说起

从事 Linux 嵌入式驱动开发的人,都很熟悉下面的一些基础知识, 比如,对于一个 char 类型的设备,我想对其进行 read wirte 和 ioctl 操作,那么:

1、我们通常会在内核驱动中实现一个 file_operations 结构体,然后分配主次设备号,调用 cdev_add 函数进行注册。

2、从 /proc/devices 下面找到注册的设备的主次设备号,在用 mknod /dev/char_dev c major minor 命令行创建设备节点。

3、在用户空间 open /dev/char_dev 这个设备,然后进行各种操作。

OK,字符设备模型就这么简单,很多 ABC 教程都是一个类似的实现。

然后我们去看内核代码时,突然一脸懵逼。。。怎么内核代码里很多常用的驱动的实现不是这个样子的?没看到有 file_operations 结构体,我怎么使用这些驱动?看到了 /dev 目录下有需要的 char 设备,可是怎么使用呢?