[toc]

在这里插入图片描述

drivers/dma/dmaengine.h

struct dma_chan / struct dma_chan_dev: DMA 通道对象与其 sysfs 设备包装对象的语义建模

  • struct dma_chanDMAengine 框架中“可被客户端申请与提交事务”的通道抽象,承载运行期状态(cookie、完成度、客户端引用计数、路由信息等)以及与上层可见性的关联(sysfs/debugfs 相关字段)。
  • struct dma_chan_dev把通道映射为 Linux 设备模型中的一个 struct device,用于 sysfs 节点呈现与生命周期管理;同时保存 dma_chan * 的反向指针(也就是你前面讨论的 chan->dev->chan 这一条关键可见性路径)。

这两个结构体之所以分开,是因为 DMAengine 需要同时满足:

  1. 运行期快速访问与调度(dma_chan
  2. 设备模型可见性、热插拔/解绑生命周期、sysfs 目录树一致性(dma_chan_dev + struct device
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* @brief DMA 通道对象:由 DMA 控制器提供、由客户端申请并提交 DMA 事务
*
* 该结构体是 DMAengine 的核心运行期对象,表示一个可分配的 DMA 通道。
* 它既包含事务进度相关状态(cookie、完成情况等),也包含框架可见性与管理字段(sysfs/debugfs、引用计数等)。
*/
struct dma_chan {
/** @brief 指向提供该通道的 DMA 控制器对象(DMAengine 设备抽象),始终非 NULL */
struct dma_device *device;

/** @brief 当前使用该通道的从设备(客户端设备);未绑定时可为 NULL */
struct device *slave;

/** @brief 最近一次分配给客户端的 cookie,用于标识事务序列与查询状态 */
dma_cookie_t cookie;

/** @brief 最近一次已完成的 cookie,用于提供“完成到哪一笔事务”的可观测状态 */
dma_cookie_t completed_cookie;

/** @brief 通道的 sysfs ID;用于在 sysfs 中形成稳定编号 */
int chan_id;

/**
* @brief 通道的 sysfs 设备包装对象
*
* 注意:这里不是 struct device*,而是 dma_chan_dev*,
* 其内部嵌入 struct device,用于把通道挂到设备模型并导出 sysfs 节点。
*/
struct dma_chan_dev *dev;

/** @brief 用于 sysfs/日志的通道名称(反向关联名),由框架/驱动组织 */
const char *name;

#ifdef CONFIG_DEBUG_FS
/**
* @brief debugfs 中显示的客户端标识字符串
*
* 格式通常为 "请求者设备名:通道名",用于调试时定位“谁在使用哪个通道”。
*/
char *dbg_client_name;
#endif

/**
* @brief 挂入 dma_device->channels 链表的链表节点
*
* 用于框架枚举该控制器提供的所有通道;注销路径会从该链表安全移除。
*/
struct list_head device_node;

/**
* @brief 每 CPU 私有数据指针(per-cpu)
*
* 语义:将某些频繁更新的统计/状态按 CPU 维度分离,降低跨 CPU 争用。
* 在单核 STM32H750 场景下:
* - 逻辑上仍是 per-cpu API,但实际只有一个 CPU 实例;
* - 该设计仍保持与 SMP 内核一致的接口与生命周期。
*
* 注:该指针是否为 NULL 常被用作“通道是否已完成注册/可见”的判据之一。
*/
struct dma_chan_percpu __percpu *local;

/**
* @brief 客户端引用计数:有多少客户端持有该通道引用
*
* 该计数用于注销安全性检查(例如注销时仍被持有会触发告警),
* 并参与框架的资源生命周期判断。
*/
int client_count;

/**
* @brief 该通道在 mem-to-mem 分配表中的出现次数
*
* 用于框架的通道表维护与重平衡逻辑,表征该通道被纳入全局分配索引的程度。
*/
int table_count;

/** @brief DMA 路由器对象指针;用于需要路由/多路复用的 SoC 场景 */
struct dma_router *router;

/** @brief 路由器相关的通道私有数据;由路由器/驱动解释 */
void *route_data;

/**
* @brief 客户端-通道关联的私有数据
*
* 用于特定客户端与通道绑定时保存上下文信息;
* 其解释权属于具体驱动或上层封装逻辑。
*/
void *private;
};

/**
* @brief 通道 sysfs 设备包装对象:把 dma_chan 映射进 Linux 设备模型
*
* 该结构体的关键点是“嵌入 struct device”,使通道成为设备模型中的一个 device 节点,
* 从而获得统一的生命周期管理、sysfs 展示与引用计数语义。
*/
struct dma_chan_dev {
/**
* @brief 反向指针:指向被包装的 DMA 通道对象
*
* 这是你前面讨论的关键可见性路径:
* - 注册时设置为 chan
* - 注销时在 dma_list_mutex 保护下置为 NULL,以切断对外可见入口
*/
struct dma_chan *chan;

/**
* @brief 嵌入的设备模型对象
*
* 用于注册/注销到内核设备模型,形成 sysfs 节点并参与设备生命周期管理。
*/
struct device device;

/** @brief 父 dma_device 的 dev_id,用于 sysfs 层面的关联与标识 */
int dev_id;

/**
* @brief 指示该通道是否使用与父 dma_device 不同的 dma-mapping 方式
*
* 该字段用于表达“通道级别的 DMA 映射策略差异”,例如某些通道经由特殊互联/地址域。
* 对无 MMU 的 STM32H750:
* - 仍可能涉及 DMA 地址能力差异(例如总线可达性、地址掩码、路由器路径),
* - 但不等同于 MMU 虚拟地址转换语义。
*/
bool chan_dma_dev;
};

这一组定义在头文件中的静态内联函数共同构成了一个高效、轻量级且线程安全的框架, 用于追踪Linux内核中异步DMA传输的生命周期。它们是通用DMA引擎(DMA Engine)子系统的基石, STM32 DMA驱动等具体实现都依赖这个框架来管理传输任务。

其核心原理是实现了一个**”票据系统” (ticket system)**:

  1. 初始化 (dma_cookie_init): DMA通道像一个票据分发机, 初始化时将”已分发”和”已完成”的票号都重置为起始值。
  2. 分配 (dma_cookie_assign): 每当一个新的DMA传输任务(由dma_async_tx_descriptor表示)被提交时, 就会从分发机取一张新的、唯一的、顺序递增的票(cookie), 并将这张票贴在任务上。同时, 分发机记下”最后分发的票号”。
  3. 完成 (dma_cookie_complete): 当DMA硬件完成一个任务并触发中断时, 中断处理程序会拿出完成任务上的票, 并更新一个公告板, 上面写着”最后完成的票号”。
  4. 查询 (dma_cookie_status): 任何时候, 任何人都可以拿着自己手里的票号去和公告板上的”最后完成票号”以及分发机的”最后分发票号”做比较。通过比较, 就可以确定这个任务是已经完成、正在处理、还是仍在排队。

这个系统巧妙地利用了单调递增的整数, 使得状态查询可以无锁 (lock-free) 执行, 极大地提高了性能。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* dma_cookie_init - 初始化一个DMA通道的cookie
* @chan: 要初始化的dma通道
*/
static inline void dma_cookie_init(struct dma_chan *chan)
{
/*
* chan->cookie: 代表最后分配出去的cookie.
* chan->completed_cookie: 代表最后一个确认完成的cookie.
* DMA_MIN_COOKIE: cookie的起始值, 通常是1, 因为0被用作无效值.
* 原理: 将通道的两个核心追踪器都重置为起始状态, 为新的传输序列做准备.
* 这通常在DMA驱动的probe函数中为每个通道调用一次.
*/
chan->cookie = DMA_MIN_COOKIE;
chan->completed_cookie = DMA_MIN_COOKIE;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* dma_cookie_assign - 为描述符分配一个DMA引擎cookie
* @tx: 需要cookie的描述符
*
* 原理: 这是一个票据分发器. 它原子地(在调用者持有的锁的保护下)递增通道的cookie计数器,
* 并将这个新的、唯一的ID同时赋给描述符和通道的"最后分配"追踪器.
*/
static inline dma_cookie_t dma_cookie_assign(struct dma_async_tx_descriptor *tx)
{
struct dma_chan *chan = tx->chan;
dma_cookie_t cookie;

cookie = chan->cookie + 1; // <-- 计算下一个cookie值.
if (cookie < DMA_MIN_COOKIE) // <-- 处理32位整数溢出回绕的情况.
cookie = DMA_MIN_COOKIE;
tx->cookie = cookie; // <-- 将新cookie赋给描述符.
chan->cookie = cookie; // <-- 更新通道的"最后分配"cookie.

return cookie;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* dma_cookie_complete - 完成一个描述符
* @tx: 要完成的描述符
*
* 原理: 这是中断处理程序的核心动作. 它更新通道的"公告板", 宣告此cookie代表的任务已经完成.
* 将描述符自身的cookie清零是一个重要的安全措施, 防止因代码逻辑错误导致同一个任务被意外地"完成"两次.
*/
static inline void dma_cookie_complete(struct dma_async_tx_descriptor *tx)
{
// BUG_ON确保我们不会去完成一个从未被分配cookie的任务.
BUG_ON(tx->cookie < DMA_MIN_COOKIE);
// <-- 核心操作: 更新通道的"最后完成"cookie.
tx->chan->completed_cookie = tx->cookie;
// <-- 安全措施: 作废描述符上的cookie.
tx->cookie = 0;
}

/**
* dma_async_is_complete - test a cookie against chan state
* @cookie: transaction identifier to test status of
* @last_complete: last know completed transaction
* @last_used: last cookie value handed out
*
* dma_async_is_complete() is used in dma_async_is_tx_complete()
* the test logic is separated for lightweight testing of multiple cookies
*/
static inline enum dma_status dma_async_is_complete(dma_cookie_t cookie,
dma_cookie_t last_complete, dma_cookie_t last_used)
{
if (last_complete <= last_used) {
if ((cookie <= last_complete) || (cookie > last_used))
return DMA_COMPLETE;
} else {
if ((cookie <= last_complete) && (cookie > last_used))
return DMA_COMPLETE;
}
return DMA_IN_PROGRESS;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* dma_cookie_status - 报告cookie的状态
* @chan: dma通道
* @cookie: 我们感兴趣的cookie
* @state: 用于返回详细状态的结构体
*
* 原理: 这是一个无锁查询函数. 它首先原子地读取"最后分配"和"最后完成"的快照,
* 然后调用 dma_async_is_complete (一个处理整数回绕的比较函数) 来判断给定的cookie
* 是否小于等于"最后完成"的cookie.
*/
static inline enum dma_status dma_cookie_status(struct dma_chan *chan,
dma_cookie_t cookie, struct dma_tx_state *state)
{
dma_cookie_t used, complete;

used = chan->cookie; // <-- 读取"最后分配"的cookie.
complete = chan->completed_cookie; // <-- 读取"最后完成"的cookie.
/*
* barrier(): 这是一个内存屏障. 在无锁的上下文中, 它至关重要.
* 它确保了编译器和CPU不会重排上面两个读操作和下面 dma_async_is_complete 中的读操作.
* 这保证了我们总是基于一个一致的(尽管可能略微过时)状态快照来进行判断.
*/
barrier();
if (state) { // <-- 如果调用者需要详细信息, 填充state结构.
state->last = complete;
state->used = used;
state->residue = 0; // 剩余字节数默认为0, 具体驱动会更新它.
state->in_flight_bytes = 0; // 同上
}
// 调用核心比较逻辑, 返回 DMA_COMPLETE 或 DMA_IN_PROGRESS.
return dma_async_is_complete(cookie, complete, used);
}

dma_set_residuedma_set_in_flight_bytes: 状态设置辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* dma_set_residue - 设置状态中的剩余字节数
* 原理: 这是一个简单的、安全的设置器. 它避免了调用者每次都需要检查 state 指针是否为NULL.
*/
static inline void dma_set_residue(struct dma_tx_state *state, u32 residue)
{
if (state)
state->residue = residue;
}

/*
* dma_set_in_flight_bytes - 设置状态中正在传输的字节数
* 原理: 同上, 一个安全的设置器.
*/
static inline void dma_set_in_flight_bytes(struct dma_tx_state *state,
u32 in_flight_bytes)
{
if (state)
state->in_flight_bytes = in_flight_bytes;
}

DMA引擎回调辅助函数集

此代码片段定义了一组静态内联函数, 它们是Linux内核DMA引擎(DMA Engine)框架的通用辅助工具。它们的核心原理是提供一套标准化的、安全的、并且向后兼容的机制, 用于处理DMA传输完成后的回调操作

当一个设备驱动(例如SPI驱动)请求一次DMA传输后, 它会提供一个”回调函数”。当DMA控制器硬件完成传输并触发中断时, DMA控制器驱动需要调用这个回调函数来通知原始的设备驱动”你的数据传输已完成”。这组辅助函数就是为了让这个通知过程变得更加简洁、健壮和统一。


struct dmaengine_desc_callback: DMA引擎描述符回调信息结构体

这是一个纯粹的数据结构, 作为一个临时容器来存储一次回调所需的所有信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 结构体定义: dmaengine_desc_callback
* 作用: 临时存储从一个DMA描述符中提取出的回调函数信息.
*/
struct dmaengine_desc_callback {
/*
* .callback: 一个函数指针, 指向一个简单的回调函数.
* dma_async_tx_callback 的类型定义为: void (*)(void *param);
* 这是旧版的回调类型.
*/
dma_async_tx_callback callback;
/*
* .callback_result: 一个函数指针, 指向一个更详细的回调函数.
* dma_async_tx_callback_result 的类型定义为: void (*)(void *param, const struct dmaengine_result *result);
* 这是新版的回调类型, 能够传递一个包含传输结果(如成功/失败, 剩余字节数)的结构体.
*/
dma_async_tx_callback_result callback_result;
/*
* .callback_param: 一个 void 指针, 用于传递给回调函数的参数.
* 通常, 提交DMA请求的驱动会把自身的上下文指针(如指向其设备结构体的指针)放在这里.
*/
void *callback_param;
};

dmaengine_desc_get_callback: 获取DMA描述符中的回调信息

此函数用于从一个DMA传输描述符中安全地复制出回调信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* dmaengine_desc_get_callback - 获取传入的回调函数
* @tx: 传输描述符
* @cb: 用于保存回调信息的临时结构体
*
* 将传入的tx描述符结构体中的回调信息填充到传入的cb结构体中.
* 这个操作不需要加锁.
*/
static inline void
dmaengine_desc_get_callback(struct dma_async_tx_descriptor *tx,
struct dmaengine_desc_callback *cb)
{
/*
* 将 tx 描述符中的 callback 成员 (函数指针) 复制到 cb 结构体中.
*/
cb->callback = tx->callback;
/*
* 将 tx 描述符中的 callback_result 成员 (函数指针) 复制到 cb 结构体中.
*/
cb->callback_result = tx->callback_result;
/*
* 将 tx 描述符中的 callback_param 成员 (void 指针) 复制到 cb 结构体中.
*/
cb->callback_param = tx->callback_param;
}

dmaengine_desc_callback_invoke: 调用回调函数

此函数负责执行存储在临时结构体中的回调函数, 并处理了新旧两种回调类型的兼容性问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* dmaengine_desc_callback_invoke - 调用cb结构体中的回调函数
* @cb: 保存着回调信息的临时结构体
* @result: 传输结果
*
* 使用cb结构体中的参数来调用cb结构体中提供的回调函数.
* 是否需要加锁取决于驱动程序的具体实现.
*/
static inline void
dmaengine_desc_callback_invoke(struct dmaengine_desc_callback *cb,
const struct dmaengine_result *result)
{
/*
* 在栈上定义并初始化一个"虚拟的"成功结果.
* 如果调用者没有提供有效的 'result', 并且需要调用新版的回调函数时, 将使用这个默认结果.
*/
struct dmaengine_result dummy_result = {
.result = DMA_TRANS_NOERROR, // 结果: 没有错误
.residue = 0 // 剩余字节数: 0
};

/*
* 优先检查新版的回调函数指针是否存在.
*/
if (cb->callback_result) {
/*
* 如果调用者没有提供结果 (result == NULL), 则使用我们创建的虚拟成功结果.
* 这确保了新版回调函数总是能接收到一个有效的 result 指针.
*/
if (!result)
result = &dummy_result;
/*
* 调用新版的回调函数, 传入参数和传输结果.
*/
cb->callback_result(cb->callback_param, result);
/*
* 如果新版回调不存在, 再检查旧版的回调函数指针是否存在.
*/
} else if (cb->callback) {
/*
* 调用旧版的回调函数, 只传入参数.
*/
cb->callback(cb->callback_param);
}
}

dmaengine_desc_get_callback_invoke: 获取并立即调用DMA回调

这是一个便利的封装函数, 将获取和调用两个步骤合二为一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* dmaengine_desc_get_callback_invoke - 获取tx描述符中的回调并立即调用它.
* @tx: DMA异步传输描述符
* @result: 传输结果
*
* 在一个函数内调用 dmaengine_desc_get_callback() 和 dmaengine_desc_callback_invoke(),
* 因为对于驱动来说, 这两步之间通常不需要做额外的工作.
* 是否需要加锁取决于驱动程序的具体实现.
*/
static inline void
dmaengine_desc_get_callback_invoke(struct dma_async_tx_descriptor *tx,
const struct dmaengine_result *result)
{
/*
* 在栈上定义一个临时的回调信息结构体.
*/
struct dmaengine_desc_callback cb;

/*
* 第一步: 从 tx 描述符中获取回调信息到 cb.
*/
dmaengine_desc_get_callback(tx, &cb);
/*
* 第二步: 立即调用存储在 cb 中的回调.
*/
dmaengine_desc_callback_invoke(&cb, result);
}

dmaengine_desc_callback_valid: 检查回调是否有效

这是一个简单的检查函数, 用于判断一个描述符是否有关联的完成回调。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* dmaengine_desc_callback_valid - 验证cb中的回调是否有效
* @cb: 回调信息结构体
*
* 返回一个布尔值, 验证cb中的回调是否有效.
* 这个操作不需要加锁.
*/
static inline bool
dmaengine_desc_callback_valid(struct dmaengine_desc_callback *cb)
{
/*
* 如果 cb->callback 指针不为NULL, 或者 cb->callback_result 指针不为NULL,
* 表达式为真, 函数返回 true.
* 只有当两个指针都为NULL时, 才返回 false.
*/
return cb->callback || cb->callback_result;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
static inline int dmaengine_pause(struct dma_chan *chan)
{
if (chan->device->device_pause)
return chan->device->device_pause(chan);

return -ENOSYS;
}

static inline int dmaengine_resume(struct dma_chan *chan)
{
if (chan->device->device_resume)
return chan->device->device_resume(chan);

return -ENOSYS;
}
static inline enum dma_status dmaengine_tx_status(struct dma_chan *chan,
dma_cookie_t cookie, struct dma_tx_state *state)
{
return chan->device->device_tx_status(chan, cookie, state);
}



static inline struct dma_async_tx_descriptor *dmaengine_prep_dma_cyclic(
struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len,
size_t period_len, enum dma_transfer_direction dir,
unsigned long flags)
{
if (!chan || !chan->device || !chan->device->device_prep_dma_cyclic)
return NULL;

return chan->device->device_prep_dma_cyclic(chan, buf_addr, buf_len,
period_len, dir, flags);
}


/**
* dmaengine_terminate_async() - Terminate all active DMA transfers
* @chan: The channel for which to terminate the transfers
*
* Calling this function will terminate all active and pending descriptors
* that have previously been submitted to the channel. It is not guaranteed
* though that the transfer for the active descriptor has stopped when the
* function returns. Furthermore it is possible the complete callback of a
* submitted transfer is still running when this function returns.
*
* dmaengine_synchronize() needs to be called before it is safe to free
* any memory that is accessed by previously submitted descriptors or before
* freeing any resources accessed from within the completion callback of any
* previously submitted descriptors.
*
* This function can be called from atomic context as well as from within a
* complete callback of a descriptor submitted on the same channel.
*
* If none of the two conditions above apply consider using
* dmaengine_terminate_sync() instead.
*/
static inline int dmaengine_terminate_async(struct dma_chan *chan)
{
if (chan->device->device_terminate_all)
return chan->device->device_terminate_all(chan);

return -EINVAL;
}

/**
* dmaengine_synchronize() - Synchronize DMA channel termination
* @chan: The channel to synchronize
*
* Synchronizes to the DMA channel termination to the current context. When this
* function returns it is guaranteed that all transfers for previously issued
* descriptors have stopped and it is safe to free the memory associated
* with them. Furthermore it is guaranteed that all complete callback functions
* for a previously submitted descriptor have finished running and it is safe to
* free resources accessed from within the complete callbacks.
*
* The behavior of this function is undefined if dma_async_issue_pending() has
* been called between dmaengine_terminate_async() and this function.
*
* This function must only be called from non-atomic context and must not be
* called from within a complete callback of a descriptor submitted on the same
* channel.
*/
static inline void dmaengine_synchronize(struct dma_chan *chan)
{
might_sleep();

if (chan->device->device_synchronize)
chan->device->device_synchronize(chan);
}

/**
* dmaengine_terminate_sync() - Terminate all active DMA transfers
* @chan: The channel for which to terminate the transfers
*
* Calling this function will terminate all active and pending transfers
* that have previously been submitted to the channel. It is similar to
* dmaengine_terminate_async() but guarantees that the DMA transfer has actually
* stopped and that all complete callbacks have finished running when the
* function returns.
*
* This function must only be called from non-atomic context and must not be
* called from within a complete callback of a descriptor submitted on the same
* channel.
*/
static inline int dmaengine_terminate_sync(struct dma_chan *chan)
{
int ret;

ret = dmaengine_terminate_async(chan);
if (ret)
return ret;

dmaengine_synchronize(chan);

return 0;
}
/**
* dma_async_issue_pending - flush pending transactions to HW
* @chan: target DMA channel
*
* This allows drivers to push copies to HW in batches,
* reducing MMIO writes where possible.
*/
static inline void dma_async_issue_pending(struct dma_chan *chan)
{
chan->device->device_issue_pending(chan);
}

drivers/dma/dmaengine.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
这段注释的中文翻译如下(保持技术语义,不增加引申):

* 这段代码实现了 **DMA 子系统**。它为内核中的其他代码提供了一个**与硬件无关的接口**,用于使用(如果存在)**异步内存拷贝能力**,并允许不同的硬件 DMA 驱动注册并声明自己提供这种能力。

* 由于我们要加速的操作本身已经相对很快,因此代码在设计上会**尽可能避免引入额外开销**,例如锁带来的开销。

---

### 锁相关说明(LOCKING)

* 该子系统维护了一个全局的 `dma_device` 结构体链表,该链表由一个互斥量 `dma_list_mutex` 进行保护。

* 某个子系统(内核中的使用方)可以通过先调用 `dmaengine_get()`,再调用 `dma_find_channel()` 来获取一个 DMA 通道;如果它需要一个独占通道,则可以调用 `dma_request_channel()`。

* 一旦一个通道被分配,就会对其对应的驱动模块持有一个引用(reference),以防止该驱动在通道仍在使用期间被卸载移除。

* 每个 DMA 设备都有一个 `channels` 链表。这个链表在遍历时**不加锁**,但在设备注册完成之后就**不会再被修改**;它只由具体的 DMA 驱动在注册之前完成设置。

* 更详细内容可参考 `Documentation/driver-api/dmaengine`。

DMA引擎 事务类型与通道查找表

此代码片段属于Linux内核DMA引擎(dmaengine)子系统的核心部分。它的核心作用是定义DMA引擎支持的所有操作类型, 并创建一个全局的、高性能的查找表(channel_table), 以便内核可以快速地为特定的DMA任务(如内存拷贝、异或计算)找到一个最合适的DMA通道

该机制最关键的特性是它为每个CPU核心都维护一个独立的查找表副本。这一原理是通过__percpu关键字实现的, 它的主要目的是在多核系统中避免锁竞争, 从而极大地提升性能。当一个CPU上的驱动程序需要一个DMA通道时, 它可以直接访问自己私有的查找表, 无需与其它CPU同步。

原理与工作流程:

  1. enum dma_transaction_type (DMA事务类型): 这个枚举定义了dmaengine框架所能理解的所有标准DMA操作。这包括:

    • 简单的内存操作: DMA_MEMCPY (内存拷贝), DMA_MEMSET (内存填充)。
    • 复杂的计算卸载: DMA_XOR, DMA_PQ (常用于RAID加速)。
    • 外设相关的传输类型: DMA_SLAVE (例如, 从SPI外设到内存), DMA_CYCLIC (用于音频等循环缓冲)。
    • 这些枚举值是DMA控制器驱动和DMA客户端驱动之间沟通能力的”通用语言”。
  2. channel_table (通道查找表): 这是一个全局数组, 但被__percpu修饰。

    • channel_table[DMA_MEMCPY] 这一项专门用来存放能执行”内存拷贝”的DMA通道。
    • __percpu意味着每个CPU核心都有自己独立的channel_table副本。CPU 0写入channel_table的数据不会影响CPU 1的副本。
  3. dma_channel_table_init() (初始化函数):

    • 这是一个在内核启动早期通过arch_initcall调用的初始化函数。
    • 它首先初始化一个dma_cap_mask_all位掩码, 初始时包含所有定义的操作类型。
    • 然后, 它从这个掩码中移除了一些不代表具体”内存到内存”操作的类型, 如DMA_SLAVE。因为这个查找表专门用于优化memcpy, xor等”计算卸载”类操作的通道查找, 而DMA_SLAVE有其自己的查找机制。
    • 最关键的一步是循环, 它遍历所有有效的操作类型(cap), 并为每一种类型调用alloc_percpu()。这个调用为channel_table[cap]每一个CPU核心上都分配了内存。
    • 函数包含了完善的错误处理, 如果在初始化过程中内存不足, 它会释放所有已分配的资源并报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* dma_transaction_type - DMA事务类型/索引的枚举 */
enum dma_transaction_type {
DMA_MEMCPY, // 内存到内存拷贝
DMA_XOR, // 对多个源进行异或操作, 存入一个目标
DMA_PQ, // P+Q校验码计算 (RAID6)
// ... 其他类型
DMA_SLAVE, // 外设到内存或内存到外设的传输
DMA_CYCLIC, // 循环模式DMA, 用于音频等
// ...
DMA_TX_TYPE_END, // 标志事务类型的结束, 用于定义数组大小
};

/* --- 客户端和设备注册 --- */

/* 定义一个dma能力位掩码, 用于遍历所有操作类型. */
static dma_cap_mask_t dma_cap_mask_all;

/* dma_chan_tbl_ent - 用于追踪每个核心/每种操作的通道分配情况的条目 */
struct dma_chan_tbl_ent {
struct dma_chan *chan; // 指向与此条目关联的DMA通道
};

/*
* 为内存到内存卸载操作提供者的per-cpu查找表.
* 这是一个数组, 数组的每个元素都是一个指向 per-cpu 数据的指针.
*/
static struct dma_chan_tbl_ent __percpu *channel_table[DMA_TX_TYPE_END];

/* dma_channel_table_init - 初始化DMA通道表 */
static int __init dma_channel_table_init(void)
{
enum dma_transaction_type cap;
int err = 0;

/* 将 dma_cap_mask_all 的所有位都设置为1. */
bitmap_fill(dma_cap_mask_all.bits, DMA_TX_TYPE_END);

/*
* 'interrupt', 'private', 'slave' 是通道的能力, 但不与具体
* 的内存操作关联, 因此它们不需要在channel_table中占有条目.
* 这个表专门用于 memcpy, xor 等 "计算/卸载" 操作.
*/
clear_bit(DMA_INTERRUPT, dma_cap_mask_all.bits);
clear_bit(DMA_PRIVATE, dma_cap_mask_all.bits);
clear_bit(DMA_SLAVE, dma_cap_mask_all.bits);

/* 遍历所有有效的DMA能力掩码位. */
for_each_dma_cap_mask(cap, dma_cap_mask_all) {
/*
* 为每种能力类型, 分配一个 per-cpu 的 dma_chan_tbl_ent 实例.
* 这意味着每个CPU核心都会有自己的 dma_chan_tbl_ent 实例.
*/
channel_table[cap] = alloc_percpu(struct dma_chan_tbl_ent);
if (!channel_table[cap]) {
err = -ENOMEM; /* 内存不足 */
break;
}
}

/* 如果在循环中发生了错误. */
if (err) {
pr_err("dmaengine dma_channel_table_init failure: %d\n", err);
/* 清理所有已经成功分配的per-cpu内存. */
for_each_dma_cap_mask(cap, dma_cap_mask_all)
free_percpu(channel_table[cap]);
}

return err;
}
/*
* arch_initcall - 这是一个内核初始化宏, 确保此函数在系统启动的早期阶段被调用,
* 在任何可能使用DMA的驱动程序初始化之前.
*/
arch_initcall(dma_channel_table_init);

DMA引擎 Unmap内存池与总线初始化

此代码片段展示了Linux内核DMA引擎(dmaengine)子系统在启动时进行的两个关键初始化步骤: 1) 创建一组用于管理DMA”unmap”操作元数据的内存池(Memory Pools), 2) 注册dma设备类, 为后续的DMA控制器设备创建/sys/class/dma/目录。

这部分代码的核心原理是通过预分配机制来解决在中断上下文中进行内存分配的难题, 并为不同复杂度的DMA传输提供内存优化的解决方案


dmaengine_init_unmap_pool: 初始化DMA Unmap内存池

1. 问题背景: 为什么需要内存池?
当一个驱动程序提交一个DMA传输任务时, dmaengine框架需要一个数据结构来存储这次传输的相关信息, 特别是所有需要被DMA控制器访问的内存缓冲区的地址。这些缓冲区在传输完成后必须被”unmap”, 以便CPU可以再次安全地访问它们。这个”unmap”操作通常是在DMA传输完成中断的处理程序中触发的。

关键约束: 内核的中断处理程序绝对不能睡眠。而常规的内存分配函数kmalloc(..., GFP_KERNEL)在系统内存不足时可能会睡眠等待。因此, 在中断上下文中直接调用kmalloc是禁止的, 否则会导致系统死锁或崩溃。

2. 解决方案: mempool_t (内存池)
内存池是解决这个问题的标准内核机制。它的原理是:

  • 在系统启动时(此时可以安全地睡眠), 就预先分配一批固定大小的内存对象。
  • 将这些预分配的对象存放在一个”池子”里。
  • 当驱动程序在不能睡眠的上下文(如中断处理)中需要一个对象时, 它可以从池子中无阻塞地、保证成功地获取一个。
  • 当对象使用完毕后, 再将其归还到池子中, 而不是立即释放给系统。

3. dmaengine_init_unmap_pool 的工作流程:
此函数就是建立这一套内存池系统的过程。

  • 定义多种大小的池: unmap_pool[]数组定义了多个不同大小的池。例如, __UNMAP_POOL(2)用于存储需要unmap两个地址的简单传输(如一个源, 一个目的)。而__UNMAP_POOL(128)则用于需要unmap 128个地址的复杂”分散-聚集”(scatter-gather)传输, 这在RAID计算中很常见。这种分池策略避免了为简单任务分配过大的元数据结构, 从而节省了内存。
  • 创建SLAB缓存(kmem_cache): 在循环中, 对于每一种池大小, 它首先调用kmem_cache_createkmem_cache是Linux SLAB/SLUB分配器的核心, 它是一个专门用来高效分配和释放大量同尺寸对象的高速缓存。这本身就是一种性能优化。
  • 创建内存池(mempool_t): 然后, 它在创建好的kmem_cache之上, 调用mempool_create_slab_pool来创建真正的内存池。这个内存池会从SLAB缓存中预取一批对象备用。
  • 错误处理: 函数包含了健壮的错误处理。如果在创建任何一个池的过程中失败, 它会调用dmaengine_destroy_unmap_pool()(未在此片段中显示)来清理所有已经成功创建的池, 确保系统不会处于一个不一致的状态。

dma_bus_init: DMA总线初始化

这是一个更高层的初始化函数, 它编排了dmaengine子系统的基础设置。

  • 它首先调用dmaengine_init_unmap_pool()来确保内存池被优先创建, 因为它们是dmaengine后续所有操作的基础。
  • 如果内存池创建成功, 它会调用class_register(&dma_devclass)。这个函数会在/sys文件系统中创建/sys/class/dma/。之后, 当具体的DMA控制器驱动(如STM32的DMA驱动)注册其设备时, 对应的设备节点就会出现在这个目录下, 例如/sys/class/dma/dma0chan1
  • 最后, 它初始化dmaenginedebugfs接口, 为调试提供便利。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/* dmaengine_unmap_pool: 描述一个用于DMA unmap操作的内存池 */
struct dmaengine_unmap_pool {
struct kmem_cache *cache; // 指向底层的SLAB缓存
const char *name; // 池的名称, 用于调试
mempool_t *pool; // 指向内存池的指针
size_t size; // 池中对象能容纳的dma_addr_t的数量
};

/* __UNMAP_POOL: 一个便捷宏, 用于在数组中初始化一个池的定义. */
#define __UNMAP_POOL(x) { .size = x, .name = "dmaengine-unmap-" __stringify(x) }

/* unmap_pool: 静态定义的内存池数组. */
static struct dmaengine_unmap_pool unmap_pool[] = {
__UNMAP_POOL(2), // 一个用于简单传输(最多2个地址)的池
#if IS_ENABLED(CONFIG_DMA_ENGINE_RAID) // 仅当内核配置了RAID支持时
__UNMAP_POOL(16), // 用于更复杂传输的池
__UNMAP_POOL(128),
__UNMAP_POOL(256),
#endif
};

/* dmaengine_init_unmap_pool: 初始化unmap内存池 */
static int __init dmaengine_init_unmap_pool(void)
{
int i;

/* 遍历所有定义好的池. */
for (i = 0; i < ARRAY_SIZE(unmap_pool); i++) {
struct dmaengine_unmap_pool *p = &unmap_pool[i];
size_t size;

/* 计算池中每个对象实际需要的内存大小. */
size = sizeof(struct dmaengine_unmap_data) +
sizeof(dma_addr_t) * p->size;

/* 步骤1: 创建一个SLAB缓存, 用于高效分配/释放这种大小的对象. */
p->cache = kmem_cache_create(p->name, size, 0,
SLAB_HWCACHE_ALIGN, NULL);
if (!p->cache)
break; // 如果创建失败, 退出循环.

/* 步骤2: 在SLAB缓存之上创建一个内存池, 预分配至少1个对象. */
p->pool = mempool_create_slab_pool(1, p->cache);
if (!p->pool)
break; // 如果创建失败, 退出循环.
}

/* 如果所有池都成功创建. */
if (i == ARRAY_SIZE(unmap_pool))
return 0; // 返回成功.

/* 如果循环中途失败, 清理所有已创建的池. */
dmaengine_destroy_unmap_pool();
return -ENOMEM; // 返回内存不足错误.
}

/* dma_bus_init: DMA总线初始化函数 */
static int __init dma_bus_init(void)
{
int err = dmaengine_init_unmap_pool(); // 首先初始化内存池.

if (err)
return err;

/* 注册 "dma" 设备类, 在 /sys/class/ 下创建 "dma" 目录. */
err = class_register(&dma_devclass);
if (!err)
dmaengine_debugfs_init(); // 如果类注册成功, 初始化debugfs接口.

return err;
}
/* 使用 arch_initcall 确保此函数在内核启动早期被调用. */
arch_initcall(dma_bus_init);

dma_async_tx_descriptor_init: 初始化DMA传输描述符

此函数是通用DMA引擎(DMA Engine)框架中的一个基础”构造函数”。它的核心原理是为一个新创建的DMA传输描述符 (dma_async_tx_descriptor) 执行最基本、最必要的初始化步骤, 主要是将其与它所属的DMA通道 (dma_chan) 牢固地关联起来

可以把它看作是给一张空白的DMA任务单盖上”所属部门”的印章。在执行这个操作之前, 描述符只是一块内存; 执行之后, 它就正式成为了某个特定DMA通道的一个待处理任务。

这是一个非常轻量级的函数, 但它的作用至关重要, 因为描述符与通道的关联是后续所有DMA操作(提交、启动、查询状态)的基础。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/*
* dma_async_tx_descriptor_init - 初始化一个 dma_async_tx_descriptor
* @tx: 要初始化的传输描述符
* @chan: 该描述符所属的DMA通道
*/
void dma_async_tx_descriptor_init(struct dma_async_tx_descriptor *tx,
struct dma_chan *chan)
{
/*
* 1. 核心操作: 将描述符的 `chan` 成员指向它所属的通道.
* 这就在"任务单"(tx)和"执行者"(chan)之间建立了一个直接的链接.
* 之后, 任何拿到这个描述符的代码都可以通过 `tx->chan` 快速找到控制它的DMA通道.
*/
tx->chan = chan;
/*
* 2. 条件性初始化锁.
* #ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH 是一个预处理指令,
* 意味着下面的代码只有在内核编译时配置了 CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH 选项时才会被包含.
* 这个配置用于支持一种高级功能, 即一个准备好的DMA任务可以在不同的通道之间切换.
* 为了在多核系统上安全地进行这种切换, 描述符本身需要一个锁来保护其内部状态.
* 对于大多数嵌入式系统(包括典型的STM32H750应用场景), 这个功能通常是关闭的,
* 因此这部分代码通常不会被编译, 函数的执行体就只有上面的一行赋值语句.
*/
#ifdef CONFIG_ASYNC_TX_ENABLE_CHANNEL_SWITCH
spin_lock_init(&tx->lock);
#endif
}
/*
* 将函数导出, 使其对其他内核模块可用.
*/
EXPORT_SYMBOL(dma_async_tx_descriptor_init);

dma_list_mutexdma_device/dma_chan/dma_device_list: 全局可见性与注册注销一致性总结

这次讨论的核心结论是:dma_list_mutex 在 DMAengine 中并不是“只保护 dma_device_list 这个链表”,而是用于维护DMAengine 核心全局状态与其不变量的一把统一互斥锁。它保障注册/注销/重平衡等路径对外呈现的状态切换是原子且一致的。


dma_device_list 的语义

  • dma_device_list 表示 DMAengine 框架中全局已注册 DMA 控制器集合
  • 一旦某个 struct dma_device 进入该集合,它及其下属资源就进入“框架全局可见、可枚举、可分配”的状态。

struct dma_devicestruct dma_chan 的关系与“全局性”

  • struct dma_device 是 DMAengine 对一个 DMA 控制器的抽象,它包含一个 channels 链表,链表元素是 struct dma_chan

  • struct dma_chan 虽然从属某个 dma_device,但在框架语义上属于全局可见资源的一部分,因为:

    1. 框架会全局枚举通道用于分配与匹配;
    2. 框架维护全局索引结构(如通道表/重平衡逻辑)会读取并依赖通道状态;
    3. sysfs/debugfs 通过 dma_chan_dev 将通道暴露到设备模型,外部路径可通过该入口间接触达通道。

因此,“通道不是独立的全局链表节点”不代表它不是全局可见状态;它的可见性与分配资格仍是 DMAengine 核心必须一致管理的全局属性。


dma_list_mutex 保护的对象

dma_list_mutex 保护的不仅是某一个变量,而是以下共享状态及其一致性关系

  1. 全局设备集合与全局索引

    • dma_device_list
    • dma_ida(设备 dev_id 分配器)
    • 与通道分配相关的全局表/索引(例如重平衡涉及的结构)
  2. 设备/通道的“可见性切换点”

    • device->chancnt 等与通道数量相关的框架判断依据
    • chan->dev->chan 等“通过 device model 入口能否访问到通道”的关键指针

锁的本质目的是:保证注册/注销/重平衡路径看到的状态是自洽的,而不是看到“半注册/半注销”的中间态。


注册 vs 注销:为什么有的地方你看到没加锁、有的地方加了锁

DMAengine 的典型组织方式是把生命周期分成两段:

  1. 构建阶段(construction)

    • 仅初始化对象内部字段
    • 对外不可见或不会被全局分配/枚举路径触达
    • 因此某些字段赋值(如 chan->dev->chan = chan)可能不需要立刻拿 dma_list_mutex
  2. 发布/撤销发布阶段(publish/unpublish)

    • 把对象加入/移出全局集合或全局索引
    • 切换可见性入口(例如断开 chan->dev->chan
    • 必须在 dma_list_mutex 下完成关键切换,以防并发路径获取新引用或观察到中间态

注销路径里,__dma_async_device_channel_unregister() 的核心就是在锁下完成“撤销发布”,然后把 device_unregister()free_percpu() 这类重操作放到锁外,降低锁持有时间并避免锁顺序问题。

channel_table[cap][cpu]->chan: DMAengine 通道选择缓存、硬件通道注册与“置 NULL 是否影响正在 DMA”

1) channel_table[cap][cpu]->chan 是什么,有什么作用

  1. 对象类型与语义
    channel_table[cap][cpu]->chan 保存的是一个 struct dma_chan * 指针,其语义是:
    针对能力 cap、CPU cpu 的“推荐通道缓存指针”
    它不是数量,不是硬件寄存器配置,也不是“CPU 固定绑定某硬件通道”的规则表。

  2. 为什么是 per-CPU
    per_cpu_ptr(channel_table[cap], cpu) 取得的是该能力维度表在某个 CPU 上的实例,目的是在多 CPU 场景下减少共享争用并引入“邻近性偏好”。单核系统仍沿用该抽象接口,但语义不变。

  3. 它的作用范围
    channel_table 的作用仅限于 “通道选择/分配阶段”

    • 快速给出一个候选 dma_chan
    • 避免每次分配都遍历 dma_device_listdevice->channels 做全量扫描
      因此它是性能优化缓存,不是执行路径的必需结构。

2) 为什么要先清空再重新平衡(重建推荐通道)

你看到的两行代码体现了一个明确的缓存维护模式:失效(invalidate)→ 重建(rebuild)

  1. 先清空的目的
    per_cpu_ptr(channel_table[cap], cpu)->chan = NULL; 的目的不是“停止 DMA”,而是:

    • 宣告旧推荐结果无效
    • 避免重平衡过程中旧指针被继续当作可靠候选
    • 让并发路径在看到 NULL 时走慢路径重新选择,而不是继续使用旧指针
  2. 再写回新值的目的
    per_cpu_ptr(channel_table[cap], cpu)->chan = chan; 将重平衡算法选出的“当前最优”通道写回缓存,使之后同类请求回到快路径。

1
2
3
4
5
6
7
8
9
/*
* 使 [cap][cpu] 的推荐通道缓存失效,避免旧指针在重平衡期间被误用
*/
per_cpu_ptr(channel_table[cap], cpu)->chan = NULL;

/*
* 将重平衡计算得到的最优通道写回缓存,用于后续快路径通道选择
*/
per_cpu_ptr(channel_table[cap], cpu)->chan = chan;

3) 清空 channel_table 会不会把正在 DMA 的通道“断掉”

不会。

  1. 正在执行的 DMA 不依赖 channel_table
    正在执行的 DMA 事务由以下要素维持:

    • 客户端已持有的 struct dma_chan *chan(通道句柄)
    • 已提交的描述符(dma_async_tx_descriptor)与驱动回调(如 device_issue_pending
    • 硬件寄存器与 DMA 状态机
  2. channel_table 只影响未来“选通道”
    将缓存置为 NULL 只会导致:

    • 下次通道选择时缓存未命中
    • 走慢路径重新扫描/重新选择
      它不会撤销通道引用,不会改硬件寄存器,也不会终止当前搬运。
  3. 补充的严谨点
    硬件负责搬运本身;软件仍负责完成通知、cookie 更新、错误处理、链式推进等,但这些完成路径走 chan 的中断/回调链路,而不是 channel_table


4) DMA 硬件通道是如何在 DMAengine 中“注册并被使用”的

  1. 硬件规模的输入
    设备树中的 dma-channels(或等价属性)描述的是该 DMA 控制器实例的物理通道数量上限

  2. 驱动 probe 时创建软件通道对象
    控制器驱动在 probe 中会根据 nr_channels 初始化对应数量的 struct dma_chan(通常组织在 device->channels 链表中,并且注册后该链表结构一般不再修改)。

  3. 注册到 DMAengine
    驱动通过注册接口把 struct dma_device 暴露给 DMAengine 核心,使得:

    • dma_device 进入全局可见集合(如 dma_device_list
    • channels 进入全局可枚举/可选择资源图
  4. 客户端使用方式
    客户端不是通过 channel_table 来“执行 DMA”,而是通过框架 API:

    • 先“申请/获得”一个 dma_chan *(可能命中缓存,也可能全量扫描)
    • 然后在该 chan 上准备描述符、提交、issue_pending
    • 完成由中断/轮询推进
  5. channel_table 在其中的定位
    它仅用于“获得通道时的快速候选”,并不参与“已获得通道后的传输执行”。


__dma_async_device_channel_register: 注册单个DMA通道并创建其sysfs接口

此函数是dma_async_device_register内部使用的一个核心辅助函数。它的作用是将一个由DMA控制器驱动程序定义的、代表硬件DMA通道的struct dma_chan实例, 完全地、正式地注册到内核中

其核心原理是为这个抽象的DMA通道创建一个具体的软件实体, 使其对内核的其他部分(特别是设备模型和sysfs)可见。这个过程包括三个关键步骤:

  1. 资源分配: 为通道分配必要的软件资源, 包括用于存储运行时状态的per-CPU数据(chan->local)和一个用于在sysfs中表示该通道的struct dma_channel_dev结构(chan->dev)。
  2. 身份分配: 调用IDA(ID Allocator)机制, 从其父DMA设备的ID池中为该通道分配一个唯一的、局部的通道ID号。
  3. Sysfs注册: 填充一个标准的struct device结构, 设置其类别为dma, 父设备为DMA控制器设备, 并根据设备ID和通道ID构建一个唯一的名称(例如dma0chan1)。最后, 调用device_register将这个设备正式注册到内核的设备模型中, 这会在/sys/class/dma/目录下创建一个对应的条目。

这个函数通过goto语句实现了非常健壮的错误处理流程。如果在注册过程中的任何一步失败, 它都会精确地回滚(undo)所有已经成功执行的步骤, 例如释放已分配的ID和内存, 确保系统不会因部分失败的注册而遗留任何悬空资源。在STM32H750这样的单核系统上, alloc_percpu虽然是为多核设计的, 但它会优雅地退化为只分配一个实例, 保持了代码的可移植性和一致性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
/*
* 静态函数声明: __dma_async_device_channel_register
* 这是 dma_async_device_register 的一个内部辅助函数.
* @device: 指向父 dma_device 设备的指针.
* @chan: 指向需要被注册的 dma_chan 结构体的指针.
* @name: 一个可选的、用于sysfs的自定义名称.
* @return: 成功时返回 0, 失败时返回负的错误码.
*/
static int __dma_async_device_channel_register(struct dma_device *device,
struct dma_chan *chan,
const char *name)
{
int rc;

/*
* 为通道的 'local' 成员分配 per-CPU 内存.
* per-CPU 变量用于存储每个CPU核心私有的数据, 以避免在多核系统上加锁.
* 在 STM32H750 这样的单核系统上, 这等效于分配一个单独的实例.
*/
chan->local = alloc_percpu(typeof(*chan->local));
if (!chan->local)
return -ENOMEM;
/*
* 为 chan->dev 分配内存, chan->dev (类型为 dma_channel_dev) 主要用于在sysfs中表示这个通道.
* kzalloc 会分配内存并将其内容清零.
*/
chan->dev = kzalloc(sizeof(*chan->dev), GFP_KERNEL);
if (!chan->dev) {
rc = -ENOMEM;
goto err_free_local; // 如果失败, 跳转到清理 per-CPU 内存的标签.
}

/*
* 调用 ida_alloc 从父设备 'device' 的通道ID分配器 (chan_ida) 中获取一个唯一的ID号.
* IDA (ID Allocator) 是内核中用于动态分配0到INT_MAX范围内唯一ID的机制.
*/
chan->chan_id = ida_alloc(&device->chan_ida, GFP_KERNEL);
if (chan->chan_id < 0) {
pr_err("%s: unable to alloc ida for chan: %d\n",
__func__, chan->chan_id);
rc = chan->chan_id; // 保存错误码.
goto err_free_dev; // 如果分配失败, 跳转到清理 chan->dev 的标签.
}

/*
* 开始填充用于在sysfs中注册的 device 结构体.
* 设置其设备类别为 dma_devclass, 这会使它出现在 /sys/class/dma/ 目录下.
*/
chan->dev->device.class = &dma_devclass;
/*
* 设置其父设备为 DMA 控制器设备本身.
*/
chan->dev->device.parent = device->dev;
/*
* 在 sysfs 设备和 dma_chan 结构之间建立一个反向链接.
*/
chan->dev->chan = chan;
/*
* 记录父DMA设备的ID.
*/
chan->dev->dev_id = device->dev_id;
/*
* 如果调用者没有提供自定义名称.
*/
if (!name)
/*
* 使用父设备ID和通道ID构建一个标准的名称, 例如 "dma0chan1".
*/
dev_set_name(&chan->dev->device, "dma%dchan%d", device->dev_id, chan->chan_id);
else
/*
* 否则, 使用提供的自定义名称.
*/
dev_set_name(&chan->dev->device, "%s", name);
/*
* 调用 device_register() 将这个新构建的设备正式注册到内核的设备模型中.
* 执行成功后, 在 sysfs 中就会出现对应的条目.
*/
rc = device_register(&chan->dev->device);
if (rc)
goto err_out_ida; // 如果注册失败, 跳转到释放ID的标签.

/*
* 初始化客户端计数器.
*/
chan->client_count = 0;
/*
* 父设备的总通道数加一.
*/
device->chancnt++;

/*
* 成功返回.
*/
return 0;

/*
* 错误处理回滚路径.
*/
err_out_ida:
/*
* 释放刚刚从IDA中分配的ID, 将其归还到池中.
*/
ida_free(&device->chan_ida, chan->chan_id);
err_free_dev:
/*
* 释放为 chan->dev 分配的内存.
*/
kfree(chan->dev);
err_free_local:
/*
* 释放为 chan->local 分配的 per-CPU 内存.
*/
free_percpu(chan->local);
/*
* 将 local 指针置为 NULL, 防止悬空指针.
*/
chan->local = NULL;
/*
* 返回导致失败的错误码.
*/
return rc;
}

min_chan: 在指定能力与 CPU 邻近性约束下,选择 table_count 最小的通道并更新其计数

它的算法语义可以精确概括为:

  • dma_device_list 的所有 dma_device 中,筛选出:

    • 具备目标能力 cap
    • 且不带 DMA_PRIVATE(表示不可用于公共分配/正在拆除或仅私用)
  • 在每个 device->channels 中,筛选出 client_count != 0 的通道(即“已被至少一个客户端引用/占用”的通道)

  • 从这些候选通道中选出 table_count 最小者作为 min

  • 若存在“NUMA/CPU 邻近”的候选通道,则优先在邻近集合中选 table_count 最小者作为 localmin

  • 最终选 localmin(若存在)否则选 min,并对其 table_count++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
* @brief 在匹配能力与 CPU 邻近性条件下,选择 table_count 最小的 DMA 通道
*
* 该函数遍历 dma_device_list,并在符合能力要求且非 DMA_PRIVATE 的设备中,
* 选择一个已被使用(client_count != 0)的通道;优先选择与给定 cpu 邻近的通道集合,
* 在集合内按 table_count 最小原则选取;若邻近集合为空,则在全局候选集合中按 table_count 最小选取。
*
* 重要约束:
* - 必须在 dma_list_mutex 互斥保护下调用,以保证:
* 1) dma_device_list 遍历期间设备集合不发生并发注册/注销变化;
* 2) cap_mask / DMA_PRIVATE 状态检查与设备可用性保持一致;
* 3) 对 chan->table_count 的读-改-写更新不发生并发竞争;
* 4) 与通道表重建/重平衡等全局逻辑保持一致的观察边界。
*
* @param cap 需要匹配的 DMA 事务能力类型
* @param cpu 期望通道“靠近”的 CPU 编号(用于邻近性选择)
*
* @return 返回选中的通道指针;若不存在候选则返回 NULL
*/
static struct dma_chan *min_chan(enum dma_transaction_type cap, int cpu)
{
/* 用于遍历全局 DMA 设备链表的迭代变量 */
struct dma_device *device;

/* 用于遍历设备下通道链表的迭代变量 */
struct dma_chan *chan;

/* 全局候选集合中 table_count 最小的通道 */
struct dma_chan *min = NULL;

/* 邻近集合中 table_count 最小的通道 */
struct dma_chan *localmin = NULL;

/**
* 遍历 DMAengine 全局设备链表。
* 该遍历假设在 dma_list_mutex 保护下进行,避免与并发注册/注销交错。
*/
list_for_each_entry(device, &dma_device_list, global_node) {

/**
* 能力筛选与可用性筛选:
* - 不具备 cap 能力的设备直接跳过
* - 带 DMA_PRIVATE 标志的设备直接跳过(表示不可参与公共分配/正在拆除等策略含义)
*/
if (!dma_has_cap(cap, device->cap_mask) ||
dma_has_cap(DMA_PRIVATE, device->cap_mask))
continue;

/**
* 遍历该设备的通道链表。
* 该链表结构在设备注册完成后通常不再改变,但其通道字段会在运行期变化,
* 因此仍需要在统一锁边界下进行“选择 + 计数更新”的一致性维护。
*/
list_for_each_entry(chan, &device->channels, device_node) {

/**
* 跳过未被任何客户端使用的通道。
* 该逻辑决定了本函数只在“已被使用”的通道集合上进行最小计数选择。
*/
if (!chan->client_count)
continue;

/**
* 更新全局最小 table_count 候选。
* table_count 的语义是“该通道在 mem-to-mem 分配表中出现的次数”,
* 用于做负载均衡式的选择倾向。
*/
if (!min || chan->table_count < min->table_count)
min = chan;

/**
* 若该通道与 cpu 邻近,则参与邻近集合的最小 table_count 选择。
* 邻近性判断由 dma_chan_is_local() 给出。
*/
if (dma_chan_is_local(chan, cpu))
if (!localmin ||
chan->table_count < localmin->table_count)
localmin = chan;
}
}

/**
* 若存在邻近候选,则优先选择邻近集合中的最小者;
* 否则选择全局集合中的最小者。
*/
chan = localmin ? localmin : min;

/**
* 若选中了通道,则将其 table_count 增加 1。
* 该更新必须与选择过程处于同一互斥边界,否则会出现并发调用导致的丢更新与不公平选择。
*/
if (chan)
chan->table_count++;

/* 返回最终选择的通道指针 */
return chan;
}

dma_channel_rebalance: 重新平衡并分配全局DMA通道

此函数是Linux DMA引擎框架的核心调度器和负载均衡器。它的主要作用是构建和更新一个名为channel_table的全局快速查找表。这个表的存在, 使得当一个客户端驱动请求一个特定功能(如内存拷贝)的DMA通道时, 内核可以立即为其提供一个当前最优的选择, 而无需在每次请求时都去遍历系统中所有已注册的DMA控制器和通道。

该函数的原理可以概括为**”先拆毁, 再重建”**的负载均衡策略:

  1. 完全重置 (Teardown): 函数首先进入一个”拆毁”阶段, 为即将进行的重新分配清理环境。

    • 它遍历整个channel_table, 将其中每一个条目都清空为NULL。这确保了旧的、可能已过时的分配关系被完全抹除。
    • 它遍历系统中所有公共的(非DMA_PRIVATE)DMA通道, 将它们各自的table_count(一个用于衡量该通道被分配了多少次任务的”负载计数器”)清零。
  2. 优化检查: 在重建之前, 它会检查一个全局引用计数dmaengine_ref_count。如果这个计数为零, 意味着当前系统中没有任何客户端驱动正在使用或等待DMA通道。在这种情况下, 填充查找表是毫无意义的, 函数会提前退出, 这是一个重要的性能优化。

  3. 重新分配 (Rebuild): 这是函数的核心逻辑。它会系统地、从头开始地重建channel_table

    • 它遍历所有可能的DMA能力(cap, 如DMA_MEMCPY, DMA_XOR, DMA_CYCLIC等)。
    • 对于每一种能力, 它会遍历所有当前在线的CPU核心(cpu)。
    • 在循环的内部, 它调用一个关键的辅助函数min_chan(cap, cpu)。这个函数是负载均衡算法的实现者, 它会搜索整个系统中所有可用的公共DMA通道, 找出那个**支持当前能力(cap)并且当前负载最低(table_count最小)**的通道。
    • 最后, 它将min_chan找到的最优通道, 填入channel_table[cap][cpu]这个槽位中。

对于用户指定的STM32H750(单核, 非SMP)架构, 此函数的行为会相应地简化和调整:

  • 所有for_each_*_cpu循环只会迭代一次, cpu的值始终为0。
  • 函数的目标从文档注释中描述的”CPU隔离”转变为**”操作隔离”。这意味着, dma_channel_rebalance会尝试为每一种不同类型的DMA操作分配一个不同的、专用的DMA通道**(如果硬件资源允许的话)。例如, 它可能会将dma0chan1分配给DMA_MEMCPY操作(channel_table[DMA_MEMCPY][0] = &dma0chan1), 同时将dma0chan2分配给DMA_CYCLIC操作(channel_table[DMA_CYCLIC][0] = &dma0chan2)。这样做可以避免让同一个硬件通道在不同类型的任务之间频繁切换上下文, 从而简化驱动逻辑并可能提高性能。

此函数必须在dma_list_mutex锁的保护下调用, 因为它读取和修改了多个全局共享的数据结构(channel_table, dma_device_list等), 这个锁确保了整个”再平衡”操作的原子性和线程安全性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/**
* dma_channel_rebalance - 重新分配可用的通道
*
* 在SMP(多核)情况下, 优化CPU隔离(每个CPU为一种操作类型获取一个专用通道).
* 在非SMP(单核)情况下, 优化操作隔离(避免让通道执行多种任务).
*
* 必须在 dma_list_mutex 锁的保护下调用.
*/
static void dma_channel_rebalance(void)
{
struct dma_chan *chan;
struct dma_device *device;
int cpu;
int cap;

/*
* 第一阶段: 撤销上一次的分配结果 (拆毁)
*/

/* 遍历所有可能的DMA能力(capability) */
for_each_dma_cap_mask(cap, dma_cap_mask_all)
/* 遍历所有可能的CPU核心 */
for_each_possible_cpu(cpu)
/*
* 将全局查找表 channel_table 中对应 [能力][CPU] 的条目清空.
* per_cpu_ptr 用于获取 per-CPU 变量的指针.
*/
per_cpu_ptr(channel_table[cap], cpu)->chan = NULL;

/* 遍历全局 dma_device_list 链表中的每一个已注册的DMA设备 */
list_for_each_entry(device, &dma_device_list, global_node) {
/* 跳过标记为私有的DMA设备, 它们不参与全局平衡 */
if (dma_has_cap(DMA_PRIVATE, device->cap_mask))
continue;
/* 遍历该设备的每一个通道 */
list_for_each_entry(chan, &device->channels, device_node)
/* 将每个通道的"负载计数器" (table_count) 清零 */
chan->table_count = 0;
}

/*
* 第二阶段: 优化检查
*/

/* 如果没有任何客户端(驱动)正在使用或等待DMA引擎, 就没有必要填充查找表 */
if (!dmaengine_ref_count)
return;

/*
* 第三阶段: 重新分配可用的通道 (重建)
*/

/* 遍历所有可能的DMA能力 */
for_each_dma_cap_mask(cap, dma_cap_mask_all)
/* 遍历所有当前在线的CPU核心 (对于单核系统, 这个循环只会执行一次, cpu=0) */
for_each_online_cpu(cpu) {
/*
* 调用 min_chan, 这是负载均衡算法的核心.
* 它会查找全系统中支持 'cap' 能力且当前负载最低的通道.
*/
chan = min_chan(cap, cpu);
/*
* 将找到的最优通道 'chan' 填充到全局查找表的 [能力][CPU] 条目中.
*/
per_cpu_ptr(channel_table[cap], cpu)->chan = chan;
}
}

dma_async_device_register: 注册一个DMA控制器设备

此函数是Linux内核DMA引擎(DMA Engine)框架的核心入口点。当一个DMA控制器的硬件驱动(例如STM32H750的DMA或MDMA驱动)在探测(probe)过程中完成了自身的初始化后, 就会调用此函数, 将其所代表的DMA控制器及其所有的通道(channels)正式地、完整地注册到内核中。完成此调用后, 该DMA控制器就对系统的其他部分(即”客户端”驱动, 如SPI, I2C, Crypto等)变得可见、可发现和可使用

其核心原理是一个全面的验证、初始化和集成过程, 确保只有功能完整、行为正确的DMA驱动才能被系统接受:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* dma_async_device_register - 注册发现的DMA设备
* @device: 指向 &struct dma_device 的指针
*
* 调用此例程后, 除了在 device_release() 回调中, 不应释放该结构体.
* device_release() 将在 dma_async_device_unregister() 被调用且没有其他引用后被调用.
*/
int dma_async_device_register(struct dma_device *device)
{
int rc;
struct dma_chan* chan;

if (!device)
return -ENODEV; // 如果设备指针为空, 返回"无此设备"错误

/* 验证设备例程 */
if (!device->dev) {
pr_err("DMAdevice must have dev\n"); // 设备必须有关联的 struct device
return -EIO;
}

// 从驱动程序中获取模块所有者信息, 用于引用计数
device->owner = device->dev->driver->owner;

/*
* 定义一个宏, 用于检查: 如果设备声明了某个能力(capability),
* 那么它必须提供实现该能力的函数指针.
*/
#define CHECK_CAP(_name, _type) \
{ \
if (dma_has_cap(_type, device->cap_mask) && !device->device_prep_##_name) { \
dev_err(device->dev, \
"Device claims capability %s, but op is not defined\n", \
__stringify(_type)); /* __stringify将宏参数转为字符串 */ \
return -EIO; \
} \
}

// 使用宏对各种DMA能力进行检查
CHECK_CAP(dma_memcpy, DMA_MEMCPY);
CHECK_CAP(dma_xor, DMA_XOR);
CHECK_CAP(dma_xor_val, DMA_XOR_VAL);
CHECK_CAP(dma_pq, DMA_PQ);
CHECK_CAP(dma_pq_val, DMA_PQ_VAL);
CHECK_CAP(dma_memset, DMA_MEMSET);
CHECK_CAP(dma_interrupt, DMA_INTERRUPT);
CHECK_CAP(dma_cyclic, DMA_CYCLIC);
CHECK_CAP(interleaved_dma, DMA_INTERLEAVE);

#undef CHECK_CAP // 检查完毕, 取消宏定义

// 检查其他必需的函数指针是否存在
if (!device->device_tx_status) {
dev_err(device->dev, "Device tx_status is not defined\n");
return -EIO;
}

if (!device->device_issue_pending) {
dev_err(device->dev, "Device issue_pending is not defined\n");
return -EIO;
}

// device_release 是可选的, 但没有它, 热拔插可能不安全
if (!device->device_release)
dev_dbg(device->dev,
"WARN: Device release is not defined so it is not safe to unbind this driver while in use\n");

// 初始化内核引用计数器
kref_init(&device->ref);

// 如果设备支持所有传输类型, 则给它打上通用的 ASYNC_TX 标志
if (device_has_all_tx_types(device))
dma_cap_set(DMA_ASYNC_TX, device->cap_mask);

// 为设备获取一个全局唯一的ID号
rc = get_dma_id(device);
if (rc != 0)
return rc;

// 初始化用于分配通道ID的IDA (ID Allocator)
ida_init(&device->chan_ida);

// 遍历设备的所有通道, 并为它们在sysfs中创建条目
list_for_each_entry(chan, &device->channels, device_node) {
rc = __dma_async_device_channel_register(device, chan, NULL);
if (rc < 0)
goto err_out; // 如果失败, 跳转到错误清理
}

// 锁定全局DMA列表互斥锁, 准备修改全局列表
mutex_lock(&dma_list_mutex);
/* 如果有等待的客户端, 需要为公共通道增加引用计数 */
if (dmaengine_ref_count && !dma_has_cap(DMA_PRIVATE, device->cap_mask))
list_for_each_entry(chan, &device->channels, device_node) {
/* 如果有客户端已经在等待这个通道, 我们需要代表它们获取引用 */
if (dma_chan_get(chan) == -ENODEV) {
rc = -ENODEV;
mutex_unlock(&dma_list_mutex);
goto err_out; // 如果获取失败, 说明有问题, 清理退出
}
}
// 使用RCU安全地将设备添加到全局DMA设备链表的尾部
list_add_tail_rcu(&device->global_node, &dma_device_list);
// 如果设备是私有的, 增加私有设备计数
if (dma_has_cap(DMA_PRIVATE, device->cap_mask))
device->privatecnt++;
// 触发通道再平衡, 尝试将新通道分配给等待的客户端
dma_channel_rebalance();
// 解锁
mutex_unlock(&dma_list_mutex);

// 为设备注册DebugFS接口
dmaengine_debug_register(device);

return 0; // 成功

err_out: // 错误清理路径
// 如果没有任何通道被成功注册, 只需释放设备ID
if (!device->chancnt) {
ida_free(&dma_ida, device->dev_id);
return rc;
}

// 遍历所有通道, 撤销它们的注册
list_for_each_entry(chan, &device->channels, device_node) {
if (chan->local == NULL)
continue;
mutex_lock(&dma_list_mutex);
chan->dev->chan = NULL; // 断开sysfs设备与通道的连接
mutex_unlock(&dma_list_mutex);
device_unregister(&chan->dev->device); // 注销sysfs设备
free_percpu(chan->local); // 释放per-cpu数据
}
return rc; // 返回错误码
}
EXPORT_SYMBOL(dma_async_device_register);

__dma_async_device_channel_unregister: 从 DMAengine 注销单个通道并回收其内核对象

此函数是DMAengine 核心在注销 DMA 设备时,用于逐个撤销通道对外可见性与回收通道对象资源的内部函数。它由 dma_async_device_unregister() 在遍历 device->channels 时调用。

其核心原理是:把一个已注册的 dma_chan 从“框架可分配/可引用”的状态转换为“不可见/不可再引用”,并释放与该通道绑定的 ID、device model 对象以及 percpu 私有数据

  1. 快速判定是否已注册:以 chan->local 是否为 NULL 作为“该通道是否已经完成注册”的判据;若未注册则直接返回,避免重复注销。
  2. 引用安全性告警:若通道仍存在 client_count 引用且驱动未提供 device_release 路径,则触发一次告警,提示存在“注销时仍被客户端持有引用”的风险。
  3. 受互斥保护的状态更新:在 dma_list_mutex 保护下更新通道计数与通道指针,使框架视角下该通道立刻不可再被访问到。
  4. 回收全局/设备级 ID 与内核对象:释放 chan_ida 分配的 chan_id,注销 device 对象,并释放 percpuchan->local
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* @brief 注销一个 DMA 通道并释放其框架资源
*
* @param device DMA 设备对象,持有该通道的全局管理信息与通道 ID 分配器
* @param chan 待注销的 DMA 通道对象
*/
static void __dma_async_device_channel_unregister(struct dma_device *device,
struct dma_chan *chan)
{
/**
* chan->local 通常指向该通道的 percpu 私有数据。
*
* 这里以 chan->local == NULL 作为“通道未完成注册或已被注销”的判据:
* - 避免重复注销导致二次释放
* - 使注销流程具备幂等性(对未注册对象无副作用)
*/
if (chan->local == NULL)
return;

/**
* 若驱动未提供 device_release,并且仍有客户端持有通道引用,则告警。
*
* 这里的 client_count 表示框架层面仍存在对该通道的外部引用,
* 在注销阶段出现非零通常意味着调用时机或引用管理存在问题。
*
* WARN_ONCE 只告警一次,避免在循环注销中刷屏。
*/
WARN_ONCE(!device->device_release && chan->client_count,
"%s called while %d clients hold a reference\n",
__func__, chan->client_count);

/**
* 进入 dma_list_mutex 互斥区,保证框架全局/共享状态更新的原子性。
*
* 该互斥锁用于保护 DMAengine 中与“设备/通道可见性”相关的共享数据,
* 防止与并发的注册、分配、重平衡、引用获取路径发生竞态。
*/
mutex_lock(&dma_list_mutex);

/**
* device->chancnt 表示该 dma_device 当前已注册的通道数量。
* 注销一个通道时必须递减,且需要与其他并发路径一致地更新。
*/
device->chancnt--;

/**
* 断开通道设备对象与 dma_chan 的反向关联。
*
* chan->dev 是 DMA 通道对应的“设备模型对象”(device model wrapper),
* chan->dev->chan 指向实际的 dma_chan。
*
* 将其置空的意义是:
* - 立即阻止通过该 device model 路径继续访问 dma_chan
* - 在注销过程中建立“不可再被发现”的条件
*/
chan->dev->chan = NULL;

/**
* 退出互斥区。
*
* 注意:互斥区内只做最关键的共享状态更新,
* 后续的 device_unregister/free_percpu 可能触发更复杂的路径,
* 放在锁外可避免锁持有时间过长与潜在锁顺序问题。
*/
mutex_unlock(&dma_list_mutex);

/**
* 释放该通道占用的通道 ID。
*
* device->chan_ida 是该 DMA 设备内部的 ID 分配器,
* chan->chan_id 是注册阶段分配的唯一通道编号。
*/
ida_free(&device->chan_ida, chan->chan_id);

/**
* 注销通道对应的 device model 对象。
*
* device_unregister 会把该 device 从内核设备模型中移除,
* 并触发相应的引用计数与释放路径,使该通道不再对外可见。
*/
device_unregister(&chan->dev->device);

/**
* 释放通道的 percpu 私有数据。
*
* 该释放必须发生在“框架已不可见”的条件建立之后,
* 否则可能出现并发路径仍访问 chan->local 导致 use-after-free。
*/
free_percpu(chan->local);
}

为什么这里需要 mutex_lock(&dma_list_mutex);

原因不是“多核才需要锁”,而是存在并发访问共享状态的可能性,即使在单核系统也一样成立(抢占、软中断、工作队列、线程调度都会造成并发交错)。

这把锁在这里至少同时保护了两类“必须一致更新”的共享状态:

  1. device->chancnt 的一致性
    这个计数会被其他路径读取/依赖(例如注册/注销、通道表重构、甚至调试/枚举路径)。若不加锁,可能出现:

    • 并发注销导致计数丢更新(lost update)
    • 并发注册/注销导致计数短暂错误,进而影响框架逻辑判断
  2. chan->dev->chan 的可见性切换
    这是把通道从“可通过 device model 找到”切换为“不可找到”的关键动作。若不加锁,可能出现:

    • 另一路径正通过 chan->dev->chan 获取通道指针并继续使用(在你置空之前/之后交错)
    • 造成“拿到半注销对象”的竞态窗口,进而引发 use-after-free 风险

这也是为什么函数在锁内只做“最关键的共享状态切换”,然后把 device_unregister()free_percpu() 放到锁外:
锁用来建立一致性边界;重资源回收放到边界外执行,减少锁占用并避免锁嵌套风险。

dma_async_device_unregister / dmaenginem_async_device_register: 注销 DMA 设备与 devm 管理式注册回滚

你贴的这段代码来自 DMAengine 核心层,语义很集中:

  • dma_async_device_unregister将一个 struct dma_device 从 DMAengine 全局视角彻底移除,并确保在移除过程中不会再被通道分配路径使用。
  • dmaenginem_async_device_register在完成注册后,挂一个 devm action,保证驱动解绑(detach)时自动触发上面的 unregister,实现“注册/回滚”的资源生命周期绑定。

关键点在于 DMAengine 框架内部的并发一致性与对象生命周期管理。即使单核,也必须用 mutex / 引用计数,因为内核上下文切换与中断上下文会带来并发可见性问题。


核心原理概述

  1. 先拆通道、再拆设备:先把 device->channels 链表里每个通道从框架注销(调用 __dma_async_device_channel_unregister),避免残留通道入口被上层继续引用。

  2. 全局互斥保护关键全局结构dma_list_mutex 保护 DMAengine 的全局设备列表、通道表(channel_table)与 IDA 分配等共享资源。

  3. DMA_PRIVATE 强制“不可再被分配”:注销时先设置 DMA_PRIVATEcap_mask,其目的不是表达硬件能力,而是 让通道选择/重平衡逻辑把该设备视为不可供一般客户端使用,避免被加入 channel_table

  4. 重平衡与回收:调用 dma_channel_rebalance() 刷新全局通道表,然后释放 dev_idida_free),最后 dma_device_put() 递减引用,允许对象最终释放。

  5. devm action 的语义dmaenginem_async_device_register 在注册成功后,通过 devm_add_action_or_reset() 把“反注册动作”绑定到 device->dev 的生命周期:

    • 后续驱动 detach 时自动执行 unregister
    • 若 action 添加失败,会立即执行回滚(reset),避免半注册状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/**
* @brief 从 DMAengine 框架注销一个 DMA 设备
*
* 该函数通常由具体 DMA 控制器驱动在退出路径或解绑路径触发。
* DMAengine 框架会通过模块引用等机制,避免在通道仍被使用时发生非法注销。
*
* @param device 指向待注销的 DMA 设备对象
*/
void dma_async_device_unregister(struct dma_device *device)
{
struct dma_chan *chan;
struct dma_chan *n;

/**
* 注销 debug 侧的记录与接口。
*
* 该调用不应改变设备实际功能状态,但会移除调试设施对该 device 的引用。
*/
dmaengine_debug_unregister(device);

/**
* 遍历 device->channels 链表,逐个注销通道。
*
* 使用 list_for_each_entry_safe 的原因是:
* - __dma_async_device_channel_unregister 可能会把当前 chan 从链表移除
* - safe 版本允许在遍历过程中安全删除元素
*
* 这里的“先通道后设备”是框架生命周期约束:
* - 通道是客户端可见的分配单元
* - 必须先断开通道入口,再拆除设备全局可见性
*/
list_for_each_entry_safe(chan, n, &device->channels, device_node)
__dma_async_device_channel_unregister(device, chan);

/**
* 进入全局互斥区,保护 DMAengine 全局结构一致性。
*
* 即使在单核系统中,内核依然可能在不同上下文交错访问这些全局结构,
* 因此必须使用互斥锁来保证注销过程的原子性与可见性。
*/
mutex_lock(&dma_list_mutex);

/**
* 将 DMA_PRIVATE 置入 capability mask。
*
* 这里的 DMA_PRIVATE 不是“硬件能力”的含义,而是框架层策略标记:
* - 表示该设备正在拆除,不应再被通道选择/分配逻辑纳入 channel_table
* - 防止注销过程中出现新的通道分配到该 device 的竞态
*/
dma_cap_set(DMA_PRIVATE, device->cap_mask);

/**
* 重新平衡 DMA 通道表。
*
* 该步骤会根据当前系统中的 DMA 设备集合与其能力掩码,
* 重建/调整 channel_table 等全局索引结构,使被注销设备不再参与分配。
*/
dma_channel_rebalance();

/**
* 释放该设备在 DMAengine 中分配的 dev_id。
*
* ida_free 释放的是框架全局唯一 ID 资源,避免 ID 泄漏与重用冲突。
*/
ida_free(&dma_ida, device->dev_id);

/**
* 递减设备对象引用计数。
*
* 当引用计数归零时,设备对象及其相关资源才可能被最终释放。
* 该调用必须在全局互斥保护下进行,以保证与其他引用获取路径一致。
*/
dma_device_put(device);

/**
* 退出全局互斥区,完成注销。
*/
mutex_unlock(&dma_list_mutex);
}
EXPORT_SYMBOL(dma_async_device_unregister);

/**
* @brief devm action 的包装回调:用于在设备解绑时自动执行注销
*
* @param device 传入的 action 数据指针,实际类型为 struct dma_device *
*/
static void dmaenginem_async_device_unregister(void *device)
{
dma_async_device_unregister(device);
}

/**
* @brief 以 devm 管理方式注册 DMA 设备,并绑定自动回滚动作
*
* 该函数完成两件事:
* 1) 调用 dma_async_device_register 完成正常注册
* 2) 通过 devm_add_action_or_reset 绑定自动注销回调,确保驱动 detach 时清理干净
*
* @param device 指向待注册的 DMA 设备对象
* @return 成功返回 0,失败返回负错误码
*/
int dmaenginem_async_device_register(struct dma_device *device)
{
int ret;

/**
* 执行 DMA 设备注册。
*
* 注册成功后,该 device 将进入 DMAengine 的全局管理结构,
* 客户端驱动可以通过 capability 匹配与通道分配逻辑使用它。
*/
ret = dma_async_device_register(device);
if (ret)
return ret;

/**
* 将“注销动作”绑定到 device->dev 的 managed 资源列表。
*
* devm_add_action_or_reset 的关键语义:
* - 添加 action 成功:后续 device detach 时自动调用 dmaenginem_async_device_unregister
* - 添加 action 失败:立即执行 dmaenginem_async_device_unregister 完成回滚,避免泄漏
*/
return devm_add_action_or_reset(device->dev,
dmaenginem_async_device_unregister,
device);
}

dma_chan_get: 安全地获取并声明一个DMA通道的所有权

此函数是Linux DMA引擎框架内部的一个核心资源管理函数。它的主要作用是在一个客户端驱动程序正式开始使用一个DMA通道之前, 执行一个多层次的、健壮的资源获取和引用计数流程。它不仅仅是返回一个指针, 而是作为一个”最终的守门员”, 确保与该通道关联的所有底层资源(内核模块、DMA设备、通道特定内存)都已准备就绪, 并且其生命周期已被正确管理。

此函数被设计为在dma_list_mutex锁的保护下调用, 其核心原理是为一个即将被激活的DMA通道, 安全地”上线”其所有依赖资源, 并更新其使用状态

这个过程分为两种主要路径:

  1. 通道已被占用 (共享路径):

    • 如果chan->client_count大于0, 意味着这个通道已经被一个或多个客户端驱动占用。在这种情况下, 它只执行两个简单的引用计数操作: __module_get增加DMA控制器驱动模块的引用计数, chan->client_count++增加本通道的客户端计数。这允许多个客户端共享同一个通道(如果硬件和驱动支持)。
  2. 通道首次被使用 (独占或首次获取路径):

    • 如果chan->client_count为0, 函数会执行一个完整、严谨的”上线”流程:
      • 模块存活检查: 它首先调用try_module_get。这是第一道、也是最关键的一道防线。它尝试增加DMA控制器驱动模块的引用计数, 但如果该模块正在被卸载(rmmod), 这个操作会失败。这从根本上杜绝了在驱动卸载过程中仍能成功获取其硬件资源的竞态条件, 是保证系统稳定性的关键。
      • 设备存活检查: 接下来, 它调用kref_get_unless_zero来增加DMA控制器设备的引用计数。这确保了DMA设备本身不是正在被释放的过程中, 防止了”use-after-free”类型的错误。
      • 按需资源分配: 它会检查并调用DMA驱动提供的可选回调函数device_alloc_chan_resources。这是一个重要的内存优化机制。它允许DMA驱动将那些只有在通道被实际使用时才需要的、可能很消耗内存的资源(如DMA描述符池)的分配工作, 推迟到通道首次被获取时才执行, 而不是在驱动初始化时就为所有通道预分配。
      • 状态更新: 在所有检查和分配都成功后, 它才将chan->client_count从0增加到1, 正式将该通道标记为”使用中”。
      • 全局平衡通知: 最后, 对于非私有通道, 它调用balance_ref_count。这个函数会通知DMA引擎的全局调度器(dma_channel_rebalance), 系统中现在有了一个活跃的客户端, 这可能会触发一次全局的DMA通道负载均衡计算。

在STM32H750这样的单核系统上, 即使不存在多核并发, 此函数及其锁机制依然至关重要。dma_list_mutex可以防止在多个设备驱动的探测(probe)函数并发执行时(由于任务抢占)对全局DMA状态产生竞争。而按需的资源分配机制对于资源相对有限的嵌入式系统来说, 是一个非常有价值的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* dma_chan_get - 尝试获取一个DMA通道的父驱动模块
* @chan: 要获取的通道
*
* 必须在 dma_list_mutex 锁的保护下调用.
*/
static int dma_chan_get(struct dma_chan *chan)
{
/*
* 获取拥有此通道的内核模块的指针.
*/
struct module *owner = dma_chan_to_owner(chan);
int ret;

/*
* 路径1: 通道已被一个或多个客户端使用 (共享情况).
*/
if (chan->client_count) {
/*
* 已经有客户端了, 我们只需要简单地增加引用计数.
* __module_get: 强制增加模块引用计数.
*/
__module_get(owner);
/*
* 增加本通道的客户端计数.
*/
chan->client_count++;
return 0; // 成功
}

/*
* 路径2: 通道首次被使用 (client_count 为 0).
*/

/*
* 步骤 1: 模块存活检查.
* 尝试获取模块引用. 如果模块正在被卸载, 此函数会失败.
*/
if (!try_module_get(owner))
return -ENODEV; // 模块不可用, 获取失败.

/*
* 步骤 2: 设备存活检查.
* 安全地获取DMA设备(device)的引用. 如果设备正在被释放(ref为0), 此函数会失败.
*/
ret = kref_get_unless_zero(&chan->device->ref);
if (!ret) {
ret = -ENODEV; // 设备不可用, 获取失败.
goto module_put_out; // 跳转到清理步骤1(释放模块引用).
}

/*
* 步骤 3: 按需分配通道资源.
* 如果DMA驱动提供了这个回调函数, 就在这里调用它.
*/
if (chan->device->device_alloc_chan_resources) {
ret = chan->device->device_alloc_chan_resources(chan);
if (ret < 0)
goto err_out; // 如果资源分配失败, 跳转到清理步骤1和2.
}

/*
* 步骤 4: 更新状态.
* 所有检查和分配都已成功, 将客户端计数从0增加到1.
*/
chan->client_count++;

/*
* 步骤 5: 通知全局调度器.
* 如果这不是一个私有通道, 调用 balance_ref_count 来更新全局客户端计数,
* 这可能会触发 dma_channel_rebalance.
*/
if (!dma_has_cap(DMA_PRIVATE, chan->device->cap_mask))
balance_ref_count(chan);

return 0; // 成功

/*
* 健壮的错误回滚路径
*/
err_out:
/*
* 清理步骤2: 释放对DMA设备的引用.
*/
dma_device_put(chan->device);
module_put_out:
/*
* 清理步骤1: 释放对模块的引用.
*/
module_put(owner);
return ret; // 返回错误码.
}

dma_get_slave_channel: 独占性地获取一个指定的DMA通道

此函数是Linux DMA引擎框架中一个非常特殊且重要的API。它的核心作用是允许一个驱动程序尝试以独占的方式, 获取一个它已经通过其他方式(例如, of_xlate函数)识别出的、确切的物理DMA通道

与通用的dma_request_channel(它会根据能力自动选择一个可用的通道)不同, 此函数的目标是”我需要这一个通道, 而且在我使用期间, 不希望通用的分配系统再把它分配给别人”。

其核心原理是通过一个巧妙的”私有化”机制来确保独占性:

  1. 锁定全局状态: 函数首先获取dma_list_mutex全局互斥锁。这是至关重要的, 因为它即将修改一个DMA控制器设备的全局可见状态。
  2. 检查可用性: 它做的第一件事, 也是最关键的检查, 是chan->client_count == 0。这个计数器记录了当前有多少个”客户端”正在使用这个通道。如果计数不为0, 意味着该通道已经被占用了, 独占请求立即失败。
  3. 执行”私有化”: 如果通道当前是空闲的, 函数会执行一系列原子操作来”保留”它:
    • dma_cap_set(DMA_PRIVATE, device->cap_mask);: 这是整个机制的核心。它会给该通道所属的整个DMA控制器设备打上DMA_PRIVATE(私有)的标志。这个标志就像一个”请勿打扰”的牌子, 它会告诉通用的dma_request_channel函数:”这个DMA控制器当前处于私有模式, 不要再从中自动分配任何通道给新的通用请求”。
    • device->privatecnt++;: 它会增加该DMA控制器设备上的私有通道计数器。这允许多个通道被同一个或不同的驱动以这种方式独占。
    • err = dma_chan_get(chan);: 它调用一个内部函数来正式地”获取”这个通道, 这通常会使chan->client_count从0变为1。
  4. 健壮的错误回滚: 如果在正式获取通道时(虽然不太可能)发生错误, 它会执行一个精确的回滚操作: 它会递减privatecnt计数器。如果这个计数器减到0, 意味着这是该设备上最后一个被独占的通道, 那么它就会调用dma_cap_clear(DMA_PRIVATE, ...)移除整个设备的”私有”标志, 使其重新对通用分配系统开放。
  5. 释放锁并返回: 完成所有操作后, 它会释放全局锁, 并返回结果——成功时是原始的通道指针, 失败时是NULL

在STM32H750这样的系统中, 这个函数通常是在stm32_dma_of_xlate函数的内部被调用的。of_xlate从设备树中精确地识别出了客户端驱动需要的硬件通道(例如dma1, stream 7), 然后它就会调用dma_get_slave_channel来独占性地获取这个通道的软件句柄, 并返回给客户端驱动。即使是在单核系统中, dma_list_mutex锁也是必不可少的, 因为它能防止在多个设备驱动的探测(probe)函数并发执行时(由于任务抢占)对全局DMA状态产生竞争。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* dma_get_slave_channel - 尝试独占性地获取一个指定的通道
* @chan: 目标通道
*/
struct dma_chan *dma_get_slave_channel(struct dma_chan *chan)
{
/*
* 获取全局DMA列表互斥锁, 以保护对通道客户端计数和设备私有状态的并发访问.
* __dma_request_channel (通用请求函数)也会获取这个锁.
*/
mutex_lock(&dma_list_mutex);

/*
* 核心检查: 只有当没有任何客户端正在使用这个通道时 (client_count == 0),
* 我们才有可能独占性地获取它.
*/
if (chan->client_count == 0) {
struct dma_device *device = chan->device;
int err;

/*
* "私有化"步骤 1:
* 给该通道所属的整个DMA控制器设备打上 DMA_PRIVATE 标志.
* 这会阻止 dma_request_channel 再从这个设备自动分配通道.
*/
dma_cap_set(DMA_PRIVATE, device->cap_mask);
/*
* "私有化"步骤 2:
* 增加该设备的私有通道计数器.
*/
device->privatecnt++;
/*
* "私有化"步骤 3:
* 调用 dma_chan_get 来正式获取对该通道的引用 (这会增加 client_count).
*/
err = dma_chan_get(chan);
if (err) {
/*
* 如果 dma_chan_get 失败 (理论上不应发生, 但作为保护).
*/
dev_dbg(chan->device->dev,
"%s: failed to get %s: (%d)\n",
__func__, dma_chan_name(chan), err);
chan = NULL; // 将返回值设为NULL, 表示失败.

/*
* 错误回滚: 递减私有计数器. 如果计数器归零,
* 意味着这是最后一个私有通道, 必须清除整个设备的 DMA_PRIVATE 标志,
* 使其恢复为公共可用状态.
*/
if (--device->privatecnt == 0)
dma_cap_clear(DMA_PRIVATE, device->cap_mask);
}
} else {
/*
* 如果 client_count 不为0, 说明通道已被占用, 独占请求失败.
*/
chan = NULL;
}

/*
* 释放全局互斥锁.
*/
mutex_unlock(&dma_list_mutex);


/*
* 返回结果: 成功时是有效的chan指针, 失败时是NULL.
*/
return chan;
}
EXPORT_SYMBOL_GPL(dma_get_slave_channel);

dma_request_chan: 统一的DMA从通道请求接口

本代码片段展示了Linux内核中一个关键且通用的API——dma_request_chan。这是设备驱动程序(“客户端”)用来向DMA子系统请求一个专用的、用于外设数据传输的DMA通道(“从通道”)的标准入口点。其核心功能是提供一个统一的接口,它能自动通过现代的固件描述(如设备树)或旧的过滤机制来查找并分配一个合适的DMA通道,同时优雅地处理驱动间的加载顺序依赖问题(即“探测延迟”,Probe Deferral)。

实现原理分析

该函数是连接设备驱动与具体DMA控制器驱动的核心枢纽。它将复杂的DMA通道匹配和分配过程抽象化,其实现策略是“固件优先,过滤为辅”。

  1. 统一的固件优先方法:

    • 函数首先获取设备的固件节点(fwnode),这是一个通用的句柄,可以是设备树(Device Tree)节点或ACPI节点。
    • 设备树路径: 如果是设备树系统(is_of_node),它会调用of_dma_request_slave_channel。这个函数会解析设备驱动对应的设备树节点中的dmasdma-names属性。例如,一个UART的设备树节点可能会有dmas = <&mdma_channel_x ...>dma-names = "rx"dma_request_chan(dev, "rx")就会精确地请求到mdma_channel_x这个物理通道。这是最现代、最精确的匹配方式。
    • ACPI路径: 如果是ACPI系统,则调用acpi_dma_request_slave_chan_by_name,执行类似的功能。
  2. 优雅的依赖处理 (EPROBE_DEFER):

    • if (PTR_ERR(chan) == -EPROBE_DEFER): 这是一个至关重要的机制。当of_dma_request_slave_channel返回-EPROBE_DEFER时,意味着设备树中虽然描述了DMA通道,但对应的DMA控制器驱动尚未初始化完成dma_request_chan会立即将这个错误码返回给调用者(如stm32_usart_serial_probe)。调用者看到这个错误后,会中止自己的初始化并同样返回-EPROBE_DEFER。这会通知内核驱动核心:“我的依赖(DMA控制器)还没准备好,请稍后**重新探测(re-probe)**我”。这完美地解决了驱动加载顺序的问题。
  3. 旧式过滤回退机制:

    • 如果基于固件的查找失败(但不是因为-EPROBE_DEFER),代码会进入一个回退路径。
    • mutex_lock(&dma_list_mutex): 锁定一个全局列表。
    • list_for_each_entry_safe(d, _d, &dma_device_list, global_node): 它会遍历系统中所有已注册的DMA控制器设备。
    • dma_filter_match: 对于每一个DMA控制器,它会使用一个filter函数和name来判断这个控制器是否能为当前设备dev服务。这是一种比较旧的、基于平台数据或硬件特定逻辑的匹配方法。
    • 如果回退机制也找不到任何匹配的通道,它会返回-EPROBE_DEFER,寄希望于一个合适的DMA控制器可能在未来才被注册。
  4. 通道分配后的处理:

    • 一旦成功获取到一个dma_chan,函数会执行一系列“收尾”工作:
    • chan->slave = dev;: 建立从DMA通道到客户端设备的反向链接,正式确立主从关系。
    • sysfs_create_link: 在sysfs中创建符号链接。这极大地增强了系统的可观测性。例如,它会在DMA通道的sysfs目录下创建一个名为slave的符号链接,指向使用它的设备(如ttySTM0);同时,在设备的目录下也会创建一个指向DMA通道的链接。这使得系统管理员可以轻松地从命令行查看到设备与DMA通道的绑定关系。

特定场景分析:单核、无MMU的STM32H750平台

硬件交互

  1. 设备树是关键: 在STM32H750平台上,dma_request_chan的执行完全依赖于设备树(DTS)的正确配置。当stm32_usart_serial_probe调用dma_request_chan(&pdev->dev, "rx")时:
    • dev就是usart1platform_device
    • of_dma_request_slave_channel函数会被调用。
    • 它会查找usart1节点下的dmas = <&dma1_stream0 ...>dma-names = "rx", ...属性。
    • 它发现”rx”对应的是dma1_stream0。于是,它会向dma1(STM32的DMA控制器之一)的驱动请求分配第0个流(stream)。
    • dma1的驱动会返回一个代表DMA1_Stream0这个物理硬件的struct dma_chan实例。
  2. 硬件连接的软件体现: 这个返回的dma_chan不仅仅是一个数据结构,它内部包含了操作DMA1_Stream0所有相关寄存器(如配置寄存器CR、源地址PAR、目标地址M0AR等)所需的信息和函数指针。dma_request_chan的成功调用,标志着STM32 USART的硬件DMA请求信号(USART_DR寄存器有数据)与STM32 DMA控制器的物理通道之间的软件链路已经建立。

单核环境影响

  • DMA的主要目的是将数据搬运工作从CPU卸载,这对于只有一个核心的STM32H750来说,可以极大地提升性能,使其在DMA传输数据的同时能够执行其他计算任务。
  • 函数中的mutex_lock(&dma_list_mutex)在单核系统上用于防止在遍历全局DMA设备列表时被其他任务抢占,从而保证了列表的完整性。

无MMU影响

  • dma_request_chan函数本身不处理内存地址,所以与MMU无关。
  • 然而,它所属的DMA引擎框架是MMU/IOMMU感知的。在无MMU的系统上,后续的DMA操作(如dma_map_single或使用dma_alloc_coherent返回的地址)会直接使用物理地址。DMA引擎框架的抽象使得设备驱动代码(如STM32 USART驱动)几乎无需关心底层是物理地址还是DMA虚拟地址,从而具有很好的可移植性。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
static const struct dma_slave_map *dma_filter_match(struct dma_device *device,
const char *name,
struct device *dev)
{
int i;

if (!device->filter.mapcnt)
return NULL;

for (i = 0; i < device->filter.mapcnt; i++) {
const struct dma_slave_map *map = &device->filter.map[i];

if (!strcmp(map->devname, dev_name(dev)) &&
!strcmp(map->slave, name))
return map;
}

return NULL;
}

/**
* @brief 尝试分配一个独占的DMA从通道。
* @param dev 指向客户端设备的指针。
* @param name 从通道的名称(如 "rx" 或 "tx")。
* @return struct dma_chan* 成功则返回指向DMA通道的指针,失败则返回错误指针。
*/
struct dma_chan *dma_request_chan(struct device *dev, const char *name)
{
struct fwnode_handle *fwnode = dev_fwnode(dev);
struct dma_device *d, *_d;
struct dma_chan *chan = NULL;

/* 首先尝试通过固件节点(Device Tree或ACPI)来获取通道。 */
if (is_of_node(fwnode))
chan = of_dma_request_slave_channel(to_of_node(fwnode), name);
else if (is_acpi_device_node(fwnode))
chan = acpi_dma_request_slave_chan_by_name(dev, name);

/* 如果返回EPROBE_DEFER,表示依赖的DMA控制器未就绪,直接返回此错误。 */
if (PTR_ERR(chan) == -EPROBE_DEFER)
return chan;

/* 如果成功找到通道,则直接跳转到found标签。 */
if (!IS_ERR_OR_NULL(chan))
goto found;

/* 回退路径:通过DMA过滤映射来查找通道。 */
mutex_lock(&dma_list_mutex);
/* 遍历全局的DMA设备列表。 */
list_for_each_entry_safe(d, _d, &dma_device_list, global_node) {
dma_cap_mask_t mask;
const struct dma_slave_map *map = dma_filter_match(d, name, dev);

if (!map)
continue;

dma_cap_zero(mask);
dma_cap_set(DMA_SLAVE, mask);

/* 在此DMA控制器上查找一个候选通道。 */
chan = find_candidate(d, &mask, d->filter.fn, map->param);
if (!IS_ERR(chan))
break; /* 找到即跳出循环。 */
}
mutex_unlock(&dma_list_mutex);

if (IS_ERR(chan))
return chan;
/* 如果回退路径也找不到,则返回EPROBE_DEFER,寄希望于DMA控制器稍后注册。 */
if (!chan)
return ERR_PTR(-EPROBE_DEFER);

found:
#ifdef CONFIG_DEBUG_FS
/* 为调试目的,分配一个包含客户端设备名的字符串。 */
chan->dbg_client_name = kasprintf(GFP_KERNEL, "%s:%s", dev_name(dev), name);
#endif

/* 分配一个包含DMA和通道名的字符串。 */
chan->name = kasprintf(GFP_KERNEL, "dma:%s", name);
if (!chan->name)
return chan;
/* 建立从DMA通道到客户端设备的反向链接。 */
chan->slave = dev;

/* 在sysfs中创建从通道到客户端的符号链接。 */
if (sysfs_create_link(&chan->dev->device.kobj, &dev->kobj,
DMA_SLAVE_NAME))
dev_warn(dev, "无法创建DMA %s符号链接\n", DMA_SLAVE_NAME);
/* 在sysfs中创建从客户端到通道的符号链接。 */
if (sysfs_create_link(&dev->kobj, &chan->dev->device.kobj, chan->name))
dev_warn(dev, "无法创建DMA %s符号链接\n", chan->name);

return chan;
}
EXPORT_SYMBOL_GPL(dma_request_chan);

find_candidate: DMA私有通道的搜索与获取

本代码片段展示了dma_request_chan内部使用的一个核心辅助函数——find_candidate。其核心功能是根据指定的匹配条件(能力掩码和过滤函数),在一个DMA控制器设备上查找一个合适的、可用的私有通道,并立即尝试获取(acquire)它。这是一个“查找并锁定”的原子性操作,它通过管理私有通道计数和状态,确保了找到的通道被独占式地分配给请求者,并且它在找不到可用通道时,能够正确地返回-EPROBE_DEFER以支持驱动探测延迟机制。

实现原理分析

此函数是DMA引擎为“私有”或“专用”通道(即不通过通用池分配,而是由特定驱动直接请求的通道)设计的分配逻辑。它将“寻找”和“获取”两个步骤紧密结合,以处理并发和资源可用性问题。

  1. 第一步:寻找候选者 (private_candidate):

    • 函数的第一步是调用private_candidate(代码未显示)。这个内部函数负责实际的搜索工作。
    • 它会遍历device(一个DMA控制器)上的所有物理通道。
    • 对于每个通道,它会使用mask(例如,必须支持DMA_SLAVE能力)和fn(一个驱动提供的、硬件相关的过滤函数)来进行匹配。fn是关键,它允许进行精确的匹配,例如,fn可能会检查一个DMA通道是否连接到了指定的UART TX请求线上。
    • 如果private_candidate找到一个满足所有条件的空闲通道,它会返回该通道的指针;否则返回NULL
  2. 第二步:尝试获取与状态管理:

    • 如果private_candidate成功找到了一个候选通道(if (chan)),函数会立即尝试正式地获取它。
    • dma_cap_set(DMA_PRIVATE, device->cap_mask): 在DMA控制器设备上设置DMA_PRIVATE标志。这是一个状态标记,告知DMA引擎,该控制器至少有一个通道正在被私有(独占)使用。如注释所言,这会改变某些内部管理逻辑,如引用计数的平衡。
    • device->privatecnt++: 递增该控制器的私有通道使用计数器。
    • err = dma_chan_get(chan): 这是关键的获取操作。它会去真正地“锁定”这个通道,通常是增加通道自身的引用计数,使其不能再被分配给其他请求者。
    • 获取失败处理:
      • 如果dma_chan_get失败,说明在找到候选者和尝试获取它的极短时间窗口内,通道的状态发生了变化(例如,被另一个任务或中断获取)。
      • 代码会执行对称的清理操作:递减privatecnt计数器,并在计数器归零时清除DMA_PRIVATE标志。
      • 一个特殊情况是err == -ENODEV,这表示底层硬件设备或其模块已经被移除。这是一个罕见的竞态条件,函数的响应是果断地将整个DMA控制器设备从全局列表中移除。
  3. 返回值的意义:

    • 函数的返回值设计得非常精妙,它能向调用者传达三种不同的状态:
      1. 成功: private_candidate找到通道,且dma_chan_get成功。函数返回一个有效的struct dma_chan *指针。
      2. 需要延迟探测: private_candidate没有找到任何满足条件的空闲通道。函数返回ERR_PTR(-EPROBE_DEFER)。这告诉调用者(如dma_request_chan),现在没有可用的资源,但未来可能会有(例如,当另一个驱动释放了它占用的通道时),所以应该推迟当前驱动的初始化,稍后再试。
      3. 其他错误: private_candidate找到了通道,但dma_chan_get失败。函数返回具体的错误码(如-EBUSY)。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* @brief 在一个DMA控制器上查找并尝试获取一个满足条件的候选通道。
* @param device 要搜索的DMA控制器设备。
* @param mask 描述所需能力的掩码(如DMA_SLAVE)。
* @param fn 用于精确匹配的过滤函数。
* @param fn_param 传递给过滤函数的参数。
* @return struct dma_chan* 成功则返回获取的通道,否则返回错误指针。
*/
static struct dma_chan *find_candidate(struct dma_device *device,
const dma_cap_mask_t *mask,
dma_filter_fn fn, void *fn_param)
{
/* 调用内部函数,根据掩码和过滤函数搜索一个空闲的候选通道。 */
struct dma_chan *chan = private_candidate(mask, device, fn, fn_param);
int err;

if (chan) {
/*
* 找到了一个合适的通道,尝试获取、准备并返回它。
* 首先,在控制器设备上设置DMA_PRIVATE标志,
* 以表明该通道不会被发布到通用分配器中。
*/
dma_cap_set(DMA_PRIVATE, device->cap_mask);
device->privatecnt++; /* 增加私有通道计数。 */
err = dma_chan_get(chan); /* 尝试正式获取该通道的所有权。 */

if (err) {
/* 获取失败的处理逻辑。 */
if (err == -ENODEV) {
/* 如果设备在此期间被移除,则从全局列表中删除该DMA控制器。 */
dev_dbg(device->dev, "%s: %s 模块已被移除\n",
__func__, dma_chan_name(chan));
list_del_rcu(&device->global_node);
} else
dev_dbg(device->dev,
"%s: 获取 %s 失败: (%d)\n",
__func__, dma_chan_name(chan), err);

/* 对之前的操作进行回滚:递减私有计数,并在需要时清除标志。 */
if (--device->privatecnt == 0)
dma_cap_clear(DMA_PRIVATE, device->cap_mask);

/* 将chan转换为错误指针以返回。 */
chan = ERR_PTR(err);
}
}

/*
* 如果chan有效(表示查找和获取都成功),则返回通道指针。
* 否则(如果最初就没找到候选通道),返回-EPROBE_DEFER错误指针。
*/
return chan ? chan : ERR_PTR(-EPROBE_DEFER);
}

__dma_request_channel: 按能力掩码与过滤器申请一个独占 DMA 通道

这个函数是 DMAengine “通道分配器”的核心入口之一:在全局 DMA 设备链表中遍历所有已注册的 struct dma_device,对每个控制器调用 find_candidate() 尝试找到一个满足 mask(能力集合)与 fn(可选过滤器)的空闲通道,并以“独占/私有(DMA_PRIVATE)”语义把该通道占用起来;成功则返回该 struct dma_chan *,失败则返回 NULL

STM32H750(单核、无 MMU)场景下的意义

  • 单核不会改变此函数的并发模型:Linux 仍可能在同一核上发生抢占/中断导致的并发交错,因此 dma_list_mutex 仍是必要的全局互斥。
  • 无 MMU不是主线 Linux 的常规形态;若你是在特定移植环境中复用该逻辑,则这里的“全局链表扫描 + 错误指针语义(如 -EPROBE_DEFER)”对驱动探测顺序更敏感:依赖未就绪时必须允许延迟探测,否则会把“暂时不可用”误判为“永久无通道”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @brief 按能力掩码与过滤器从全局 DMA 控制器集合中申请一个独占 DMA 通道
* @param mask 期望通道必须满足的 capability mask;为 NULL 时表示不做能力匹配
* @param fn 可选过滤器回调;用于在候选通道上做驱动自定义约束判断
* @param fn_param 传给过滤器回调的私有参数指针
* @param np 可选设备树节点约束;非 NULL 时仅从匹配该 of_node 的控制器实例中分配
* @return 成功返回可用的 struct dma_chan*;失败返回 NULL(注意:此处不直接返回 ERR_PTR)
*/
struct dma_chan *__dma_request_channel(const dma_cap_mask_t *mask, /* 期望的通道能力集合约束 */
dma_filter_fn fn, /* 可选通道过滤器回调 */
void *fn_param, /* 过滤器回调的私有参数 */
struct device_node *np) /* 可选:控制器设备树节点约束 */
{
struct dma_device *device, *_d; /* device:当前遍历的 DMA 控制器;_d:safe 遍历用的下一节点缓存 */
struct dma_chan *chan = NULL; /* 最终要返回的通道指针;默认失败为 NULL */

/* 进入临界区:保护 dma_device_list 与通道占用状态,避免并发分配同一通道 */
mutex_lock(&dma_list_mutex); /* 获取全局 DMA 列表互斥锁 */

/* 遍历所有已注册 DMA 控制器;safe 版本允许遍历过程中发生摘链等结构性变化 */
list_for_each_entry_safe(device, _d, &dma_device_list, global_node) { /* 逐个扫描全局 DMA 控制器链表 */

/* 若上层指定了设备树节点约束,则仅在该控制器实例范围内尝试分配 */
if (np && device->dev->of_node && np != device->dev->of_node) /* np 非空且控制器有 of_node 且不匹配:跳过 */
continue; /* 继续扫描下一个控制器 */

/* 在该控制器内寻找可用通道,并以“私有/独占”语义尝试占用 */
chan = find_candidate(device, mask, fn, fn_param); /* 候选选择与占用:可能返回有效指针或 ERR_PTR */

/* 只要不是错误指针(即申请成功或至少得到可用通道指针),就结束扫描 */
if (!IS_ERR(chan)) /* IS_ERR 用于识别错误指针返回 */
break; /* 成功:跳出全局控制器扫描循环 */

/* 若返回错误指针,则该控制器不满足条件或暂不可用;继续尝试下一个控制器 */
chan = NULL; /* 统一把“本轮失败”折叠为 NULL,继续外层扫描 */
}

/* 离开临界区:分配决策已完成(成功则 chan 指向已占用通道) */
mutex_unlock(&dma_list_mutex); /* 释放全局 DMA 列表互斥锁 */

/* 调试输出:记录申请结果与通道名称;不影响功能语义 */
pr_debug("%s: %s (%s)\n", /* 输出格式:函数名 + success/fail + 通道名 */
__func__, /* 当前函数名字符串 */
chan ? "success" : "fail", /* 依据 chan 是否非空判断成功/失败 */
chan ? dma_chan_name(chan) : NULL); /* 成功则打印通道名,失败打印 NULL */

return chan; /* 成功返回通道指针;失败返回 NULL */
}

DMA_PRIVATE: 控制器私有模式标记的作用、设计原因与“通道预留”实现方式

1) DMA_PRIVATE 的作用与含义

DMA_PRIVATE 在 DMAengine 里不是“这个通道私有”,而是更接近:

  • struct dma_device 的策略标记:表示该 DMA 控制器处于“私有分配模式”,框架会倾向于把它从公共的通道分配池/重平衡视图里剔除,避免公共分配路径继续把它当成候选资源。
  • privatecnt 配套privatecnt 表示该控制器当前有多少“私有持有者”。只有当 privatecnt 归零,才允许清除 DMA_PRIVATE,让控制器回到公共可分配状态。
  • 典型使用场景:某些驱动希望“我拿到这个控制器的一个通道之后,后续也不要让公共分配器再把这个控制器分给其他不受控的客户端”,从而获得更强的控制器级可预测性(例如对 runtime PM、带宽、优先级策略的整体控制)。

重要结论:

DMA_PRIVATE 表达的是“控制器退出公共分配体系”的语义,而不是“预留几个通道”。


2) 为什么 DMAengine 把 DMA_PRIVATE 设计成“控制器级别开关”?

主要是为了保持 DMAengine 的全局分配模型简单且一致,具体原因可分为三点:

  1. 框架能力模型是 device 粒度
    DMAengine 的能力描述(cap_mask、方向、位宽、burst、残余粒度)都在 struct dma_device 上,通道选择本来就先按 device 粗筛再选 chan。把 DMA_PRIVATE 放在 device->cap_mask,能用最低成本让控制器整体改变“是否参与公共池”的状态。

  2. DMA_PRIVATE 的目标是影响全局分配视图
    DMAengine 维护全局通道表/重平衡逻辑时,需要一个粗粒度的“这个控制器要不要出现在公共候选里”的开关。若做成通道级私有,就意味着全局表必须维护额外维度(每个通道的公/私),并引入更复杂的规则(私有优先级、冲突回滚、状态同步),会显著增加维护与热路径开销。

  3. 控制器内共享状态普遍存在
    很多 DMA 控制器即使通道寄存器独立,也会共享:时钟/电源域、全局中断状态、仲裁/优先级策略、QoS 配置等。
    因此把“私有”定义为 device 级,能减少“控制器级策略被多个互不知情的客户端同时改变”的风险。

结论:

这是一个“用更强的不变量换简单实现与可维护性”的设计选择。


3) 怎么实现“一个控制器里有些公有有些私有”(你的目标:固定预留几条通道给某外设/驱动)

你要的其实是:通道预留(channel reservation),推荐的做法是“让公共分配器永远选不到那几条预留通道”,而不是把设备设为 DMA_PRIVATE

常见实现路径(从框架语义到驱动落地)如下:

3.1 预留策略 A:通过过滤器让公共分配永远跳过预留通道

思路:

  • 让“公共分配路径”的过滤器拒绝预留通道
  • 让“特定外设/驱动”的过滤器只接受预留通道

效果:

  • 同一控制器里,预留通道只会被特定客户端拿到
  • 其他客户端仍能通过公共路径拿到剩余通道
  • 不需要 DMA_PRIVATE,也不会触发控制器级“退出公共池”

在 STM32 MDMA 场景对应落地通常是:

  • 设备树 xlate(比如 stm32_mdma_of_xlate)传入 fn_param,过滤器根据 request/priority/… 以及你定义的“预留通道集合”来判定是否允许。
  • 或者在 filter_fn 里加入“如果通道 id 属于 reserved_set 且请求不是目标外设,则返回 false”。

3.2 预留策略 B:驱动内部维护 chan_reserved 位图并在过滤器中强制排除

这是一种更“硬”的预留:

  • 驱动维护一个 chan_reserved 位图(你在 STM32 MDMA 驱动里也能看到类似思想:secure 通道会被标记并排除)
  • 公共分配过滤器直接拒绝 chan_reserved 的通道
  • 特定外设路径可以走“专用 xlate/专用过滤器”去允许这些通道(或在它申请前动态切换预留位)

优点:行为稳定、成本低
缺点:需要你明确管理“谁有权用预留通道”,否则容易变成“永远不可用”的资源浪费。

3.3 预留策略 C:让预留通道在系统启动时就被目标驱动长期持有

思路:

  • 目标驱动在 probe 时提前申请并持有通道,不释放(client_count 一直非 0)
  • 公共分配扫描时会因 client_count != 0 把它当 busy 跳过

优点:实现最简单,几乎不改 DMAengine/过滤器
缺点:需要目标驱动生命周期与通道绑定,且对热插拔/错误恢复不友好


private_candidate: 在单个 DMA 控制器内筛选一个“可私有化分配”的空闲通道

private_candidate() 的核心功能是:在给定的 struct dma_device *dev(一个 DMA 控制器)内部,按顺序执行三类约束筛选,最终返回一个“当前空闲且满足条件”的 struct dma_chan *

  1. 能力匹配:如果传入了 mask,则必须通过 dma_device_satisfies_mask(dev, mask);否则该控制器直接不参与候选。([Linux Git Repositories][1])
  2. 多通道控制器的一致性约束:若该控制器 chancnt > 1 且当前还不是 DMA_PRIVATE,则要求 该控制器的所有通道都必须处于“未被公共分配”的状态(即所有通道 client_count == 0),否则拒绝返回任何通道。其目的在于:避免同一控制器出现“部分通道公共分配、部分通道私有分配”的混合状态,因为后续 find_candidate() 会把 DMA_PRIVATE 置位,语义上要把整个控制器从通用分配器里摘除。([Linux Git Repositories][1])
  3. 逐通道筛选:遍历 dev->channels,跳过 busy 通道(client_count != 0),再用可选过滤器 fn(chan, fn_param) 做驱动自定义约束筛选(例如 STM32 MDMA 里可排除 reserved/secure 通道),找到第一个满足条件的通道就返回。([Linux Git Repositories][1])

注意:private_candidate() 不做占用(不增加引用、不改 DMA_PRIVATE、不改 privatecnt),它只负责“在本控制器内找一个可行通道”。真正的“私有化占用 + 获取通道”是在上一层 find_candidate() 里完成。([Linux Git Repositories][1])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* @brief 在给定 DMA 控制器内,选择一个满足能力与过滤器条件的空闲通道(仅选择,不占用)
* @param[in] mask 期望控制器满足的能力掩码;为 NULL 表示不做能力匹配
* @param[in] dev 目标 DMA 控制器
* @param[in] fn 可选通道过滤器;返回 true 表示通道可用
* @param[in] fn_param 过滤器的私有参数
* @return 成功返回一个满足条件的 struct dma_chan*;失败返回 NULL
*/
static struct dma_chan *private_candidate(const dma_cap_mask_t *mask,
struct dma_device *dev,
dma_filter_fn fn, void *fn_param)
{
/* chan:遍历 dev->channels 时的临时指针 */
struct dma_chan *chan;

/* 若提供了能力掩码,则控制器必须满足该掩码,否则直接拒绝该控制器 */
if (mask && !dma_device_satisfies_mask(dev, mask)) {
/* 能力不匹配:仅记录调试信息,不参与通道分配 */
dev_dbg(dev->dev, "%s: wrong capabilities\n", __func__);
return NULL;
}

/*
* 多通道控制器的一致性约束:
* 当控制器拥有多个通道且尚未处于 DMA_PRIVATE 状态时,
* 必须保证控制器内不存在“已被公共分配”的通道,否则拒绝私有分配。
*
* 目的:避免把控制器切换到 DMA_PRIVATE 后出现“部分通道公共、部分通道私有”的混合状态。
*/
if (dev->chancnt > 1 && !dma_has_cap(DMA_PRIVATE, dev->cap_mask))
list_for_each_entry(chan, &dev->channels, device_node) {
/* 若存在任意已被客户端占用的通道,则认为该控制器不适合进入私有分配路径 */
if (chan->client_count)
return NULL;
}

/*
* 在控制器的通道链表中查找第一个空闲且满足过滤器条件的通道:
* - client_count != 0 视为 busy,跳过
* - fn 存在时必须通过 fn(chan, fn_param)
*/
list_for_each_entry(chan, &dev->channels, device_node) {
/* busy 通道:已被客户端持有,跳过 */
if (chan->client_count) {
dev_dbg(dev->dev, "%s: %s busy\n",
__func__, dma_chan_name(chan));
continue;
}

/* 过滤器拒绝:驱动自定义约束不通过,跳过 */
if (fn && !fn(chan, fn_param)) {
dev_dbg(dev->dev, "%s: %s filter said false\n",
__func__, dma_chan_name(chan));
continue;
}

/* 找到满足条件的空闲通道:返回(注意:这里只是返回候选,不做占用) */
return chan;
}

/* 没有任何通道满足条件 */
return NULL;
}