Linux园地 |
---|
如 何 开 发 Linux Device Driver? |
一 般 大 家 谈 到 Linux系 统 , 不 外 乎 是 它 的 网 路 连 结 能 力 、 有 多 少 好 用 的 公 用 程 式 ....等 , 而 本 文 要 讨 论 的 是 如 何 发 展 一 个 控 制 周 边 设 备 的 驱 动 程 式 , 让 各 位 读 者 能 好 好 来 运 用 各 类 周 边 设 备 。 |
Linux 作 业 系 统 的 多 工 多 人 处 理 的 能 力 , 而 且 以 Intel 处 理 机 (CPU)为 PC-Based的 作 业 环 境 , 助 长 了 大 家 对 多 工 作 业 系 统 Linux的 喜 好 。 报 刊 杂 志 和 媒 体 , 大 家 讨 论 的 Linux, 不 外 乎 是 它 的 网 路 连 结 能 力 、 有 多 少 好 用 的 公 用 程 式 、 以 及 提 供 了 多 少 的 分 享 软 体 , 和 如 何 安 装 Linux作 业 系 统 於 你 的 机 器 上 ? 有 一 天 , 当 你 发 现 身 边 有 一 张 不 错 的 介 面 卡 或 是 一 台 机 器 , 想 好 好 地 应 用 它 时 , 怎 麽 办 ? 本 篇 文 章 就 是 在 讨 论 如 何 发 展 一 个 像 这 样 的 驱 动 程 式 (Driver), 来 控 制 周 边 的 设 备 。
在 着 手 撰 写 Linux I/O Device Driver之 前 , 首 先 介 绍 一 些 相 关 的 观 念 。 UNIX Device Driver是 属 於 核 心 软 体 (Kernel)的 一 部 份 ; UNIX作 业 系 统 主 要 分 为 Kernel和 应 用 软 体 包 括 公 用 程 式 在 内 等 两 大 部 份 。 然 而 Device Driver与 作 业 系 统 之 间 的 关 系 如 图 一 所 示 。 基 本 上 , 所 有 的 UNIX系 统 架 构 都 是 以 此 为 蓝 本 而 设 计 的 。 只 是 每 一 套 UNIX作 业 系 统 也 有 不 完 全 相 同 之 处 , 如 Sun Microsystem的 Solaris系 统 与 AT&T的 System V, Interactive UNIX, 虽 是 以 System V为 版 本 , 但 相 异 处 也 不 少 。 Linux与 SunOS 4.x皆 出 自 於 Berkey版 本 的 背 景 , 也 是 有 许 多 不 同 的 地 方 。 在 这 里 就 不 针 对 这 些 问 题 作 讨 论 , 只 是 提 醒 大 家 认 清 一 套 系 统 要 注 意 其 背 景 及 架 构 , 不 要 混 为 一 谈 。
使 用 者 应 用 程 式 /dev User Space |
---|
Device Driver Kernel Space |
实 体 设 备 |
Device Driver大 致 分 为 Block Device Driver和 Character Device Driver两 类 。 而 Block Device Driver是 以 固 定 大 小 长 度 来 传 送 转 移 资 料 ; Character Device Driver是 以 不 定 长 度 的 字 元 传 送 资 料 。 且 所 连 接 的 Devices也 有 所 不 同 , Block Device大 致 是 可 以 随 机 存 取 (Random Access)资 料 的 设 备 , 如 硬 碟 机 或 光 碟 机 ; 而 Character Device刚 好 相 反 , 依 循 先 後 顺 序 存 取 资 料 的 设 备 , 如 印 表 机 、 终 端 机 等 皆 是 。
本 文 就 以 Character Device Driver发 展 的 程 序 作 说 明 , 其 他 就 不 另 行 讨 论 了 。 有 兴 趣 者 可 以 参 考 其 他 UNIX作 业 系 统 的 DDI/DKI(Device Driver Interface/ Driver-Kernel Interface)手 册 ; 因 为 Linux Device Driver发 展 手 册 或 相 关 书 籍 , 在 国 内 大 概 是 找 不 到 了 。 发 展 Device Driver有 相 当 多 的 问 题 需 要 注 意 , 其 中 最 重 要 的 就 是 备 份 你 的 程 式 和 资 料 , 还 有 一 开 机 磁 片 ; 因 为 发 展 过 程 当 中 , Driver可 能 会 产 生 Bug, 造 成 无 法 开 机 或 是 System Crash, 那 时 你 的 一 切 顷 刻 之 间 马 上 化 为 乌 有 , 真 叫 你 伤 心 欲 绝 , 欲 哭 无 泪 , 唤 不 回 来 了 。 所 以 , 记 得 做 Backup!
现 在 就 来 说 明 Linux Device Driver 发 展 的 步 骤 , 供 大 家 参 考 ; 但 不 一 定 要 严 格 遵 循 此 程 序 , 可 依 自 己 的 意 向 做 调 整 。
熟 悉 Linux Function Routines和 Data Structure
Linux提 供 一 些 Routines和 Kernel使 用 的 资 料 结 构 (Data Structure)於 核 心 软 体 (Kernel) 里 面 , 供 大 家 参 考 使 用 。 笔 者 归 纳 了 几 类 , 相 信 已 足 够 应 付 Device Driver的 开 发 。
1.记 忆 体 配 置 、 释 放 和 转 移
void *kmalloc(unsigned int size, int priority) void *kfree_s(void * obj, int size) kfree(char *) memcpy_fromfs(dest, src, size) memcpy_tofs(dest, src, size) put_fs_byte(src, dest) byte get_fs_byte(char *s)
上 面 列 印 的 Routines是 记 忆 体 的 配 置 、 释 放 、 转 移 等 Procedures。 kmalloc(), kfree()用 法 与 C Language中 使 用 於 User Program的 malloc(), free()用 法 一 样 。 只 是 其 动 态 配 置 的 空 间 是 Kernel Space而 已 ; 而 且 因 为 Kernel的 空 间 是 有 限 制 的 , 所 以 记 得 做 资 源 回 收 不 要 浪 费 记 忆 空 间 。 memcpy _fromfs()是 把 User program写 入 的 资 料 搬 移 到 Kernel Space的 位 置 ; memcpy_tofs()的 动 作 刚 好 相 反 , 是 从 Kernel Space的 位 置 搬 移 到 档 案 系 统 (File System)的 空 间 , 以 便 User Program读 取 资 料 。 这 两 者 大 致 应 用 於 Read, Write的 动 作 程 序 当 中 。
至 於 put_fs_byte()与 get_fs_byte()的 动 作 是 写 入 和 读 取 一 个 字 元 的 资 料 , 分 别 从 User program的 档 案 系 统 空 间 到 Kernel Space, 或 是 相 反 的 动 作 。
2.输 出 入 程 序
inb(port)
outb(char, port)
此 两 程 序 分 别 用 於 介 面 埠 (Interface I/O Port)以 读 取 和 写 出 一 个 字 元 , 达 到 控 制 介 面 、 读 取 状 态 、 输 出 入 资 料 的 目 的 。
3.巨 集 指 令
cli() sti() minor=MINOR(inode->i_rdev) major=MAJOR(inode->i_rdev) boolean suser() save_flags(long flag) restore_flags(long flag)
cli()与 sti()的 功 能 和 C语 言 中 的 disable(), enable()以 及 Assembly语 言 的 cli, sti是 一 样 的 ; 分 别 用 以 清 除 插 断 旗 帜 (Interrupt flag), 达 到 抑 制 插 断 的 发 生 , 和 设 定 旗 帜 , 允 许 插 断 於 下 一 个 指 令 执 行 结 束 後 可 以 回 应 任 何 型 式 插 断 的 发 生 。 接 下 来 解 释 major number和 minor number的 意 义 。 major number用 以 识 别 Devices的 类 别 , 而 minor number用 以 区 别 每 一 类 别 中 , 每 一 个 Device。 举 个 例 子 来 说 明 : 你 可 以 用 ls-l command於 目 录 /d ev中 列 印 资 料 如 下 :
crw-rw-rw- 1 bin bin 5, 0 Oct 9 14:00 /dev/tty1a crw-rw-rw- 1 bin bin 5, 1 Oct 9 14:02 /dev/tty1b crw-rw-rw- 1 bin bin 5, 2 Oct 9 14:05 /dev/tty1c
范 例 中 , 数 目 5代 表 tty Device的 major Device number; 而 其 中 0, 1, 2 就 是 代 表 minor number, 分 别 表 示 Device tty1a, tty1b, tty1c。 说 明 至 此 , 相 信 你 已 了 解 什 麽 是 major number, minor number 了 , 那 又 怎 麽 使 用 呢 ? 在 User Program 中 , 根 本 不 用 考 虑 这 个 问 题 , 仅 是 使 用 open()来 开 启 /dev/tty1a Device, 以 得 到 一 个 file descriptor而 已 ; 然 而 , 在 Kernel Device Driver中 , open、 close、 read、 write等 函 数 就 是 利 用 minor number来 区 别 到 底 是 那
一 个 Device要 求 服 务 (Service)。 只 要 是 应 用 inode Kernel资 料 结 构 来 取 得 minor number。 用 法 如 下 :
minor= MINOR(inode->i_rdev)
至 於 inode structure将 於 资 料 结 构 单 元 中 介 绍 。 suser()这 是 个 检 核 使 用 者 是 否 为 supervisor privilege的 函 数 , 以 决 定 使 用 者 的 使 用 权 限 。 save_flags(), restore _flags()是 用 以 存 取 系 统 旗 帜 (System Flags)的 函 数 。
4.插 断 函 数
request_irq(dev_irq, handler()) irqaction(irq, (struct sigaction *)) free_irq(dev_irq) interruptible_sleep_on(struct wait_queue *) wake_up(struct wait_queue *) wake_up_interruptible(struct wait_queue *)
第 四 点 提 到 的 是 与 插 断 动 作 有 关 的 函 数 , 不 见 得 每 一 个 Device Driver皆 会 用 得 到 ; 因 为 有 些 Device Driver是 使 用 询 问 (Polling)的 方 式 来 撰 写 的 , 其 应 用 固 定 周 期 对 需 要 的 Device做 询 问 , 是 否 需 要 Service, 再 做 进 一 步 处 理 ; 至 於 比 较 有 效 率 的 Interrupt方 式 还 是 要 了 解 。
request_irq()与 irqaction()两 者 的 功 用 是 相 同 的 ; 都 是 向 系 统 注 册 (Register)本 身 Device Driver使 用 的 IRQ和 Interrupt Handler是 什 麽 。 其 作 用 与 MS-DOS下 写 C语 言 使 用 的 setvect()函 数 相 类 似 , 只 不 过 在 Linux Driver就 不 需 要 自 己 enable IRQ的 宣 告 罢 了 。 free_irq()的 功 用 就 是 释 放 (Rel ease)IRQ的 使 用 权 , 归 还 给 系 统 。
在 写 Device Driver的 I/O动 作 时 , 不 能 与 在 写 一 般 的 User Program的 I/O一 样 , 放 任 其 不 管 。 一 定 要 注 意 其 I/O是 否 在 Busy或 Waiting状 态 , 做 出 必 要 的 反 应 动 作 。 例 如 发 现 Device在 Busy状 态 时 , 最 好 呼 叫 interrupt_sleep_on()去 休 息 一 下 , 把 执 行 权 (Running)交 还 给 系 统 ; 若 发 现 是 在 Waiting状 态 时 , 就 可 借 用 wake_up()或 是 wait_up_interruptible()函 数 , 把 你 的 Sleeping Process叫 醒 来 工 作 。
5.其 他
printk() register_chrdev(unsigned int major, const char *name, struct file_operations &fops) unregister_chrdev(unsigned int major, const char *name)有 经 验 的 软 体 工 程 师 , 一 定 有 这 样 的 经 验 , 在 软 体 开 发 的 时 候 , 早 就 事 先 埋 下 伏 笔 , 将 有 需 要 查 证 、 除 错 (Debug)的 资 料 做 个 处 理 , 且 配 合 测 试 软 体 做 个 安 排 , 以 便 节 省 除 错 时 间 。 printk()的 功 能 与 printf()相 似 , 只 是 仅 能 应 用 於 Kernel的 软 体 中 。 register_chrdev()是 Character Device Driver用 来 向 系 统 注 册 (Register)的 函 数 , 其 参 数 包 括 Character Device的 major number, Driver名 称 以 及 档 案 作 业 (File Operation)指 定 的 几 个 函 数 宣 告 的 名 称 。 对 於 File Operation的 结 构 及 如 何 应 用 , 将 於 後 面 做 详 细 的 讨 论 。
unregister_chrdev()顾 名 思 义 , 就 是 系 统 取 消 注 册 名 称 。
6.资 料 结 构
struct file_operations fops struct file *filp struct wait_queue dev_q struct inode dev_node struct sigaction sa
上 式 所 列 几 个 资 料 结 构 在 开 发 Device Driver时 , 是 比 较 常 见 的 。 像 struct file_operations是 Device Driver的 主 要 结 构 , 其 定 义 於 /usr/src/linux/include 目 录 下 的 fs.h档 案 。 Driver中 使 用 到 主 要 函 数 如 xx_read、 xx_write、 xx_open、 xx_ close等 必 须 以 此 结 构 加 以 宣 称 。 现 在 列 印 此 结 构 如 下 :
struct file_operations { int (*lseek)(....); int (*read) (...); int (*write) (...); int (*readdir) (...); int (*select) (...); int (*ioctl) (...); int (*mmap) (....); int (*open) (...); void (*release) (...); int (*fsync) (....); int (*fasync) (...); };
详 细 结 构 请 参 考 fs.h, 它 的 每 一 个 element, 都 是 一 函 数 指 标 , 指 向 你 的 Device Driver所 写 的 函 数 。 我 们 举 个 例 子 如 下 , 或 是 参 阅 本 文 的 Listing程 式 。
static struct file_operations testty_fops = { NULL, testty_read, testty_write, NULL, NULL, NULL, NULL, testty_open, testty_close, NULL, NULL, };
读 者 只 要 将 此 资 料 结 构 在 Driver中 宣 告 , 然 後 应 用 register_chrdev()向 kernel system注 册 , 宣 告 你 的 major number和 Driver名 称 , 此 时 你 已 接 近 完 成 一 半 的 工 作 了 。 接 下 来 就 依 照 你 宣 告 的 函 数 名 称 , 逐 一 完 成 相 关 的 程 式 , 此 刻 你 的 Device Driver 就 算 完 成 了 coding的 动 作 了 。 其 馀 的 我 们 後 面 再 慢 慢 介 绍 了 。
struct file结 构 定 义 於 fs.h include file 中 , 假 如 你 的 Driver是 应 用 於 I/O Device 方 面 时 , 此 结 构 大 致 配 合 function的 参 数 (Parameter)来 使 用 , 其 elements的 应 用 就 少 了 ; 假 如 是 data file方 面 的 应 用 , 那 就 应 用 许 多 了 , 包 括 file mode、 file’ s offset position等 。
struct wait_queue是 一 个 Process处 理 时 , 发 生 I/O动 作 需 要 进 入 睡 眠 (Sleep)或 苏 醒 (Wake up)状 态 时 , 所 用 到 的 一 个 结 构 ; 用 以 记 录 Process目 前 所 处 的 状 况 (St- atus)。 像 interruptible_sleep_on(), wake_up()及 wake_up_interruptible()皆 以 此 结 构 为 主 , 配 合 Driver来 完 成 I/O的 动 作 , 仅 须 宣 告 一 参 数 於 这 些 函 数 中 就 可 以 了 。 接 下 来 就 继 续 说 明 inode结 构 的 应 用 。
inode结 构 是 组 成 file system的 元 素 , 其 内 容 为 file owner identifier、 file type 、 file access permissions, 档 案 存 取 的 时 间 纪 录 , 档 案 的 大 小 , 档 案 资 料 於 disk address的 表 格 (table), 以 及 file link的 数 目 , 还 有 许 多 结 构 的 嵌 入 , 详 细 的 内 容 可 参 阅 fs.h中 的 定 义 。 其 中 的 i_rdev系 用 来 决 定 Device的 minor和 major number:
minor=MINOR(inode->i_rdev)
major=MAJOR(inode->i_rdev)
当 你 取 得 minor number时 , 你 就 可 决 定 目 前 工 作 的 Device是 那 一 个 需 要 服 务 (Service), 以 便 取 得 Device Driver中 的 结 构 , 得 到 Current Device的 状 态 。 Struct Sigaction是 用 来 向 Kernel系 统 注 册 IRQ及 插 断 处 理 程 式 (Interrupt Handler) 的 结 构 。 例 如 宣 告 一 变 数 为 sa: struct sigaction sa;
sa.sa_handler = test_interrupt; sa.sa_flags = SA_INTERRUPT; sa.sa_mask = 0; sa.sa_restorer = NULL; irqaction(irq, &sa);
应 用 上 面 几 行 statements宣 告 你 的 Interrupt handler函 数 了 ; 亦 可 使 用 requestirq(dev_irq, handler)来 完 成 相 同 的 功 能 。
程 式 码 开 发
Linux Device Driver其 实 很 简 单 , 最 难 的 仍 然 是 I/O卡 的 控 制 , 只 要 你 对 你 的 Device很 了 解 , 其 馀 的 follow或 参 考 本 文 就 可 迎 刃 而 解 了 。 首 先 就 比 较 相 关 的 函 数 着 手 , 例 如 xx_open()、 xx_close()、 xx_read()、 xx_write()等 。 当 然 名 称 不 一 定 要 这 样 宣 告 , 随 你 的 意 思 , 以 清 楚 有 意 义 为 原 则 。 将 函 数 宣 告 於 struct file_operations结 构 中 , 然 後 依 照 所 定 义 的 函 数 名 称 去 开 发 这 些 函 数 , 可 参 考 本 文 所 附 的 程 式 ; 其 中 有 一 个 做 i nitialize动 作 的 函 数 xx_init(long kmem _start), 并 没 有 宣 告 於 file_operations当 中 。 此 函 数 的 内 容 除 了 利 用 register _chrdev()向 Kernel系 统 注 册 之 外 , 可 以 做 一 些 Hardware或 是 Software方 面 初 值 设 定 。
对 於 long xx_init(long kmem_start) 函 数 , 还 有 一 些 其 他 的 动 作 要 处 理 。 在 同 一 个 工 作 目 录 /usr/src/linux/Drivers/char 下 , 找 到 mem.c程 式 , 然 後 修 改 此 程 式 中 的 chr_dev_init()函 数 , 增 加 下 列 几 行 於 函 数 中 。 TEST_DRIVE是 自 己 宣 告 的 defined constant。
#ifdef TEST_DRIVE kmem_start = xx_init(kmem_start); #endif
因 为 此 函 数 系 用 来 安 排 各 种 Devices占 用 记 忆 体 (Memory)的 空 间 , 包 括 记 忆 体 本 身 在 内 。 其 他 辅 助 性 的 函 数 可 依 Driver的 性 质 , 自 行 增 减 於 整 个 软 体 的 开 发 。 假 设 Driver的 相 关 功 能 都 Coding完 毕 之 後 , 接 下 来 都 是 重 复 性 的 工 作 包 括 错 误 更 正 、 编 译 、 测 试 等 等 。 现 在 我 们 继 续 说 明 如 何 更 动 系 统 档 案 , 达 到 编 译 程 式 , Build Up系 统 Kernel的 目 的 。 c建 立 Makefile及 Update .config
请 於 /usr/src/linux/Drivers/char目 录 下 , 将 你 的 Device Driver名 称 键 入 於 Makefile 中 , 以 备 编 译 程 式 时 使 用 。
#ifdef TEST_DRIVE OBJS := $(OBJS) Driver_name.o SRCS := $(SRCS) Driver_name.c #endif
接 着 请 於 目 录 /usr/src/linux下 工 作 , 更 改 .config档 案 内 容 ; 请 在 有 关 char Device 位 置 部 份 增 加
TEST_DRIVE=TEST_DRIVE
还 有 请 於 config.in档 案 中 , 在 适 当 的 地 方 加 入 一 行
bool ‘ Char TEST_DRIVE Driver support’ TEST_DRIVE y
以 便 在 执 行 make config时 , 是 否 需 要 将 test_Driver一 起 包 括 进 去 做 configure的 动 作 , 其 中 字 串 内 容 依 自 己 Driver的 功 能 , 自 行 描 述 即 可 ; 再 来 就 继 续 介 绍 下 一 步 骤 。
执 行 make config及 make dep
此 动 作 系 做 Kernel各 种 设 备 (Device)的 安 排 , 例 如 有 无 印 表 机 、 光 碟 机 、 PPP、 SLIP 等 等 。 针 对 系 统 的 需 要 设 备 , 做 适 当 的 调 整 。 make dep针 对 系 统 做 一 致 性 的 调 整 , 例 如 有 那 些 档 案 经 过 更 改 之 後 , 它 会 做 适 度 Update日 期 , 以 配 合 编 译 程 式 处 理 。 步 骤 四 做 过 一 次 之 後 , 视 需 要 再 做 , 不 用 每 次 都 处 理 , 因 为 它 花 费 太 多 时 间 。
建 立 Kernel系 统
在 command prompt下 , 执 行 make zImage指 令 , 以 建 立 Kernel系 统 软 体 ; 此 动 作 就 会 开 始 编 译 你 开 发 的 Driver程 式 。 假 如 发 现 有 Syntax方 面 的 错 误 时 , 必 须 更 正 Driver程 式 , 再 重 复 make zImage的 动 作 , 一 直 到 正 确 无 误 後 , 才 能 载 入 系 统 中 执 行 。
进 行 系 统 测 试
准 备 一 1.44MB的 磁 片 , 藉 着 此 指 令
#fdformat -n /dev/fd0H1440
将 此 磁 片 格 式 化 (Format), 以 便 复 制 一 Kernel zImage到 磁 片 来 。 请 执 行
#cp zImage /dev/fd #co zUnage /dev/fd
指 令 就 可 以 了 。 为 什 麽 不 直 接 Update根 (Root)目 录 下 的 档 案 vmlinuz呢 ? 因 为 如 此 做 太 冒 险 了 , 可 能 会 毁 掉 系 统 造 成 无 法 开 机 ; 所 以 保 守 一 点 , 有 耐 心 一 点 , 一 步 一 步 来 , 用 Floopy开 机 吧 !
如 果 顺 利 的 话 , 你 可 以 在 萤 幕 上 看 到 , 藉 着 printk()於 xx_init()函 数 中 , 列 印 出 来 的 initial讯 息 ; 不 幸 的 话 , 只 好 从 Hard Drive 重 新 开 机 了 , 紧 接 着 开 始 Debug, 抓 虫 了 。 一 切 步 骤 从 头 开 始 , 更 改 程 式 Driver, make zImage, 制 作 Floppy, 再 开 机 吧 !
请 你 再 写 一 个 测 试 程 式 包 括 Read、 Write、 Ioctl等 动 作 的 程 式 以 及 准 备 各 类 的 测 试 资 料 , 以 便 进 一 步 测 试 Driver(测 试 程 式 部 份 请 见 光 碟 \AUTHOR\LINUX子 目 录 )。 可 能 你 已 在 Driver当 中 , 早 就 安 排 加 入 了 printk(), 此 时 你 可 能 会 发 现 , 为 什 麽 没 有 发 现 所 要 列 印 的 Message呢 ? 不 要 慌 , 因 为 它 不 会 在 萤 幕 上 出 现 , 此 时 只 有 在 /var/
adm/messages档 案 中 , 可 以 找 到 它 。 请 你 从 尾 巴 找 起 , 可 能 会 比 较 快 一 点 ; 因 为 它 记 录 系 统 太 多 东 西 了 。 还 有 如 果 你 的 测 试 程 式 , 发 现 Driver有 问 题 的 话 , 请 继 续 重 复 上 面 所 提 过 的 步 骤 , 一 步 一 步 地 继 续 抓 虫 , 一 直 到 零 错 误 为 止 ; 有 一 点 要 特 别 注 意 的 是 , 在 run测 试 程 式 之 前 , 请 你 在 /dev目 录 下 , 键 入 指 令
#mknod drive_name c major_no minor_no
其 中 name, major number就 是 在 Driver中 , xx_init()用 於 注 册 所 宣 称 的 参 数 ; 至 於 minor number依 你 的 Driver软 体 可 以 控 制 的 能 力 来 键 入 。 可 以 ls-l来 察 看 , 所 键 的 Device是 否 正 确 ?
crw-r--r-- 1 root root 29, 0 Nov. 21 05:14 /dev/testty0 crw-r--r-- 1 root root 29, 1 Nov. 21 05:14 /dev/testty1 crw-r--r-- 1 root root 29, 2 Nov. 21 05:14 /dev/testty2
此 例 是 键 入 叁 个 Devices。 Linux Device Driver的 制 作 过 程 很 简 单 , 但 Driver程 式 的 Debug需 要 花 一 点 时 间 和 经 验 , 希 望 本 篇 文 章 能 节 省 你 一 点 摸 索 的 时 间 , 以 及 对 你 有 点 帮 助 。