首頁 > 軟體

Linux核心DCB子系統

2020-06-16 18:05:01

q1. 網路裝置是怎麼利用linux核心的DCB子系統,來達到融合網路流量的各種各樣的QoS需求的?

q2.融合網絡卡或者儲存流量是否也可以使用到DCB子系統,他們是怎樣工作的?

本文將對上面這兩個問題進行解答;本文首先大體介紹了DCB機制和它的使用環境;然後介紹一個使用DCB的應用程式lldpad的例子;再然後介紹一個DCB子系統中重要的資料結構;最後介紹DCB核心模組和驅動的具體實現。

Overview
首先,DCB是什麼呢?

整個DCB過程,是要把各種各樣的流量,可能是FCoE流量,可能是一般的TCP流量,還可能是其他的視訊流量等等,他們對於QoS的要求各不相同,有的要求不丟包,有的要求頻寬保證等等,但是他們也許都需要從這一個網路介面傳送到他們各自的目的地中去。由於意識到這個需求,在linux核心中,2.4?或者更早的版本中加入了TC(流量分類)模組,用來對不同的流量型別進行不同的處理。之前說到驅動模組是網路介面的agent,網路裝置很可能也和核心一樣,對於不同的流量型別也有不同的QoS處理,不過如果網路裝置沒有這個多佇列和相關的處理,那麼也不影響QoS的處理,只不過不能提速,在網絡卡這一塊可能會成為瓶頸。使用了多個佇列的網絡卡,在內部可能有一個處理器來進行這些複雜的多佇列處理,然後驅動的DCB程式碼部分很可能需要完成TC對映的功能。但是如果說網絡卡沒有多佇列,核心的QoS任然可以發揮它的作用(當然要消耗CPU是必須的啦)。

驅動是網絡卡的agent,就相當於網絡卡是個聾啞人,主機是不能和網絡卡說話的,通過驅動,網絡卡和主機能夠進行互動。主機說要從這介面傳送包出去,於是呼叫驅動的傳送函數,把包傳送出去。如果網絡卡物理層收到包,則通過驅動的中斷函數來處理這些包(為了提高效率,現在也有可能使用到NAPI)。本來傳送和接受都好說,當然是用最快的速度處理發包和收包就好。但是當流量型別漸漸複雜的時候,就要針對不同的流量進行不同的處理。比如說我們的主機上有飛機,坦克,自行車,當都必須經過這個介面出去時,我們按照飛機的優先順序最高,自行車優先順序最低,只有當飛機和坦克都過去了之後,自行車才能夠通行(我們的主機記憶體很多,給飛機一個隊,坦克一個隊,自行車一個隊)。如果這個介面很寬,我們還可以把這個介面設定幾個佇列,要出去的直接在介面上拍隊(當然也是先通過記憶體的平滑緩衝)。道路上認為一次只能通過一個東西。

使用者的流量特徵我們可以通過lldpad設定,設定了這個值,而且設定的而這些資訊是要和對端交換機進行互動的,於是通過那個介面發出去,設定的這個值是要和核心互動的,當核心協定棧收到這些資訊的時候,需要設定相應的佇列和演算法(同時還要獲取硬體資訊,如果有必要還要把tc佇列對映到網絡卡佇列),和驅動互動的另外一個原因很可能是因為dcb-lldp需要使用驅動本身要傳遞的網絡卡DCB資訊和要求。

關於儲存流量,比如FCoE流量,是不會經過核心的Qdisc的,他經過的是FcoE模組,在FCoE模組中也是會使用DCB模組的額,從而達到和Qdisc一樣的目的?

Q.FCoE模組為何也要用到dcb模組中的函數?

A.FCoE模組作為FCoE協定的處理模組,需要設定相應的FCoE型別和DCB型別的包的DCB引數。(這是不是假如它不再經過802.1p網路層次?)

Q.發揮了什麼作用?

A.獲取了FCoE和FIP型別的的優先順序

Up(FCoE優先順序);Fup(FIP優先順序)

static void fcoe_dcb_create(struct fcoe_interface *fcoe)

{

#ifdef CONFIG_DCB

    int dcbx;

  u8 fup, up;

  struct net_device *netdev = fcoe->realdev;

  struct fcoe_port *port = lport_priv(fcoe->ctlr.lp);

        struct dcb_app app = {

                                .priority = 0,

                        .protocol = ETH_P_FCOE

                        };


    ...


                        app.selector = DCB_APP_IDTYPE_ETHTYPE;

                    up = dcb_getapp(netdev, &app);

                      app.protocol = ETH_P_FIP;

              fup = dcb_getapp(netdev, &app);

              }


                port->priority = ffs(up) ? ffs(up) - 1 : 0;

fcoe->ctlr.priority = ffs(fup) ? ffs(fup) - 1 : port->priority;


 

 

除了DCB初始化函數,fcoe_dcb_create;核心提供了DCB,NET和CPU通知鏈註冊/登出和工作佇列的介面函數。

 

 

LLDPAD

這個應用程式是用來設定Intel網路裝置的DCB特性的。

Listed below are the applicablestandards:

 Enhanced Transmission Selection: IEEE 802.1Qaz

 Lossless Traffic Class

  Priority Flow Control: IEEE 802.1Qbb

  Congestion Notification: IEEE 802.1Qau

 DCB Capability exchange protocol (DCBX): IEEE 802.1Qaz

那麼怎麼使用lldpad設定DCB特性呢?比如使用使用者介面lldpad。


Dcbtool
Dcbtool可以用來查詢和設定DCB乙太網介面的DCB特性。通用的命令有gc, sc等。(對應的DCB模組的get和set函數),主要可以參考https://github.com/jrfastab/lldpad比如:<gc|go> dcbx:gets the configured or operational version of the  DCB  capabilities  exchange  protocol.  可以設定本地interface的設定特性。sc <ifname> <feature> <args>              sets the configuration of feature on interface ifname.這些特性feature包括:        dcb    DCB state of the port        pg    priority groupspgid:xxxxxxxxPriority group ID for the 8  priorities.  From  left  to  right(priorities  0-7),  x  is  the  corresponding  priority group IDvalue, which can be 0-7 for priority groups with bandwidth allo-cations  or f (priority group ID 15) for the unrestricted prior-ity group.        pfc    priority flow control(特殊的引數有pfcup: xxxxxxxx)x是0或1,1指的是這個相應的優先順序(總共有8個優先順序0-7嘛)使用傳輸的pause幀機制,0就表示不使用。        app:<subtype> 特殊的引數是appcfg:xx xx是一個16進位制的值代表一個8bit的bitmap,某一位為1代表著這個subtype使用這個優先順序。              application specific data      subtype can be:      ---------------      0|fcoe Fiber Channel over Ethernet (FCoE)      下面是dcbtool的使用例子:達到的目的:使得PFC pause發生作用的傳輸優先順序是3,並且將FCoE流量分配到這個第三優先順序上。      dcbtool sc eth2 pfc pfcup:00010000      dcbtool sc eth2 app:0 appcfg:08(app:0是表示FcoE流量)另外頻寬分配的部分就是pg:比如      dcbtool sc eth2 pg pgid:0000111f pgpct:25,75,0,0,0,0,0,0
使用Netlink和核心DCB子系統互動

在netnetlinkaf_netlink.c中:

 

static int __init netlink_proto_init(void)

       

        sock_register(&netlink_family_ops);

       

}

 

static const struct net_proto_familynetlink_family_ops = {

      .family = PF_NETLINK,

      .create = netlink_create,

      .owner    = THIS_MODULE, /* for consistency 8) */

};

 
 

 

 

在netlink_create中呼叫

static int __netlink_create(struct net *net, struct socket *sock,

                        struct mutex *cb_mutex, int protocol)

{

      struct sock *sk;

      struct netlink_sock *nlk;

 

      sock->ops = &netlink_ops;

      sk = sk_alloc(net, PF_NETLINK, GFP_KERNEL, &netlink_proto);

     

      …

}

 

static const struct proto_opsnetlink_ops= {

      .family = PF_NETLINK,

      .owner = THIS_MODULE,

      .release =      netlink_release,

      .bind =          netlink_bind,

      .connect =    netlink_connect,

      .socketpair = sock_no_socketpair,

      .accept =      sock_no_accept,

      .getname =    netlink_getname,

      .poll =          datagram_poll,

      .ioctl =  sock_no_ioctl,

      .listen =  sock_no_listen,

      .shutdown =  sock_no_shutdown,

      .setsockopt =      netlink_setsockopt,

      .getsockopt =      netlink_getsockopt,

      .sendmsg =  netlink_sendmsg,//這可能就是和那些系統呼叫對應的函數

      .recvmsg =    netlink_recvmsg,

      .mmap =              sock_no_mmap,

      .sendpage =  sock_no_sendpage,

};

 
 

 

 

 

資料結構dcbnl_ops
這裡的dcbnl_ops應該算是routing子系統中的一個需要用到的結構。為了進一步理解,還是把這個過程弄清楚吧。  到現在lldpad怎麼操作,基本上梳理清楚,至於lldpad怎麼和核心程式進行互動。應該要看一看struct dcbnl_rtnl_ops出現在核心的哪些部分。include/net/dcbnl.h定義這個結構
定義:include/net/dcbnl.h, line 46
·        *·          43  * Ops struct for the netlink callbacks.  Used by DCB-enabled drivers through* the netdevice struct.·          45  */·          46 struct dcbnl_rtnl_ops {·          47        /* IEEE 802.1Qaz std */·        56        int (*ieee_delapp) (struct net_device *, struct dcb_app *);·        59·          60        /* CEE std */·          61        u8  (*getstate)(struct net_device *);·                        u16 *);·          96        int (*peer_getapptable)(struct net_device *, struct dcb_app *);·          97 ·          98        /* CEE peer */·          99        int (*cee_peer_getpg) (struct net_device *, struct cee_pg *);·        100        int (*cee_peer_getpfc) (struct net_device *, struct cee_pfc *);·        101 };·        102·        103 #endif /* __NET_DCBNL_H__ */·        104
referenced in by驅動(DCB enabled)和DCB模組
然後referenced in很多的地方:被很多的驅動參照,比如broadcom的bnx2x(這個編譯核心的時候沒有必要選上吧)。mellanox/mlx4;/qlogic/qlcnic;intel/ixgbe;等這些基本上都是定義並且實現了這個結構;並且把這個結構作為netdev的子結構傳遞給netdev結構體。 然後在net/dcb/dcbnl.c(DCB模組)中有這些函數static int dcbnl_build_peer_app(struct net_device *netdev, struct sk_buff* skb,int app_nested_type, int app_info_type, int app_entry_type)然後這個裡面使用了一個變數指標ops,將這個netdev的dcbnl_ops傳遞給它。const struct dcbnl_rtnl_ops *ops = netdev->dcbnl_ops; static int dcbnl_ieee_fill(struct sk_buff *skb, struct net_device *netdev)1035 {1036        struct nlattr *ieee, *app;1037        struct dcb_app_type *itr;1038        const struct dcbnl_rtnl_ops *ops = netdev->dcbnl_ops;1039        int dcbx; static int dcbnl_cee_pg_fill(struct sk_buff *skb, struct net_device *dev,1142                              int dir)1143 {1144        u8pgid, up_map, prio, tc_pct;1145        const struct dcbnl_rtnl_ops *ops = dev->dcbnl_ops; 還有dcbnl_notifydcbnl_cee_fill(dcbnl_cee_get/* Handle CEE DCBX GET commands. */DCB模組
Net/dcb目錄

主要是和應用程式互動,解析應用程式的包,執行相關的功能,然後去呼叫變數的callback函數進行get或者set操作,再將結果反饋給應用程式lldpad。

DCB子系統註冊rtnetlink
感覺看一下這個3.2的程式碼完全沒有什麼問題的呀。差不多就是那個論文中提到的那樣子

Note:翻譯自程式碼,可是翻譯起來真的好帶感呢。

__rtnl_register函數:註冊一個rtnetlink訊息型別(是提供給模組自己註冊的哦)

引數

@protocol:協定家族或者PF_UNSPEC

@msgtype:rtnetlink的訊息型別

@doit:每次請求訊息呼叫的函數指標

@dumpit:每次dump請求NLM_F_DUM呼叫的函數指標

@calit:計算dump訊息大小的指標函數

 

static struct rtnl_link*rtnl_msg_handlers[RTNL_FAMILY_MAX + 1];

 

int __rtnl_register(int protocol, intmsgtype,

                  rtnl_doit_func doit, rtnl_dumpit_funcdumpit,

                  rtnl_calcit_func calcit){

struct rtnl_link *tab;

      intmsgindex;

 

      BUG_ON(protocol< 0 || protocol > RTNL_FAMILY_MAX);

      msgindex= rtm_msgindex(msgtype);

 

      tab= rtnl_msg_handlers[protocol];

      if(tab == NULL) {

              tab= kcalloc(RTM_NR_MSGTYPES, sizeof(*tab), GFP_KERNEL);

              if(tab == NULL)

                    return-ENOBUFS;

 

              rtnl_msg_handlers[protocol]= tab;

      }

 

      if(doit)

              tab[msgindex].doit= doit;

 

      if(dumpit)

              tab[msgindex].dumpit= dumpit;

 

      if(calcit)

              tab[msgindex].calcit= calcit;

 

      return0;

}

EXPORT_SYMBOL_GPL(__rtnl_register);

void rtnl_register(int protocol, intmsgtype,

                rtnl_doit_func doit, rtnl_dumpit_funcdumpit,

                rtnl_calcit_func calcit)

{

      if(__rtnl_register(protocol, msgtype, doit, dumpit, calcit) < 0)

              panic("Unableto register rtnetlink message handler, "

                    "protocol = %d, message type =%dn",

                    protocol, msgtype);

}

EXPORT_SYMBOL_GPL(rtnl_register);

當訊息到達doit之後:

Dcb/dcbnl.c中的

static int __init dcbnl_init(void)

{

      INIT_LIST_HEAD(&dcb_app_list);

 

      rtnl_register(PF_UNSPEC,RTM_GETDCB, dcb_doit, NULL, NULL);

      rtnl_register(PF_UNSPEC,RTM_SETDCB, dcb_doit, NULL, NULL);

 

      return0;

}

然後這個dcb_doit做了很多的事情,比如說解析skb和其他頭部,然後進行相關的操作。

static int dcb_doit(struct sk_buff *skb,structnlmsghdr *nlh, void *arg)

{

      structnet *net = sock_net(skb->sk);

      structnet_device *netdev;

      structdcbmsg  *dcb = (struct dcbmsg *)NLMSG_DATA(nlh);

      structnlattr *tb[DCB_ATTR_MAX + 1];

      u32pid = skb ? NETLINK_CB(skb).pid : 0;

      intret = -EINVAL;

 

      if(!net_eq(net, &init_net))

              return-EINVAL;

 

      ret= nlmsg_parse(nlh, sizeof(*dcb), tb, DCB_ATTR_MAX,

                      dcbnl_rtnl_policy);

      if(ret < 0)

              returnret;

 

      if(!tb[DCB_ATTR_IFNAME])

              return-EINVAL;

 

      netdev= dev_get_by_name(&init_net, nla_data(tb[DCB_ATTR_IFNAME]));

      if(!netdev)

              return-EINVAL;

 

      if(!netdev->dcbnl_ops)

              gotoerrout;

 

      switch(dcb->cmd) {

      caseDCB_CMD_GSTATE:

              ret= dcbnl_getstate(netdev, tb, pid, nlh->nlmsg_seq,

                                  nlh->nlmsg_flags);

/**

 * enum dcbnl_attrs - DCB top-level netlink attributes

 *

 * @DCB_ATTR_UNDEFINED: unspecified attribute to catch errors

 * @DCB_ATTR_IFNAME: interface name of the underlying device (NLA_STRING)

 * @DCB_ATTR_STATE: enable state of DCB in the device (NLA_U8)

 * @DCB_ATTR_PFC_STATE: enable state of PFC in the device (NLA_U8)

 * @DCB_ATTR_PFC_CFG: priority flow control configuration (NLA_NESTED)

 * @DCB_ATTR_NUM_TC: number of traffic classes supported in the device (NLA_U8)

 * @DCB_ATTR_PG_CFG: priority group configuration (NLA_NESTED)

 * @DCB_ATTR_SET_ALL: bool to commit changes to hardware or not (NLA_U8)

 * @DCB_ATTR_PERM_HWADDR: MAC address of the physical device (NLA_NESTED)

 * @DCB_ATTR_CAP: DCB capabilities of the device (NLA_NESTED)

 * @DCB_ATTR_NUMTCS: number of traffic classes supported (NLA_NESTED)

 * @DCB_ATTR_BCN: backward congestion notification configuration (NLA_NESTED)

 * @DCB_ATTR_IEEE: IEEE 802.1Qaz supported attributes (NLA_NESTED)

 * @DCB_ATTR_DCBX: DCBX engine configuration in the device (NLA_U8)

 * @DCB_ATTR_FEATCFG: DCBX features flags (NLA_NESTED)

 * @DCB_ATTR_CEE: CEE std supported attributes (NLA_NESTED)

 */
 

 

struct nlattr {

        __u16          nla_len;

        __u16          nla_type;

};
 

 

struct nlmsghdr {

        __u32                nlmsg_len;      /* Length of message including header */

        __u16                nlmsg_type;    /* Message content */

        __u16                nlmsg_flags;    /* Additional flags */

        __u32                nlmsg_seq;      /* Sequence number */

        __u32                nlmsg_pid;      /* Sending process port ID */

};
 

 

自定義的一些DCB屬性,比如:

/* DCB netlink attributes policy */

static const struct nla_policy dcbnl_rtnl_policy[DCB_ATTR_MAX + 1] = {

        [DCB_ATTR_IFNAME]      = {.type = NLA_NUL_STRING, .len = IFNAMSIZ - 1},

        [DCB_ATTR_STATE]      = {.type = NLA_U8},

        [DCB_ATTR_PFC_CFG]    = {.type = NLA_NESTED},

        [DCB_ATTR_PG_CFG]      = {.type = NLA_NESTED},

        [DCB_ATTR_SET_ALL]    = {.type = NLA_U8},

        [DCB_ATTR_PERM_HWADDR] = {.type = NLA_FLAG},

        [DCB_ATTR_CAP]        = {.type = NLA_NESTED},

        [DCB_ATTR_PFC_STATE]  = {.type = NLA_U8},

        [DCB_ATTR_BCN]        = {.type = NLA_NESTED},

        [DCB_ATTR_APP]        = {.type = NLA_NESTED},

        [DCB_ATTR_IEEE]          = {.type = NLA_NESTED},

        [DCB_ATTR_DCBX]        = {.type = NLA_U8},

        [DCB_ATTR_FEATCFG]    = {.type = NLA_NESTED},

};
 

 

關於rtmsg的解析,是這樣實現的

/**

 * nlmsg_parse - parse attributes of a netlink message

 * @nlh: netlink message header

 * @hdrlen: length of family specific header

 * @tb: destination array with maxtype+1 elements

 * @maxtype: maximum attribute type to be expected

 * @policy: validation policy

 *

 * See nla_parse()

 */

static inline int nlmsg_parse(const struct nlmsghdr *nlh, int hdrlen,

                          struct nlattr *tb[], int maxtype,

                          const struct nla_policy *policy)

{

      if (nlh->nlmsg_len < nlmsg_msg_size(hdrlen))

              return -EINVAL;

 

      return nla_parse(tb, maxtype, nlmsg_attrdata(nlh, hdrlen),

                      nlmsg_attrlen(nlh, hdrlen), policy);

}

Parses a stream of attributes and stores a pointer to each attribute in

 * the tb array accessible via the attribute type. Attributes with a type

 * exceeding maxtype will be silently ignored for backwards compatibility

 * reasons. policy may be set to NULL if no validation is required.

 *

 * Returns 0 on success or a negative error code.

 */

int nla_parse(struct nlattr **tb, int maxtype, const struct nlattr *head,

            int len, const struct nla_policy *policy)

{

      const struct nlattr *nla;

      int rem, err;

 

      memset(tb, 0, sizeof(struct nlattr *) * (maxtype + 1));

 

      nla_for_each_attr(nla, head, len, rem) {

              u16 type = nla_type(nla);

 

              if (type > 0 && type <= maxtype) {

                    if (policy) {

                            err = validate_nla(nla, maxtype, policy);

                            if (err < 0)

                                  goto errout;

                    }

 

                    tb[type] = (struct nlattr *)nla;

              }

      }

 

 
 

 

關於dcbmsg結構是在dcbnl.h中定義的

struct dcbmsg {

      __u8              dcb_family;

      __u8              cmd;

      __u16              dcb_pad;

};

命令包括:

/**

 * enum dcbnl_commands - supported DCB commands

 *

 * @DCB_CMD_UNDEFINED: unspecified command to catch errors

 * @DCB_CMD_GSTATE: request the state of DCB in the device

 * @DCB_CMD_SSTATE: set the state of DCB in the device

 * @DCB_CMD_PGTX_GCFG: request the priority group configuration for Tx

 * @DCB_CMD_PGTX_SCFG: set the priority group configuration for Tx

 * @DCB_CMD_PGRX_GCFG: request the priority group configuration for Rx

 * @DCB_CMD_PGRX_SCFG: set the priority group configuration for Rx

 * @DCB_CMD_PFC_GCFG: request the priority flow control configuration

 * @DCB_CMD_PFC_SCFG: set the priority flow control configuration

 * @DCB_CMD_SET_ALL: apply all changes to the underlying device

 * @DCB_CMD_GPERM_HWADDR: get the permanent MAC address of the underlying

 *                        device.  Only useful when using bonding.

 * @DCB_CMD_GCAP: request the DCB capabilities of the device

 * @DCB_CMD_GNUMTCS: get the number of traffic classes currently supported

 * @DCB_CMD_SNUMTCS: set the number of traffic classes

 * @DCB_CMD_GBCN: set backward congestion notification configuration

 * @DCB_CMD_SBCN: get backward congestion notification configration.

 * @DCB_CMD_GAPP: get application protocol configuration

 * @DCB_CMD_SAPP: set application protocol configuration

 * @DCB_CMD_IEEE_SET: set IEEE 802.1Qaz configuration

 * @DCB_CMD_IEEE_GET: get IEEE 802.1Qaz configuration

 * @DCB_CMD_GDCBX: get DCBX engine configuration

 * @DCB_CMD_SDCBX: set DCBX engine configuration

 * @DCB_CMD_GFEATCFG: get DCBX features flags

 * @DCB_CMD_SFEATCFG: set DCBX features negotiation flags

 * @DCB_CMD_CEE_GET: get CEE aggregated configuration

 * @DCB_CMD_IEEE_DEL: delete IEEE 802.1Qaz configuration

 */
 

 

 

 

一個標準的netlink replay call的例子如下,比如get的時候就是基本上呼叫了它。

static int dcbnl_reply(u8 value, u8 event, u8 cmd, u8 attr, u32 pid,

                      u32 seq, u16 flags)

{

        struct sk_buff *dcbnl_skb;

        struct dcbmsg *dcb;

        struct nlmsghdr *nlh;

        int ret = -EINVAL

        dcbnl_skb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);

        if (!dcbnl_skb)

                  return ret;

 

        nlh = NLMSG_NEW(dcbnl_skb, pid, seq, event, sizeof(*dcb), flags);

 

        dcb = NLMSG_DATA(nlh);

        dcb->dcb_family = AF_UNSPEC;

        dcb->cmd = cmd;

        dcb->dcb_pad = 0;

 

        ret = nla_put_u8(dcbnl_skb, attr, value);

        if (ret)

                  goto err;

 

        /* end the message, assign the nlmsg_len. */

        nlmsg_end(dcbnl_skb, nlh);

        ret = rtnl_unicast(dcbnl_skb, &init_net, pid);
 

驅動定義dcbnl_ops
驅動:比如82599的驅動Ixgbe,或者netfpga的驅動都可以根據自己的需要來定義這個結構dcbnl_ops中的函數指標。Dcbnl_ops就是實現了很多的函數體,這些函數體都要驅動的dcb.c(差不多類似的名字)中實現。這個在netfpga的時候,主要是要和lldpad進行互動,82555可能設定了硬體的DCB設定,可是netfpga的硬體並沒有這個功能。不過DCB模組也會和FCoE模組進行互動,好像呼叫的就是DCB模組中的函數(⊙▽⊙不知道這些函數還有沒有呼叫那個變數的函數,如果呼叫了,才有意思呢。!關於麗麗總是在問的app 優先順序。。還是要結合lldpad一起看會比較好吧。其實我倒是不在乎這個)。

setall應該是重置

get/set pfc tcnum app等。

82599的硬體初始化
這些硬體的設定利用的結構是adapter->dcb_cfg(與硬體相關的結構體),這個是網絡卡的預設DCB設定。

參考文獻
lldpad的readme
lldpad原始碼
ixgbe原始碼
核心相關資料


IT145.com E-mail:sddin#qq.com