网络设备驱动程序

1.1  网络设备的特殊性

网络设备作为Linux的三类设备之一,有非常特殊的地方。每一个字符设备或块设备在文件系统中都有一个相应的特殊设备文件来表示该设备,如/dev/hda1、/dev/sda1、/dev/tty1等。网络设备与它们不同,所有网络设备都抽象为一个接口,这个接口提供了对所有网络设备的操作集合。网络接口不存在于Linux的文件系统中,在/dev目录下没有对应的设备名,但每个网络设备有自己的设备名称,从设备名称可以看出设备类型,如lo表示回环设备,eth表示以太网设备。同类型的多个设备从0向上编号,如以太网设备的编号为 eth0、eth1…ethn等。使用ifconfig命令可以查看当前活动的网卡信息。

 
 
  1. [root@/home]ifconfig  
  2. eth0      Link encap:Ethernet  HWaddr 00:0C:29:10:26:00    
  3.           inet addr:192.168.1.101  Bcast:192.168.1.255  Mask:255.255.255.0  
  4.           inet6 addr: fe80::20c:29ff:fe10:2600/64 Scope:Link  
  5.           UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1  
  6.           RX packets:14106 errors:0 dropped:0 overruns:0 frame:0  
  7.           TX packets:21977 errors:0 dropped:0 overruns:0 carrier:0  
  8.           collisions:0 txqueuelen:1000   
  9.           RX bytes:1416177 (1.3 MiB)  TX bytes:22373971 (21.3 MiB)  
  10.           Interrupt:16 Base address:0x2024   
  11. lo        Link encap:Local Loopback    
  12.           inet addr:127.0.0.1  Mask:255.0.0.0  
  13.           inet6 addr: ::1/128 Scope:Host  
  14.           UP LOOPBACK RUNNING  MTU:16436  Metric:1  
  15.           RX packets:3273 errors:0 dropped:0 overruns:0 frame:0  
  16.           TX packets:3273 errors:0 dropped:0 overruns:0 carrier:0  
  17.           collisions:0 txqueuelen:0   
  18.           RX bytes:6961876 (6.6 MiB)  TX bytes:6961876 (6.6 MiB)  

网络应用程序通过Socket套接字接口、网络协议层、网络驱动程序访问网络设备。Linux的网络系统主要基于BSD UNIX的socket机制。在网络子系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。Linux网络子系统为系统提供协议层支持、数据缓冲和流量控制等。

1.2  sk_buff结构

sk_buff结构是整个Linux内核网络子系统中最核心的数据结构。Linux网络各层之间的数据传送都要通过sk_buff结构。sk_buff结构定义如下:

 
 
  1. struct sk_buff {  
  2.     struct sk_buff      *next; //双向链表指针  
  3.     struct sk_buff      *prev;  
  4.     struct sock         *sk; // 拥有这个sk_buff的sock结构  
  5.     ktime_t         tstamp; //到达或发送的时间戳  
  6.     struct net_devic    e   *dev;//关联的网络设备  
  7.         struct  dst_entry   *dst; // 路由子系统中使用  
  8.     struct  sec_path        *sp; // IPSec协议用于跟踪传输的信息  
  9.     char            cb[48];//各层私有数据  
  10.     unsigned int        len, //当前协议数据包的长度  
  11.                 data_len; //分片中数据的长度  
  12.     __u16   mac_len,//mac头的长度  
  13.             hdr_len;  
  14.     union {  
  15.         __wsum      csum;  
  16.         struct {  
  17.             __u16   csum_start;  
  18.             __u16   csum_offset;  
  19.         };  
  20.     };  
  21.     __u32       priority;  
  22.     __u8        local_df:1,  
  23.                 cloned:1,  
  24.                 ip_summed:2,  
  25.                 nohdr:1,  
  26.                 nfctinfo:3;  
  27.     __u8        pkt_type:3,// 帧的类型  
  28.                 fclone:2,  
  29.                 ipvs_property:1,  
  30.                 nf_trace:1;  
  31.     __be16      protocol;//协议,包括IP、IPV6、ARP等  
  32.    // 在缓冲区释放时完成某些动作的函数  
  33.     void            (*destructor)(struct sk_buff *skb);  
  34. #if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)  
  35.     struct nf_conntrack *nfct;  
  36.     struct sk_buff      *nfct_reasm;  
  37.     #endif  
  38.     #ifdef CONFIG_BRIDGE_NETFILTER //netfilter防火墙使用  
  39.     struct nf_bridge_info   *nf_bridge;  
  40.     #endif  
  41.     int             iif;  
  42.     #ifdef CONFIG_NETDEVICES_MULTIQUEUE  
  43.     __u16           queue_mapping;  
  44.     #endif  
  45.     #ifdef CONFIG_NET_SCHED  
  46.     __u16           tc_index;  
  47.     #ifdef CONFIG_NET_CLS_ACT  
  48.     __u16           tc_verd;    /*流量控制机制*/  
  49.     #endif  
  50.     #endif  
  51.     #ifdef CONFIG_NET_DMA  
  52.     dma_cookie_t        dma_cookie;  
  53.     #endif  
  54.     #ifdef CONFIG_NETWORK_SECMARK  
  55.     __u32           secmark;  
  56.     #endif  
  57.     __u32           mark;  
  58.     sk_buff_data_t      transport_header;//传输层头部  
  59.     sk_buff_data_t      network_header;//网络层头部  
  60.     sk_buff_data_t      mac_header;//MAC层头部  
  61.     sk_buff_data_t      tail;  
  62.     sk_buff_data_t      end;  
  63.     unsigned char       *head,*data;//数据区  
  64.     unsigned int        truesize;//真实大小  
  65.     atomic_t        users; // 引用计数  
  66. };  

1.3  Linux网络设备驱动程序架构

网络设备被抽象为统一的接口供系统访问,应用层对各种网络设备的访问都采用统一的形式,也就是套接字形式,它具有硬件无关性。

网络设备驱动程序最重要的结构是struct net_device。struct net_device结构保存一个网络接口的重要信息,是网络驱动程序的核心。struct net_device结构非常庞大,下面介绍该结构中几个最重要的成员:

 
 
  1. struct net_device  
  2. {  
  3.     char     name[IFNAMSIZ]; //设备名  
  4.     struct hlist_node   name_hlist;  
  5.     unsigned long       mem_end; //共享内存的尾地址  
  6.     unsigned long       mem_start; //共享内存的首地址  
  7.     unsigned long       base_addr; //设备的I/O地址  
  8.     unsigned int        irq; //设备的中断号  
  9.     unsigned char       if_port;  
  10.     unsigned char       dma;    //设备用的DMA通道  
  11.     unsigned long       state;  
  12.     struct list_head    dev_list;  
  13.     int (*init)(struct net_device *dev);//设备初始化函数  
  14.     unsigned long       features;//设备特性  
  15.     struct net_device   *next_sched;  
  16.     int         ifindex;  
  17.     int         iflink;  
  18.     struct net_device_stats* (*get_stats)(struct net_device *dev);//获取统计数据  
  19.     struct net_device_stats stats;//统计数据  
  20.     …  
  21.     int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);//数据发送  
  22.     const struct ethtool_ops *ethtool_ops;  
  23.     const struct header_ops *header_ops;  
  24.     int  (*open)(struct net_device *dev); //打开网络接口  
  25.     int  (*stop)(struct net_device *dev);//关闭网络接口  
  26.     void     (*change_rx_flags)(struct net_device *dev,int flags);  
  27.     void     (*set_rx_mode)(struct net_device *dev);  
  28.     //配置多播地址链表  
  29.     void     (*set_multicast_list)(struct net_device *dev);  
  30.     //配置MAC地址  
  31.     int  (*set_mac_address)(struct net_device *dev,void *addr);  
  32.     int  (*validate_addr)(struct net_device *dev); //地址验证  
  33.     //执行特殊的ioctl命令  
  34.     int  (*do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd);  
  35.     //改变接口配置  
  36.     int  (*set_config)(struct net_device *dev,struct ifmap *map);  
  37.     //当MTU改变时驱动程序要做一些特殊的事情,就应该写这个函数  
  38.     int  (*change_mtu)(struct net_device *dev, int new_mtu);  
  39.     void     (*tx_timeout) (struct net_device *dev); //发送超时处理  
  40.     void     (*vlan_rx_register)(struct net_device *dev,struct vlan_group *grp);  
  41.     void     (*vlan_rx_add_vid)(struct net_device *dev,unsigned short vid);  
  42.     void     (*vlan_rx_kill_vid)(struct net_device *dev,unsigned short vid);  
  43.     int  (*neigh_setup)(struct net_device *dev, struct neigh_parms *);  
  44.     …  
  45. };  

网络设备注册和注销函数原型如下:
 
 
  1. int register_netdev(struct net_device *dev);  
  2. void unregister_netdev(struct net_device *dev); 

网络设备驱动程序与网络子系统直接交互。两者交互的基本单位是sk_buff结构。网络系统下发数据要通过dev_queue_xmit函数,而dev_queue_xmit函数会调用网络设备的hard_start_xmit接口。网络设备收到数据后会产生一个中断,在中断处理程序中驱动程序申请一块sk_buff,从硬件读出数据放置到申请好的缓冲区里,接下来填充sk_buff中的一些成员,最后驱动程序调用netif_rx把数据传送给协议层,netif_rx将数据放入处理队列然后返回,真正的处理是在中断返回以后,这样可以减少中断时间。
 
 
  1. int dev_queue_xmit(struct sk_buff *skb);  
  2. int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev);  
  3. int netif_rx(struct sk_buff *skb);  

网络设备驱动程序的主要功能是实现struct net_device中的函数接口。

(1)open接口

 
 
  1. int  (*open)(struct net_device *dev); 

open接口在网络设备被激活的时候被调用。它主要完成资源和中断的申请、DMA的注册等工作。使用ifconfig命令可以激活网络设备。

(2)hard_start_xmit接口

 
 
  1. int (*hard_start_xmit) (struct sk_buff *skb,struct net_device *dev); 

hard_start_xmit接口用来将网络子系统发来的数据通过网卡发送到物理网络。struct net_device中没有读数据接口,读数据一般在网卡中断中处理。

(3)get_stats接口

 
 
  1. struct net_device_stats* (*get_stats)(struct net_device *dev); 

get_stats函数返回一个net_device_stats结构,该结构保存了所管理的网络设备接口的详细的流量与错误统计信息:
 
 
  1. struct net_device_stats  
  2. {  
  3.     unsigned long   rx_packets;//接收的总包数  
  4.     unsigned long   tx_packets;//发送的总包数  
  5.     unsigned long   rx_bytes;   //接收总字节数  
  6.     unsigned long   tx_bytes; //发送总字节数  
  7.     unsigned long   rx_errors;  //收到的错包数量  
  8.     unsigned long   tx_errors;  //发送的错包数量  
  9.     unsigned long   rx_dropped;//丢弃的接收包数量  
  10.     unsigned long   tx_dropped; //丢弃的发送包数量  
  11.     unsigned long   multicast;  //接收的多播包数  
  12.     unsigned long   collisions;  
  13.     /*详细的接收错误*/  
  14.     unsigned long   rx_length_errors;//长度错误  
  15.     unsigned long   rx_over_errors;//环行缓冲溢出错误  
  16.     unsigned long   rx_crc_errors;  //CRC校验错误  
  17.     unsigned long   rx_frame_errors;//帧对齐错误  
  18.     unsigned long   rx_fifo_errors; //接收缓冲溢出错误  
  19.     unsigned long   rx_missed_errors;//接收者遗漏错误  
  20.     /*详细的发送错误*/  
  21.     unsigned long   tx_aborted_errors;  
  22.     unsigned long   tx_carrier_errors;  
  23.     unsigned long   tx_fifo_errors;  
  24.     unsigned long   tx_heartbeat_errors;  
  25.     unsigned long   tx_window_errors;  
  26.       
  27.     unsigned long   rx_compressed;  
  28.     unsigned long   tx_compressed;  
  29. };  

(4)do_ioctl接口
 
 
  1. int  (*do_ioctl)(struct net_device *dev,struct ifreq *ifr, int cmd); 

Linux有一些特定的socket ioctl,定义在sockios.h头文件中。网络子系统一般已经实现了这些ioctl命令,也有的命令需要在自定义的驱动程序中实现。常用的ioctl命令有:
 
 
  1. #define SIOCGIFMTU       0x8921     /*获取MTU大小*/  
  2. #define SIOCSIFMTU       0x8922     /*配置MTU大小*/  
  3. #define SIOCSIFNAME      0x8923     /*配置接口名称*/  
  4. #define SIOCSIFHWADDR  0x8924       /*配置硬件地址*/  
  5. #define SIOCGIFENCAP     0x8925     /*获取封装属性*/  
  6. #define SIOCSIFENCAP     0x8926     /*配置封装属性*/  
  7. #define SIOCGIFHWADDR    0x8927     /*获取硬件地址*/  
  8. #define SIOCADDMULTI     0x8931     /*增加广播地址*/  
  9. #define SIOCDELMULTI     0x8932         /*删除广播地址*/  
  10. #define SIOCGIFBRDADDR 0x8919       /*得到广播地址*/  
  11. #define SIOCSIFBRDADDR   0x891a     /*配置广播地址*/  

例1.11  使用SIOCGIFHWADDR获取MAC地址

本例介绍如何使用SIOCGIFHWADDR获取网卡MAC地址。参考代码如下:

 
 
  1. int fd;   
  2. struct ifreq ifr;  
  3. fd = socket(AF_INET, SOCK_DGRAM, 0);   
  4. ifr.ifr_addr.sa_family = AF_INET;   
  5. strncpy(ifr.ifr_name, "eth0", IFNAMSIZ-1);   
  6. ioctl(fd, SIOCGIFHWADDR, &ifr);   
  7. close(fd);   
  8. printf("%.2x:%.2x:%.2x:%.2x:%.2x:%.2x\n",   
  9. (unsigned char)ifr.ifr_hwaddr.sa_data[0],   
  10. (unsigned char)ifr.ifr_hwaddr.sa_data[1],   
  11. (unsigned char)ifr.ifr_hwaddr.sa_data[2],   
  12. (unsigned char)ifr.ifr_hwaddr.sa_data[3],   
  13. (unsigned char)ifr.ifr_hwaddr.sa_data[4],   
  14. (unsigned char)ifr.ifr_hwaddr.sa_data[5]);  

do_ioctl一般用来实现驱动程序私有的ioctl命令,命令的类型在SIOCDEVPRIVATE和SIOCDEVPRIVATE+15之间。

(5)set_multicast_list接口

 
 
  1. void     (*set_multicast_list)(struct net_device *dev); 

set_multicast_list接口在设备地址更改或dev->flags 被修改时调用。在以太网的默认初始化函数中,dev->flags被配置为IFF_BROADCAST|IFF_MULTICAST,表示以太网卡是可广播的,并且是能够进行组播发送的。对dev->flags配置或清除IFF_PROMISC标志时,会调用set_multicast_list函数通知板卡上的硬件过滤器。

(6)set_mac_address接口

 
 
  1. int set_mac_address(struct net_device *dev, void *p) 

该接口用来配置网络设备的MAC地址,MAC地址就存放在p指针中。

(7)stop接口

 
 
  1. int  (*stop)(struct net_device *dev); 

stop接口在网卡状态由up转为down时被调用,一般用来释放资源。

例1.12  虚拟网络设备驱动程序实例

下面的例子是一个虚拟网卡驱动程序,代码见光盘\src\1drivermodel\1-11net。核心代码如下所示:

 
 
  1. static char  netbuffer[100];  
  2. struct net_device *simnetdevs;  
  3. void simnetrx(struct net_device *dev, int len, unsigned char *buf)  
  4. {  
  5.     struct sk_buff *skb;  
  6.     struct simnetpriv *priv = (struct simnetpriv *) dev->priv;  
  7.     skb = dev_alloc_skb(len+2);  
  8.     if (!skb) {  
  9.         printk("simnetrx can not allocate more memory to store the packet. drop the packet\n");  
  10.         priv->stats.rx_dropped++;  
  11.         return;  
  12.     }  
  13.     skb_reserve(skb, 2);  
  14.     memcpy(skb_put(skb, len), buf, len);  
  15.     skb->devdev = dev;  
  16.     skb->protocol = eth_type_trans(skb, dev);  
  17.     /* 不需要校验 */  
  18.     skb->ip_summed = CHECKSUM_UNNECESSARY;   
  19.     priv->stats.rx_packets++;  
  20.     netif_rx(skb);//将数据送入内核网络层  
  21.     return;  
  22. }  
  23. static irqreturn_t simnet_interrupt (int irq, void *dev_id)  
  24. {  
  25.     struct net_device *dev;  
  26.     dev = (struct net_device *) dev_id;  
  27.     //从硬件获取数据  
  28.     simnetrx(dev,100,netbuffer);  
  29.     return IRQ_HANDLED;  
  30. }  
  31. int simnetopen(struct net_device *dev)  
  32. {  
  33.     int ret=0;  
  34.     printk("simnetopen\n");  
  35.     //申请中断  
  36.     ret = request_irq(IRQ_NET_CHIP, simnet_interrupt, IRQF_SHARED,dev->name, dev);  
  37.     if (ret) return ret;  
  38.     printk("request_irq ok\n");  
  39.     netif_start_queue(dev);  
  40.     return 0;  
  41. }  
  42. int simnetrelease(struct net_device *dev)  
  43. {  
  44.     printk("simnetrelease\n");  
  45.     netif_stop_queue(dev);            
  46.     return 0;  
  47. }  
  48. //虚拟硬件发送  
  49. void simnethw_tx(char *buf, int len, struct net_device *dev)  
  50. {  
  51.     struct simnetpriv *priv;  
  52.     /* 长度检查 */  
  53.     if (len < sizeof(struct ethhdr) + sizeof(struct iphdr))  
  54.     {  
  55.         printk("Bad packet! It's size is less then 34!\n");  
  56.         return;  
  57.     }  
  58.     /* 保存状态*/  
  59.     priv = (struct simnetpriv *) dev->priv;  
  60.     priv->stats.tx_packets++;  
  61.     priv->stats.rx_bytes += len;  
  62.     /* 释放内存 */  
  63.     dev_kfree_skb(priv->skb);  
  64. }  
  65. //发送数据包  
  66. int simnettx(struct sk_buff *skb, struct net_device *dev)  
  67. {  
  68.     int len;  
  69.     char *data;  
  70.     struct simnetpriv *priv = (struct simnetpriv *) dev->priv;  
  71.     len = skb->len < ETH_ZLEN ? ETH_ZLEN : skb->len;  
  72.     data = skb->data;  
  73.     /*添加时间戳 */  
  74.     dev->trans_start = jiffies;  
  75.     //记录skb以便在simnethw_tx中释放  
  76.     priv->skbskb = skb;  
  77.     simnethw_tx(data, len, dev);  
  78.     return 0;   
  79. }  
  80. //处理发送超时  
  81. void simnettx_timeout (struct net_device *dev)  
  82. {  
  83.     struct simnetpriv *priv = (struct simnetpriv *) dev->priv;  
  84.     priv->stats.tx_errors++;  
  85.     netif_wake_queue(dev);  
  86.     return;  
  87. }  
  88. // ioctl控制  
  89. int simnetioctl(struct net_device *dev, struct ifreq *rq, int cmd)  
  90. {  
  91.     return 0;  
  92. }  
  93. struct net_device_stats *simnetstats(struct net_device *dev)  
  94. {  
  95.     struct simnetpriv *priv = (struct simnetpriv *) dev->priv;  
  96.     return &priv->stats;  
  97. }  
  98. //变更mtu  
  99. int simnetchange_mtu(struct net_device *dev, int new_mtu)  
  100. {  
  101.     unsigned long flags;  
  102.     spinlock_t *lock = &((struct simnetpriv *) dev->priv)->lock;  
  103.     if (new_mtu < 68)  
  104.         return -EINVAL;  
  105.     spin_lock_irqsave(lock, flags);  
  106.     dev->mtu = new_mtu;  
  107.     spin_unlock_irqrestore(lock, flags);  
  108.     return 0;   
  109. }  
  110. //设备初始化  
  111. void simnetinit(struct net_device *dev)  
  112. {  
  113.     struct simnetpriv *priv;  
  114.     ether_setup(dev);  
  115.     dev->opensimnetopen;  
  116.     dev->stopsimnetrelease;  
  117.     dev->hard_start_xmit = simnettx;  
  118.     dev->do_ioctlsimnetioctl;  
  119.     dev->get_statssimnetstats;  
  120.     dev->change_mtu = simnetchange_mtu;    
  121.     dev->tx_timeout = simnettx_timeout;  
  122.     //配置MAC地址,注意如果(0x01 & addr[0])为真,则是multicast地址  
  123.     dev->dev_addr[0] = 0x18;  
  124.     dev->dev_addr[1] = 0x02;  
  125.     dev->dev_addr[2] = 0x03;  
  126.     dev->dev_addr[3] = 0x04;  
  127.     dev->dev_addr[4] = 0x05;  
  128.     dev->dev_addr[5] = 0x06;  
  129.     dev->flags|= IFF_NOARP;//不支持ARP  
  130.     dev->features|= NETIF_F_NO_CSUM;  
  131.     priv = netdev_priv(dev);  
  132.     memset(priv, 0, sizeof(struct simnetpriv));  
  133.     spin_lock_init(&priv->lock);  
  134. }  
  135. //模块卸载  
  136. void simnetcleanup(void)  
  137. {  
  138.     if (simnetdevs)   
  139.     {  
  140.         unregister_netdev(simnetdevs);  
  141.         free_netdev(simnetdevs);  
  142.     }  
  143.     return;  
  144. }  
  145. //模块初始化  
  146. int simnetinit_module(void)  
  147. {  
  148.     int result,ret = -ENOMEM;  
  149.     //分配网络设备  
  150.     simnetdevs=alloc_netdev(sizeof(struct simnetpriv), "eth%d",  
  151.             simnetinit);  
  152.     if (simnetdevs == NULL)  
  153.         goto out;  
  154.     ret = -ENODEV;  
  155.     //注册网络设备  
  156.     if ((result = register_netdev(simnetdevs)))  
  157.         printk("demo: error %i registering device \"%s\"\n",result, simnetdevs->name);  
  158.     else  
  159.         ret = 0;  
  160. out:  
  161.     if (ret)   
  162.         simnetcleanup();  
  163.     return ret;  
  164. }  
  165. module_init(simnetinit_module);  
  166. module_exit(simnetcleanup);  

运行结果如下:
 
 
  1. [root@urbetter /home]# insmod  demo.ko   
  2. [root@urbetter /home]# ifconfig eth1 192.168.1.23  
  3. simnetopen  
  4. request_irq ok  
  5. [root@urbetter /home]# ifconfig eth1 up  
  6. [root@urbetter /home]# ping 192.168.1.23  
  7. PING 192.168.1.23 (192.168.1.23): 56 data bytes  
  8. 64 bytes from 192.168.1.23: seq=0 ttl=64 time=1.347 ms  
  9. 64 bytes from 192.168.1.23: seq=1 ttl=64 time=0.301 ms  
  10. 64 bytes from 192.168.1.23: seq=2 ttl=64 time=0.266 ms  
  11. ^C  
  12. --- 192.168.1.23 ping statistics ---  
  13. 3 packets transmitted, 3 packets received, 0% packet loss  
  14. round-trip min/avg/max = 0.266/0.638/1.347 ms  
  15. [root@urbetter /home]# ifconfig eth1  
  16. eth1      Link encap:Ethernet  HWaddr 18:02:03:04:05:06    
  17.           inet addr:192.168.1.23  Bcast:192.168.1.255  Mask:255.255.255.0  
  18.           UP BROADCAST RUNNING NOARP MULTICAST  MTU:1500  Metric:1  
  19.           RX packets:0 errors:0 dropped:0 overruns:0 frame:0  
  20.           TX packets:0 errors:0 dropped:0 overruns:0 carrier:0  
  21.           collisions:0 txqueuelen:1000   
  22.           RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)  
  23. [root@urbetter /home]# ifconfig eth1 192.168.1.26  
  24. [root@urbetter /home]# ifconfig eth1 down  
  25. simnetrelease  
  26. [root@urbetter /home]# ifconfig eth1 192.168.1.26  
  27. simnetopen  
  28. request_irq ok  

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值