当前位置: 首页 > news >正文

贵阳网站设计有哪些建设摩托车官网官方网站

贵阳网站设计有哪些,建设摩托车官网官方网站,网站被黑解决方案,阿里云的网站模版好吗线程概念 一、理解线程1. Linux中的线程2. 重新定义线程和进程3. 进程地址空间之页表4. 线程和进程切换5. 线程的优点6. 线程的缺点7. 线程异常8. 线程用途9. 线程和进程 二、线程控制1. pthread 线程库#xff08;1#xff09;pthread_create()#xff08;2#xff09;pth… 线程概念 一、理解线程1. Linux中的线程2. 重新定义线程和进程3. 进程地址空间之页表4. 线程和进程切换5. 线程的优点6. 线程的缺点7. 线程异常8. 线程用途9. 线程和进程 二、线程控制1. pthread 线程库1pthread_create()2pthread_join()3pthread_exit()4pthread_cancel()5简单使用 pthread 库 2. 理解线程库1线程 id2线程栈3线程局部存储 3. 分离线程 一、理解线程 什么是线程呢下面我们直接说定义再理解。线程就是进程内的一个执行分支线程的执行粒度要比进程细。 1. Linux中的线程 下面我们开始理解一下Linux中的线程。我们以前说过一个进程被创建出来要有自己对应的进程PCB的也就是 task_struct也要有自己的地址空间、页表经过页表映射到物理内存中。所以在进程角度我们能看到进程所有的资源时目前就能通过地址空间来看所以地址空间是进程的资源窗口 以前我们谈的进程它所创建的地址空间内的所有资源都是由一个 task_struct 所享有的那么页表也是属于它独有的。那么如果我们再创建一个“进程”但是不再给这个“进程”创建新的地址空间和页表它只需要在创建时指向“父进程”的地址空间。将来“父进程”就将代码区中的代码分一部分给这个“子进程”以及其它数据分一部分给它此时我们就可以让“父进程”在运行的时候“子进程”也在运行。那么该父进程能创建一个就能创建很多个如下图 那么我们新创建出来的“子进程”它们在执行粒度上要比“父进程”的执行粒度要更细一些因为以前“父进程”需要执行全部代码而这些“子进程”只需要执行一部分代码所以为了明显区分这些“子进程”和“父进程”我们把这种形式的“子进程”称为线程 所以在 Linux 中线程在进程“内部”执行也就是线程在进程的地址空间内运行。那么它为什么要在进程的地址空间内运行呢首先任何执行流要执行都要有资源而地址空间是进程的资源窗口 那么在 CPU 看来它知道这个 task_struct 是进程还是线程吗它需要知道吗并不需要因为CPU只有调度执行流的概念 2. 重新定义线程和进程 那么有了上面的基础我们现在重新定义线程和进程的概念。 线程我们认为线程是操作系统调度的基本单位 所以什么到底什么是进程呢我们以前说的进程指的是 task_struct 和代码数据但是今天很显然已经有分歧了因为它只是地址空间的一个执行分支一个执行分支不能代表整个进程那么我们现在需要重新理解一下了全部 task_struct 执行流都叫做进程执行流地址空间都叫做进程所占有的资源页表和该进程所占用的物理内存我们把这一整套才称之为进程如下图 进程进程是承担分配系统资源的基本实体 那么执行流是资源吗是的所以不要认为一个进程能被调度它就是进程的所有它只是进程内部的一个执行流资源被CPU执行了所以进程和线程之间的关系是进程内部是包含线程的因为进程是承担分配系统资源的基本实体而线程是进程内部的执行流资源 那么如何理解我们以前学的进程呢其实就是操作系统以进程为单位给我们分配资源只是我们以前进程内部只有一个执行流资源也就是只有一个 task_struct只是我们可以认为以前我们学的进程只是进程的一种特殊情况 管理线程 那么既然操作系统要对进程管理如果线程多起来了操作系统要对线程管理吗很明显如果不对线程管理那么线程就不知道自己属于哪个进程更不知道应该执行哪个进程的代码所以必须得对线程管理所以需要先描述再组织进行管理 所以除了Linux之外大多数操作系统都是对线程重新进行先描述再组织重新为线程建立一个内核数据结构对线程管理起来而这个结构叫做 struct tcb除此之外还要把进程和线程之间关联起来。实际上这样做太复杂了维护的关系太复杂了。那么 Linux 中没有重新为线程重新设计一个内核数据结构而是复用进程的数据结构和管理算法 3. 进程地址空间之页表 我们上面的进程中创建线程后给线程分配一部分代码和数据也就是资源那么我们应该如何理解基于地址空间的多个执行流分配资源的情况呢怎么知道哪部分资源给哪个线程呢接下来我们基于地址空间理解一下。 首先CPU里面有一个CR3寄存器它会保存页表的地址方便找到进程的页表。我们也知道物理内存被分为许多的页框每个页框的大小为 4KB。下面我们理解一下虚拟地址是如何转换为物理地址的我们以32位的计算机为例也就是虚拟地址也是32位的。 接下来我们展开说一说页表。首先页表不是一个整体我们假设页表是一个整体就单单是一个映射关系如下图每一列分别是虚拟地址、物理地址、权限假设每一行就10个字节单单这一个页表建立整个虚拟空间的地址映射关系就需要有 2^32 个映射条目这样算下来这个页表就已经几十G了所以页表不可能是这个形式的。 其实 32 位的虚拟地址不是一个整体其实是将它分为了 10 10 12其中 10 10 分别代表一级和二级目录。 其中第一级页表只有 1024 个条目也就是一个数组因为用 10 个比特位表示的最大值就是 1024所以这 10 个比特位代表的十进制数就是该一级页表的下标而一级页表中存放的是二级页表的地址所以只需要拿着前十位找到二级页表的地址找到二级页表然后拿着次十位也是 10 个比特位把它转为十进制数然后在二级页表中索引它的下标那么二级页表中存的是什么呢存的是页框的起始地址如下图 其实这个一级页表就叫做页目录我们把页目录里面的内容叫做页目录表项把二级页表里面的内容叫做页表表项。所以我们就能通过虚拟地址的前 20 位找到物理内存中页框的起始地址。 那么剩下的 12 位呢那么我们知道 2^12 的大小刚好就是 4096如果取字节为单位也就是页框的大小所以剩下的 12 个比特位就是作为某个物理地址的页框中的偏移量也就是说物理地址 页框起始地址 虚拟地址的最后12位所以这就是虚拟地址到物理地址转换的过程 在正常情况下我们不可能将虚拟空间全部用完所以二级页表也不一定全部存在。所以当需要访问一个虚拟地址时怎么知道这个虚拟地址在不在物理内存中呢就有可能在查页目录的时候它的二级页表的目录根本就不存在说明就没有被加载到内存这个时候就是缺页中断。另外也有可能二级页表和页框没有建立映射关系在二级页表中还有一个字段中的标记位会记录页框是否存在。 那么就有一个问题了我们通过页表找到的是物理内存的某一个地址可是对于某一个类型可能是 int、double 等等我们并不是访问一个字节呀对于上面两种类型我们访问的是 4、8 个字节啊。这时候就能体现了类型的价值例如一个整型变量 a占4个字节就要有4个地址但是为什么我们 a只拿到了一个地址因为我们只能取一个地址那么4个地址中只能取最小的那一个由于有类型的存在我们只要从下往上连续读取 4 个字节就能找到它了也就是根据起始地址偏移量读取该变量。那么CPU怎么知道根据什么类型读取多少字节呢其实类型是给 CPU 看的CPU在读取类型时是知道有多少字节的我们根据软件帮CPU找到起始地址接下来CPU就要读取内存读的过程把物理内存在硬件上拷贝给CPU拷贝的时候CPU就知道拷贝多少字节了 所以我们上面说的 CR3 寄存器中指向的其实是页目录的地址任何一个进程必须得有页目录。如果对物理地址进行访问的时候如果物理地址不存在或者越界了CPU 中的 CR2 寄存器保存的是引起缺页中断或者异常的虚拟地址完成建立物理地址后就会去 CR2 取回对应的虚拟地址。 最后我们谈上面的内容都是为了理解如何进行资源分配的线程的资源全部都是通过地址空间来的而代码和数据都是通过地址空间页表映射来的所以线程分配资源的本质就是分配地址空间范围 4. 线程和进程切换 为什么线程比进程要更轻量化呢 创建和释放更加轻量化切换更加轻量化 线程切换时线程的上下文肯定是要切换的但是页表不需要切换地址空间不需要切换所以线程在切换的时候只是局部在切换所以线程切换的效率更高。 线程在执行本质就是进程在执行因为线程是进程的执行分支。线程在执行本质就是进程在调度CPU内有一个硬件级别的缓存叫做 cachecache 也是根据局部性原理将线程/进程当前访问的代码附近的代码都加载到 cache 中所以在进程调度的时候它应该会越跑越快因为它的命中率会越来越高这部分 cache 我们称为进程运行时的热数据热数据就是这部分数据被高频访问所以CPU在硬件上它就会把对应的数据加载到 cache 里。所以在调度的时候它切换的是一个进程中的多个线程那么它在切换的时候此时上下文虽然一直在变化但是 cache 里的数据一直不变或者少量的更新因为每一个线程很多属性都是共享的就是为了让多个线程同时访问所以数据就可以在一个进程内部的多个线程互相调度的时候CPU当前 cache 中的数据就可以被多个线程用上所以在线程切换的时候只需要切换线程不需要对 cache 保存。但是当线程的所有时间片用完了整个进程也要被切换CPU寄存器要保存最重要的是热缓存数据需要被丢弃掉把另一个进程放上来需要重新缓存 cache 中的数据就要需要由冷变热这就需要一段时间。所以线程切换的效率更高更重要的是体现在 cache 数据不需要重新被缓存 5. 线程的优点 创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。 6. 线程的缺点 性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。 健壮性降低 编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。 缺乏访问控制 进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。 编程难度提高 编写与调试一个多线程程序比单线程程序困难得多。 7. 线程异常 单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。 8. 线程用途 合理的使用多线程能提高CPU密集型程序的执行效率合理的使用多线程能提高IO密集型程序的用户体验如生活中我们一边写代码一边下载开发工具就是多线程运行的一种表现。 9. 线程和进程 进程是资源分配的基本单位线程是调度的基本单位线程共享进程数据但也拥有自己的一部分数据: 线程ID一组寄存器(线程上下文)栈errno信号屏蔽字调度优先级 进程的多个线程共享同一地址空间因此代码区数据区都是共享的如果定义一个函数在各线程中都可以调用如果定义一个全局变量在各线程中都可以访问到。除此之外各线程还共享以下进程资源和环境 文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户 id 和组 id 二、线程控制 1. pthread 线程库 因为 Linux 中没有专门为线程设计一个内核数据结构所以内核中并没有很明确的线程的概念而是用进程模拟的线程只有轻量级进程的概念。这就注定了 Linux 中不会给我们直接提供线程的系统调用只会给我们提供轻量级进程的系统调用可是我们用户需要线程的接口所以在用户和系统之间Linux 开发者们给我们开发出来一个 pthread 线程库这个库是在应用层的它是对轻量级进程的接口进行了封装为用户提供直接线程的接口虽然这个是第三方库但是这个库是几乎所有的 Linux 平台都是默认自带的所以在 Linux 中编写多线程代码需要使用第三方库 pthread 线程库 1pthread_create() 接下来我们介绍 pthread 库中的第一个接口创建一个线程 int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);其中第一个参数是一个输出型参数一旦我们创建好线程我们是需要线程 id 的所以该参数就是把线程 id 带出来第二个参数 attr 为线程的属性我们不用关心设为 nullptr 即可。 第三个参数是一个函数指针类型也就是说我们需要传一个函数进去。当我们创建线程的时候我们是想让执行流执行代码的一部分那么我们就可以把该线程要执行入口函数地址传进去线程一启动就会转而执行该指针指向的函数处关于该函数指针的返回值和参数都是 void*因为 void* 可以接收或者返回任意指针类型这样就可以支持泛型了。而第四个参数 arg 是一个输入型参数当线程创建成功新线程回调线程函数的时候如果需要参数这个参数就是给线程函数传递的也就是说该参数是给第三个参数函数指针中的参数传递的。 而函数的返回值如果我们创建成功就返回0如果失败会返回错误码而没有设置 errno. 最后我们在编译的时候需要加上 -lpthread 指定库名称。 示例代码 void* pthread_handler(void* attr){while(1){cout i am a new thread, pid: getpid() endl;sleep(2);}}int main(){pthread_t tid;pthread_create(tid, nullptr, pthread_handler, nullptr);while(1){cout i am main thread, pid: getpid() endl;sleep(1);}return 0;}如上图我们以前写的代码中是不可能出现两个死循环的但是使用创建线程之后就可以了这就说明它们是不同的执行流。而它们的 pid 是一样的就说明它们是同一个进程。 而我们右侧终端中正在查看两个执行流其中查看执行流的指令为ps -aL我们上面循环打印了方便观察我们看到 pid 是一样的但是 LWP 是什么呢为什么会不一样呢在 Linux 中没有具体的线程概念只有轻量级进程的概念所以 CPU 在调度时不仅仅只要看 pid更重要的是每一个轻量级进程也要有自己对应的标识符所以轻量级进程就有了 LWP (light weight process)这样的标识符所以 CPU 是按照 LWP 来进行调度的 但是我们如果杀掉上面任意一个执行流的 LWP默认整个进程都会被终止这就是线程的健壮性差的原因。 如果我们定义一个函数或者全局变量分别在两个执行流中执行它们都可以读取到该函数和全局变量如下代码 void Print(const string str){cout str endl;}void* pthread_handler(void* attr){while(1){cout i am a new thread, pid: getpid() , val val endl;Print(i am new thread);sleep(2);}}int main(){pthread_t tid;pthread_create(tid, nullptr, pthread_handler, nullptr);while(1){cout i am main thread, pid: getpid() , val val endl;Print(i am main thread);val;sleep(1);}return 0;}有关线程的 id 的问题我们后面再谈。 2pthread_join() 那么创建线程后是主线程先运行还是新线程先运行呢不确定要看CPU先调度谁那么肯定的是主线程是最后退出的因为主线程退了整个进程就退出了所以主线程要进行线程等待如果主线程不进行线程等待会导致类似于僵尸进程的问题而 pthread_join() 就是进行线程等待的接口。 int pthread_join(pthread_t thread, void **retval);其中第一个参数为线程的 id第二个参数 retval 我们先不管后面再介绍设为 nullptr 即可。下面我们简单写一个程序 void* pthread_handler(void* attr){int cnt 5;while(cnt--){cout i am a new thread, pid: getpid() endl;sleep(1);}}int main(){pthread_t tid;pthread_create(tid, nullptr, pthread_handler, nullptr);pthread_join(tid, nullptr);cout main thread quit... endl;return 0;}结果如下 我们可以看到当新线程在运行的时候主线程并没有直接运行结束而是进行阻塞等待 接下来我们说一下第二个参数 retval其实我们给线程分配的函数它的返回值是直接写入 pthread 库中的而 retval 也是被封装在库中所以我们可以根据 retval 读取到函数的返回值也就是说这个 retval 就是一个输出型参数首先我们需要定义一个 void* 类型的变量然后将这个变量取地址当作 pthread_join 的第二个参数传入即可例如以下代码 void* pthread_handler(void* attr){return (void*)1234;}int main(){pthread_t tid;pthread_create(tid, nullptr, pthread_handler, nullptr);void* retval;pthread_join(tid, retval);cout main thread quit, retval (long long)retval endl;return 0;}3pthread_exit() 那么除了在函数中直接 return 终止线程外还有什么方法吗有的pthread_exit() 接口就是用来终止线程的 void pthread_exit(void *retval);参数就是和 void* 返回值一样。注意线程内不能使用 exit() 系统接口终止线程因为 exit() 是用来终止进程的例如 void* pthread_handler(void* attr){pthread_exit((void*)1234);}4pthread_cancel() 除了上面的方法pthread_cancel() 也可以取消一个线程参数就是目标线程的 id int pthread_cancel(pthread_t thread);返回值如下 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉 pthread_join 第二个参数 retval 所指向的单元里存放的是常数PTHREAD_ CANCELED也就是 -1. 5简单使用 pthread 库 假设我们现在需要写一个线程进行整数相加代码如下 Request 类为一个需求类_start 和 _end 为需要求的整数相加的范围。 class Request{public:Request(int start, int end):_start(start),_end(end){}~Request(){cout ~Request() endl;}public:int _start;int _end;};Result 类为一个结果的类Run 方法为求和方法_result 为计算结果_exitcode 为记录计算结果是否可靠。 class Result{public:Result(int result, int exitcode):_result(result),_exitcode(exitcode){}void Run(int start, int end){for(int i start; i end; i){_result i;}}~Result(){cout ~Result() endl;}public:int _result; // 计算结果int _exitcode; // 计算结果是否可靠};下面为测试代码 void* countSum(void* args){Request* rq static_castRequest*(args);Result* res new Result(0, 0);res-Run(rq-_start, rq-_end);return res;}int main(){Request* rq new Request(1, 100);pthread_t tid;pthread_create(tid, nullptr, countSum, rq);void* res;pthread_join(tid, res);Result* req static_castResult*(res);cout req-_result endl;delete req;delete rq;return 0;}结果如下 所以线程的参数和返回值不仅仅可以用来进行传递一般参数也可以传递对象 2. 理解线程库 1线程 id 我们上面学习了 pthread_create() 接口但是第一个参数就是线程的 id我们至今都没有介绍过它所以我们可以尝试打印一下看一下它究竟长什么样如下 int main(){pthread_t tid;pthread_create(tid, nullptr, mythread, nullptr);cout tid tid endl;pthread_join(tid, nullptr);return 0;}我们可以看到 tid 是一个非常大的数字假设我们换成十六进制呢如下图 我们可以看到它很像一个地址。 如果线程想要获得自己的线程 id还可以通过线程库中的接口获得如下 pthread_t pthread_self(void);返回值就是线程的 id. 那么这个线程 id 究竟是什么呢因为 Linux 中没有明确的线程概念所以没有直接提供线程的系统接口只能给我们提供轻量级进程的系统接口那么系统中是怎么创建轻量级进程呢其实是用 clone() 接口如下 其实这个接口就是创建一个子进程fork() 的底层原理和 clone() 类似但是 clone() 是专门用来创建轻量级进程的。第一个参数函数指针类型就是新创建执行流要执行的函数地址入口第二个参数 child stack 就是自己自定义的栈第三个参数就是是否让地址空间共享后面的参数就不用关心了。 所以这个接口就被线程库封装了给我们提供的就是我们上面所介绍的线程库的接口。所以clone() 允许用户传入一个回调函数和一个用户空间来代表这个轻量级进程运行过程中所执行的代码它在运行中的临时变量全部放在用户空间栈上。也就是说线程库需要封装 clone() 的话线程库中每一个线程都要给 clone() 提供执行方法还要在线程库中开辟空间。所以线程的概念是库给我们维护的。另外我们用的第三方线程库是需要加载到内存里的而且是加载到共享区中那么在 pthread 库里面每个创建好的线程它就要为该线程在库里面开辟一段空间用来充当新线程的栈也就是说新线程的栈是在共享区当中的 那么线程的概念是库给我们维护的也就是说线程库要维护线程的概念不需要维护线程的执行流。也就是线程库中的线程在底层中对应的其实是轻量级进程的执行流但是线程相关的属性等字段必须需要库来维护所以线程库注定了要维护多个线程属性的集合所以线程库需要先描述再组织管理这些线程如下图 所以我们每创建一个线程在线程库中就要为我们创建线程库级别的线程我们把它叫做线程控制块。所以这个线程控制块我们就可以理解成 tcb那么对于每一个 tcb 在库中可以理解成用数组的方式进行管理维护。所以为了让我们快速找到在共享库中的每一个 tcb我们把每一个 tcb 在内存中的起始地址称为线程的 tid即线程的 id 2线程栈 每一个线程在运行时一定要有自己独立的栈结构因为每一个线程都要有自己的调用链也就是说每一个线程都要有自己调用链所对应的栈帧结构。这个栈结构会保存任何一个执行流在运行过程中的所有临时变量。其中主线程用地址空间提供的栈结构即可而新线程则是首先在库中创建一个线程控制块这个控制块中有包含默认大小的空间就是线程栈然后库就要帮我们调用系统接口 clone() 帮我们创建执行流最重要的是它会帮我们把线程栈传递给 clone() 作为它的第二个参数 所以所有对应的非主线程的栈都在库中进行维护即在共享区中维护具体来说是在 pthread 库中 tid 指向的线程控制块中 我们可以写代码验证一下每一个线程都有自己独立的栈代码链接验证独立栈. 结果如下test_stack 是三个线程里的临时变量它们的地址都不一样 同时我们也可以验证全局变量是可以被所有线程同时看到并访问的。 其实线程和线程之间几乎没有秘密虽然它们是独立的栈但是线程上的数据也是可以被其它线程访问到的。 3线程局部存储 我们知道全局变量是可以被所有线程访问的但是假设我们的线程想要一个私有的全局变量呢我们可以在一个全局变量前加上 __thread如下 __thread int g_val 100;接下来我们使用上面的代码设置这样一个全局变量并打印它的信息出来观察 我们发现每一个线程中的 g_val 的地址都是不一样的而且对 g_val 运算的时候它们互不干扰所以这个 g_val 加上 __thread就变成了线程的全局变量其实 __thread 不是 C/C 提供的而是一个编译选项。我们发现打印出来的地址非常大因为它是在堆栈之间的地址它是位于线程控制块的线程局部存储区域 注意线程局部存储只能定义内置类型不能定义自定义类型 3. 分离线程 默认情况下新创建的线程是 joinable 的线程退出后需要对其进行 pthread_join 操作否则无法释放资源从而造成系统泄漏。 如果不关心线程的返回值join 是一种负担因为主线程需要等待其它线程这个时候我们可以告诉系统当线程退出时自动释放线程资源这就叫做线程的分离可以用如下接口 int pthread_detach(pthread_t thread);其中参数就是线程的 tid. 可以是线程组内其他线程对目标线程进行分离也可以是线程自己分离 pthread_detach(pthread_self());
http://www.tj-hxxt.cn/news/228430.html

相关文章:

  • 英语培训学校网站怎么做网站开发一年多少钱
  • cad做彩图那个网站应用好用wordpress安装包文件
  • 网站设计建设合同怎样做 云知梦 网站 付费网站
  • 巴南网站建设哪家好龙岗模板网站建设
  • 深圳网站建设合同东莞浩智网站建设开发
  • 做什么类型网站可以吸引用户东莞能做网站的公司
  • 家庭农场做网站的好处黑龙江省网站备案
  • 哈尔滨建设工程信息网站网页界面设计与制作邓文达
  • 网站建设是怎么赚钱的wordpress图片上传到
  • 哪家公司网站建设口碑好温州哪里有网站
  • 北京公司建网站要多少费用建设网站需要下载神呢软件吗
  • 网站建设实训报告模板建筑设计专业的网站
  • 绍兴网站制作推广企业网站建站费用
  • 南京电子商务网站建设wordpress 分词标签
  • 昆明岭蓝网站建设公司网站建设yankt
  • 为什么企业建设银行网站打不开佛山市企业网站seo联系方式
  • 南京网站搭建商务网站建设管理思路
  • 学习做ppt 的网站网站设计深圳哪家强?
  • WordPress单页随机常州网络优化排名
  • 建一个平台网站需要多少钱婚庆网站建设策划案
  • 网站开发技术选型品牌设计作品
  • 建站程序选择荆州北京网站建设
  • 华为公司网站建设方案建设教育网站费用
  • 为什么搜索不到刚做的网站用ppt做网站方法
  • 新颖的网络营销方式seo网络推广专员
  • 网站导航如何用响应式做网站优化排名工具
  • 邢台市做网站电话档案网站建设优秀代表
  • 网站建设的出发点闵行网站建设公司
  • 华汇建设集团有限公司网站新东方考研班收费价格表
  • 上海地区做旧物回收的网站公关公司提供的服务有哪些