做嵌入式 Linux 驱动开发,迟早都会撞上一个叫 Common Clock Framework(CCF)的东西。尤其当你开始适配新的主控芯片、给新平台做 bring-up,或者看到设备树里满屏的clocks = <&xxx 3>却一脸懵的时候,你就知道你绕不开它了。
这个框架说白了一句话:Linux 内核里用来统一管理所有“时钟资源”的那套机制。它的作用就是让不同芯片的时钟控制逻辑,长在一个统一的接口模型里,让驱动工程师用同一套 API 去玩转原本各家完全不一样的时钟硬件。这篇文章不打算写成一本正经的文档,我就按我自己的理解,结合平时实际调试中踩过的坑,把这套东西从架构到细节给你捋一遍。不管你是刚入行的开发,还是被时钟问题折腾得够呛的老手,应该都能从里面找到点有用的东西。
1. 这个框架到底解决了什么问题
1.1 没有统一框架之前的世界
早年的内核代码里,时钟管理一直是一笔糊涂账。每家 SoC 厂商都有自己的私货,A 厂定义一套xxx_clk_set_rate(),B 厂又定义一套yyy_set_clk(),名字五花八门,行为千奇百怪。驱动层想调用某个时钟,得直接去链接厂商提供的私有 API,换一个平台全部重写,内核里充满了大量复制粘贴。你要是从老内核代码里翻过旧平台支持,一定见过那种一个文件里几万个函数头、靠#ifdef撑起半边天的战神级代码。
这种模式的问题非常明显:驱动和硬件强耦合,换平台等于重写驱动;不同的厂子各搞一套,平台代码没法复用;而且开发者根本没法用一个统一的方式去描述“树”形结构的时钟拓扑——父时钟源、PLL、分频器、门控、多路选择器之间的级联关系。在没有通用框架的年代,光是搞清“这个时钟的爸爸是谁”就得翻数据手册,更别说做动态调频和功耗管理了。
所以内核社区后来就做了个决定:把时钟抽象成通用的对象,谁家芯片的时钟控制再特殊,也得按标准接口塞进来。这就是 Common Clock Framework 的来历。它不是一个硬件规范,而是一个软件架构层面的统一抽象层。
1.2 CCF 的设计目标
CCF 的核心目标拆开看其实就这么几件事:
- 统一的时钟对象模型:不管底层是 PLL、分频器、多路选择器还是门控,都抽象成
struct clk_hw,通过统一的struct clk_ops操作集合去访问。 - 设备和时钟资源的解耦:设备驱动只需要通过
clk_get()、devm_clk_get()这类 API 拿一个struct clk句柄,完全不关心这个时钟在芯片里是怎么来的。 - 支持动态构建时钟树:通过 parent 指针把一堆独立注册的时钟节点串成一棵树,并且在内核启动时就有能力把这棵树完整地描述出来。
- 为电源管理和驱动框架提供统一的调频入口:比如 cpufreq、runtime PM、音频子系统这些模块,都依赖 CCF 的接口做时钟切换。
这几点看起来简单,但实际落地牵扯到非常多细节,比如原子上下文、rate 传播、parent 切换、时钟门控计数等。后文会一个个拆开讲。
1.3 哪些人必须吃透它
和 CCF 打交道的角色大致分三类。
第一类是芯片厂商的 BSP 工程师,他们要把芯片数据手册里那些 CLK 控制器翻译成内核代码,注册出一个个 clk node,这类人必须深入理解struct clk_hw和struct clk_ops的每一处细节。第二类是普通的设备驱动开发者,比如你写一个 I2C 控制器驱动、网卡驱动或者 MMC 驱动,你基本不会去实现时钟,但你一定会调clk_prepare_enable()、clk_get_rate()、clk_set_rate()。你要是不理解框架的语义,很容易写出“原子上下文调用可睡眠接口”这种致命 bug。第三类是做系统调试和功耗优化的工程师,他们要读懂clk_summary、分析时钟树合理性、排查莫名挂死问题。
不管你属于哪一种,先把这个框架的模型吃透,后面写代码、查问题都会顺很多。
2. 先把几个核心数据结构吃透
2.1 clk_hw:驱动模块真正面对的东西
CCF 抽象出来一个叫struct clk_hw的结构,这是整个框架的基石。所谓 hw,就是硬件抽象。每个挂在框架上的时钟,都对应一个struct clk_hw实例,由具体的平台驱动负责分配和填充。
struct clk_hw { struct clk_core *core; struct clk *clk; const struct clk_init_data *init; };字段解释一下:core是指向内核内部核心结构的指针,框架自己维护,驱动基本不用碰;clk是这个 hw 对应的句柄,框架内部用;init是初始化数据,包含这个时钟的名字、parent 列表和操作函数集合,驱动在注册前必须填好。
实际写驱动的时候,我们通常会把struct clk_hw嵌在自己的私有结构体里面,而不是单独分配它。比如:
struct my_clk { void __iomem *base; u32 enable_reg; struct clk_hw hw; }; #define to_my_clk(_hw) container_of(_hw, struct my_clk, hw)这样做的好处一目了然:用container_of可以从框架给的struct clk_hw *找回我们自己的上下文结构,这在内核里是最常见的写法。
2.2 clk_ops:硬件操作的函数指针集合
如果说struct clk_hw是“身份证”,那struct clk_ops就是“技能列表”。它定义了这个时钟能做什么:能不能调频率、能不能开关、要不要重新计算频率、能不能切 parent 等等。
struct clk_ops { int (*prepare)(struct clk_hw *hw); void (*unprepare)(struct clk_hw *hw); int (*enable)(struct clk_hw *hw); void (*disable)(struct clk_hw *hw); unsigned long (*recalc_rate)(struct clk_hw *hw, unsigned long parent_rate); long (*round_rate)(struct clk_hw *hw, unsigned long rate, unsigned long *parent_rate); int (*set_rate)(struct clk_hw *hw, unsigned long rate, unsigned long parent_rate); int (*set_parent)(struct clk_hw *hw, u8 index); u8 (*get_parent)(struct clk_hw *hw); ... 还有不少其他接口,但上面这些最常用 };这里有几个关键点,新手特别容易搞混:
prepare/unprepare和enable/disable是两种不同语义的操作。前者可以睡眠(可能慢,比如 PLL 锁定要等硬件就绪),后者必须在原子上下文快速完成(比如直接写一个寄存器门控位)。recalc_rate是按当前硬件状态计算实际频率。如果硬件寄存器没有缓存实际频率,这个回调必须实地算出来。round_rate是向上或向下取整出硬件实际能给的频率,它纯粹是“算数”,不做任何硬件写操作。框架建议调频流程是:先round_rate问“能不能给这个频率”,再set_rate真正去写。
一个只有 gate 功能的简单门控时钟,ops 可能就两三个函数;一个 PLL 时钟,ops 会多很多。
2.3 从句柄到核心:clk 和 clk_core 的关系
CCF 里边其实有好几层。对设备驱动来说,它拿到的是struct clk *,这是一个句柄(consumer 的视角)。同一个时钟节点可以被很多个设备引用,每个引用都是一个独立的struct clk实例,但它们都指向同一个内部struct clk_core。
我常用一个生活化类比:struct clk_core是“路由器”本身,struct clk *是“路由器上的一根网线接口”。每个插网线的设备只关心这根线有没有网、网速多少,不需要懂路由器内部电路。多个设备可以同时插这条路,但底层那台路由器就一个实例。
从struct clk_hw到struct clk_core的对应关系是:一个 hw 对应一个 core,core 是框架内部的正式节点。驱动注册上去以后,框架自动给它建 core,然后通过核心里的 refcount、树指针、次数计数器等数据管理这个节点。
2.4 一颗时钟的完整注册流程
我自己第一次写时钟驱动时,最大的困惑就是“代码应该从哪开始,怎么把一颗时钟挂到框架里”。其实套路很简单:
第一步,定义一个私有结构体,把寄存器地址、偏移、mask 这些东西放进去,同时内嵌struct clk_hw:
struct mux_clk { void __iomem *base; u32 reg_offset; u32 shift; struct clk_hw hw; };第二步,填struct clk_init_data:
struct clk_init_data init = { .name = "pll0", .ops = &mux_clk_ops, .parent_names = parents, .num_parents = ARRAY_SIZE(parents), .flags = CLK_SET_RATE_PARENT, };第三步,把 hw 的init指向这个 init,调用注册函数:
hw->init = &init; ret = clk_hw_register(NULL, hw);注意clk_hw_register()的参数第一个是struct device *dev,对于平台驱动里用devm_clk_hw_register()管理生命周期会更舒服,毕竟资源释放不用自己操心。
如果你看到老代码里用clk_register(),那是老 API 了。现在内核代码基本全面转向clk_hw_register()和devm_clk_hw_register()路线,用struct clk_hw已经是标准姿势。
3. 设备树对接与 provider/consumer 模型
3.1 时钟控制器节点怎么写
内核里时钟框架天然和设备树深度绑定。一个时钟控制器(provider)在设备树里通常长这样:
clk_controller: clk-controller { compatible = "vendor,xxx-clk"; reg = <0x10000000 0x1000>; #clock-cells = <1>; clocks = <&osc 0>; clock-names = "osc"; };这里#clock-cells = <1>表示这个 provider 输出的时钟后面需要一个整数索引来区分。比如这颗芯片的时钟控制器管理了 pll0、pll1、div2、gate5 等等,那它们分别编号 0、1、2、3……消费者设备就可以用clocks = <&clk_controller 3>引用编号 3 那个时钟。
如果#clock-cells = <0>,说明这个 provider 只有一个输出时钟,消费者直接clocks = <&clk_controller>就能引用,不需要索引。
这个细节看着不起眼,但真的很容易踩坑。我见过有人在设备树里给#clock-cells = <0>的节点后面硬加一个<0>索引,结果驱动clk_get()直接返回错误。
3.2 消费者如何拿时钟
设备驱动这边拿 clock 的标准姿势很固定。基本流程是先解析设备树,比如在probe函数里:
struct clk *clk; clk = devm_clk_get(dev, "name"); if (IS_ERR(clk)) { /* 处理错误,比如 -EPROBE_DEFER */ }这里devm_clk_get()的第二个参数对应设备树里的clock-names。如果你不想用名字,也可以用索引版本devm_of_clk_get(),从 phandle 里拿索引。
推荐的写法是带名字的,不仅可读性好,而且clock-names和clocks在设备树里对应着写,不容易乱套。
还有一种情况是可选的时钟,比如某个驱动在部分平台上有时钟,在另一些平台没有任何时钟。这时用:
clk = devm_clk_get_optional(dev, "name");devm_clk_get_optional()在找不到时钟时返回NULL而不是-ENOENT,判断起来非常省事。
3.3 prepare 和 enable 为什么要拆开
在 CCF 里,打开了时钟要调用clk_prepare_enable(),关闭则调用clk_disable_unprepare()。但你会看到 API 里其实拆成了两对:clk_prepare()/clk_unprepare()和clk_enable()/clk_disable()。为什么这么别扭?这确实是有原因的。
一个时钟的“打开”动作,在不同硬件上可能工作量完全不一样:一个纯门控时钟,写一个 bit 就完事,几个纳秒就能搞定;但一个 PLL,你要配置分频比、等待锁相环稳定,这个过程可能很慢,甚至需要 sleep 等待中断或轮询状态。问题是,内核里有些上下文是不允许睡眠的,比如中断处理函数、自旋锁保护的临界区。
所以 CCF 做了个非常务实的妥协:
prepare/unprepare:允许睡眠,适合做“慢操作”,比如 PLL 锁定等待、初始化配置。enable/disable:必须在原子上下文可以安全调用,适合做“快操作”,比如寄存器写一个门控位。
驱动开发时,你在普通进程上下文就直接clk_prepare_enable()一步到位。但在中断上下文里,你只能clk_enable(),并且必须在驱动初始化时提前clk_prepare()好。这个约束如果你忽略了,跑着跑着会收到内核的“sleeping function called from invalid context”警告,严重一点直接导致内核 panic。这不是危言耸听,我就因为这个 bug 排查过半天,最后发现自己傻乎乎在中断回调里调了clk_prepare_enable()。
3.4 时钟的爹是谁:parent 与时钟树
设备树和 CCF 的另一个大话题是“时钟树”的形成。每个时钟都能指定自己的 parent,也就是上游的时钟源。比如:
osc ----> pll0 ----> div0 ----> clk_outpll0 的 parent 是 osc,div0 的 parent 是 pll0,clk_out 的 parent 是 div0。注册时在clk_init_data里把parent_names或parent_hws填好,框架会在所有时钟注册完成后自动连接并构建出拓扑树。
现代内核更推荐用parent_hws方式,它指向一个struct clk_hw *,不用按名字搜索,效率更高,还能避免重名时钟导致连接错乱的问题。老代码里常见的.parent_names = {"osc", "pll1"}其实容易踩坑,我曾经在一个平台上把两个名字差不多的 parent 搞混,导致一半外设时钟频率完全不对,后来换成parent_hws才彻底清爽。
4. 频率管理里面那些真正的坑
4.1 一条 rate 请求的漫长旅程
当你对一个 clock 调用clk_set_rate(clk, 48000000),框架不会简单粗暴地直接调用你这个节点的set_rate。它要做的事比你想的多得多:
它先从你请求的 rate 开始,调用clk_round_rate()算出这个节点硬件能支持的最优频率。如果这个节点自己的分频能力做不到 48MHz,它可能要考虑改 parent 的频率,这样才能在传递关系上更接近目标频率。要不要动 parent,取决于CLK_SET_RATE_PARENT这个 flag。如果设置了,框架会把率请求继续往父节点传播;如果没设置,父节点频率不动,当前节点就硬着头皮用自己能力范围内的频率。
最后才是真正调用set_rate回调修改硬件。硬件改完以后,框架会重新计算从当前节点往下的所有子孙节点的实际频率并更新缓存。
这个过程的顺序非常讲究:先 round 再 set,先上后下地传递。所以你在给某外设设频率时,实际被修改的可能不止一个节点。不理解这一点,你去看频率明明设置了但测出来不对,就会一头雾水。
4.2 那几个要命的 flags
struct clk_init_data里的 flags 字段,在调频场景里非常关键。我整理了一份常用 flag 的速查表:
| Flag 名称 | 含义 | 使用注意 |
|---|---|---|
CLK_SET_RATE_PARENT | 该时钟调频时允许把请求传播给父时钟 | 不设这个,很多中间节点调频会受限 |
CLK_SET_RATE_GATE | 该时钟只有在 gate 打开时才能改频率 | 改频率前必须确保 clock 是 enable 状态 |
CLK_GET_RATE_NOCACHE | 每次获取频率都重新回读硬件,不信任缓存 | 硬件频率可能被外部篡改时很有用 |
CLK_IGNORE_UNUSED | 启动阶段清理未使用时钟时保留该时钟 | 有些关键时钟不能关,必须设 |
CLK_IS_CRITICAL | 内核认为这个时钟永远不能关 | 比如 DDR 时钟、某些总线时钟,驱动没引用也不许关 |
这里我特别想聊CLK_IGNORE_UNUSED和CLK_IS_CRITICAL的区别。框架在启动后期会扫描整棵时钟树,把所有没有被驱动使用(enable 计数为 0)的时钟关掉,以节省功耗。但有些时钟虽然暂时没人用,不代表可以关,比如某些平台的上总线挂载时钟、调试模块的时钟,一旦关了系统直接挂死。这时候要么设CLK_IGNORE_UNUSED让框架跳过它,要么设CLK_IS_CRITICAL从语义上声明“它是关键时钟”。后者更推荐,因为它会让框架始终维护它的 enable 引用计数,防止其他逻辑误关。有次我图省事设了CLK_IGNORE_UNUSED,结果某次 suspend/resume 流程里去强制关“无用”时钟时依然出了事,换成CLK_IS_CRITICAL就稳了。
4.3 动态调频的一个现实案例
说一个我实际调试过的例子。某平台需要给 USB 外设提供 48MHz 的参考时钟,设备树里配好了assigned-clock-rates = <48000000>,但我用示波器测 MCO 引脚,发现实际上是 50MHz。第一反应是怀疑 PLL 频率不对,但查了 PLL 输出又没问题。后来把整棵时钟树的clk_summary拉出来,才发现 div 节点的 parent 还没来得及切到目标 PLL,而是停在了一个 25MHz 的振荡源上;而且这个分频节点只有 1/1、1/2、1/4 这几挡。50MHz 其实是 100MHz 的 PLL 输出经过二分频得到的,跟 48MHz 根本不搭边。
排查结论是这个节点的 driver 里没实现set_parent,框架尝试切 parent 时直接返回了-ENOTSUPP,于是只能拿着旧 parent 硬调。后来补上 mux 逻辑,频率才准确切到 48MHz。这个案例让我彻底意识到:光看一个节点的set_rate是不够的,你得盯着 parent 关系、flags 和 ops 全集一起看。
调频时还有一个常见的“缓冲垫”思路:像这种 mux + div + gate 的复合节点,建议拆成独立的节点注册,每一个点都实现全自己的 ops,避免把一堆功能揉在一个时钟节点里。虽然性能上有微小的损耗,但调试起来简直轻松一百倍。
5. 调试手段与常见问题排查
5.1 先把 clk_summary 玩熟
凡是碰到时钟相关问题,我第一件事永远是开 debugfs 看clk_summary。这是我排障的“听诊器”。
cat /sys/kernel/debug/clk/clk_summary输出大概是这样:
clock enable_cnt prepare_cnt rate accuracy phase ---------------------------------------------------------------------------------------------------------------- osc 3 3 25000000 0 0 pll0 1 1 100000000 0 0 div0 1 1 50000000 0 0 clk_out 1 1 50000000 0 0每一行的缩进,就代表节点在时钟树里的层次关系。enable_cnt 和 prepare_cnt 分别表示谁 enable 了它、谁 prepare 了它。如果某个时钟enable_cnt为 0,但你觉得它应该被打开,那设备驱动可能根本没请求这个时钟。rate 列是该节点当前实际频率。后面还有 accuracy 和 phase 列,某些平台需要关注。
clk_summary最值钱的地方不是给你数值,而是把整棵树的拓扑直观地摊开。我经常在调试的时候连续抓两次clk_summary,观察某个节点 enable_cnt 和 rate 的变化,马上就能定位到是哪个驱动在操作时钟。
如果在某些老内核上找不到 debugfs 节点,检查内核配置CONFIG_COMMON_CLK和CONFIG_DEBUG_FS有没有打开。
5.2 每个 debugfs 节点的隐藏技能
clk_summary之外,debugfs 下每个 clock 节点自身也有自己的目录,里面藏着不少宝:比如clk_rate、clk_parent、clk_enable_count等。有些内核版本还支持往clk_rate里写值来直接改频率,这在调试时特别爽:你可以先手工写一个频率,看硬件反应,确定可行后再去改驱动里的配置。
很多平台的 CCF 调试还会依赖 /sys/kernel/debug/clk/clk_dump,它会输出整个时钟树的 JSON 表示。这对写脚本自动化分析和查树结构简直神器。我自己就用它生成过一棵树,然后仔细比对数据手册,确认每个节点的 parent 有没有接错。
5.3 那些高频踩坑点
下面把我在实际项目中反复见过的问题整理成一张速查表,方便排查时对号入座:
| 常见现象 | 大概率原因 | 排查方向 |
|---|---|---|
clk_get()返回-EPROBE_DEFER | 对应 provider 还没注册,设备树时序不对 | 检查 provider 的节点是否 ready;确认#clock-cells和索引是否正确 |
| 某个外设频率完全不对 | parent 关系接错,或没实现set_parent | 看clk_summary的树结构,核实每个节点对应的物理硬件 |
| 系统启动挂死,总是卡在某外设初始化 | 某个关键时钟被clk_disable_unused关了 | 给关键时钟设CLK_IS_CRITICAL,或找驱动主动 keep reference |
| 休眠唤醒后外设失灵 | suspend 期间时钟被关,没有重新开启 | 在驱动resume回调里重新clk_prepare_enable() |
| 内核报 “sleeping function called from invalid context” | 在原子上下文调用clk_prepare()或clk_set_rate() | 检查中断/自旋锁范围里是否有慢速时钟操作 |
| 时钟频率实际值和设置值不一致 | 中间节点的 round_rate 不支持精确频率 | 用clk_round_rate()查硬件能力,再看 parent flags 是否允许传播 |
这张表覆盖了我遇到的大部分问题。如果你不在这张表里,那就去看clk_summary和 dmesg 里的时钟警告日志,基本都能有线索。
5.4 一个典型的坑:clk_disable_unused 的“半夜偷袭”
我特别想讲一个非常隐蔽的问题。有些平台启动的时候一切正常,但过一会儿某个外设突然挂了,甚至整个系统崩掉。这种问题查起来极其痛苦,因为触发时机飘忽不定。
本质上,内核会在启动后期运行一个clk_disable_unused()函数,把整棵时钟树上所有没有被使用的时钟全部关掉。如果你某个外设的时钟虽然没被任何驱动启用,但它其实还在给硬件模块供电或者提供时钟信号,那么这一关,你的外设就直接躺尸了。
这个问题最难排查的地方在于,它发生在启动早期,可能一分钟乃至几秒后就稳定了,后面的驱动再去访问那个外设才发现早已断供。解决办法就是前面说的:定位到具体时钟,设置CLK_IS_CRITICAL,或者确保相关驱动在clk_disable_unused执行之前完成时钟 enable。
这类问题让我养成一个习惯:新平台 bring-up 阶段,第一件事先把整棵clk_summarydump 下来,然后对照数据手册把关键时钟标一遍,该设 critical 的提前设好,免得后面被一些“玄学”问题折磨。
6. 从一片空白到能动手干活
6.1 实操建议:怎么从零开始接触一套新平台的时钟驱动
如果你拿到一个新板子,完全不知道它的时钟拓扑,怎么快速上手?我会按这个顺序来:
第一步,看设备树里clk-controller节点的compatible,找到对应的驱动源文件。第二步,打开驱动文件,先看所有.ops的实现,了解每个节点的能力,这比直接看寄存器手册快得多。第三步,用 debugfs 抓clk_summary和clk_dump,跟设备树里的节点逐一对应起来。第四步,从 root 时钟或某个固定 oscillator 出发,沿着 parent 链逐层验证频率是否与手册一致。
只要这四步走完,你对这套平台的时钟体系基本就有概念了。接着动手改或者调试,就心里有底了。
6.2 直接可以抄的工具函数习惯
我写驱动的时候有点强迫症,一个时钟必须配套几个顺手的小函数。如果你也写平台相关代码,可以参考这样的组织方式:
static inline struct clk *get_clock_handle(struct device *dev) { return devm_clk_get(dev, "ref"); } static int enable_ref_clock(struct device *dev) { struct clk *clk = get_clock_handle(dev); if (IS_ERR(clk)) return PTR_ERR(clk); return clk_prepare_enable(clk); } static void disable_ref_clock(struct device *dev) { struct clk *clk = get_clock_handle(dev); if (!IS_ERR_OR_NULL(clk)) clk_disable_unprepare(clk); }这种小工具函数虽然简单,但能有效避免在每个驱动文件里到处散落IS_ERR检查和clk_disable_unprepare漏调的问题。Devm 系列 API 是内核推荐的做法,生命周期跟随设备自动释放,能少写很多错误处理分支。还有一个小技巧:在probe()里调用clk_get后不要立刻clk_prepare_enable,先做一次完整检查,确定拿到的是你要的时钟,再开。防止中途出错导致时钟悄悄开了一半没人关。
6.3 一份不算完整但很实用的 API 记忆法
最后分享一个记 API 的心法。CCF 的 API 命名非常规则,你只要掌握几个前缀和动词,就能推出一大半:
前缀有三组,clk_(通用句柄操作)、devm_clk_(设备管理的版本,自动释放)、clk_hw_(驱动注册时钟用的底层接口)。动词呢,get/put对应引用和释放,prepare/unprepare对应慢速启用和停用,enable/disable对应快速开关,set_rate/get_rate/round_rate对应频率操作,set_parent/get_parent对应 parent 切换。
你看,是不是根本不用硬背?比如你知道devm_clk_get()是设备管理拿句柄,那devm_clk_get_optional()自然是拿一个可选句柄;你知道clk_hw_register()是注册,那么devm_clk_hw_register()自然是设备生命周期管理的注册版本。内核的这个命名习惯,真的是越用越舒服。
我最早被 CCF 折腾得死去活来时,也曾想过放弃直接去改寄存器操作,但后来发现,这套框架带来的收益远超学习成本。只要愿意多花一点时间把数据结构、ops、rate 传播逻辑理解透,它真的能帮你省下大把调试时间。尤其是当你需要同时管理几十颗时钟时,CCF 的 debugfs 工具链就像一张活地图,直接告诉你哪里有问题,而不是让你对着寄存器手册发呆。做嵌入式这行,能有一个如此清晰易用的子系统,真的值得好好掌握。