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

Linux文件fd-重定向-缓冲区

文章目录

  • 一、共识原理
  • 二、浅谈C语言文件操作
    • 2.1w方式打开文件
    • 2.2a方式打开文件
    • 2.3C标准输入输出流
  • 三、文件系统调用
    • 3.1open
      • 3.1.1比特位方式的标记位传递方式
      • 3.1.2open函数标记位
    • 3.2close
    • 3.3write
      • 3.3.1O_TRUNC
      • 3.3.2O_APPEND
  • 四、文件fd
    • 4.1原理
    • 4.2验证fd
    • 4.3file的引用计数
  • 五、重定向
  • 5.1文件描述符的分配规则
    • 5.2dup2函数
      • 5.2.1追加重定向
    • 5.2.2输入重定向
    • 5.3>、>>、<与重定向之间的关系
      • 5.3.1stdout与stderr结合操作
  • 5.4如何理解linux下一切皆文件
  • 六、文件缓冲区
    • 6.1前置
    • 6.2C语言缓冲区的刷新问题
    • 6.3为什么要有这个缓冲区
      • 6.3.1解决效率问题--用户的效率问题
      • 6.3.2.配合格式化
    • 6.4这个缓冲区在哪里--FILE结构体
    • 6.5fork打印两次问题

一、共识原理

1.文件=内容+属性
2.文件分为打开的文件和没打开的文件
3.打开的文件是由进程打开的–本质是研究文件和进程的关系
本章主要谈论的是打开的文件
文件被打开,必须要由磁盘加载到内存
进程可以一次性打开多个文件
操作系统内部,一定存在大量被打开的文件,操作系统就需要对这些被打开的文件做管理–先描述,再组织

二、浅谈C语言文件操作

2.1w方式打开文件

#include <stdio.h>
int main()
{FILE* f=fopen("log.txt","w");if(f==NULL){
perror("open failed");
return 1;}fclose(f);return 0;
}

fopen当我们以写的方式打开的时候,如果没有这个文件的话,它会默认在当前路径下新建一个文件
当前路径就是进程所在的路径,操作系统可以通过类似pwd操作或者getcwd函数来获取到当前路径,如果更改了当前文件的cwd就可以将文件更改到其他的工作目录

  chdir("/home/wyx");
[wyx@hcss-ecs-000a ~]$ ll | grep log.txt
-rw-rw-r-- 1 wyx wyx0 Sep 21 15:50 log.txt

如果有这个文件的话,以写的方式打开它会清空文件的内容并从文件的开始处写

const char*newfile="hello today";
fwrite(newfile,strlen(newfile),1,f);

strlen(newfile)的话是不需要加1的,因为\0是C语言定义出来的,也属于字符,被文件写入的话就属于无效的信息

2.2a方式打开文件

a是在文件的结尾处进行写入也就是以追加写的方式

 FILE* f=fopen("log.txt","a"); 
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
hello today
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
hello today
hello today

2.3C标准输入输出流

C程序默认在启动的时候,会打开三个标准输入输出流
stdin:键盘文件
stdout:显示器文件
stderr:显示器文件

  fprintf(stdout,newfile);
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
hello today

在C语言眼中,像普通文件写入和显示器文件写入都是没有区别的,返回的都是FILE*指针,代表我C语言都可以用这个指针去访问你们的文件

三、文件系统调用

文件其实是在磁盘上的,磁盘是外部设备,访问磁盘文件其实是访问硬件
用户->程序->操作系统->硬件驱动->硬件
用户想访问硬件就必须要通过操作系统,但是操作系统不相信任何人,所以操作系统在上层给我提供了系统调用
几乎所有的库只要是访问硬件设备,必定要封装系统调用
因为操作系统很多,windows,linux等,我们没有那么大的能力去了解每一个系统的系统调用接口,也不通用,所以语言层面就给我提供库函数封装这些接口,让我们站在语言层面以统一的视角去访问文件

3.1open

   #include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>int open(const char *pathname, int flags);int open(const char *pathname, int flags, mode_t mode);int creat(const char *pathname, mode_t mode);

1.open–帮助我们打开或者创建一个文件或设备
2.pathname–要打开的文件名
flags–要打开的模式,比如r读还是w写,实际是以位图的角度操作的
3.mode–指明打开文件的权限

3.1.1比特位方式的标记位传递方式

#include <stdio.h>
#include <unistd.h>
#include <string.h>#define ONE (1<<0) //1
#define TWO (1<<1) //2
#define THREE (1<<2) //4void bit(int flags)
{
if(flags&ONE)printf("功能1\n");
if(flags&TWO)printf("功能2\n");
if(flags&THREE)printf("功能3\n");
}
int main()
{
bit(ONE);
printf("---------\n");
bit(TWO);
printf("---------\n");
bit(ONE|TWO);
printf("---------\n");
bit(ONE|THREE);
printf("---------\n");
bit(ONE|TWO|THREE);
printf("---------\n");
return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
功能1
---------
功能2
---------
功能1
功能2
---------
功能1
功能3
---------
功能1
功能2
功能3
---------

按位或|的操作主要是有1的都为1,所以通过|把1都保留了下来
按位与&就是筛选掉与右边共同有1的位数,比如011和001就保存下来了001,我们就可以通过这种方式来执行不同的功能

3.1.2open函数标记位

O_RDONLY:以只读方式打开文件
O_WRONLY:以只写方式打开文件,并不会帮助我们新建文件
O_RDWR:以读写方式打开文件。
O_CREAT:若⽂件不存在,创建该文件。通过mode参数来设置其文件权限
O_TRUNC:如果文件已存在且成功打开,将长度设置为0
O_APPEND:在文件末尾处进行写入

这个就是像我们上面宏定义的方式,我们可以通过传递多种标记位来一次性完成多种操作

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY);
if(fd < 0)
{
perror("open fail\n");
return 1;
}
return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
[wyx@hcss-ecs-000a lesson8_file]$ ll
total 20
---S--x--T 1 wyx wyx0 Sep 21 19:03 log.txt

我们在通过系统调用打开文件的时候,文件不存在必须要先O_CREAT创建文件才能执行后续的操作

---S--x--T 1 wyx wyx0 Sep 21 19:03 log.txt

但是我们会观察到文件此刻的权限完全是乱的,所以我们要设置权限才不会乱码

int fd=open("log.txt",O_CREAT|O_WRONLY,0666);   
[wyx@hcss-ecs-000a lesson8_file]$ ll
total 20
-rw-rw-r-- 1 wyx wyx0 Sep 21 19:08 log.txt

这时候我们观察到设置权限为0666,但是实际只有0664,实际上是因为umask值为0002

umask(0);
int fd=open("log.txt",O_CREAT|O_WRONLY,0666);
-rw-rw-rw- 1 wyx wyx0 Sep 21 19:11 log.txt

就近原则系统使用进程的umask

3.2close

  #include <unistd.h>int close(int fd);

close只需要使用open的返回值–文件描述符就可以关闭

close(fd);

3.3write

 ssize_t  write(int  fd,  const void*buf, size_t count);

向一个文件描述符中写入内容

 const char* message="hello\n";write(fd,message,strlen(message)); 
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
hello
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
hello

说明每次都从文件的开始处写入

const char* message="aa\n"; 
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
aa
lo

但是我们又改内容写了时候发现后面的东西又不全了,原因是它会覆盖式的向文件进行写入,但是它不会对文件内容做清空

3.3.1O_TRUNC

int fd=open("log.txt",O_CREAT|O_WRONLY|O_TRUNC,0666);   
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
aa

所以我们要做到每次向里面写入的时候都要做清空

3.3.2O_APPEND

O_APPEND向文件里追加写入

 int fd=open("log.txt",O_CREAT|O_WRONLY|O_APPEND,0666);  
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
aa
hello

所以像fopen(“log.txt”,“a”);就是封装了上面的那种形式

四、文件fd

4.1原理

在这里插入图片描述
1.磁盘向内存中写入文件操作系统就势必要对文件做管理,做管理就要先描述,再组织
2.先描述操作系统就要创建一个结构体来维护被打开文件的信息,比如文件在磁盘的什么位置,是谁打开的权限是什么等
3.进程PCB里面也有一个指向结构体的指针,这个结构体里面放了一个指针数组
4.所以进程要维护被打开的文件的信息,只需要把文件结构体的地址填入到地址里面,分配数组的下标
** int open(const char pathname, int flags);*
所以open的返回值int就是数组的下标fd
5.文件描述符fd实际就是数组的下标
6.左侧是进程管理,右侧是文件管理

4.2验证fd

int main()
{
umask(0);
int fd1 = open("log1.txt", O_CREAT | O_WRONLY, 0666);
int fd2 = open("log2.txt", O_CREAT | O_WRONLY, 0666);
int fd3 = open("log3.txt", O_CREAT | O_WRONLY, 0666);
int fd4 = open("log4.txt", O_CREAT | O_WRONLY, 0666);
printf("%d\n", fd1);
printf("%d\n", fd2);
printf("%d\n", fd3);
printf("%d\n", fd4);
return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
3
4
5
6

这是连续的数组下标,但是0,1,2去哪里了呢
这个刚好是三个,我们前面说的stdin,stdout,stderr也是文件,也都可以被那个FILE接受
但是操作系统不认这个FILE
,它只认fd,所以我们大胆的猜测这是0,1,2的数组下标在系统启动的时候这三个数组下标都已经被占用了

int main()
{
const char* message = "hello 1 and 2\n";
write(1, message, strlen(message));
write(2, message, strlen(message));
return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
hello 1 and 2
hello 1 and 2

我们向1和2文件描述符写入发现信息都可以打印在显示屏上这就印证了上面的想法

int main()
{
char buffer[1024];
read(0, buffer, sizeof(buffer));
printf("%s\n", buffer);
return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
abcd
abcd

我们从0号文件描述符里面read一开始就会发现它会等待我们输入
所以0,1,2默认就已经打开了,因为我们连文件都没有创建,就可以直接进行使用
为什么要默认呢,因为这是我们电脑天然就要使用的东西

FILE是C库自己封装的结构体,这里面必须封装文件描述符,因为像之前所说的,不同的操作系统有不同的对文件的管理,可能参数也不一样,所以要封装来适应不同的系统

int main()
{
printf("stdin->%d\n", stdin->_fileno);
printf("stdout->%d\n", stdout->_fileno);
printf("stderr->%d\n", stderr->_fileno);
return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
stdin->0
stout->1
stderr->2

之前说过FILE是结构体,stdin等又刚好是这个类型的结构体,所以我们就能访问到这个结构体里面的文件描述符

4.3file的引用计数

学过C++的都对引用计数都不陌生
一个文件是可以被多个进程打开的
比如1和2文件描述符都指向同一个显示器文件,当一个进程断开和一个文件链接的时候,引用计数就会–,调用close(1),引用计数做–
将1号下标的地址置空,如果为0了,系统再回收这个struct file对象

五、重定向

5.1文件描述符的分配规则

#define filename "test.txt"
int main()
{close(0);   int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,0666);if(fd<0){
perror("open fail");
return 1;}printf("fd->%d\n",fd);const char*message="hello today\n";int cnt=5;while(cnt--){write(fd,message,strlen(message));}close(fd);return 0;
}

依照我们对文件描述符fd的了解,这个文件一开始分配的是3号下标

[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
fd->0
close(1); 

当我们改成关闭1号文件描述符的时候,printf就不向显示屏上打印信息了,这个我们也可以理解,毕竟printf默认就像1号文件描述符上打,但我们可以设想是filename占据了1号下标的文件描述符

    close(2); 
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
fd->2

我们关闭2号文件描述符文件就自动分配到了2号文件描述符的位置
结论:文件描述符对应的分配规则是从0下标开始,寻找最小的没有使用的数组位置,它的下标就是新文件的文件描述符

close(1);write(1,message,strlen(message));

当我们关闭1号文件描述符,再向1号文件描述符写入的时候,就会发现原本应该打印到显示器上的信息打印到了filename文件中,这叫输出重定向
在这里插入图片描述
关闭了1号文件描述符,根据文件描述符的分配规则,会自动分配最小没使用的数组位置,将1号文件描述符里面的地址置空,再将3号文件描述符的地址覆盖上去,这样1号文件描述符就写入了新的地址
write不知道你在操作系统底层把1号文件描述符改了,还是正常往1号文件描述符写入
这就是重定向的原理,重定向的本质就是对数组下标里面的内容做修改
但是这种你必须得先关闭一次再重新打开一个文件,不是很清晰

5.2dup2函数

   #include <unistd.h>int dup(int oldfd);int dup2(int oldfd, int newfd);

上面那种把1号文件描述符置空再将3号文件描述符拷贝过去有点繁琐
我们可以直接将3号文件描述符的地址拷贝到1号文件描述符,这样就覆盖式的写入,1号文件描述符自然就指向了新的文件

 dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, butnote the following:

dup2使newfd成为oldfd的一份拷贝,所以上面的例子3号文件描述符是oldfd,1号是newfd

5.2.1追加重定向

在这里插入图片描述

int main()
{int fd=open(filename,O_CREAT|O_WRONLY|O_APPEND,0666);if(fd<0){
perror("open");
return 1;}dup2(fd,1);close(fd);const char* message="hello today\n";int cnt=5;while(cnt--){
write(1,message,strlen(message));}return 0;
}

这就完成了一次追加重定向

5.2.2输入重定向

#define filename "test.txt"
int main()
{int fd=open(filename,O_CREAT|O_RDONLY,0666);if(fd<0){
perror("open");
return 1;}dup2(fd,0);close(fd);char buffer[1024];ssize_t s=read(0,buffer,sizeof(buffer)-1);if(s>0){
buffer[s]='\0';
printf("echo->%s\n",buffer);  }return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
echo->aaaaaaaaaaaaaa 

原本从键盘上读入的信息现在改成向文件中读入这叫输入重定向

5.3>、>>、<与重定向之间的关系

比如

ls > test.txt

1.当我们在命令行输入这串字符串的时候,会被bash进行解析打散
2.bash会先对这行字符串检测有没有>、>>、<之类的符号,定义宏来区分这三个符号,如果有以符号为分隔符,先将符号设置为’\0’继续向后检测,检测到的字符串保存到特定的字符串里,设置变量来与符号匹配,比如rdir=IN_RDIR,后面那个定义成宏,设定一个数字
3.在bash创建子进程执行普通命令或者bash执行内建命令的时候会先判断变量与哪个宏匹配,比如与>匹配就进入对应的函数,通过系统调用open创建对应的文件,再通过dup2函数将文件的地址拷贝到显示器文件1的地址
4.这样原本ls这样的程序原本是向显示器文件1号描述符打印的,但是程序只知道文件描述符不知道哪个文件,所以原本向显示器文件打的文本信息就改成向自定义文件里面写入了
5.ls本身是字符串,bash要将它进行程序替换将它的代码和数据加载到内存,然后ls要进行重定向的操作,但是这样的命令本身就是向1号fd里面写入的,谁是1号对它来说没有关系
在这里插入图片描述
6.所以我们对内核数据结构文件描述符表的改变和内存管理二者就能进行解耦

5.3.1stdout与stderr结合操作

stdout对应的是1号文件描述符就是显示器文件
stderr对应的是2号文件描述符也是显示器文件
它们主要的作用就是便于区分普通信息和错误的信息

[wyx@hcss-ecs-000a lesson8_file]$ ./myfile 1> test.txt 2>&1

在前面的1号文件描述符重定向到文件里面去,代表1号文件描述符里面已经覆盖了文件的地址
2>&1:这个操作代表把2号文件描述符重定向到1号里面去,所以2号文件描述符覆盖了1号文件描述符的地址,也就是文件的地址

5.4如何理解linux下一切皆文件

1.不同的外设其实是用相同的结构体struct devic管理起来的,毫无疑问里面都要配对读写方法
比如键盘有读文件但是写文件关闭,显示器写文件有读文件关闭,磁盘读写文件都有
2.像不同的设备都有对应的struct file对象,不同的设备厂家内部所提供的读写方法也是不一样的,但是管你叫什么磁盘读写还是显示器读写,我操作系统给你定义的struct file对象里面统一包含一个f_ops指针,指向一个struct operation_func,里面都有着统一命名的读写方法的函数指针
3.当上层调用read等系统调用接口时,上层只管调用struct file里面的f_ops指针,由指针向下一层一层的找到方法
4.这种位于struct file之上的我们叫作虚拟文件系统(VFS)
5.通过这种多态的方式在struct file里面实现类似基类,下层是派生类的方式来进行管理,这种用文件结构体来对设备和资源做管理的方式我们叫作linux下一切皆文件

六、文件缓冲区

6.1前置

int main()
{const char* srg="hello today\n";const char* mysrg="hello linux\n";printf("hello yesterday\n");fprintf(stdout,"hello tomorrow\n");fwrite(srg,strlen(srg),1,stdout);write(1,mysrg,strlen(mysrg));fork();return 0;
}
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile
hello yesterday
hello tomorrow
hello today
hello linux
[wyx@hcss-ecs-000a lesson8_file]$ ./myfile > log.txt
[wyx@hcss-ecs-000a lesson8_file]$ cat log.txt
hello linux
hello yesterday
hello tomorrow
hello today
hello yesterday
hello tomorrow
hello today

我们会发现在创建子进程后,属于C语言的信息被打印了两次,但是直接运行就只是打印了一次
在我们关闭了1号文件描述符的情况下带’\n’和不带’\n’,我们可以来观察一下是什么样的结构

const char* srg="hello today\n";printf("hello yesterday\n");fprintf(stdout,"hello tomorrow\n");fwrite(srg,strlen(srg),1,stdout);close(1);
[wyx@hcss-ecs-000a file_fd]$ ./myfile
hello yesterday
hello tomorrow
hello today

带\n的正常打印

[wyx@hcss-ecs-000a file_fd]$ ./myfile

不带\n的不输出消息

const char* mysrg="hello linux";   
write(1,mysrg,strlen(mysrg));close(1);  
[wyx@hcss-ecs-000a file_fd]$ ./myfile
hello linux

但是write不带\n把1号文件描述符关了照样能打印出来
C语言的信息已经写入到了缓冲区,但是这个缓冲区一定不在操作系统内部,不是系统级别的缓冲区
在这里插入图片描述
因为你如果是系统级别的缓冲区的话,当你在close的时候,他就会直接找到你这个文件,将你文件的缓冲区刷新到磁盘上
当你使用write的时候,它自动就把信息写入到系统级别的缓冲区里面,当你close的时候自动刷新到磁盘
在这里插入图片描述
也就是说使用C语言的函数的时候,会将信息先写到C语言提供的缓冲区,当在合适的时候,比如碰到了\n,fwrite等,它才会调用write接口,将信息写入到内核级别的缓冲区
当没有碰到强制刷新的时候,信息会先保存在缓冲区,但是将1号文件描述符关了,就把显示器文件关掉了,想刷新也找不到系统缓冲区了
显示器的文件刷新方案是行刷新,所以在printf执行完就会立即遇到\n的时候,将数据进行刷新
刷新的本质,就是将数据通过1+write写入到内核中
目前我们认为,只要将数据刷新到了内核,数据就可以到硬件了

6.2C语言缓冲区的刷新问题

1.无缓冲–直接刷新,写到C语言缓冲区直接调用write接口写入到内核
2.行缓冲–不刷新,碰到\n直接刷新–显示器,因为显示器的数据是给人看的,人是按行,为单位看的
3.全缓冲–缓冲区写满了才刷新–向普通文件文件写入,将零碎的数据打包成一个块就会减少I/O次数,提高运行效率
4.进程退出的时候也会刷新

6.3为什么要有这个缓冲区

6.3.1解决效率问题–用户的效率问题

a.无缓冲,就像点一份外卖,要求外卖骑手立马送到你家里去,哪怕点了一杯奶茶,速度快,但是效率低,如果你下午点五杯奶茶就要送五趟
b.行缓冲,就像要发车的班车一样,到点了立马发车,不管你车上有几个人,行刷新既保证了数据的完整性,又比无缓冲高效
c.全缓冲,就像快递公司打包你全校同学的快递,这样我一次性就可以送走,不需要往返,但是准备时间长,效率极高,但是延迟大
好处:减少系统调用次数,匹配数据块大小,提高磁盘I/O效率
可以适应不同的场景

6.3.2.配合格式化

像printf这种都是打印的字符串
但是你在C语言定义的是int变量,需要转化成字符串
转化号的字符串会被组装好放到缓冲区里面
如果没有缓冲区比如iam 25year,就要先将i am先write出去,再将2和5转化为字符刷新出去,再打印year这种拆分式的操作需要很多次的I/O操作,你想你五个快递分五次发出去,五个快递打包成一个快递发出去哪个块

6.4这个缓冲区在哪里–FILE结构体

int fflush(FILE *stream);
int fprintf(FILE *stream, const char *format, ...);
size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);

像我们进行文件操作的时候都离不开这个FILE结构体
fflush这种将C语言缓冲区调用write刷新到内核的也离不开FILE结构体
所以FILE里面还有对应打开文件的缓冲区
在这里插入图片描述
比如我们调用fprint函数将字符串写入到stdout里面去,stdout是FILE结构体的,里面包含着fd,还包含着语言缓冲区,将字符串拷贝到stdout结构体内部的缓冲区里面,再调用write接口刷新到内核
如果打开了10个文件就有10个对应的缓冲区

6.5fork打印两次问题

fork();[wyx@hcss-ecs-000a lesson8_file]$ ./myfile > log.txt

1.重定向是向文件做操作,采用的是全缓冲的形式
2.全缓冲之前打印的信息无论是否有加\n都会被放到缓冲区中
3.当我们要将缓冲区刷新出去的时候本质是对缓冲区做清空,就是对数据做修改
4.对数据做修改子进程就要发生写时拷贝将缓冲区拷贝一份

http://www.dtcms.com/a/400690.html

相关文章:

  • SpringAI Alibaba 集成与简单使用
  • 网站规划与建设规划书wordpress如何添加目录菜单
  • 常州市网站建设设计深圳国外网站制作公司
  • 万网建站流程网络规划设计师属于高级职称吗
  • wordpress建站小百科网站手机模板和pc模板要分开做
  • 网站设计建设公司1.2婚庆网站建设的目的
  • wordpress插件系统大连百度推广seo
  • 张家港做网站的公司用公司注册公司需要什么资料
  • 网站制作替我们购买域名wordpress docker镜像
  • 化肥厂的网站摸板wordpress修改登陆地址后缀
  • 网站设计制作价钱网站开发河南
  • jsp 网站开发永州做网站的公司
  • 广州哪里有做网站icp网站备案系统
  • 手机网站的作用asp装饰公司网站源码
  • 淘宝客网站虚拟主机什么是网络营销促销?网络营销促销有何作用?
  • 池州网站制作哪家好高端网名好听又有个性
  • HTML电影订票网站开发网站流量评价有哪几方面
  • 进行网站开发的所有步骤欧模网室内设计网官网
  • GDB 知识体系
  • 山东专业企业网站建设深圳酒店品牌设计公司
  • 海南做网站的公司有哪些PHP企业网站开发实践
  • 做返利网站能赚钱唐山建设集团网站
  • 网站建设营销模板营销手段有哪些
  • 学网站建设要多久督查营商环境建设网站
  • 网站制作模板北京商检局做产地证的网站
  • 建设音乐网站的目的猪八戒网可以做网站吗
  • 西安建站套餐四川网站建设 招标
  • Bandicam (班迪录屏) 8.2.2.2531 _Win中文_便携版安装教程
  • jsp网站开发实例 pdfwordpress支持的数据量
  • seo自带 网站建设中国500强企业名称