作为Linux开发人员,我们经常会遇到需要处理二进制数据的情况。在这些过程中,至关重要。本文将介绍这些东西,以及它们如何在Linux中使用。
创新互联一直在为企业提供服务,多年的磨炼,使我们在创意设计,全网整合营销推广到技术研发拥有了开发经验。我们擅长倾听企业需求,挖掘用户对产品需求服务价值,为企业制作有用的创意设计体验。核心团队拥有超过十余年以上行业经验,涵盖创意,策化,开发等专业领域,公司涉及领域有基础互联网服务服务器托管德阳、成都app开发、手机移动建站、网页设计、网络整合营销。
u8和u32的定义
u8和u32是两种不同的数据类型,在Linux内核中使用。它们的名称可能会有一点迷惑,因为它们实际上是无符号整数类型,而不是8位或32位整数类型。但是,在大多数系统中,u8的大小确实为8位,而u32的大小确实为32位。
u8和u32都是在stdint.h头文件中定义的类型。u8的定义如下:
typedef __u8 uint8_t;
typedef unsigned char __u8;
u32的定义如下:
typedef __u32 uint32_t;
typedef unsigned int __u32;
值得注意的是,u8和u32的定义中,typedef关键字被使用。typedef的作用是为现有的数据类型创建一个别名。在这种情况下,它们分别为uint8_t和uint32_t。
u8和u32的用途
在Linux内核开发中,u8和u32用于处理各种数据类型,包括网络协议、文件系统和设备驱动程序等。
例如,在网络协议中,ip.h头文件中有这样一个定义:
typedef u32 __be32;
这里的__be32表示网络字节序中的32位整数。它实际上是一个u32类型,可以用于处理网络字节序中的数据。
文件系统和设备驱动程序也使用u8和u32类型。在这些情况下,u8和u32通常用于表示不同的二进制数据结构。例如,在block.h头文件中,有以下定义:
typedef unsigned short sector_t;
typedef struct {
sector_t start_sect;
unsigned int count;
} sector_t;
这里的sector_t是一个16位无符号整数类型。sectors_t结构体中包含一个start_sect成员,它是一个sector_t类型,用于表示数据块的起始部分。同时,count成员是一个unsigned int类型,用于表示数据块的长度。
在Linux内核中使用u8和u32
在Linux内核中,对于u8和u32数据类型的操作,通常会使用一些宏和函数来实现。例如,对于u8类型,以下宏被定义:
#define __u8_to_user(ptr, x) (*(__u8 __user *)(ptr) = (x))
#define __u8_from_user(ptr) (*(__u8 __user *)(ptr))
这些宏用于将u8类型从用户空间复制到内核空间,或从内核空间复制到用户空间。将数据存储到用户区域时,使用“__u8_to_user”宏。反过来,将数据从用户区域读取到内核区域时,使用“__u8_from_user”宏。
对于u32类型,也有类似的宏和函数。例如:
#define __le32_to_cpu(x) ((__u32)(__le32)(x))
#define __cpu_to_le32(x) ((__le32)(__u32)(x))
#define le32_to_cpu(x) ((__force __u32)(__le32)(x))
#define cpu_to_le32(x) ((__force __le32)(__u32)(x))
#define put_unaligned_le32(x,p) ((__u32 *)(p))[0] = __cpu_to_le32(x)
#define get_unaligned_le32(p) __le32_to_cpu(((const __le32 *)(p))[0])
这些宏和函数用于将u32类型从不同字节顺序的数据格式复制到本机字节顺序。例如,在网络通信中,经常需要将数据内容从大端字节顺序转换为小端字节顺序,或从小端字节顺序转换为大端字节顺序。函数“__le32_to_cpu”和“__cpu_to_le32”可以用于完成这个处理。而“put_unaligned_le32”和“get_unaligned_le32”可以用于将u32类型原子性地存储到内存中,或者从内存中读取。
在Linux内核开发中,处理二进制数据是一项非常重要的任务。u8和u32是两种用于处理二进制数据的无符号整数类型。这些类型通常用于表示不同的二进制数据结构,并且在网络协议、文件系统和设备驱动程序中发挥重要作用。在Linux内核中,为了操作这些数据类型,需要使用一些宏和函数,从而使开发人员可以轻松地在内核空间中处理二进制数据。
成都网站建设公司-创新互联为您提供网站建设、网站制作、网页设计及定制高端网站建设服务!
Linux内核提供了一个称为USB core的子系统来处理了大部分USB设备的复杂工作,所以这里所描述的是驱动程序和USB core之间的接口。
在USB设备组织结构中,从上到下分为设备(device)、知尺配置(config)、接口(interface)和端点(endpoint)四个层次。
对于裂猛返这四个层次的简单描述如下:
1、设备通常具有一个或多个的配置
2、配置经常具有一个或多个的接口
3、接口通常具有一个或多个的设置
4、接口没有或具有一个以上的端点
设备
代表了一个插入的USB设备,在内核使用数据结构 struct u_device来描述整个USB设备。(include/linux/u.h)
struct u_device {
肆饥 int devnum; //设备号,是在USB总线的地址
char devpath ; //用于消息的设备ID字符串
enum u_device_state state; //设备状态:已配置、未连接等等
enum u_device_speed speed; //设备速度:高速、全速、低速或错误
struct u_tt *tt; //处理传输者信息;用于低速、全速设备和高速HUB
int ttport; //位于tt HUB的设备口
unsigned int toggle; //每个端点的占一位,表明端点的方向( = IN, = OUT)
struct u_device *parent; //上一级HUB指针
struct u_bus *bus; //总线指针
struct u_host_endpoint ep0; //端点0数据
struct device dev; //一般的设备接口数据结构
struct u_device_descriptor descriptor; //USB设备描述符
struct u_host_config *config; //设备的所有配置
struct u_host_config *actconfig; //被激活的设备配置
struct u_host_endpoint *ep_in; //输入端点数组
struct u_host_endpoint *ep_out; //输出端点数组
char **rawdescriptors; //每个配置的raw描述符
unsigned short bus_mA; //可使用的总线电流
u8 portnum;//父端口号
u8 level; //USB HUB的层数
unsigned can_submit:1; //URB可被提交标志
unsigned discon_suspended:1; //暂停时断开标志
unsigned persist_enabled:1; //USB_PERSIST使能标志
unsigned have_langid:1; //string_langid存在标志
unsigned authorized:1;
unsigned authenticated:1;
unsigned wu:1; //无线USB标志
int string_langid; //字符串语言ID
/* static strings from the device */ //设备的静态字符串
char *product; //产品名
char *manufacturer; //厂商名
char *serial; //产品串号
struct list_head filelist; //此设备打开的ufs文件
#ifdef CONFIG_USB_DEVICE_CLASS
struct device *u_classdev; //用户空间访问的为ufs设备创建的USB类设备
#endif
#ifdef CONFIG_USB_DEVICEFS
struct dentry *ufs_dentry; //设备的ufs入口
#endif
int maxchild; //(若为HUB)接口数
struct u_device *children;//连接在这个HUB上的子设备
int pm_usage_cnt; //自动挂起的使用计数
u32 quirks;
atomic_t urbnum; //这个设备所提交的URB计数
unsigned long active_duration; //激活后使用计时
#ifdef CONFIG_PM //电源管理相关
struct delayed_work autosuspend; //自动挂起的延时
struct work_struct autoresume; //(中断的)自动唤醒需求
struct mutex pm_mutex; //PM的互斥锁
unsigned long last_busy; //最后使用的时间
int autosuspend_delay;
unsigned long connect_time; //之一次连接的时间
unsigned auto_pm:1; //自动挂起/唤醒
unsigned do_remote_wakeup:1; //远程唤醒
unsigned reset_resume:1; //使用复位替代唤醒
unsigned autosuspend_disabled:1; //挂起关闭
unsigned autoresume_disabled:1; //唤醒关闭
unsigned skip_sys_resume:1; //跳过下个系统唤醒
#endif
struct wu_dev *wu_dev; //(如果为无线USB)连接到WUSB特定的数据结构
};
配置
一个USB设备可以有多个配置,并可在它们之间转换以改变设备的状态。比如一个设备可以通过下载固件(firmware)的方式改变设备的使用状态(我感觉类似FPGA或CPLD),那么USB设备就要切换配置,来完成这个工作。一个时刻只能有一个配置可以被激活。Linux使用结构 struct u_host_config 来描述USB配置。我们编写的USB设备驱动通常不需要读写这些结构的任何值。可在内核源码的文件include/linux/u.h中找到对它们的描述。
struct u_host_config {
struct u_config_descriptor desc; //配置描述符
char *string; /* 配置的字符串指针(如果存在) */
struct u_interface_assoc_descriptor *intf_assoc; //配置的接口联合描述符链表
struct u_interface *interface; //接口描述符链表
struct u_interface_cache *intf_cache;
unsigned char *extra; /* 额外的描述符 */
int extralen;
};
接口
USB端点被绑为接口,USB接口只处理一种USB逻辑连接。一个USB接口代表一个基本功能,每个USB驱动控制一个接口。所以一个物理上的硬件设备可能需要一个以上的驱动程序。这可以在“晕到死 差屁”系统中看出,有时插入一个USB设备后,系统会识别出多个设备,并安装相应多个的驱动。
USB 接口可以有其他的设置,它是对接口参数的不同选择. 接口的初始化的状态是之一个设置,编号为0。 其他的设置可以以不同方式控制独立的端点。
USB接口在内核中使用 struct u_interface 来描述。USB 核心将其传递给USB驱动,并由USB驱动负责后续的控制。
struct u_interface {
struct u_host_interface *altsetting; /* 包含所有可用于该接口的可选设置的接口结构数组。每个 struct u_host_interface 包含一套端点配置(即struct u_host_endpoint结构所定义的端点配置。这些接口结构没有特别的顺序。*/
struct u_host_interface *cur_altsetting; /* 指向altsetting内部的指针,表示当前激活的接口配置*/
unsigned num_altsetting; /* 可选设置的数量*/
/* If there is an interface association descriptor then it will list the associated interfaces */
struct u_interface_assoc_descriptor *intf_assoc;
int minor; /* 如果绑定到这个接口的 USB 驱动使用 USB 主设备号, 这个变量包含由 USB 核心分配给接口的次设备号. 这只在一个成功的调用 u_register_dev后才有效。*/
/*以下的数据在我们写的驱动中基本不用考虑,系统会自动设置*/
enum u_interface_condition condition; /* state of binding */
unsigned is_active:1; /* the interface is not suspended */
unsigned sysfs_files_created:1; /* the sysfs attributes exist */
unsigned ep_devs_created:1; /* endpoint “devices” exist */
unsigned unregistering:1; /* unregistration is in progress */
unsigned needs_remote_wakeup:1; /* driver requires remote wakeup */
unsigned needs_altsetting0:1; /* switch to altsetting 0 is pending */
unsigned needs_binding:1; /* needs delayed unbind/rebind */
unsigned reset_running:1;
struct device dev; /* 接口特定的设备信息 */
struct device *u_dev;
int pm_usage_cnt; /* usage counter for autosuspend */
struct work_struct reset_ws; /* for resets in atomic context */
};
struct u_host_interface {
struct u_interface_descriptor desc; //接口描述符
struct u_host_endpoint *endpoint; /* 这个接口的所有端点结构体的联合数组*/
char *string; /* 接口描述字符串 */
unsigned char *extra; /* 额外的描述符 */
int extralen;
};
端点
USB 通讯的最基本形式是通过一个称为端点的东西。一个USB端点只能向一个方向传输数据(从主机到设备(称为输出端点)或者从设备到主机(称为输入端点))
端点在内核中使用结构 struct u_host_endpoint 来描述,它所包含的真实端点信息在另一个结构中:struct u_endpoint_descriptor(端点描述符,包含所有的USB特定数据)。
struct u_host_endpoint {
struct u_endpoint_descriptor desc; //端点描述符
struct list_head urb_list; //此端点的URB对列,由USB核心维护
void *hcpriv;
struct ep_device *ep_dev; /* For sysfs info */
unsigned char *extra; /* Extra descriptors */
int extralen;
int enabled;
};
/*—*/
/* USB_DT_ENDPOINT: Endpoint descriptor */
struct u_endpoint_descriptor {
__u8 bLength;
__u8 bDescriptorType;
__u8 bEndpointAddress; /*这个特定端点的 USB 地址,这个8位数据包含端点的方向,结合位掩码 USB_DIR_OUT 和 USB_DIR_IN 使用, 确定这个端点的数据方向。*/
__u8 bmAttributes; //这是端点的类型,位掩码如下
__le16 wMaxPacketSize; /*端点可以一次处理的更大字节数。驱动可以发送比这个值大的数据量到端点, 但是当真正传送到设备时,数据会被分为 wMaxPakcetSize 大小的块。对于高速设备, 通过使用高位部分几个额外位,可用来支持端点的高带宽模式。*/
__u8 bInterval; //如果端点是中断类型,该值是端点的间隔设置,即端点的中断请求间的间隔时间,以毫秒为单位
/* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));
#define USB_DT_ENDPOINT_SIZE 7
#define USB_DT_ENDPOINT_AUDIO_SIZE 9 /* Audio extension */
/*
* Endpoints
*/
#define USB_ENDPOINT_NUMBER_MASK 0x0f /* in bEndpointAddress 端点的 USB 地址掩码 */
#define USB_ENDPOINT_DIR_MASK 0x80 /* in bEndpointAddress 数据方向掩码 */
#define USB_DIR_OUT 0 /* to device */
#define USB_DIR_IN 0x80 /* to host */
#define USB_ENDPOINT_XFERTYPE_MASK 0x03 /* bmAttributes 的位掩码*/
#define USB_ENDPOINT_XFER_CONTROL 0
#define USB_ENDPOINT_XFER_ISOC 1
#define USB_ENDPOINT_XFER_BULK 2
#define USB_ENDPOINT_XFER_INT 3
#define USB_ENDPOINT_MAX_ADJUSTABLE 0x80
/*—*/
写一个USB的驱动程序最 基本的要做四件事:驱动程序要支持的设备、注册USB驱动程序、探测和断开、提交和控制urb(USB请求块)
驱动程序支持的设备:有一个结构体struct u_device_id,这个结构体提供了一列不同类型的该驱动程序支持的USB设备,对于一个只控制一个特定的USB设备的驱动程序来说,struct u_device_id表被定义为:
/* 驱动程序支持的设备列表 */
static struct u_device_id skel_table = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
{ } /* 终止入口 */
};
MODULE_DEVICE_TABLE (u, skel_table);
对 于PC驱动程序,MODULE_DEVICE_TABLE是必需的,而且u必需为该宏的之一个值,而USB_SKEL_VENDOR_ID和 USB_SKEL_PRODUCT_ID就是这渣碧个特殊设备的制造商和产品的ID了,我们在程序中把定义的值改为我们这款USB的,如:
/* 定义制造商和产品的ID号 */
#define USB_SKEL_VENDOR_ID 0x1234
#define USB_SKEL_PRODUCT_ID 0x2345
这两个值可以通过命令lsu,当然你得先把USB设备先插到主机上了。或者查看厂商的USB设备的手册也能得到,在我机器上运行lsu是这样的结果:
Bus 004 Device 001: ID 0000:0000
Bus 003 Device 002: ID 1234:2345 Abc Corp.
Bus 002 Device 001: ID 0000:0000
Bus 001 Device 001: ID 0000:0000
得到这两个值后把它定义到程序里就可以了。
注册USB驱动程序:所 有的USB驱动程序都必须创建的结构体是struct u_driver。这个结构体必须由USB驱动程序来填写,包括许多回调函数和变量,它们向USB核心代码描述USB驱动程序。创建一个有效的 struct u_driver结构体,只须要初始化五个字段就可以了,在框架程序中是这样的:
static struct u_driver skel_driver = {
.owner = THIS_MODULE,
.name =”skeleton”,
.probe = skel_probe,
.disconnect = skel_disconnect,
.id_table = skel_table,
};
探测和断开:当 一个设备被安装而USB核心认为该驱动程序应该处理时,探测函数被调用,探测函数检查传递给它的设备信息,确定驱动程序是否真的适合该设备。当驱动程序因 为某种原因不应该控制设备时,断开函数被调用,它可以做一些清理工作。探测回调函数中,USB驱动程序初始化任何可能用于控制USB设备的局部结构体,它 还把所需的启梁仿任何设备相关信息保存到一个局部结构体中悄纤,
提交和控制urb:当驱动程序有数据要发送到USB设备时(大多数情况是在驱动程序的写函数中),要分配一个urb来把数据传输给设备:
/* 创建一个urb,并且给它分配一个缓存*/
urb = u_alloc_urb(0, GFP_KERNEL);
if (!urb) {
retval = -ENOMEM;
goto error;
}
当urb被成功分配后,还要创建一个DMA缓冲区来以高效的方式发送数据到设备,传递给驱动程序的数据要复制到这块缓冲中去:
buf = u_buffer_alloc(dev->udev, count, GFP_KERNEL, &urb->transfer_dma);
if (!buf) {
retval = -ENOMEM;
goto error;
}
if (copy_from_user(buf, user_buffer, count)) {
retval = -EFAULT;
goto error;
}
当数据从用户空间正确复制到局部缓冲区后,urb必须在可以被提交给USB核心之前被正确初始化:
/* 初始化urb */
u_fill_bulk_urb(urb, dev->udev,
u_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),
buf, count, skel_write_bulk_callback, dev);
urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
然后urb就可以被提交给USB核心以传输到设备了:
/* 把数据从批量OUT端口发出 */
retval = u_submit_urb(urb, GFP_KERNEL);
if (retval) {
err(“%s – failed submitting write urb, error %d”, __FUNCTION__, retval);
goto error;
}
当urb被成功传输到USB设备之后,urb回调函数将被USB核心调用,在我们的例子中,我们初始化urb,使它指向skel_write_bulk_callback函数,以下就是该函数:
static void skel_write_bulk_callback(struct urb *urb, struct pt_regs *regs)
{
struct u_skel *dev;
dev = (struct u_skel *)urb->context;
if (urb->status &&
!(urb->status == -ENOENT ||
urb->status == -ECONNRESET ||
urb->status == -ESHUTDOWN)) {
dbg(“%s – nonzero write bulk status received: %d”,
__FUNCTION__, urb->status);
}
/* 释放已分配的缓冲区 */
u_buffer_free(urb->dev, urb->transfer_buffer_length,
urb->transfer_buffer, urb->transfer_dma);
}
有时候USB驱动程序只是要发送或者接收一些简单的数据,驱动程序也可以不用urb来进行数据的传输,这是里涉及到两个简单的接口函数:u_bulk_msg和u_control_msg ,在这个USB框架程序里读操作就是这样的一个应用:
/* 进行阻塞的批量读以从设备获取数据 */
retval = u_bulk_msg(dev->udev,
u_rcvbulkpipe(dev->udev, dev->bulk_in_endpointAddr),
dev->bulk_in_buffer,
min(dev->bulk_in_size, count),
&count, HZ*10);
/*如果读成功,复制到用户空间 */
if (!retval) {
if (copy_to_user(buffer, dev->bulk_in_buffer, count))
retval = -EFAULT;
else
retval = count;
}
u_bulk_msg接口函数的定义如下:
int u_bulk_msg(struct u_device *u_dev,unsigned int pipe,
void *data,int len,int *actual_length,int timeout);
其参数为:
struct u_device *u_dev:指向批量消息所发送的目标USB设备指针。
unsigned int pipe:批量消息所发送目标USB设备的特定端点,此值是调用u_sndbulkpipe或者u_rcvbulkpipe来创建的。
void *data:如果是一个OUT端点,它是指向即将发送到设备的数据的指针。如果是IN端点,它是指向从设备读取的数据应该存放的位置的指针。
int len:data参数所指缓冲区的大小。
int *actual_length:指向保存实际传输字节数的位置的指针,至于是传输到设备还是从设备接收取决于端点的方向。
int timeout:以Jiffies为单位的等待的超时时间,如果该值为0,该函数一直等待消息的结束。
如果该接口函数调用成功,返回值为0,否则返回一个负的错误值。
u_control_msg接口函数定义如下:
int u_control_msg(struct u_device *dev,unsigned int pipe,__u8 request,__u8requesttype,__u16 value,__u16 index,void *data,__u16 size,int timeout)
除了允许驱动程序发送和接收USB控制消息之外,u_control_msg函数的运作和u_bulk_msg函数类似,其参数和u_bulk_msg的参数有几个重要区别:
struct u_device *dev:指向控制消息所发送的目标USB设备的指针。
unsigned int pipe:控制消息所发送的目标USB设备的特定端点,该值是调用u_sndctrlpipe或u_rcvctrlpipe来创建的。
__u8 request:控制消息的USB请求值。
__u8 requesttype:控制消息的USB请求类型值。
__u16 value:控制消息的USB消息值。
__u16 index:控制消息的USB消息索引值。
void *data:如果是一个OUT端点,它是指身即将发送到设备的数据的指针。如果是一个IN端点,它是指向从设备读取的数据应该存放的位置的指针。
__u16 size:data参数所指缓冲区的大小。
int timeout:以Jiffies为单位的应该等待的超时时间,如果为0,该函数将一直等待消息结束。
如果该接口函数调用成功,返回传输到设备或者从设备读取的字节数;如果不成功它返回一个负的错误值。
这两个接口函数都不能在一个中断上下文中或者持有自旋锁的情况下调用,同样,该函数也不能被任何其它函数取消,使用时要谨慎。
我们要给未知的USB设备写驱动程序,只需要把这个框架程序稍做修改就可以用了,前面我们已经说过要修改制造商和产品的ID号,把0xfff0这两个值改为未知USB的ID号。
#define USB_SKEL_VENDOR_IDxfff0
#define USB_SKEL_PRODUCT_ID 0xfff0
还 有就是在探测函数中把需要探测的接口端点类型写好,在这个框架程序中只探测了批量(USB_ENDPOINT_XFER_BULK)IN和OUT端点,可 以在此处使用掩码(USB_ENDPOINT_XFERTYPE_MASK)让其探测其它的端点类型,驱动程序会对USB设备的每一个接口进行一次探测, 当探测成功后,驱动程序就被绑定到这个接口上。再有就是urb的初始化问题,如果你只写简单的USB驱动,这块不用多加考虑,框架程序里的东西已经够用 了,这里我们简单介绍三个初始化urb的辅助函数:
u_fill_int_urb :它的函数原型是这样的:
void u_fill_int_urb(struct urb *urb,struct u_device *dev,
unsigned int pipe,void *transfer_buff,
int buffer_length,u_complete_t complete,
void *context,int interval);
这个函数用来正确的初始化即将被发送到USB设备的中断端点的urb。
u_fill_bulk_urb :它的函数原型是这样的:
void u_fill_bulk_urb(struct urb *urb,struct u_device *dev,
unsigned int pipe,void *transfer_buffer,
int buffer_length,u_complete_t complete)
这个函数是用来正确的初始化批量urb端点的。
u_fill_control_urb :它的函数原型是这样的:
void u_fill_control_urb(struct urb *urb,struct u_device *dev,unsigned int pipe,unsigned char *setup_packet,void *transfer_buffer,int buffer_length,u_complete_t complete,void *context);
这个函数是用来正确初始化控制urb端点的。
u8 是 unsigned char
u16 是 unsigned short
u32 是 unsigned int
u8 * 就缺丛敬表示指向unsigned char(无符号字符类型)的伏慎指针,郑陪属于指针类型。
“u8*”是使用typedef或者define重好物新定义过的,
一般代表unsigned char* ,指向无符号字符灶羡数据类型隐袜拍的指针
u8 * :指向unsigned char(无符号字符类型)的指针
unsigned char,字符型,占1个字节
linux u8 u32 定义的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux u8 u32 定义,了解Linux中u8和u32的定义和用途,怎样写linux下的USB设备驱动程序,C语言“u8 *“什么类型?的信息别忘了在本站进行查找喔。
香港云服务器机房,创新互联(www.cdcxhl.com)专业云服务器厂商,回大陆优化带宽,安全/稳定/低延迟.创新互联助力企业出海业务,提供一站式解决方案。香港服务器-免备案低延迟-双向CN2+BGP极速互访!
网站名称:了解Linux中u8和u32的定义和用途(linuxu8u32定义)
新闻来源:http://www.shufengxianlan.com/qtweb/news17/129817.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联