公告:缅怀Dennis Ritchie活动开赛啦!
小白的博客——提升自已,分享别人

xiaobai.blog.chinaunix.net

好的女人可以不共享。好的技术绝对要共享!

首页 | 博文目录 | 相册 | 博客圈 | 关于我 | 留言

个人资料
diytvgy
微博论坛
发纸条打招呼加关注加好友
  • 博客访问:74424
  • 博文数量:42
  • 博客积分:687
  • 博客等级:中校
  • 关注人气: 4
  • 注册时间:2010-12-01 18:55:26

文章分类
全部博文(42)
linux内核相关知识(1)
闲话(3)
linux内核与设备驱动(34)
ubuntu(2)
未分类博文(2)

订阅我的博客
  • 订阅
  • 订阅到鲜果
  • 订阅到抓虾
  • 订阅到Google
好友
  • tekkama

  • 小雅贝贝

  • luozhiy

  • Knivo

  • embedtek

  • sillybo

  • wang2kk

  • 嵌入小凯

  • send_li

  • CU官方博

  • songtao

  • lxhhust

最近来访
  • fh265
    1小时前

  • shenhai
    10月30日

  • a275532
    10月14日

  • Knivo
    10月13日

  • 10月13日

  • lxr215
    10月10日

  • luozhiy
    10月9日

  • chafe
    10月8日

  • mournju
    10月7日

  • zotozo
    10月7日

  • high_way
    10月7日

  • wangxin
    10月4日

字体大小:大 中 小博文
linux设备驱动归纳总结(六):3.中断下半部之tasklet (2011-01-23 15:03)转载
标签:  linux  设备驱动  下半部  软中断  tasklet  分类: 6中断

linux设备驱动归纳总结(六):3.中断的上半部和下半部——tasklet

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

一、什么是下半部

中断是一个很霸道的东西,处理器一旦接收到中断,就会打断正在执行的代码,调用中断处理函数。如果在中断处理函数中没有禁止中断,该中断处理函数执行过程中仍有可能被其他中断打断。出于这样的原因,大家都希望中断处理函数执行得越快越好

另外,中断上下文中不能阻塞,这也限制了中断上下文中能干的事。

基于上面的原因,内核将整个的中断处理流程分为了上半部和下半部。上半部就是之前所说的中断处理函数,它能最快的响应中断,并且做一些必须在中断响应之后马上要做的事情。而一些需要在中断处理函数后继续执行的操作,内核建议把它放在下半部执行。

拿网卡来举例,在linux内核中,当网卡一旦接受到数据,网卡会通过中断告诉内核处理数据,内核会在网卡中断处理函数(上半部)执行一些网卡硬件的必要设置,因为这是在中断响应后急切要干的事情。接着,内核调用对应的下半部函数来处理网卡接收到的数据,因为数据处理没必要在中断处理函数里面马上执行,可以将中断让出来做更紧迫的事情。

可以有三种方法来实现下半部:软中断、tasklet和等待队列。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

二、软中断

软中断一般很少用于实现下半部,但tasklet是通过软中断实现的,所以先介绍软中断。字面理解,软中断就是软件实现的异步中断,它的优先级比硬中断低,但比普通进程优先级高,同时,它和硬中断一样不能休眠

软中断是在编译时候静态分配的,要用软中断必须修改内核代码。

kernel/softirq.c中有这样的一个数组:

51static struct softirq_action softirq_vec[NR_SOFTIRQS]__cacheline_aligned_in_smp;

内核通过一个softirq_action数组来维护的软中断NR_SOFTIRQS是当前软中断的个数,待会再看他在哪里定义。

先看一下softirq_action结构体:

/*include/linux/interrupt.h*/

265 struct softirq_action

266 {

267 void (*action)(struct softirq_action *); //软中断处理函数

268 };

一看发现,结构体里面就一个软中断函数,他的参数就是本身结构体的指针。之所以这样设计,是为了以后的拓展,如果在结构体中添加了新成员,也不需要修改函数接口。在以前的内核,该结构体里面还有一个data的成员,用于传参,不过现在没有了。

接下来看一下如何使用软中断实现下半部

一、要使用软中断,首先就要静态声明软中断:

/*include/linux/interrupt.h*/

246 enum

247 {

248 HI_SOFTIRQ=0, //用于tasklet的软中断,优先级最高,为0

249 TIMER_SOFTIRQ, //定时器的下半部

250 NET_TX_SOFTIRQ, //发送网络数据的软中断

251 NET_RX_SOFTIRQ, //接受网络数据的软中断

252 BLOCK_SOFTIRQ,

253 TASKLET_SOFTIRQ, //也是用于实现tasklet

254 SCHED_SOFTIRQ,

255 HRTIMER_SOFTIRQ,

256 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

257 //add by xiaobai 2011.1.18

258 XIAOBAI_SOFTIRQ, //这是我添加的,优先级最低

259

260 NR_SOFTIRQS, //这个就是上面所说的软中断结构体数组成员个数

261 };

上面通过枚举定义了NR_SOFTIRQS(10)个软中断的索引号,优先级最高是0HI_SOFTIRQ),最低是我刚添加上去的XIAOBAI_SOFTIRQ,优先级为9

二、定义了索引号后,还要注册处理程序。

通过函数open_sofuirq来注册软中断处理函数,使软中断索引号与中断处理函数对应。该函数在kernel/softirq.c中定义:

/*kernel/softirq.c */

321 void open_softirq(int nr, void (*action)(struct softirq_action *))

322 {

323 softirq_vec[nr].action = action;

324 }

其实该函数就是把软中断处理函数的函数指针存放到对应的结构体中,一般的,我们自己写的模块是不能调用这个函数的,为了使用这个函数,我修改了内核:

322 void open_softirq(int nr, void (*action)(struct softirq_action *))

323 {

324 softirq_vec[nr].action = action;

325 }

326 EXPORT_SYMBOL(open_softirq); //这是我添加的,导出符号,这样我编写的程序就能调用

在我的程序中如下调用:

/*6th_irq_3/1st/test.c*/

13 void xiaobai_action(struct softirq_action *t) //软中断处理函数

14 {

15 printk("hello xiaobai!\n");

16 }

。。。。。。。。

48 open_softirq(XIAOBAI_SOFTIRQ, xiaobai_action);

三、在中断处理函数返回前,触发对应的软中断。

在中断处理函数完成了必要的操作后,就应该调用函数raise_sotfirq触发软中断,让软中断执行中断下半部的操作。

/*kernel/softirq.c*/

312 void raise_softirq(unsigned int nr)

313 {

314 unsigned long flags;

315

316 local_irq_save(flags);

317 raise_softirq_irqoff(nr);

318 local_irq_restore(flags);

319 }

所谓的触发软中断,并不是指马上执行该软中断,不然和在中断上执行没什么区别。它的作用只是告诉内核:下次执行软中断的时候,记得执行我这个软中断处理函数。

当然,这个函数也得导出符号后才能调用:

/*kernel/softirq.c*/

312 void raise_softirq(unsigned int nr)

313 {

314 unsigned long flags;

315

316 local_irq_save(flags);

317 raise_softirq_irqoff(nr);

318 local_irq_restore(flags);

319 }

320 EXPORT_SYMBOL(raise_softirq);

在我的程序中如下调用:

/*6th_irq_3/1st/test.c*/

18 irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数

19 {

20 printk("key down\n");

21 raise_softirq(XIAOBAI_SOFTIRQ);

22 return IRQ_HANDLED;

23 }

经过三步,使用软中断实现下半部就成功了,看一下完整的函数:

/*6th_irq_3/1st/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <linux/interrupt.h>

5

6 #define DEBUG_SWITCH 1

7 #if DEBUG_SWITCH

8 #define P_DEBUG(fmt, args...) printk("<1>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args)

9 #else

10 #define P_DEBUG(fmt, args...) printk("<7>" "<kernel>[%s]"fmt, __FUNCTI ON__, ##args)

11 #endif

12

13 void xiaobai_action(struct softirq_action *t) //软中断处理函数

14 {

15 printk("hello xiaobai!\n");

16 }

17

18 irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数

19 {

20 printk("key down\n");

21 raise_softirq(XIAOBAI_SOFTIRQ); //触发软中断

22 return IRQ_HANDLED;

23 }

24

25 static int __init test_init(void) //模块初始化函数

26 {

27 int ret;

28

29 /*注册中断处理函数:

30 * IRQ_EINT1:中断号,定义在"include/mach/irqs.h"

31 * irq_handler:中断处理函数

32 * IRQ_TIRGGER_FALLING:中断类型标记,下降沿触发中断

33 * ker_INT_EINT1:中断的名字,显示在/proc/interrupts等文件中

34 * NULL;现在我不使用dev_id,所以这里不传参数

35 */

36 ret = request_irq(IRQ_EINT1, irq_handler,

37 IRQF_TRIGGER_FALLING, "key INT_EINT1", NULL);

38 if(ret){

39 P_DEBUG("request irq failed!\n");

40 return ret;

41 }

42

43 /*fostirq*/

44 open_softirq(XIAOBAI_SOFTIRQ, xiaobai_action); //注册软中断处理程序

45

46 printk("hello irq\n");

47 return 0;

48 }

49

50 static void __exit test_exit(void) //模块卸载函数

51 {

52 free_irq(IRQ_EINT1, NULL);

53 printk("good bye irq\n");

54 }

55

56 module_init(test_init);

57 module_exit(test_exit);

58

59 MODULE_LICENSE("GPL");

60 MODULE_AUTHOR("xoao bai");

61 MODULE_VERSION("v0.1");

注意。在上面的程序,只是为了说明如何实现上下半步,而我的中断上下半步里面的操作是毫无意义的(只是打印)。上下半步的作用我在一开始就有介绍。

接下来验证一下:

[root: 1st]# insmod test.ko

hello irq

[root: 1st]# key down //上半部操作

hello xiaobai! //下半部操作

key down

hello xiaobai!

key down

hello xiaobai!

[root: 1st]# rmmod test

good bye irq

上面介绍,触发软中断函数raise_softirq并不会让软中断处理函数马上执行,它只是打了个标记,等到适合的时候再被实行。如在中断处理函数返回后,内核就会检查软中断是否被触发并执行触发的软中断。

软中断会在do_softirq中被执行,其中核心部分在do_softirq中调用的__do_softirq中:

/*kernel/softirq.c*/

172 asmlinkage void __do_softirq(void)

173 {

。。。。。。

194 do {

195 if (pending & 1) { //如果被触发,调用软中断处理函数

196 int prev_count = preempt_count();

197

198 h->action(h); //调用软中断处理函数

199

200 if (unlikely(prev_count != preempt_count())) {

201 printk(KERN_ERR "huh, entered softirq %td %p"

202 "with preempt_count %08x,"

203 " exited with %08x?\n", h - softirq_vec,

204 h->action, prev_count, preempt_count());

205 preempt_count() = prev_count;

206 }

207

208 rcu_bh_qsctr_inc(cpu);

209 }

210 h++; //下移,获取另一个软中断

211 pending >>= 1;

212 } while (pending); //大循环内执行,知道所有被触发的软中断都执行完

。。。。。。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

三、tasklet

上面的介绍看到,软中断实现下半部的方法很麻烦,一般是不会使用的。一般,我们使用tasklet——利用软中断实现的下半部机制

在介绍软中断索引号的时候,有两个用于实现tasklet的软中断索引号:HI_SOFTIRQTASKLET_SOFTIRQ。两个tasklet唯一的区别就是优先级的大小,一般使用TAKSLET_SOFTIRQ

先看一下如何使用tasklet,用完之后再看内核中是如何实现的:

步骤一、编写tasklet处理函数,定义并初始化结构体tasklet_struct

内核中是通过tasklet_struct来维护一个tasklet,介绍一下tasklet_struct里面的两个成员:

/*linux/interrupt.h*/

319 struct tasklet_struct

320 {

321 struct tasklet_struct *next;

322 unsigned long state;

323 atomic_t count;

324 void (*func)(unsigned long); //tasklet处理函数

325 unsigned long data; //给处理函数的传参

326 };

所以,在初始化tasklet_struct之前,需要先写好tasklet处理函数,如果需要传参,也需要指定传参,你可以直接传数据,也可以传地址。我定义的处理函数如下:

/*6th_irq_3/2nd/test.c*/

15 void xiaobai_func(unsigned long data)

16 {

17 printk("hello xiaobai!, data[%d]\n", (int)data); //也没干什么事情,仅仅打印。

18 }

同样,可以通过两种办法定义和初始化tasklet_struct

1、静态定义并初始化

/*linux/interrupt.h*/

328 #defineDECLARE_TASKLET(name, func, data)\

329 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

330

331 #defineDECLARE_TASKLET_DISABLED(name, func, data)\

332 struct tasklet_struct name = { NULL, 0, ATOMIC_INIT

上面两个函数都是定义一个叫nametasklet_struct,并指定他的处理函数和传参分别是funcdata。唯一的区别是,DCLARE_TASKLET_DISABLED初始化后的处于禁止状态,暂时不能被使用。

2、动态定义并初始化

跟以往的一样,需要先定义结构体,然后把结构体指针传给tasklet_init来动态初始化:

/*kernel/softirq.c*/

435 void tasklet_init(struct tasklet_struct *t,

436 void (*func)(unsigned long), unsigned long data)

在我的程序中,使用动态定义并初始化:

/*6th_irq_3/2nd/test.c*/

13 struct tasklet_struct xiaobai_tasklet; //定义tasklet结构体

32 tasklet_init(&xiaobai_tasklet, xiaobai_func, (unsigned long)123);

我这里的传参直接传一个数值123。这操作也相当于:

DECLEAR_TASKLET(xiaobai_tasklet, xiaobai_func, (unsigned long)123);

步骤二、在中断返回前调度tasklet

跟软中断一样(其实tasklet就是基于软中断实现),这里说的调度并不是马上执行,只是打个标记,至于什么时候执行就要看内核的调度。

调度使用函数tasklet_schedule或者tasklet_hi_schedule,两个的区别是一个使用TASKLET_SOFTIRQ,另一个使用HI_SOFTIRQ。这两个函数都是一tasklet_struct指针为参数:

/*linux/interrupt.h*/

365 static inline void tasklet_schedule(struct tasklet_struct *t)

373 static inline void tasklet_hi_schedule(struct tasklet_struct *t)

在我的函数中,使用tasklet_schedule

/*6th_irq_3/2nd/test.c*/

23 tasklet_schedule(&xiaobai_tasklet);

步骤三、当模块卸载时,将tasklet_struct结构体移除:

/*kernel/softirq.c*/

447 void tasklet_kill(struct tasklet_struct *t)

确保了tasklet不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果tasklet正在运行,程序会休眠,等待直到它执行完毕

另外,还有禁止与激活tasklet的函数。被禁止的tasklet不能被调用,直到被激活:

/*linux/interrupt.h*/

386 static inline void tasklet_disable(struct tasklet_struct *t) //禁止

393 static inline void tasklet_enable(struct tasklet_struct *t) //激活

最后附上程序:

/*6th_irq_3/2nd/test.c*/

1 #include <linux/module.h>

2 #include <linux/init.h>

3

4 #include <linux/interrupt.h>

5

。。。。省略。。。。

13 struct tasklet_struct xiaobai_tasklet; //定义tasklet结构体

14

15 void xiaobai_func(unsigned long data)

16 {

17 printk("hello xiaobai!, data[%d]\n", (int)data);

18 }

19

20 irqreturn_t irq_handler(int irqno, void *dev_id) //中断处理函数

21 {

22 printk("key down\n");

23 tasklet_schedule(&xiaobai_tasklet);

24 return IRQ_HANDLED;

25 }

26

27 static int __init test_init(void) //模块初始化函数

28 {

29 int ret;

30

31 /*tasklet*/

32 tasklet_init(&xiaobai_tasklet, xiaobai_func, (unsigned long)123);

33

41 ret = request_irq(IRQ_EINT1, irq_handler,

42 IRQF_TRIGGER_FALLING, "key INT_EINT1", NULL);

43 if(ret){

44 P_DEBUG("request irq failed!\n");

45 return ret;

46 }

47

48 printk("hello irq\n");

49 return 0;

50 }

51

52 static void __exit test_exit(void) //模块卸载函数

53 {

54 tasklet_kill(&xiaobai_tasklet);

55 free_irq(IRQ_EINT1, NULL);

56 printk("good bye irq\n");

57 }

58

59 module_init(test_init);

60 module_exit(test_exit);

最后验证一下,还是老样子,上下半步只是打印一句话,没有实质操作:

[root: 2nd]# insmod test.ko

hello irq

[root: 2nd]# key down //上半部操作

hello xiaobai!, data[123] //下半部操作

key down

hello xiaobai!, data[123]

[root: 2nd]# rmmod test

good bye irq

既然知道怎么使用tasklet,接下来就要看看它是怎么基于软中断实现的

上面说明的是单处理器的情况下,如果是多处理器,每个处理器都会有一个tasklet_vectasklet_hi_vec链表,这个情况我就不介绍了。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

四、总结

这节介绍了如何通过软中断(tasklet也是软中断的一种实现形式)机制来实现中断下半部。使用软中断实现的优缺点很明显:

优点:运行在软中断上下文,优先级比普通进程高,调度速度快。

缺点:由于处于中断上下文,所以不能睡眠。

也许有人会问,那软中断和tasklet有什么区别?

个人理解,tasklet是基于软中断实现的,基本上和软中断相同。但有一点不一样,如果在多处理器的情况下,内核不能保证软中断在哪个处理器上运行(听起来像废话),所以,软中断之间需要考虑共享资源的保护。而tasklet,内核可以保证,两个同类型(TASKLET_SOFTIRQHI_SOFTIRQ)的tasklet不能同时执行,那就说明,同类型tasklet之间,可以不考虑同类型tasklet之间的并发情况。

一般的,优先考虑使用tasklet

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

源代码: 6th_irq_3(1).rar

分享到:新浪微博QQ空间开心网豆瓣人人网twitterfb


   

阅读(975)┊ 评论 (1)┊收藏(0)┊举报┊打印
前一篇:linux设备驱动归纳总结(六):2.分享中断号
[发评论] 评论 重要提示:警惕虚假中奖信息!
  • 回复 举报

    songtao0728 2011-08-09 15:10

    请教一个问题:
    我在按你的方面试验软中断,编译通过后,下载到开发板后,加载出现:
    soft: Unknown symbol raise_softirq
    soft: Unknown symbol open_softirq
    insmod: cannot insert 'soft.ko':unknown symbol in module or invalid parameter
    这是不是因为我的开发板的系统没有这两个函数的导出符号所导致的?
    我如果不重新编译开发板的内核的话,是不是还有别的办法解决?
发评论
验证码:  更换一张

linux设备驱动归纳总结(六):3.中断下半部之tasklet相关推荐

  1. linux块设备驱动中断程序,linux设备驱动归纳总结(六):1.中断的实现

    linux设备驱动归纳总结(六):1.中断的实现 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  2. linux设备驱动归纳总结(六):2.分享中断号【转】

    linux设备驱动归纳总结(六):2.分享中断号 转自:http://blog.chinaunix.net/uid-25014876-id-90837.html xxxxxxxxxxxxxxxxxxx ...

  3. 【Linux开发】linux设备驱动归纳总结(六):3.中断的上半部和下半部——tasklet...

    linux设备驱动归纳总结(六):3.中断的上半部和下半部--tasklet xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  4. linux设备驱动归纳总结

    前言: (总结已经基本写完,这段时间我会从新排版和修正.错误总会有的,望能指正!) 前段时间学习了嵌入式驱动,趁着没开始找工作,这段时间我会每天抽出时间来复习. 我的总结是根据学习时的笔记(李杨老师授 ...

  5. 【Linux开发】linux设备驱动归纳总结(四):5.多处理器下的竞态和并发

    linux设备驱动归纳总结(四):5.多处理器下的竞态和并发 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  6. linux设备驱动归纳总结(四):1.进程管理的相关概念【转】

    本文转载自;http://blog.chinaunix.net/uid-25014876-id-64866.html linux设备驱动归纳总结(四):1.进程管理的相关概念 xxxxxxxxxxxx ...

  7. 【Linux开发】linux设备驱动归纳总结(十二):简单的数码相框

    linux设备驱动归纳总结(十二):简单的数码相框 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...

  8. linux设备驱动归纳总结--转载小白的博客

    主要是为了自己浏览方便... linux设备驱动归纳总结 linux设备驱动归纳总结(一):内核的相关基础概念 linux设备驱动归纳总结(二):模块的相关基础概念 linux设备驱动归纳总结(三): ...

  9. linux设备驱动归纳总结(十二):简单的数码相框

    http://blog.chinaunix.net/uid-25014876-id-116926.html 其实代码很简单,实现lcd驱动,使lcd能够显示图片,当按下按键后切换图片. 先要说明一下几 ...

最新文章

  1. php5.6 64位配置,centos(64位) 安装PHP5.6,配置LNMP
  2. Hadoop 运行jar包时 java.lang.ClassNotFoundException: Class com.zhen.mr.RunJob$HotMapper not found...
  3. Qt之QStackedWidget
  4. JAVA语法——经典题目01
  5. mft按钮设计_火力发电厂典型MFT逻辑控制实现.pdf
  6. jquery select css样式,css配合jquery美化 select
  7. mysql hibernate 分页查询_Hibernate + MySQL 分页类的实现
  8. ISO14001环境管理体系问答篇
  9. 推荐一个 Linux 刻盘工具 gcdw(转)
  10. 3.0.0 alpha 重磅发布!九大新功能、全新 UI 解锁调度系统新能力
  11. Chapter 1 贝叶斯推断的思想
  12. javaScript 琐碎
  13. PPT 问题 PowerPoint 储存此文件时发生错误
  14. 【MySQL】MySQL常用SQL关键字
  15. 盘一盘那些提效/创意的宝藏网站
  16. 【TCP wrappers】关于/etc/hosts.allow /etc/hosts.deny
  17. android+如何设置单屏壁纸,给你一个设置单屏壁纸的软件
  18. abovedisplayskip无效_latex中页面距离的设置
  19. C语言的C89、C99和C11标准(上)
  20. Go 如何使用session

热门文章

  1. 企业即时通讯软件给企业带来的价值
  2. 删除PowerPoint的备注
  3. 移动硬盘安装Kali所碰到到问题
  4. 苹果二代TWS无线耳机AirPods调研
  5. 262. Trips and Users
  6. 「掘虫者说」The timestamp difference between admin and executor exceeds the limit
  7. 程序员应该多久跳槽一次?为何贵圈跳槽如此频繁?
  8. OSChina 周三乱弹 —— 哽住
  9. 低功耗读卡(RFID)电路与程序实现
  10. 微赛智慧体育从微信入口提供SaaS服务,力求统一体育服务行业标准