Linux 内核是如何感知到硬件上的 NUMA 信息的?

article/2025/8/12 18:40:25

大家好,我是飞哥!

在 Linux 程序运行过程中,有一个对性能影响较大的特性,那就是 NUMA。在不少公司中,都通过 numactl 等命令对运行的服务进行了 NUMA 绑定,进而提高程序的运行性能。

那么我们今天来深入了解一下 NUMA 的原理。在硬件上的 NUMA 组成为什么会影响程序的运行性能,Linux 操作系统又是如何识别 NUMA 信息,来将 CPU 和内存进行分组划分 node 的。

一、NUMA 介绍

NUMA 全称是 Non-uniform memory access,是非一致性内存访问的意思。不过这段话还是由点费解,我们需要看看硬件才能更好地理解它。

现代的 CPU 都在硬件内部实现了一个内存控制器,内存条都会和这个内存控制器进行相连。

之前我们在 深入了解服务器 CPU 的型号、代际、片内与片间互联架构 一文中提到过,服务器 CPU 和个人 PC CPU 的一个很大的区别就是扩展性。在一台服务器的内部是支持插2/4/8等多 CPU 的。每个 CPU 都可以连接几条的内存。两个 CPU 之间如果想要访问对方上连接的内存条,中间就得跨过 UPI 总线。

下面是一台服务器的实际内部图片。中间两个银色长方形的东东是罩着散热片的 CPU,每个 CPU 旁边都有一些内存插槽,支持插入多条内存。

CPU 扩展性的设计极大地提升了服务器上的 CPU 核数与内存容量。但同时也带来了另外一个问题,那就是 CPU 物理核在访问不同的内存条的时候延迟是不同的。这就是非一致性内存访问的含义。

其实不仅仅是跨 CPU 访问存在延时差异。在服务器高核心 CPU 上,由于 Mesh 架构、以及存在两个内存控制器,物理核访问不同的内存控制器上的内存条也会有差异。只不过这个差异没有跨 CPU 差异大。

这种问题的出现使得 Linux 操作系统不得不关注内存访问速度不平均的问题。你在 Linux 上执行 numactl 命令可以查看你机器上的 NUMA 配置情况。拿我手头的一台虚拟机来举例。

# numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3
node 0 size: 7838 MB
node 0 free: 6208 MB
node 1 cpus: 4 5 6 7
node 1 size: 7934 MB
node 1 free: 6589 MB
node distances:
node   0   10:  10  201:  20  10

上面的输出中展示了 Linux 把所有的 CPU 核心和内存分成了两个 node。其中 node 0 中的拥有的 CPU 核心是 0、1、2、3 这四个核,总共拥有 7838 MB 的内存。node 1 中拥有的核心是 4、5、6、7 四个核,拥有的内存是 7934 MB。

另外 node distances 这里显示了跨 node 进行内存访问时一个大概的延时差距。同 node 内部的内存访问肯定是最快的,跨 node 则相对较慢。

那么内核是如何识别到底层的 NUMA 信息的呢?

二、Linux 对 NUMA 信息的读取

2.1 Linux 内核识别如何识别内存属于哪个节点

在计算机的体系结构中,除了操作系统和硬件外,其实中间还存在着一层固件,英文名叫 firmware。它是位于主板上的使用 SPI Nor Flash 存储着的软件。起着在硬件和操作系统中间承上启下的作用。它负责着硬件自检、初始化硬件设备、加载操作系统引导程序,并提供接口将控制权转移到操作系统。

回到我们今天的话题。那么 CPU 和内存条之间这种访问非一致性特点,Linux 就是通过固件来获得这个知识的。其中在 Linux 和固件中间的接口规范是 ACPI(Advanced Configuration and Power Interface),高级配置和电源接口。

这是较新的 6.5 版本的文档地址: https://uefi.org/sites/default/files/resources/ACPI_Spec_6_5_Aug29.pdf。感兴趣的同学可以下载下来。

在这个接口规范中的第 17 章中描述了 NUMA 相关的内容。在 ACPI 中定义了两个表,分别是:

  • SRAT(System Resource Affinity Table)。在这个表中表示的是 CPU 核和内存的关系图。包括有几个 node,每个 node 里面有那几个 CPU 逻辑核,有哪些内存。

  • SLIT(System Locality Information Table)。在这个表中记录的是各个结点之间的距离。

有了这个规范,CPU 读取这两个表就可以获得 NUMA 系统的 CPU 及物理内存分布信息。操作系统在启动的时候会执行 start_kernel 这个核心函数,然后会调用到

//file:arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{...
// 保存物理内存检测结果e820__memory_setup();...// membloc内存分配器初始化e820__memblock_setup();// 内存初始化(包括 NUMA 机制初始化)initmem_init();
}

在 setup_arch 中显示调用了 e820__memory_setup 来保存物理内存检测结果。然后调用 e820__memblock_setup 初始化内存分配器。详情参见Linux 内核“偷吃”了你的内存! 一文。在 initmem_init 完成了 NUMA 的初始化。

在 initmem_init 中,依次调用了 x86_numa_init、numa_init、x86_acpi_numa_init,最后执行到了 acpi_numa_init 函数中来读取 ACPI 中的 SRAT 表,获取到各个 node 中的 CPU 逻辑核、内存的分布信息。

//file:drivers/acpi/numa/srat.c
int __init acpi_numa_init(void)
{...// 解析 SRAT 表中的 NUMA 信息// 具体包括:CPU_AFFINITY、MEMORY_AFFINITY 等if (!acpi_table_parse(ACPI_SIG_SRAT, acpi_parse_srat)) {...}...
}

在 SRAT 表读取并解析完成后,Linux 操作系统就知道了内存和 node 的关系了。numa 信息都最后保存在了 numa_meminfo 这个数据结构中,这是一个全局的列表,每一项都是(起始地址, 结束地址, 节点编号)的三元组,描述了内存块与 NUMA 节点的关联关系。

//file:arch/x86/mm/numa.c
static struct numa_meminfo numa_meminfo __initdata_or_meminfo;//file:arch/x86/mm/numa_internal.h
struct numa_meminfo {int   nr_blks;struct numa_memblk blk[NR_NODE_MEMBLKS];
};

2.2 memblock 分配器 关联 NUMA 信息

在此之后,Linux 就可以通过 numa_meminfo 数组来获取硬件 NUMA 信息了。前面在 一文中我们提到了内核的 memblock 内存分配器。有了 numa_meminfo 数组,memblock 就可以根据这个信息读取到自己各个 region 分别是属于哪个 node 的了。

这件工作是在 numa_init 中开始的。

//file:arch/x86/mm/numa.c
static int __init numa_init(int (*init_func)(void))
{...//2.1 把numa相关的信息保存在 numa_meminfo 中init_func();//2.2 memblock 添加 NUMA 信息,并为每个 node 申请对象numa_register_memblks(&numa_meminfo);...
// 用于将各个CPU core与NUMA节点关联numa_init_array();
return0;
}

在 numa_register_memblks 中完成了三件事情

  • 将每一个 memblock region 与 NUMA 节点号关联

  • 为每一个 node 都申请一个表示它的内核对象(pglist_data)

  • 再次打印 memblock 信息

//file:arch/x86/mm/numa.c
static int __init numa_register_memblks(struct numa_meminfo *mi)
{...
//1.将每一个 memblock region 与 NUMA 节点号关联
for (i = 0; i < mi->nr_blks; i++) {
struct numa_memblk *mb = &mi->blk[i];memblock_set_node(mb->start, mb->end - mb->start,&memblock.memory, mb->nid);}...
//2.为所有可能存在的node申请pglist_data结构体空间 for_each_node_mask(nid, node_possible_map) {...
//为nid申请一个pglist_data结构体alloc_node_data(nid);}//3.打印MemBlock内存分配器的详细调试信息memblock_dump_all();
}

这个函数的详细逻辑就不展开了。我们来看下 memblock_dump_all。如果你开启了 memblock=debug 启动参数,在它执行完后,memblock 内存分配器的信息再次被打印了出来。

[    0.010796] MEMBLOCK configuration:
[    0.010797]  memory size = 0x00000003fff78c00 reserved size = 0x0000000003d7bd7e
[    0.010797]  memory.cnt  = 0x4
[    0.010799]  memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes on node 0 flags: 0x0
[    0.010800]  memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes on node 0 flags: 0x0
[    0.010801]  memory[0x2] [0x0000000100000000-0x000000023fffffff], 0x0000000140000000 bytes on node 0 flags: 0x0
[    0.010802]  memory[0x3] [0x0000000240000000-0x000000043fffffff], 0x0000000200000000 bytes on node 1 flags: 0x0
[    0.010803]  reserved.cnt  = 0x7
[    0.010804]  reserved[0x0] [0x0000000000000000-0x00000000000fffff], 0x0000000000100000 bytes on node 0 flags: 0x0
[    0.010806]  reserved[0x1] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes on node 0 flags: 0x0
[    0.010807]  reserved[0x2] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes on node 0 flags: 0x0
[    0.010808]  reserved[0x3] [0x00000000bffe0000-0x00000000bffe3d7d], 0x0000000000003d7e bytes on node 0 flags: 0x0
[    0.010809]  reserved[0x4] [0x000000023fffb000-0x000000023fffffff], 0x0000000000005000 bytes flags: 0x0
[    0.010810]  reserved[0x5] [0x000000043fff9000-0x000000043fffdfff], 0x0000000000005000 bytes flags: 0x0
[    0.010811]  reserved[0x6] [0x000000043fffe000-0x000000043fffffff], 0x0000000000002000 bytes on node 1 flags: 0x0

不过这次不同的是,每一段内存地址范围后面都跟上了 node 的信息,例如 on node 0、on node 1 等。

三、操作系统内存识别过程总结

在刚开始操作系统启动的时候,操作系统通过 e820 读取到了内存的布局,并将它打印到了日志中。

[    0.000000] BIOS-provided physical RAM map:
[    0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009fbff] usable
[    0.000000] BIOS-e820: [mem 0x000000000009fc00-0x000000000009ffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000000f0000-0x00000000000fffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffd9fff] usable
[    0.000000] BIOS-e820: [mem 0x00000000bffda000-0x00000000bfffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000feff4000-0x00000000feffffff] reserved
[    0.000000] BIOS-e820: [mem 0x00000000fffc0000-0x00000000ffffffff] reserved
[    0.000000] BIOS-e820: [mem 0x0000000100000000-0x000000043fffffff] usable

接着内核创建了 memblock 内存分配器来进行系统启动时的内存管理。如果开启了 memblock=debug 启动参数,同样能把它打印出来。

[    0.010238] MEMBLOCK configuration:
[    0.010239]  memory size = 0x00000003fff78c00 reserved size = 0x0000000003c6d144
[    0.010240]  memory.cnt  = 0x3
[    0.010241]  memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes flags: 0x0
[    0.010243]  memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes flags: 0x0
[    0.010244]  memory[0x2] [0x0000000100000000-0x000000043fffffff], 0x0000000340000000 bytes flags: 0x0
[    0.010245]  reserved.cnt  = 0x4
[    0.010246]  reserved[0x0] [0x0000000000000000-0x0000000000000fff], 0x0000000000001000 bytes flags: 0x0
[    0.010247]  reserved[0x1] [0x00000000000f5a40-0x00000000000f5b83], 0x0000000000000144 bytes flags: 0x0
[    0.010248]  reserved[0x2] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes flags: 0x0
[    0.010249]  reserved[0x3] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes flags: 0x0

不过到这里,Linux 操作系统还不知道内存的 NUMA 信息。它通过 ACPI 接口读取固件中的 SRAT 表,将 NUMA 信息保存到 numa_meminfo 数组中。从此,Linux 就知道了硬件上的 NUMA 信息,并对 memblock 内存分配器也设置了 node 信息。并再次将其打印了出来。这次 memblock 的每一个 region 中就都携带了 node 信息。

[    0.010796] MEMBLOCK configuration:
[    0.010797]  memory size = 0x00000003fff78c00 reserved size = 0x0000000003d7bd7e
[    0.010797]  memory.cnt  = 0x4
[    0.010799]  memory[0x0] [0x0000000000001000-0x000000000009efff], 0x000000000009e000 bytes on node 0 flags: 0x0
[    0.010800]  memory[0x1] [0x0000000000100000-0x00000000bffd9fff], 0x00000000bfeda000 bytes on node 0 flags: 0x0
[    0.010801]  memory[0x2] [0x0000000100000000-0x000000023fffffff], 0x0000000140000000 bytes on node 0 flags: 0x0
[    0.010802]  memory[0x3] [0x0000000240000000-0x000000043fffffff], 0x0000000200000000 bytes on node 1 flags: 0x0
[    0.010803]  reserved.cnt  = 0x7
[    0.010804]  reserved[0x0] [0x0000000000000000-0x00000000000fffff], 0x0000000000100000 bytes on node 0 flags: 0x0
[    0.010806]  reserved[0x1] [0x0000000001000000-0x000000000340cfff], 0x000000000240d000 bytes on node 0 flags: 0x0
[    0.010807]  reserved[0x2] [0x0000000034f31000-0x000000003678ffff], 0x000000000185f000 bytes on node 0 flags: 0x0
[    0.010808]  reserved[0x3] [0x00000000bffe0000-0x00000000bffe3d7d], 0x0000000000003d7e bytes on node 0 flags: 0x0
[    0.010809]  reserved[0x4] [0x000000023fffb000-0x000000023fffffff], 0x0000000000005000 bytes flags: 0x0
[    0.010810]  reserved[0x5] [0x000000043fff9000-0x000000043fffdfff], 0x0000000000005000 bytes flags: 0x0
[    0.010811]  reserved[0x6] [0x000000043fffe000-0x000000043fffffff], 0x0000000000002000 bytes on node 1 flags: 0x0

以上就是 Linux 内存中 NUMA 机制的初始化大概过程。

总结

在现代服务器的非统一内存访问(NUMA)是一种用于多处理器硬件架构下,识别和保存每个 CPU 核和内存条之间的连接拓扑非常的重要。因为 CPU 只是和它直连的内存访问速度最快,访问和其它 CPU 连接的内存速度将会大大下降。

Linux 通过固件读取 ACPI 规范中的 SRAT 和 SLIT 表识别 NUMA 信息,在系统启动过程中,经一系列函数调用完成 NUMA 初始化,将信息保存到numa_meminfo,并使 memblock 分配器关联 NUMA 信息。最后通过 e820 读取内存布局,再结合 ACPI 获取的 NUMA 信息完成内存识别及相关设置。

当内核有了硬件 NUMA 信息的拓扑图后,我们在应用侧就可以通过 numactl 等命令来优化程序的性能了!

不过最后要补充说一点,关于 NUMA 绑定并不是有益无害。在业界也有不同的声音。比如 Oracal 的技术大咖们认为绑定 NUMA 可能在全局内存并未用尽的情况下出现内存分配错误,导致系统出现剧烈抖动。

目前我的知识星球内容更新完七大部分的视频讲解了,分别是CPU内存硬件原理、内核内存管理、内核进程管理、内核网络管理、内核文件系统管理、内核容器原理、性能观测。总共104节视频,时长累计 2272 分钟。详情参见

https://w0279g7ggv.feishu.cn/sheets/Vgz6sSSs0h0YcgtZQbacBYLAnVf

接下来会更新和大家手头工作最为密切的性能优化相关内容,欢迎加入!



http://www.hkcw.cn/article/MCpCMqvJXE.shtml

相关文章

郑钦文说自己武汉长大不怕热 适应炎热助力法网晋级

北京时间5月30日,2025年法国网球公开赛女单第三轮比赛中,中国选手郑钦文以6-3、6-4战胜加拿大18岁新星姆博科,生涯第二次晋级法网16强。赛后,郑钦文在现场接受了采访。她提到自己在中国最热的城市之一武汉长大,因此对炎热天气很适应。去年巴黎奥运会期间的天气更热,所以她…

湖南娄底一水沟水体被染成蓝色 罐车偷排致污染

5月29日,有网友发布视频称湖南娄底双峰县一处沟渠出现蓝色水体,来源尚不清楚。视频中,水体呈现鲜艳的蓝色,发布者表示已经上报环保部门。5月30日,娄底市生态环境局双峰分局工作人员回应称,事发地点是一处沟渠,非饮用水源。工作人员发现情况后立即介入,连夜进行了处置。…

零基础开始的网工之路第十四天------Linux程序管理

目录 一、Linux程序与进程 1、程序,进程,线程的概念 2、程序和进程的区别 3、进程和线程的区别 二、Linux进程基础(生命周期) 1、进程生命周期 2、父子进程的关系 三、程序管理 1、常见的软件包类型 四、Linux操作系统启动流程详解 1、概述 2、启动流程核心阶段 1…

大模型应用开发之微调与对齐

一、指令微调Instruction Tuning 1、指令数据 大模型就像一个天赋异禀的学生&#xff0c;在前期预训练阶段通过大量的阅读知识积累拥有了大量的见识&#xff0c;但光有天赋知识却不会交流还不行&#xff0c;得有好的教材和老师教它怎么做题、怎么回答问题&#xff0c;这个“教…

黑熊从俄罗斯跨境游到中国 当地回应 提醒村民注意安全

5月29日,黑龙江省鹤岗市萝北县有渔民在下江作业时,发现一只黑熊横渡黑龙江,从俄罗斯游到了中国境内。这只黑熊上岸后消失在了山林中。视频内容显示,这只黑熊出现在萝北县太平沟乡太平沟村附近,它在黑龙江里游泳的样子显得十分迅捷。看到渔民驾船靠近时,黑熊原地用熊掌拍击…

成立国际调解院有哪些意义 重塑全球治理话语权

国际调解院的成立旨在重塑全球治理话语权。现有国际机制如海牙国际法院和WTO争端解决机制因程序冗长、成本高昂,难以满足发展中国家需求。国际调解院以“调解优先”为原则,提供低成本、高效率的非对抗性解决方案,填补了发展中国家在争端解决中的制度性空白。通过吸纳60个国家…

联合国拟大幅削减预算并裁员 应对财务困境

多家媒体披露的联合国内部文件显示,联合国秘书处计划削减20%预算,并裁员6900人,约占员工总数的20%。负责财政事务的联合国助理秘书长钱德拉穆利拉马纳坦向各部门负责人发出信函,要求执行秘书长安东尼奥古特雷斯提出的计划,在2026年实现常规预算15%至20%的实质性削减,同时…

数据集的标注

最近在了解数据集及数据集标注的相关知识。 通常情况下一些专业大型的数据集标注都是以xml文件作为标注格式&#xff0c;这篇博客以水下数据集URPC2020为例 这是URPC2020数据集中的第一张图片&#xff1a;海胆&#xff08;英文名&#xff1a;echinus&#xff09; 以下是对这张…

集成设备管理(IDM)

集成设备管理 (IDM) 是 CI-700 上的一项功能&#xff0c;可提高系统的稳健性&#xff0c;从而减少设备重新启动的需要。它通过检测系统中没有响应的设备并尝试从问题中恢复来增加 SoC 的正常运行时间来实现这一点。IDM 将识别、记录和报告因不响应而导致超时的设备。当这种情况…

哈佛校长在毕业典礼上嘲讽特朗普 国际学生获支持

5月29日,美国法官暂停了特朗普政府取消哈佛大学招收外国学生资质的政策,为哈佛大学带来了暂时的胜利。同一天,哈佛大学第374届毕业典礼如期举行。哈佛大学校长艾伦加伯在致辞中欢迎了约9000名毕业生,他说:“致2025届的同学们,你们来自街对面、全国各地以及世界各地。”当…

韩国大选民调落后12% 金文洙会输吗 局势仍存变数

韩国大选临近,6月3日注定是个不平凡的日子。目前局势显示,李在明的支持率领先金文洙,但选举结果未必如数据所显示的那样简单。最近,全国各地都在热议李在明和金文洙,李在明以49.2%的支持率领先,金文洙则为36.8%,两人相差12.4个百分点。另一个候选人李俊锡的支持率为10.3…

流浪狗为报恩帮烧烤店老板看店 奇妙缘分引发关注

这世道真是广阔,无奇不有,像个大杂烩一样。刚开始这家烤鸭铺子老板还没搬走的时候,店门口总有一只邋遢的小土狗偷偷地窝在那里。它看起来像是饿了好几万年,眼睛都快冒星星了盯着新出的烤鸭。老板顺手扔了个鸭尾巴过去,没想到这一举动竟然迎来了跟狗子的奇妙“缘分”。没多…

为何说美断供C919航发反成中国机会 国产化或催生军用潜力

美国商务部中止了向中国商飞出售技术和相关产品的部分许可证,导致C-919大飞机搭载的Leap-1C发动机难以继续供应。这一事件在国内互联网上引发了一些人的担忧,他们认为这可能会对C-919窄体干线客机的量产、使用和销售带来严重影响。然而,从另一个角度来看,这也可能是一个机会…

山东一公司多人落入清洗池致5死 事故原因正调查中

5月30日12时30分许,郓城县一废品回收企业发生一起事故。一名员工在废旧塑料清洗池边作业时不慎跌入池中,六名工友相继施救也跌入池中。事故发生后,当地消防部门和120急救人员迅速赶到现场,将七人紧急送往医院抢救。目前,事故已导致五人死亡,两人情况危重。菏泽市和郓城县…

张雨霏在母校游泳被“预备”晃到了 冠军学姐惊喜现身

5月28日,东南大学的许多学生在学校游泳馆偶遇了奥运冠军张雨霏。原来,张雨霏回到母校拍摄毕业照。当时她正在上游泳课,突然与冠军学姐相遇让学生们感到非常惊喜。张雨霏不仅拍摄了毕业照,还亲自下水示范,学生们纷纷表示“‘霏教练’的冠军大师课太值了!”2022年,张雨霏成…

专家:特朗普关税的命运一天就反转 政策博弈加剧市场动荡

5月28日,美国国际贸易法院裁定禁止执行特朗普政府多个关税行政令。彭博社称,这项裁决是特朗普政府任期内遭遇的最大司法挫折之一。特朗普政府在裁决发布几分钟后就提起了上诉。一天后,联邦巡回上诉法院批准了特朗普政府的请求,暂时中止国际贸易法院的裁决。在这两天的迅速反…

Vert.x学习笔记-WorkerContext中的PoolMetrics要怎么用

在 Vert.x 中&#xff0c;WorkerContext 的 PoolMetrics 用于监控 Worker 线程池的性能指标&#xff08;如任务队列长度、活跃线程数、任务执行耗时等&#xff09;&#xff0c;帮助开发者分析线程池负载和性能瓶颈。 一、PoolMetrics 的核心功能 通过 WorkerPool 的 poolMetri…

非遗IP也开始卷“出海”了 探索跨界新可能

在第九届中国成都国际非物质文化遗产节中,“创意生活”国际非遗品牌IP授权交易活动作为三大特色板块之一,通过多场非遗与不同行业的分享交流及专题推介洽谈,探索“非遗+”的多种可能性。5月29日,在“国际非遗品牌IP授权跨界运用对话”活动现场,以“解锁非遗品牌IP授权密码…

AI转型新范式:道可云推出“AI分阶付费模式”,破解企业AI转型高投入困局

近日&#xff0c;150位全球顶尖AI创始人齐聚红杉AI峰会闭门会议&#xff0c;经过6小时深入探讨达成共识&#xff1a;AI的核心价值已从“工具售卖”跃迁至“收益共创”。这一转变标志着全球AI产业进入“成果经济”时代。 三大趋势重构AI商业版图 **1.定价逻辑重构&#xff1a;…

“中国麻辣烫发源地”官宣揭牌 乐山五通桥获殊荣

5月29日晚,四川乐山五通桥区举办了2025年第二届小西湖美食音乐节活动。中国食品工业协会在活动中为“中国麻辣烫发源地”揭牌。经相关佐证及专家论证,中国食品工业协会认为起源于乐山市五通桥区牛华镇的“牛华麻辣烫”是中国麻辣烫的发源地。据五通桥区文化馆原馆长王中其介绍…