找回密码
 注册
关于网站域名变更的通知
查看: 243|回复: 1
打印 上一主题 下一主题

简单介绍一下linux输入子系统的概念

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2020-6-30 15:15 | 只看该作者 |只看大图 回帖奖励 |正序浏览 |阅读模式

EDA365欢迎您登录!

您需要 登录 才可以下载或查看,没有帐号?注册

x
在此文章之前,我们讲解的都是简单的字符驱动,涉及的内容有字符驱动的框架、自动创建设备节点、linux中断、poll机制、异步通知、同步互斥、非阻塞、定时器去抖动。
* o' [( S: ^7 _9 \9 x3 g  B
% |% r  }; l5 Q; Y. C7 Q: i上一节文章:
& _& B: V: @1 M6 @2 s( Y5 \' C: P* q+ i8 ?2 Q% M( H$ {; B8 l

# ^: T( q# z6 s0 j& u- ^在这一节里,我们要引入linux的分离分层的概念,linux输入子系统是一个很好的代表,在讲解如何编写input子系统的驱动之前,我们理所当然的要先好好认识一下input子系统的框架。& [  a/ v8 P" S( S: K) C' [( G
; }9 q0 ~7 U& z5 Q* i
一、linux输入子系统的框架' r; [7 I! h3 ~$ U

% L; N$ x* ~1 U; n$ x! I下图是input输入子系统框架,输入子系统由输入子系统核心层(Input Core),驱动层和事件处理层(Event Handler)
. P! p3 Z* B$ b4 S5 E" L& o2 _2 }6 W  ]& C
三部份组成。一个输入事件,如鼠标移动,键盘按键按下,joystick的移动等等通过
  D6 _' G0 g4 q
( L% `! P, @  y, c, \& o5 W( yinput driver -> Input core -> Event handler -> userspace 到达用户空间传给应用程序。5 i& _3 D3 p! k: i) W! w

1 j  i  T6 k$ u, ^2 m( z) {
2 l: N+ r3 v) I& k
' I4 k1 v! g/ I6 q2 U二、drivers/input/input.c:
8 K: k- q! l3 D. E+ W6 r
! f& Z9 ]) E! f, L7 P1 H+ R3 l入口函数input_init > err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
: W# Y1 I1 F- e
. t% r1 X% O& c* J9 h+ z& tstatic int __init input_init(void): B, j' V9 h2 z5 {
{# P4 N5 B) }3 @+ d( P) g5 Q6 C
        int err;
% T& Q2 {8 h% a% h2 g        ...
$ G, w/ W7 L* ?* [7 ~7 v9 T& g        /* 创建类 */
# W! Q1 d, O# u+ Z8 X0 o        err = class_register(&input_class);" d5 u8 C8 \' Z$ h; j3 Q1 O2 l
        ...6 E1 C% a5 {0 e6 P4 `+ f! ]9 n
        /* 注册一个字符驱动,主设备号为13 */
6 G4 F4 d! f; G7 o$ j% V        err = register_chrdev(INPUT_MAJOR, "input", &input_fops);
& g8 Q9 `% A1 W- W" L        ...
8 ^# ~5 y. e& P( c        return 0;
& \, L( b7 @3 x7 [0 Y( i}* P# t( N0 I* H4 a
只有一个open函数,其他read,write函数呢?) ?% C3 b$ q! r5 J1 _
static const struct file_operations input_fops = {9 `4 h5 q" u& h' J1 D% H
        .owner = THIS_MODULE,
& U# X$ W) p$ m- Q" t0 A+ ^        .open = input_open_file,
6 @$ w. O4 f* a! v; g7 h1 p};
( E% G+ a( @  d. A4 sinput_open_file函数
6 J/ G. A4 M4 W5 T" }  c& z* D! V" W/ H  `  t# a9 K7 w
static int input_open_file(struct inode *inode, struct file *file)- a  @7 E+ I/ s0 o. E
{4 I2 p/ C& K( X/ }! @5 E  F
        struct input_handler *handler;5 R# \' a! v' Z" N0 V7 t- D
        const struct file_operations *old_fops, *new_fops = NULL;* ~# J) B  Q; G6 K. l0 l" B
        int err;! o! C3 p/ _2 \9 G  z
        ...% e* N9 a' e6 g1 C7 q
        /* 以次设备号为下标,在input_table数组找到一项handler */9 N( U) i! ?( S' ]
        handler = input_table[iminor(inode) >> 5];
3 \! ~5 @4 F9 c3 w/ J: L5 ?       
: k9 k% i- v% h8 u$ l! W        /* 通过handler找到一个新的fops */
/ V9 {9 j) t: f+ d6 V        new_fops = fops_get(handler->fops);% W' s# T$ F  z% I4 R
        ...( f" j. h7 }4 v) N* b
        old_fops = file->f_op;
! p, g+ Y5 W6 m7 `: }        /* 从此file->f_op = new_fops */
9 P) v& l* t, T  a+ o; y        file->f_op = new_fops;
% V  g0 ]6 d* N' k3 r        ...
# `% n) z% t/ v/ c        /* 用新的new_fops的打开函数 */
  M# W  C$ O7 e) [! W4 u        err = new_fops->open(inode, file);
9 A; A9 X5 v! k' `7 i( E* N$ V        ...
9 @5 N; u/ d9 ]" j" y        return err;
& O; c& ?& {3 M; J6 Z' R; Q9 ]}
0 z$ ?' z0 [5 r" J% o6 a3 ~/ E+ sinput_handlerj结构体成员5 e) b9 b7 f+ Z+ N
struct input_handler {( I  f. ?& S! u" \- C
& N* L" s0 H: |1 m
        void *private;) A3 y3 b) z( J! s$ F
- n7 W" [" e& z+ q
        void (*event)(struct input_handle *handle, unsigned int type, unsigned int code, int value);8 \3 u8 {9 `5 O9 _
        int (*connect)(struct input_handler *handler, struct input_dev *dev, const struct input_device_id *id);
( h) P! y1 s3 ]8 N: L        void (*disconnect)(struct input_handle *handle);9 r+ h" c( M" S5 u7 F" U' k: {# T) h
        void (*start)(struct input_handle *handle);6 M" x; Y. r! J' P" p
3 t7 `' d! }  }! v
        const struct file_operations *fops;+ v; Y% u6 ]" N* ?
        int minor;
. s4 n: b& ]' q/ r6 N& j        const char *name;
$ j( s" y0 S3 ?/ w8 h& g( {- ~1 D& \
        const struct input_device_id *id_table;% m0 m( U3 s* g/ M" R
        const struct input_device_id *blacklist;/ z5 d4 B4 l8 ?+ |9 `) X- T
5 L* ~+ M. q. Z1 a
        struct list_head        h_list;; Q0 v; _& N( |6 E* p
        struct list_head        node;
8 p  Z$ u, j4 O" t& n+ _) x};; X  S0 b9 g) |' V: V- _! ^3 b$ u
问:怎么读按键?
% a- ~( @3 P1 z8 e4 o0 [APP:read > ... > file->f_op->read ( P2 o/ h( d/ q, f2 f- T

: W0 y! E& p2 ]问:input_table数组由谁构造?
' S( u4 k0 A3 C* K; K9 ?2 T
4 K6 a$ s, X3 k8 C, L) q9 _答:input_register_handler
) u4 ^. s2 k3 k8 z3 _7 F( a, b- W; C7 p* f3 }# T# ^- _/ M
三、input_register_handler函数(注册input_handler)% a: h4 S# _8 h+ G$ J8 D
6 x6 @! |; e+ a* a3 J
4 D$ Y' X  P0 q% \
int input_register_handler(struct input_handler *handler)2 g# S5 S( \! q- `
{
/ Y$ t. {* [+ V0 r  p) w) q& a6 u        struct input_dev *dev;2 h, m" @8 R% ]8 c
        ...
' E3 w2 ^0 ~* M+ c& O        INIT_LIST_HEAD(&handler->h_list);; h6 X) K! H, W- ~+ H
        ...; F. X) L3 [% |" r1 u& F
        /* 将handler放入input_table数组 */
/ X% t/ c8 w- x        input_table[handler->minor >> 5] = handler;
  r* G8 E/ n$ [0 S( ^$ T" ]$ L+ ^        ..." E# z1 H9 [- R$ ^1 A
        /* 将handler放入input_handler_list链表 */
" M5 @. C+ O& _$ B( h* ]7 T        list_add_tail(&handler->node, &input_handler_list);
8 }, N- f! }. B' K% q& h) j        ...
" r) e7 ?$ H9 j        /* 对于每个input_dev,调用input_attach_handler
2 N/ b, z& @$ B  M3 ?2 z( m         * 根据input_handler的id_table判断能否支持这个input_dev
5 l6 {) a2 N- J" Q         */2 z; W3 G$ N3 D4 O
        list_for_each_entry(dev, &input_dev_list, node)
9 N2 t5 l5 g* p6 D                input_attach_handler(dev, handler);5 q! u2 O* B) I+ ~. V: q
        ...
( T+ X5 f, D5 c' v. w1 R}: _( z5 l" i9 |! d  l4 f$ E/ S
3 l1 b, V( A- H4 v- K* P" I
四、input_register_device函数(注册inout_dev); t% b6 L" T1 B$ [

& s. {+ w7 e( [: f0 v; @int input_register_device(struct input_dev *dev)
- i- Q5 V* z8 H{0 j4 o* h4 O  K; w  Z
        ...1 n$ x$ o& ^& [  \. m0 C, e1 c
        struct input_handler *handler;
/ B) C& R0 i4 f) k# ~) z        ...
2 I2 Z, a3 E% V        device_add(&dev->dev);  k% Z; A, C* i9 w& K
        ...
" S2 C! ?8 M9 O8 o  P5 R$ u        /* 把input_dev放入input_dev_list链表 */
! I! v+ P2 z& S! H8 {        list_add_tail(&dev->node, &input_dev_list);2 Q6 b! o; `& P7 F
        ...
$ h" h8 E' A' g: `        /* 对于每一个input_handler,都调用input_attach_handler& n3 y, }3 W0 C1 ^. j$ I
         * 根据input_handler的id_table判断能否支持这个input_dev
2 T# Y8 c% N$ B' x5 A         */
( X1 J  m7 {$ r9 r1 ^2 F        list_for_each_entry(handler, &input_handler_list, node)7 s. H9 }2 V; w' l
                input_attach_handler(dev, handler);
, X+ F; K/ C7 u- v# g        ...
! ~+ C: l' I+ h$ _$ U& @8 C2 z  S6 g}2 i0 F& Z: u; K3 U0 C+ I
3 |' v$ x" ~( ~' @. E
五、input_attach_handler函数
* @8 D; S2 I6 {static int input_attach_handler(struct input_dev *dev, struct input_handler *handler)
7 T# v$ Y1 y0 x% G{
* ~5 P3 Q+ G% \! c5 m& Q$ @7 o        const struct input_device_id *id;
0 M( S! V5 A" x1 n        ...
; b& J# E/ o; W' ~        /* 根据input_handler的id_table判断能否支持这个input_dev */
2 L) s6 Z8 N8 R( i6 o0 b        input_match_device(handler->id_table, dev);( R2 _# h+ d4 O. r8 c
        ...8 b! v* E, o$ x& n3 v
        /* 若支持,则调用handler的connect函数,建立连接 */
+ [% N/ A% `/ ^  g/ Z/ p: a        handler->connect(handler, dev, id);9 `  X. d- ?# f4 K& d% |
        ...
- Q4 G- b0 t( X5 d; @}
$ G; m7 J% n5 Q
& {0 n+ k3 r2 d小总结:
% b2 w4 I# X6 X8 R7 {- Y! C8 R注册input_dev或input_handler时,会两两比较左边的input_dev和右边的input_handler,根据input_handler的id_table判断这个input_handler能否支持这个input_dev,如果能支持,则调用input_handler的connect函数建立"连接"。
  ^' o% s/ P7 {" O! V1 k: a& Y' G3 }$ b6 x* S" _4 s! S! S
问:如何建立连接connect?
5 Y) M) Q5 w: C% e2 h" \" B* ~
' ^, q; c/ V& E. [7 y7 I答:举例,evdev_connect函数
" M: g/ O9 g, X* t4 g
0 Q8 Y/ t' o1 ?$ q. l
% e8 l% x' Y/ K, Lstatic int evdev_connect(struct input_handler *handler, struct input_dev *dev,) s' j$ V0 _6 m
                         const struct input_device_id *id). E6 J) F0 c0 [2 n) J+ i
{! l* H! Q. p4 t5 z6 T2 [. {' q3 K
        struct evdev *evdev;' u4 w$ U# K7 J: D
        ...
4 t+ c, |0 ?7 v' m6 k% a- c- A& R% d
        /* 分配一个input_handle */
9 N. g( p& A7 L+ n8 M        evdev = kzalloc(sizeof(struct evdev), GFP_KERNEL); # M- B2 ?1 P$ T3 ]# F$ j
        ...
- T" G/ [3 i  x  }, V  g        snprintf(evdev->name, sizeof(evdev->name), "event%d", minor);# Y% ^9 M& Y7 t7 W5 X6 J: H
        evdev->exist = 1;
, W$ @2 u  N5 I0 Y. B* y        evdev->minor = minor;, P6 t# s# W- g4 X

1 y8 K- W8 M. ?4 P' ~        evdev->handle.dev = input_get_device(dev); // 指向左边的input_dev; ?, O  x6 a0 P
        evdev->handle.name = evdev->name;
2 @+ W& b$ {  k3 u- ]        evdev->handle.handler = handler; // 指向右边的input_handler
( T' O# Y* T0 b* V5 l8 Z        evdev->handle.private = evdev;+ g* X7 T4 {7 n0 s2 p
) d  c" X( B5 E0 s4 o+ t
        /* 设置dev结构体成员 */! I. i2 e3 P( Q) ^- S  D& ]
        dev_set_name(&evdev->dev, evdev->name);. {; r, q# d: _! u+ V
        evdev->dev.devt = MKDEV(INPUT_MAJOR, EVDEV_MINOR_BASE + minor);4 @- \/ ?) s: c' a. x
        evdev->dev.class = &input_class;
  y/ @' P; H4 t; I& y        evdev->dev.parent = &dev->dev;
* J+ b1 s  M3 E. `& t' ^        evdev->dev.release = evdev_free;
) Y3 H5 @' b- A# \5 c        device_initialize(&evdev->dev);
$ @- L* J" v/ h. \$ ~
, T* o. i' _( V* m3 O8 b/ N: b        /* 注册 */
9 W. G% m4 G) C* u. H        input_register_handle(&evdev->handle);
" @; O% X$ b) Q& P1 b; ?        ...
! i2 b& ~$ m; p# t, y% [7 M; A}4 l+ _7 R! Y9 ?, c
input_handle结构体成员
+ h/ p9 T, E/ m, ]" w, v4 @0 {2 `
struct input_handle {/ a) `; V( @( r1 X! X* L# P
% w4 }$ y, [3 U5 c1 h+ L
        void *private;
: y! _$ b8 G- ]! w  [8 Y; c$ i: @
& `. ~3 ^; s9 y: a" M        int open;; M( W* ]# C, Q, A: H+ P; t5 O
        const char *name;; }0 d& t( O) p
  S) m6 J& D/ [- d% P) U! D
        struct input_dev *dev;& H2 n! q! a3 d* S
        struct input_handler *handler;: c/ Q6 e+ K& g6 Y/ c
. ~$ z) r9 n# V" c
        struct list_head        d_node;3 x' @( G8 z6 f. j5 E
        struct list_head        h_node;" `& _/ e! P7 c) ~3 ?, `5 v9 ?: N6 v8 s
};
" P# M) u) _( s8 M, A  m问:input_register_handle如何注册?
- ~- Q! M& T1 L* y
* r+ j0 b2 a8 e" `  qint input_register_handle(struct input_handle *handle)
6 p, l; N6 d: p1 d" i9 E1 l{
8 R1 T. T; \  A; P        struct input_handler *handler = handle->handler;; v8 E4 N7 N8 H, o: y- V0 Y
        struct input_dev *dev = handle->dev;
. h9 v( f$ x) O8 n- t" z2 _        ...
9 f) m2 ?; c+ |; `. q        8 m; h2 Y9 d1 M1 C
        /* 把handle->d_node添加到dev->h_list/ w0 ?  ~! [  w. F0 v! \9 y0 v
         * 这样,就可以从dev->h_list找到handle,进而找到handler" ?# n; }+ \5 n
         */
# ?/ h' {- n2 B! O1 ^) W  k( R+ Y, v1 b        list_add_tail_rcu(&handle->d_node, &dev->h_list);; p) c0 S/ a! T( i. G' F
        ...' l, p: n0 g1 |' x8 y  }( y

. e" r  z+ B5 o) v' a  j/ T        /* 把handle->h_node添加到handler->h_list ' \7 Z% L$ |, z7 U
         * 这样,就可以从handler->h_list找到handle,进而找到dev) R8 G# U& U, Y8 W/ _
         */
0 a3 }2 Q9 r, G3 f        list_add_tail(&handle->h_node, &handler->h_list);3 G: P' `* u0 r9 F* t9 L
        ...
+ i( _( w& `* |; I4 u$ d        return 0;
+ T8 R; f+ A/ K5 b& X, C: r, a}
2 U$ v7 i/ f+ L7 e" J- t小总结:
" ]7 K; _; `+ \! O怎么建立连接connect?8 I% p5 j$ L) a! j
1. 分配一个input_handle结构体, Q7 I: D# y; f" |5 y, e
2. # z- \, m! X9 s. n2 z  E; U9 q+ \
input_handle.dev = input_dev;  // 指向左边的input_dev
1 k! l/ k" [& D8 ^input_handle.handler = input_handler;  // 指向右边的input_handler
* r9 M; ?3 }5 W' u( G3. 注册:0 d# \+ L# E3 c! h
   input_handler->h_list = &input_handle;4 |9 M6 v0 {! K, O. S% Y- N& U% c
   inpu_dev->h_list      = &input_handle;
7 w% X  W: A+ }* w1 T% l* H
  v8 b( t! P: r8 B六、怎么读按键?, R4 q" P6 F$ j
; s+ [% @$ B0 y
答:举例,evdev_read
+ a, F' Q$ v& c% i1 K2 M
6 j+ W/ @4 Q% ?static ssize_t evdev_read(struct file *file, char __user *buffer,
8 R: s# z: d% x* \  l  S0 i$ p                          size_t count, loff_t *ppos)
  Z0 `3 j5 V4 x* U) e* X  O{
8 J% R, }3 W; |' n' o8 r: ~        struct evdev_client *client = file->private_data;% d, W  }) y% N
        struct evdev *evdev = client->evdev;4 O5 ]  i5 i2 I0 Z) K- g0 m: I
        struct input_event event;$ L* Y- w% e" W3 [  f
        ...3 ?/ S& l9 E5 y$ L* O& L

' M7 H* x- }* @" T        /* 无数据并且是非阻塞方式打开,则立刻返回 */( B; c0 a. N3 ?
        if (client->head == client->tail && evdev->exist &&- b/ i# g; @! ], I0 }
            (file->f_flags & O_NONBLOCK)), {. ?) J% ]' G
                return -EAGAIN;
: n) w, t1 N9 K: c4 K+ K. c
2 M* y5 U1 y# w1 V        /* 否则休眠 */9 I  i* m5 R3 p
        retval = wait_event_interruptible(evdev->wait,2 R% W3 c# k* {
                client->head != client->tail || !evdev->exist);
8 n0 N5 {( [- _# {        ...8 `; @* G4 M  j# U
}& i  I6 p) w7 a/ \1 N; ]6 {
问:谁来唤醒?1 n( H4 R& U1 }8 [- C4 D
搜索evdev->wait发现是evdev_event唤醒的. ~2 n4 O& S+ k

7 R* x7 }2 f" W7 o9 B/ A" Y( s  rstatic void evdev_event(struct input_handle *handle,
8 ~! I- ^5 o- C/ u2 W4 ?: }2 a                        unsigned int type, unsigned int code, int value)
+ D  V* b4 V7 c. x& H{+ X. u3 r1 L% y" L, o* d$ M- H
        struct evdev *evdev = handle->private;
( Q6 N, l% X  ]1 `% U' l        struct evdev_client *client;
& S6 d( e/ P6 b- }( e/ X; v) m        struct input_event event;9 ~- p) r( U- [; ]
        ...
: U7 p2 c( r" p. F3 X& T9 T- w        /* 唤醒 */) W! P$ i: w) f
        wake_up_interruptible(&evdev->wait);
$ N3 H& ?+ q- @# J) r$ g}' E  M: Z1 T: `5 O# D1 `" W
问:evdev_event被谁调用?+ K! @2 n" x9 z/ P3 F( B
答:应该是硬件相关的代码,input_dev那层调用的在设备的中断服务程序里,确定事件是什么,然后调用相应的input_handler的event处理函数。
% ~; C. d8 y; f% A/ X8 @6 f: Z9 k/ O7 I( s0 s& q% L8 d
举例,在drivers/input/keyboard/gpio_keys.c里的gpio_keys_isr函数9 D' G( t' d8 e) ~! [  r; H. W
  o& O7 G9 L7 ?  v- A
static irqreturn_t gpio_keys_isr(int irq, void *dev_id)
& w. P; o' D9 p$ ~" {3 D{/ S' v% }' e! g/ A; a
        struct gpio_button_data *bdata = dev_id;$ L3 D! I, w' m  _) O; O" h
        struct gpio_keys_button *button = bdata->button;
; V$ N. k. J0 l$ s3 f  a6 |        ...
( k" p# O; z  `8 r2 }+ r3 S        /* 上报事件 */
& I4 f9 {3 L7 D/ g+ f        gpio_keys_report_event(bdata);1 _! [  g. z  a( w1 |# O
        return IRQ_HANDLED;8 u+ ^# I5 R" v
}
8 k1 W, {5 l% m& vgpio_keys_report_event函数
, `6 \1 z8 @1 j: G- P# ]1 X. W, U5 x
9 k2 y2 H% g  {$ fstatic void gpio_keys_report_event(struct gpio_button_data *bdata)
1 o, {& z! f+ q$ D1 @{, j4 C( F# T2 L: f& i& F
        struct gpio_keys_button *button = bdata->button;" M4 p* I* a3 }1 t, J
        struct input_dev *input = bdata->input;
3 i" |- Y4 v% S; Y0 |- V6 u/ I; n        unsigned int type = button->type ?: EV_KEY;# q3 w" `9 {: g7 G% C( a) B, D
        int state = (gpio_get_value(button->gpio) ? 1 : 0) ^ button->active_low;( Y2 c- z4 M- o  p0 v
7 W# [  x$ b0 l" k0 P4 O% F
        /* 上报事件 */: K- k2 o' Z8 T  j# G
        input_event(input, type, button->code, !!state);
2 n( ?2 z, }/ O3 ]7 Z) U3 I( J        input_sync(input);
7 O, z6 ?/ ^' W5 i2 P}
5 r' u+ `* W5 _. r  S问:input_event函数如何上报事件6 k& \: a% U. e- D" T5 \6 O3 Y

$ q  g$ H; W' N# @/ z答:& r3 U' |: z% e# Q: g
9 ~- f) A7 Q5 r7 t3 P
input_event-->input_handle_event-->input_pass_event# Q5 q$ w1 n# p) m1 g" [
list_for_each_entry_rcu(handle, &dev->h_list, d_node)
/ T# R) W% ^8 t1 P, ]if (handle->open)7 J* O# f$ p" n3 ^
handle->handler->event(handle,
9 X! |1 E" ~, c- ?3 ]4 b7 Ntype, code, value);
% }6 F' ?" n' F+ u怎么写符合输入子系统框架的驱动程序?. O3 B- z5 p7 i$ `
/ X8 A8 N! t9 O3 C2 X) u3 ^7 j* v) p
1. 分配一个input_dev结构体
$ h! I2 ?* G; `0 b1 d2. 设置
# ^& m) O  I9 N8 R3 s3 \+ K3. 注册& A8 L: B0 X) O  k* v. ]
4. 硬件相关的代码,比如在中断服务程序里上报事件
" v, x" z; q6 H! D6 a- R0 Z" ~9 H& h$ G
% F( h* I* q/ g. Z( q6 n" W3 T' F0 B
3 ~5 C- |' [$ ]3 L4 y! k
' J$ s1 n% ?6 G" [' m& O

8 Y3 W" x! O  A" W( V

该用户从未签到

2#
发表于 2020-6-30 16:33 | 只看该作者
linux输入子系统
您需要登录后才可以回帖 登录 | 注册

本版积分规则

关闭

推荐内容上一条 /1 下一条

EDA365公众号

关于我们|手机版|EDA365电子论坛网 ( 粤ICP备18020198号-1 )

GMT+8, 2025-11-25 16:36 , Processed in 0.203125 second(s), 27 queries , Gzip On.

深圳市墨知创新科技有限公司

地址:深圳市南山区科技生态园2栋A座805 电话:19926409050

快速回复 返回顶部 返回列表