38、深入探讨用户空间驱动与设备管理

深入探讨用户空间驱动与设备管理

1. 用户模式 I2C 驱动

在某些情况下,当需要支持大量慢速 I2C 设备时,从用户空间驱动这些设备是有意义的。i2c - dev 模块支持用户模式 I2C/SMBus 设备驱动的开发。用户空间代码可通过设备节点访问 I2C 主机适配器,操作第 n 个适配器时,需打开 /dev/i2c - n。获取与主机适配器设备节点关联的文件描述符后,可通过 ioctls 命令连接到与之相连的特定从设备,然后使用一系列数据访问例程与从设备交换数据。

以下是一个简单的用户模式驱动示例,用于从用户空间对 I2C EEPROM 执行常见操作:

#include <linux/i2c.h>
#include <linux/i2c-dev.h>
/* Bus addresses of the memory banks */
#define SLAVE_ADDR1  0x60
#define SLAVE_ADDR2  0x61
int main(int argc, char *argv[])
{
  /* Open the host adapter */
  if ((smbus_fp = open("/dev/i2c-0", O_RDWR)) < 0) {
    exit(1);
  }
  /* Connect to the first bank */
  if (ioctl(smbus_fp, I2C_SLAVE, EEPROM_SLAVE_ADDR1) < 0) {
    exit(1);
  }
  /* ... */
  /* Dump data from the device */
  for (reg=0; reg < length; reg++) {
    /* See i2c-dev.h from the lm-sensors package for the
       implementation of the following inline function */
    res = i2c_smbus_read_byte_data(smbus_fp, (unsigned char) reg);
    if (res < 0) {
      exit(1);
    }
    /* Dump data */
    /* ... */
  }
  /* ... */
  /* Switch to bank 2 */
  if (ioctl(smbus_fp, I2C_SLAVE, SLAVE_ADDR2) < 0) {
    exit(1);
  }
  /* Clear bank 2 */
  for (reg=0; reg < length; reg+=2){
    i2c_smbus_write_word_data(smbus_fp, (unsigned char) reg, 0x0);
  }
  /* ... */
  close(smbus_fp);
}

该示例使用了来自 lm - sensors 包的 i2c - dev.h 中的内联函数来操作与存储库关联的设备节点。

2. UIO 子系统

从 2.6.23 版本开始,内核包含了一个名为 UIO(Userspace IO)的子系统,它简化了一些用户空间驱动的实现。UIO 的目的是允许开发用于中断处理等任务的基本内核驱动,并将大部分设备 I/O 逻辑推到用户空间。UIO 对某些类型的工业 I/O 卡特别有用。UIO 源文件位于 drivers/uio/ 目录下,用户指南可在 Documentation/DocBook/uio - howto.tmpl 中找到。

3. 相关源文件与数据结构

Linux 调度器的代码位于 kernel/sched.c 中,SCSI 通用实现位于 drivers/scsi/sg.c 中,支持用户空间 USB 驱动的代码在 drivers/usb/core/devio.c 中,支持用户模式 I2C 编程的 i2c - dev 驱动位于 drivers/i2c/i2c - dev.c 中。

以下是本章使用的主要数据结构和函数的总结:
| 数据结构 | 位置(用户空间) | 描述 |
| — | — | — |
| sched_param | /usr/include/bits/sched.h | 与调度优先级相关的信息 |
| fb_var_screeninfo | /usr/include/linux/fb.h | 用于操作帧缓冲区,包含可变屏幕信息,如分辨率和像素时钟 |
| sg_io_hdr_t | /usr/include/scsi/sg.h | 管理 SCSI 通用设备的信息 |
| usb_dev_handle、usb_bus、usb_device | libusb 包中的头文件 | 从用户空间操作 USB 设备的结构 |

用户空间函数 描述
sched_getparam() 获取与给定进程关联的调度参数
sched_setscheduler() 设置与给定进程关联的调度参数
mlockall() 将调用进程的页面锁定在内存中,避免页面错误
ioperm() 控制对前 0x3FF 个 I/O 端口的访问权限
iopl() 控制对所有 I/O 端口的访问权限
outb()/outw()/outl() 向指定端口输出一个字节/字/长整型
inb()/inw()/inl() 从指定端口输入一个字节/字/长整型
mmap() 将文件或设备地址区域与一块用户虚拟内存关联
msync() 刷新对 mmap 内存区域所做的更改
munmap() mmap() 的反向操作
usb_init()、usb_find_buses()、usb_find_devices()、usb_open()、usb_control_msg()、usb_close() libusb 库提供的用于操作 usbfs 的函数
i2c_smbus_read_byte_data()、i2c_smbus_write_word_data() lm - sensors 包提供的用户空间 I2C/SMBus 数据访问例程
4. ECC 报告

许多内存控制器包含特殊的硅片,用于使用纠错码(ECC)来测量存储数据的保真度。错误检测与纠正(EDAC)驱动子系统会报告由支持 ECC 的内存控制器产生的内存错误事件。典型的 ECC DRAM 芯片能够纠正单比特错误(SBEs)并检测多比特错误(MBEs),在 EDAC 术语中,前者称为可纠正错误(CEs),后者称为不可纠正错误(UEs)。

ECC 操作对操作系统是透明的,即如果 DRAM 控制器支持 ECC,错误纠正和检测会在操作系统不参与的情况下默默进行。EDAC 的任务是报告这些事件,并允许用户制定错误处理策略,如更换可疑的 DRAM 芯片。

EDAC 驱动子系统由以下部分组成:
- 一个名为 edac_mc 的核心模块,提供一组库例程。
- 用于与支持的内存控制器交互的独立驱动,例如,与 Intel 82860 北桥中的内存控制器配合工作的驱动模块名为 i82860_edac。

EDAC 通过 sysfs 目录 /sys/devices/system/edac/ 中的文件报告错误,也会生成可从内核错误日志中获取的消息。DRAM 芯片的布局由内存控制器发出的芯片选择数量和内存控制器与 CPU 之间的数据传输宽度(通道)来指定。EDAC 的主要目标之一是找出有问题的 DRAM 芯片,因此 EDAC sysfs 节点结构是根据物理芯片布局设计的:/sys/devices/system/edac/mc/mcX/csrowY/ 对应于内存控制器 X 中的芯片选择行 Y,每个这样的目录包含检测到的 CEs 数量(ce_count)、UEs 数量(ue_count)、通道位置等详细信息。

以下是一个为不支持的内存控制器添加 EDAC 支持的示例:
假设要在一个医疗级嵌入式 x86 设备上运行 Linux,该设备的北桥芯片组是 Intel 855GME,它能够进行 ECC 报告。系统中连接到 855GME 的所有 DRAM 库都是支持 ECC 的芯片。由于 EDAC 尚未支持 855GME,下面是实现支持的代码:

/* Based on drivers/edac/i82860_edac.c */
#define I855_PCI_DEVICE_ID   0x3584 /* PCI Device ID of the memory
                                       controller in the 855 GME */
#define I855_ERRSTS_REGISTER 0x62   /* Error Status Register's offset
                                       in the PCI configuration space */
#define I855_EAP_REGISTER    0x98   /* Error Address Pointer Register's
                                       offset in the PCI configuration space */
struct i855_error_info {
  u16 errsts;  /* Error Type */
  u32 eap;     /* Error Location */
};
/* Get error information */
static void
i855_get_error_info(struct mem_ctl_info *mci,
                    struct i855_error_info *info)
{
  struct pci_dev *pdev;
  pdev = to_pci_dev(mci->dev);
  /* Read error type */
  pci_read_config_word(pdev, I855_ERRSTS_REGISTER, &info->errsts);
  /* Read error location */
  pci_read_config_dword(pdev, I855_EAP_REGISTER, &info->eap);
}
/* Process errors */
static int
i855_process_error_info(struct mem_ctl_info *mci,
                        struct i855_error_info *info,
                        int handle_errors)
{
  int row;
  info->eap >>= PAGE_SHIFT;
  row = edac_mc_find_csrow_by_page(mci, info->eap); /* Find culprit row */
  /* Handle using services provided by the EDAC core.
     Populate sysfs, generate error messages, and so on */
  if (is_MBE()) {          /* is_MBE() looks at I855_ERRSTS_REGISTER and checks
                              for an MBE. Implementation not shown */
    edac_mc_handle_ue(mci, info->eap, 0, row, "i855 UE");
  } else if (is_SBE()) {   /* is_SBE() looks at I855_ERRSTS_REGISTER and checks
                              for an SBE. Implementation not shown */
    edac_mc_handle_ce(mci, info->eap, 0, info->derrsyn, row, 0,
                      "i855 CE");
  }
  return 1;
}
/* This method is registered with the EDAC core from i855_probe() */
static void
i855_check(struct mem_ctl_info *mci)
{
  struct i855_error_info info;
  i855_get_error_info(mci, &info);
  i855_process_error_info(mci, &info, 1);
}
/* The PCI driver probe method, part of the pci_driver structure */
static int
i855_probe(struct pci_dev *pdev, int dev_idx)
{
  struct mem_ctl_info *mci;
  /* ... */
  pci_enable_device(pdev);
  /* Allocate control memory for this memory controller.
     The 3 arguments to edac_mc_alloc() correspond to the
     amount of requested private storage, number of chip-select
     rows, and number of channels in your memory layout */
  mci = edac_mc_alloc(0, CSROWS, CHANNELS);
  /* ... */
  mci->edac_check = i855_check; /* Supply the check method to the
                                   EDAC core */
  /* Do other memory controller initializations */
  /* ... */
  /* Register this memory controller with the EDAC core */
  edac_mc_add_mc(mci, 0);
  /* ... */
}
/* Remove method */
static void __devexit
i855_remove(struct pci_dev *pdev)
{
  struct mem_ctl_info *mci = edac_mc_find_mci_by_pdev(pdev);
  if (mci && !edac_mc_del_mc(mci)) {
    edac_mc_free(mci); /* Free memory for this controller. Reverse
                          of edac_mc_alloc() */
  }
}
/* PCI Device ID Table */
static const struct pci_device_id i855_pci_tbl[] __devinitdata = {
  {PCI_VEND_DEV(INTEL, I855_PCI_DEVICE_ID),
   PCI_ANY_ID, PCI_ANY_ID, 0, 0,},
  {0,},
};
MODULE_DEVICE_TABLE(pci, i855_pci_tbl);
/* pci_driver structure for this device.
   Re-visit Chapter 10 for a detailed explanation */
static struct pci_driver i855_driver = {
  .name     = "855",
  .probe    = i855_probe,
  .remove   = __devexit_p(i855_remove),
  .id_table = i855_pci_tbl,
};
/* Driver Initialization */
static int __init
i855_init(void)
{
  /* ... */
  pci_rc = pci_register_driver(&i855_driver);
  /* ... */
}
5. 频率缩放

CPU 频率(cpufreq)驱动子系统通过动态缩放 CPU 频率来辅助电源管理。如果使用合适的缩放算法(称为 governor),设备的电池续航时间可能会更长。cpufreq 支持多种架构,如 x86、ARM 和 PowerPC。要获得 cpufreq 功能,还需要启用合适的处理器驱动,例如,如果使用支持 SpeedStep 的 CPU(如 Pentium M),则需要启用 Intel Enhanced SpeedStep 驱动。

可以通过 /sys/devices/system/cpu/cpuX/cpufreq/ 目录下的文件来控制 cpufreq 的行为,其中 X 是 CPU 编号。通过分别向 scaling_max_freq 和 scaling_min_freq 写入所需的值,可以设置最大和最小频率缩放限制。查看 scaling_available_governors 的内容可以获取支持的 cpufreq governor 列表。内核支持以下几种 governor:
- performance:将 CPU 频率静态设置为 scaling_max_freq。
- powersave:将 CPU 频率设置为 scaling_min_freq。
- ondemand:根据 CPU 负载调整频率。
- conservative:是 ondemand 的一种变体,速度变化会逐步平稳进行。
- userspace:允许应用程序指定缩放技术,一些发行版将 governor 设置为 userspace,并通过在启动时启动的名为 cpuspeed 的守护进程来实现缩放算法。

也可以使用 cpufreq_register_governor() 接口实现自己的内核 governor。每个支持的 governor 都作为一个内核模块实现。以下是一个查看 cpufreq 工作情况的示例:

bash> cd /sys/devices/system/cpu/cpu0/cpufreq
bash> cat scaling_max_freq         
 Maximum frequency
1700000
bash> cat scaling_min_freq        
 Minimum frequency
600000
bash> cat cpuinfo_cur_freq        
 Current frequency
600000
bash> cat scaling_governor        
 Scaling algorithm in use
powersave
bash> cat scaling_available_frequencies
1700000 1400000 1200000 1000000 800000 600000
bash> cat scaling_available_governors
conservative ondemand powersave userspace performance
bash> echo conservative > scaling_governor
 Assign 'conservative' governor
bash> ls -lR /                    
 Switch to another terminal and
 load your system by recursively
 traversing all directories.

此时,通过查看 /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq 可以看到运行频率会随着 CPU 负载而变化。CPU 缩放代码位于 drivers/cpufreq/ 目录下,Documentation/cpu - freq/* 中包含 cpufreq sysfs 节点的详细语义。

6. 嵌入式控制器

笔记本电脑及其衍生产品通常包含一个内置的嵌入式控制器(EC),用于处理各种辅助任务,包括:
- 与键盘控制器接口
- 管理热事件
- 处理特殊按钮和 LED
- 控制系统和 CPU 风扇速度
- 监控电池电压

这些功能大多与 OEM 的硬件实现有关,不同的 OEM 使用不同的 EC。例如,IBM/Lenovo 笔记本电脑嵌入了 Renesas H8 微控制器来辅助主处理器。不过,无论控制器的品牌如何,访问 EC 的接口是标准的。BIOS 和操作系统通过 I/O 端口 0x80 从 EC 读取信息,通过 I/O 端口 0x81 向 EC 写入数据。在台式机上,这些端口用于访问键盘控制器,而不是通用的 EC。

7. ACPI

高级配置和电源接口(ACPI)是一种电源管理规范,取代了早期的标准,如高级电源管理(APM)。ACPI 负责在系统的不同电源状态之间进行转换,还负责与连接到 EC 的设备和传感器进行交互。这些设备称为 ACPI 设备,用于处理它们的内存称为 ACPI 空间。

早期的 APM 存在的主要问题是大部分电源管理策略都包含在 BIOS 固件中,而低级别代码不适合实现策略。ACPI 将策略提升到操作系统层面,使用名为 acpid 的守护进程,甚至可以将策略进一步提升到用户空间配置文件。通过在 acpid 配置文件中添加规则,可以决定在按下热键或发生热跳闸时采取的操作。

即使有了 ACPI,低级别的 BIOS 固件仍然负责与硬件交互并检测 ACPI 事件,如按下电源按钮或热传感器报告。BIOS 利用通过系统管理中断(SMIs)触发的特殊 x86 执行模式来完成此任务,SMI 执行模式对操作系统是透明的。为了将在 SMI 模式下检测到的 ACPI 事件通知给操作系统,BIOS 会触发系统控制中断(SCI)。Linux ACPI 代码请求 SCI IRQ 的部分位于 drivers/acpi/osl.c 中。

Linux ACPI 组件包括:
- 核心层:提供 ACPI 基本功能,如 ACPI 机器语言(AML)解释器。特定于 ACPI 的 BIOS 代码使用 AML 编写,AML 是一种在操作系统的 AML 解释器实现的虚拟机上运行的语言。
- ACPI 驱动:用于与标准组件(如 EC(drivers/acpi/ec.c)、按钮(drivers/acpi/button.c)和风扇(drivers/acpi/fan.c))进行交互。OEM 特定的驱动提供标准 ACPI 驱动不支持的功能,例如,drivers/misc/thinkpad_acpi.c 是为 IBM/Lenovo Thinkpads 实现额外功能的 OEM 特定驱动。在 IBM/Lenovo Thinkpad 上,/proc/acpi/ 下的文件由标准 ACPI 驱动生成,而 /proc/acpi/ibm/ 下的文件由 OEM 特定驱动生成。例如,要获取当前温度,可以执行:

bash> cat /proc/acpi/thermal_zone/THM0/temperature
temperature:       39 C

要打开 LCD 显示屏顶部的夜灯,可以使用 OEM 特定驱动:

bash> echo on > /proc/acpi/ibm/light
  • 内核线程 kacpid:ACPI 使用它来排队待执行的工作。
  • 各个设备驱动:使用 ACPI 的服务来响应系统电源状态的转换。为此,驱动程序需要在内核的设备模型中注册 suspend() 和 resume() 方法。
  • 用户空间工具:如 acpitool,用于报告各种 ACPI 设备的状态、显示热区信息并将系统挂起到不同的睡眠状态:
bash> acpitool
Battery #1     : charging, 69.08%, 01:14:02
AC adapter     : on-line
Thermal zone 1 : ok, 38 C
  • acpid 守护进程:是 ACPI 事件的策略执行器。它监听 /proc/acpi/events 以获取内核报告的电源管理事件。当按下电源按钮或发生热跳闸时,内核 ACPI 驱动通过 /proc/acpi/events 将事件发送到用户空间。acpid 读取该事件,通过 /etc/acpi/events/ 中的配置脚本处理它,并采取指定的操作。例如,如果想在按下笔记本电脑的盖子按钮时执行特定程序(/bin/lidhandler),可以在 /etc/acpi/events/acpi_handler.sh 中添加以下内容:
event=button/lid.*
action=/bin/lidhandler

还可以将 cpufreq 与 ACPI 结合使用,例如,在 /bin/lidhandler 中添加以下行,在合上笔记本电脑盖子时降低处理器频率:

echo powersave > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

可以从 www.acpi.info 下载 ACPI 规范。如果嵌入式笔记本电脑衍生产品中内置了遥测卡,并且 EC 连接到一个测量遥测强度的传感器,可以通过更新 drivers/misc/ 中相应笔记本电脑型号的“额外功能”驱动来通过 /proc/acpi/(或 /sys/bus/acpi/)访问遥测强度。例如,如果主板是 IBM/Lenovo Thinkpad 的衍生产品,可以相应地修改 drivers/misc/thinkpad_acpi.c。可以使用 ec_read() 和 ec_write() 内核函数来访问 EC 的 ACPI 空间中存储遥测强度的位置。

深入探讨用户空间驱动与设备管理

8. 各部分操作流程总结

为了更清晰地理解上述各个系统的操作流程,下面为大家梳理一下:

8.1 用户模式 I2C 驱动操作流程
graph TD;
    A[打开主机适配器,open("/dev/i2c - n", O_RDWR)] --> B[连接到特定从设备,ioctl(smbus_fp, I2C_SLAVE, 从设备地址)];
    B --> C[使用数据访问例程与从设备交换数据,如 i2c_smbus_read_byte_data 等];
    C --> D[操作完成,关闭文件描述符,close(smbus_fp)];
8.2 ECC 报告中添加新支持的操作流程
graph TD;
    A[定义新设备的 PCI 设备 ID 及相关寄存器偏移量] --> B[实现获取错误信息函数,如 i855_get_error_info];
    B --> C[实现处理错误信息函数,如 i855_process_error_info];
    C --> D[实现检查函数,如 i855_check 并注册到 EDAC 核心];
    D --> E[实现 PCI 驱动探测方法,如 i855_probe,分配控制内存并注册到 EDAC 核心];
    E --> F[实现移除方法,如 i855_remove,释放内存];
    F --> G[初始化驱动,如 i855_init 注册 PCI 驱动];
8.3 频率缩放操作流程
graph TD;
    A[进入 /sys/devices/system/cpu/cpuX/cpufreq 目录] --> B[查看相关信息,如 scaling_max_freq、scaling_min_freq 等];
    B --> C[选择合适的 governor 并写入 scaling_governor 文件];
    C --> D[改变系统负载,观察 cpuinfo_cur_freq 的变化];
9. 不同系统的关联与协同工作

上述各个系统并非孤立存在,它们之间存在着一定的关联并可以协同工作,以提升系统的整体性能和稳定性。

9.1 ACPI 与频率缩放(cpufreq)的协同

ACPI 负责系统的电源管理和状态转换,而 cpufreq 负责动态调整 CPU 频率以节省电量。当系统进入低功耗状态时,ACPI 可以通知 cpufreq 将 CPU 频率降低,从而进一步减少功耗。例如,当按下笔记本电脑的盖子时,ACPI 检测到该事件并通过 acpid 触发相应的脚本,脚本中可以包含将 cpufreq 的 governor 设置为 powersave 的命令,从而降低 CPU 频率。

9.2 ECC 报告与系统稳定性

ECC 报告系统可以及时发现内存中的错误,当检测到错误时,系统可以采取相应的措施,如更换可疑的 DRAM 芯片,以保证系统的稳定性。同时,稳定的内存环境也有助于其他系统(如 ACPI、cpufreq 等)的正常运行,因为这些系统的操作都依赖于准确的内存数据。

10. 常见问题及解决方法

在使用上述系统的过程中,可能会遇到一些常见问题,下面为大家列举并给出解决方法:

问题 可能原因 解决方法
用户模式 I2C 驱动无法打开设备节点 设备节点不存在、权限不足 检查设备节点是否正确,使用 sudo 提升权限
ECC 报告系统未检测到错误 驱动未正确加载、硬件不支持 检查驱动是否正确安装和加载,确认硬件是否支持 ECC
频率缩放不生效 governor 设置错误、处理器驱动未启用 检查 governor 设置是否正确,启用合适的处理器驱动
ACPI 事件无法触发 acpid 守护进程未运行、配置文件错误 检查 acpid 守护进程是否运行,检查配置文件是否正确
11. 总结

本文详细介绍了用户模式 I2C 驱动、UIO 子系统、ECC 报告、频率缩放、嵌入式控制器和 ACPI 等多个系统的相关知识,包括它们的原理、操作方法、代码示例以及各系统之间的关联和协同工作。通过对这些系统的深入理解和合理运用,可以提升系统的性能、稳定性和电源管理效率。同时,针对常见问题给出了解决方法,希望能帮助大家更好地应对实际开发和使用过程中遇到的问题。在未来的开发中,大家可以进一步探索这些系统的更多功能和应用场景,以满足不同的需求。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值