• 20145333《信息安全系统设计基础》第十一周学习总结


    20145333《信息安全系统设计基础》第十一周学习总结

    教材学习内容总结

    8.1异常

    1、异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是有操作系统实现的。

    2、异常:控制流中的突变,用来响应处理器状态中的某些变化。

    3、在处理器中,状态被编码为不同的位和信号。状态变化成为事件。

    4、异常表:当处理器监测到有时间发生时,通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。

    5、当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况的一种:

    (1)处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。 
    (2)处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。 
    (3)处理程序终止被中断的程序。
    

    8.1.1 异常处理

    1、系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。

    • 异常号的分配:

        (1)处理器的设计者:被除零、缺页、存储器访问违例、断点以及算数溢出。
        (2)操作系统内核的设计者分配的:系统调用和来自意外不I/O设备的信号。
      

    2、异常号:到异常表中的索引

    • 异常表基址寄存器:异常表的起始地址存放的位置。

    3、异常与过程调用的异同:

    (1)过程调用时,在跳转到处理器之前,处理器将返回地址压入栈中。然而,根据异常的类型,返回地址要么是当前指令,要么是下一条指令。 
    (2)处理器把一些额外的处理器状态压入栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。 
    (3)如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。 
    (4)异常处理程序运行在内核模式下,意味着它们对所有的系统资源都有完全的访问权限。
    

    8.1.2 异常的类别

    1、异常的分类:中断、陷阱、故障和终止。

    2、中断:异步发生,是来自处理器外部的I/O设备的信号的结果。

    (1)硬件异常中断处理程序通常称为中断处理程序。
    (2)异步异常是有处理器外部的I/O设备中的时间产生的,同步异常是执行一条指令的直接产物。
    (3)陷阱、故障、终止时同步发生的,是执行当前指令的结果,我们把这类指令叫做故障指令。 
    

    3、陷阱和系统调用

    (1)陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
    (2)普通的函数运行在用户模式中,用户模式限制了函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。系统调用运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
    

    4、故障:是由错误情况引起的。

    • 例如:abort例程会终止引起故障的应用程序。 根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止。

    • 例如:缺页故障。

    5、终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序。

    8.2 进程

    1、异常是允许操作系统提供进程的概念所需要的基本构造块。

    • 进程:一个执行中的程序的实例。

    上下文是由程序正确运行所需要的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

    2、进程提供给应用程序的关键抽象:

    • 一个独立的逻辑控制流,独占地使用处理器;

    • 一个私有的地址空间,独占地使用存储器系统。

    8.2.1 逻辑控制流

    1、程序计数器:唯一的对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,简称逻辑流。

    8.2.2 并发流

    1、并发流:一个逻辑流的执行在时间上与另一个流重叠。

    并发:多个流并发地执行的一般现象。
    多任务:一个进程和其他进程轮流运行的概念。 
    时间片:一个进程执行它的控制流的一部分的每一时间段。 
    

    多任务也叫时间分片。

    2、并行流:如果两个流并发的运行在不同的处理器核或者计算机上。

    8.3 系统调用错误处理

    错误处理包装函数:包装函数调用基本函数,检查错误,如果有任何问题就终止。

    8.4 进程控制

    8.4.1 获取进程ID

    1、每个进程都有一个唯一的正数的进程ID。

    2、getpid函数返回调用进程的PID,getppid函数返回它的父进程的PID。上面两个函数返回一个同类型为pid_t的整数值,在linux系统中,它在types.h中被定义为int。

    8.4.2 创建和终止进程

    1、进程总处于三种状态

    (1)运行:进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
    (2)停止:程序的执行被挂起,,且不会被调度。
    (3)终止:进程用永远停止了。
    

    终止原因:

    (1)收到一个信号,默认行为是终止进程
    (2)从主进程返回
    (3)调用exit函数
    

    2、父进程通过调用fork函数创建一个新的运行的子进程。

    3、子进程和父进程的异同:

    • 异:有不同的PID

    • 同:用户级虚拟地址空间,包括:文本、数据和bss段、堆以及用户栈。任何打开文件描述符,子进程可以读写父进程中打开的任何文件。

    4、fork函数: 因为父进程的PID总是非零的,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。

    fork函数的特点:

    (1)调用一次,返回两次
    (2)并发执行
    (3)相同的但是独立的地址空间
    (4)共享文件
    

    8.4.3 回收子进程

    1、当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程。
    一个终止了但还未被回收的进程称为僵死进程。

    2、一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t waitpid(pid_t pid,int *status,int options);
    //返回:若成功,返回子进程的PID;若WNOHANG,返回0;若其他错误,返回-1。
    

    默认地,当option=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。

    3、判定等待集合的成员

    有参数pid来确定的:

    (1)pid>0:等待集合是一个单独的子进程,进程ID等于pid。 
    (2)pid=-1:等待结合就是由父进程所有的子进程组成的。
    

    4、修改默认行为

    通过options设置:

    (1)WNOHANG:默认行为是挂起调用进程。 
    (2)WUNTRACED:默认行为是只返回已终止的子进程。 
    (3)WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或者已终止,那么返回值为0,或者返回值等于那个被停止或者已经终止的子进程的PID。
    

    5、检查已回收子进程的退出状态

    wait.h头文件定义了解释status参数的几个宏:

    (1)WIFEXITED:如果子进程通过调用exit或者一个返回正常终止,就返回真;
    (2)WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回真时,才会定义这个状态。
    

    6、错误条件

    (1)若调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD; 
    (2)若waitpid函数被一个信号中断,那么返回-1,并设置errno为EINTR 
    

    7、wait函数

    #include <sys/types.h>
    #include <sys/wait.h>
    
    pid_t wait(int *status);
    //返回:若成功,返回子进程的PID;若错误,返回-1。
    

    调用wait(&status)等价于调用waitpid(-1.&status,0)

    8.4.4 让进程休眠

    1、sleep函数:将进程挂起一段指定的时间

    #include <unistd.h>
    
    unsigned int sleep(unsigned int secs);
    //返回:还要休眠的秒数
    

    如果请求的时间量已经到了,返回0,否则返回还剩下的要休眠的秒数。

    2、pause函数:让调用函数休眠,直到该进程收到一个信号。

    #include <unistd.h>
    
    int pause(void);
    //返回:总是-1
    

    8.4.5 加载并运行程序

    1、execve函数:在当前进程的上下文中加载并运行一个新程序。

    #include <unistd.h>
    
    int execve(const char *filename,const char *argv[],const char *envp[]);
    //返回:若成功,则不返回,若错误,返回-1
    
    • filename:可执行目标文件

    • argv:带参数列表

    • envp:环境变量列表

    特点:execve调用一次从不返回

    2、getenv函数:在环境数组中搜素字符串“name =VALUE”,若找到了,就返回一个指向value的指针,否则它就返回NULL。

    #include <stdlib.h>
    char *getenv(const char *name);
    //返回:存在,返回指向name的指针,若无匹配的,为NULL
    

    3、注意:

    execve函数在当前进程的上下文中加载并运行一个新的进程。它会覆盖当前进程的地址空间,并没有创建一个新的进程,新的进程仍然有相同的PID,并且继承了调用execve函数时已打开的所有文件描述符。

    8.4.6 利用fork和execve运行程序

    1、外壳是一个交互型的应用级程序,它代表用户运行其他程序。

    2、外壳执行一系统的读/求值步骤,然后终止。读步骤读取来自用户的一个命令行,求值步骤解释命令行,并代表用户运行程序。

    3、eval函数:对外壳命令行求值

    4、parseline函数:解析外壳的一个输入

    8.5 信号

    底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。
    其他信号对应于内核或者其他用户进程中较高层的软件事件。

    8.5.1 信号术语

    1、发送信号的两个不同步骤:

    (1)发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。
    (2)接收信号:信号处理程序捕获信号的基本思想。 
    

    发送信号的两个原因:

    (1)内核监测到一个系统事件,比如被零除错误或者子进程终止。
    (2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
    

    2、待处理信号:一个只发出而没有被接收的信号

    • 一个进程可以有选择性地阻塞接收某种信号。

    • 待处理信号不会被接收,直到进程取消对这种信号的阻塞。

    3、一个待处理信号最多只能被接受一次,pending位向量:维护着待处理信号集合,blocked向量:维护着被阻塞的信号集合。

    代码学习内容总结

    exec1

    代码如下:

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
    char*arglist[3];
    
    arglist[0] = "ls";
    arglist[1] = "-l";
    arglist[2] = 0 ;//NULL
    printf("* * * About to exec ls -l
    ");
    execvp( "ls" , arglist );
    printf("* * * ls is done. bye");
    
    return 0;
    }
    

    execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。

    运行结果如下:

    从运行结果可以看到,exevp函数调用成功没有返回,所以没有打印出“* * * ls is done. bye”这句话。

    exec2

    它与exec1的区别就在于exevp函数的第一个参数,exec1传的是ls,exec2直接用的arglist[0],不过由定义可得这两个等价,所以运行结果是相同的。

    运行结果如下:

    exec3

    代码如下:

     #include <stdio.h>
     #include <unistd.h>
    
    int main()
    {
    char*arglist[3];
    char*myenv[3];
    myenv[0] = "PATH=:/bin:";
    myenv[1] = NULL;
    
    arglist[0] = "ls";
    arglist[1] = "-l";
    arglist[2] = 0 ;
    printf("* * * About to exec ls -l
    ");
    
    execlp("ls", "ls", "-l", NULL);
    printf("* * * ls is done. bye
    ");
    }
    

    execlp()会从PATH 环境变量所指的目录中查找符合参数file的文件名,找到后便执行该文件,然后将第二个以后的参数当做该文件的argv[0]、argv[1]……,最后一个参数必须用空指针(NULL)作结束。如果用常数0来表示一个空指针,则必须将它强制转换为一个字符指针,否则将它解释为整形参数,如果一个整形数的长度与char * 的长度不同,那么exec函数的实际参数就将出错。如果函数调用成功,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了.

    返回值:如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno 中。

    也就是说,这个代码指定了环境变量,然后依然执行了ls -l指令,成功后没有返回,所以最后一句话不会输出。运行结果同exec1.

    forkdemo1

     #include<stdio.h>
     #include<sys/types.h>
     #include<unistd.h>
    int main()
    {
    int ret_from_fork, mypid;
    mypid = getpid();  
    printf("Before: my pid is %d
    ", mypid);
    ret_from_fork = fork();
    sleep(1);
    printf("After: my pid is %d, fork() said %d
    ",
    getpid(), ret_from_fork);
    
    return 0;
    }
    

    这个代码先是打印进程pid,然后调用fork函数生成子进程,休眠一秒后再次打印进程id,这时父进程打印子进程pid,子进程返回0.

    运行结果如下:

    forkdemo2

     #include <stdio.h>
     #include <unistd.h>
    
    int main()
    {
    printf("before:my pid is %d
    ", getpid() );
    fork();
    fork();
    printf("aftre:my pid is %d
    ", getpid() );
    
    return 0;
    }
    

    这个代码调用两次fork,一共产生四个子进程,所以会打印四个aftre输出。

    运行结果如下:

    forkdemo3

     #include<stdio.h>
     #include<stdlib.h>
     #include<unistd.h>
    
    int main()
    {
    int fork_rv;
    
    printf("Before: my pid is %d
    ", getpid());
    
    fork_rv = fork();   /* create new process   */
    
    if ( fork_rv == -1 )/* check for error  */
    perror("fork");
    else if ( fork_rv == 0 ){ 
    printf("I am the child.  my pid=%d
    ", getpid());
    
    exit(0);
    }
    else{
    printf("I am the parent. my child is %d
    ", fork_rv);
    exit(0);
    }
    
    return 0;
    }
    

    fork产生子进程,父进程返回子进程pid,不为0,所以输出父进程的那句话,子进程返回0,所以会输出子进程那句话。

    运行结果如下:

    foekdemo4

     #include<stdio.h>
     #include<stdlib.h>
     #include<unistd.h>
    
    int main()
    {
    int fork_rv;
    
    printf("Before: my pid is %d
    ", getpid());
    
    fork_rv = fork();   /* create new process   */
    
    if ( fork_rv == -1 )/* check for error  */
    perror("fork");
    
    else if ( fork_rv == 0 ){ 
    printf("I am the child.  my pid=%d
    ", getpid());
    printf("parent pid= %d, my pid=%d
    ", getppid(), getpid());
    exit(0);
    }
    
    else{
    printf("I am the parent. my child is %d
    ", fork_rv);
    sleep(10);
    exit(0);
    }
    
    return 0;
    }
    

    先打印进程pid,然后fork创建子进程,父进程返回子进程pid,所以输出parent一句,休眠十秒;子进程返回0,所以输出child与之后一句。

    运行结果如下:

    forkgdb

     #include <stdio.h>
     #include <stdlib.h>
     #include <unistd.h>
    
    int  gi=0;
    int main()
    {
    int li=0;
    static int si=0;
    int i=0;
    
    pid_t pid = fork();
    if(pid == -1){
    exit(-1);
    }
    else if(pid == 0){
    for(i=0; i<5; i++){
    printf("child li:%d
    ", li++);
    sleep(1);
    printf("child gi:%d
    ", gi++);
    printf("child si:%d
    ", si++);
    }
    exit(0);
    
    }
    else{
    for(i=0; i<5; i++){
    printf("parent li:%d
    ", li++);
    printf("parent gi:%d
    ", gi++);
    sleep(1);
    printf("parent si:%d
    ", si++);
    }
    exit(0);
    
    }
    return 0;
    }
    

    这个的主要区别是在,父进程打印是先打印两句,然后休眠一秒,然后打印一句,子进程先打印一句,然后休眠一秒,然后打印两句。并且这两个线程是并发的,所以可以看到在一个线程休眠的那一秒,另一个线程在执行,并且线程之间相互独立互不干扰。

    psh1

     #include<stdio.h>
     #include<stdlib.h>
     #include<string.h>
     #include<unistd.h>
    
     #define MAXARGS 20  
     #define ARGLEN  100 
    
    int execute( char *arglist[] )
    {
    execvp(arglist[0], arglist);
    perror("execvp failed");
    exit(1);
    }
    
    char * makestring( char *buf )
    {
    char*cp;
    
    buf[strlen(buf)-1] = '';  
    cp = malloc( strlen(buf)+1 );   
    if ( cp == NULL ){  
    fprintf(stderr,"no memory
    ");
    exit(1);
    }
    strcpy(cp, buf);
    return cp;  
    }
    
    int main()
    {
    char*arglist[MAXARGS+1];
    int numargs;
    charargbuf[ARGLEN]; 
    
    numargs = 0;
    while ( numargs < MAXARGS )
    {   
    printf("Arg[%d]? ", numargs);
    if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '
    ' )
    arglist[numargs++] = makestring(argbuf);
    else
    {
    if ( numargs > 0 ){ 
    arglist[numargs]=NULL;  
    execute( arglist ); 
    numargs = 0;
    }
    }
    }
    return 0;
    }
    

    这个代码就相当于你输入要执行的指令,回车表示输入结束,然后输入的每个参数对应到函数中,再调用对应的指令。

    psh2

     #include<stdio.h>
     #include<stdlib.h>
     #include<string.h>
     #include<sys/types.h>
     #include<sys/wait.h>
     #include<unistd.h>
     #include<signal.h>
    
     #define MAXARGS 20  
     #define ARGLEN  100 
    
    char *makestring( char *buf )
    {
    char*cp;
    
    buf[strlen(buf)-1] = '';  
    cp = malloc( strlen(buf)+1 );   
    if ( cp == NULL ){  
    fprintf(stderr,"no memory
    ");
    exit(1);
    }
    strcpy(cp, buf);
    return cp;  
    }
    
    void execute( char *arglist[] )
    {
    int pid,exitstatus; 
    
    pid = fork();   
    switch( pid ){
    case -1:
    perror("fork failed");
    exit(1);
    case 0:
    execvp(arglist[0], arglist);
    perror("execvp failed");
    exit(1);
    default:
    while( wait(&exitstatus) != pid )
    ;
    printf("child exited with status %d,%d
    ",
    exitstatus>>8, exitstatus&0377);
    }
    }
    
    int main()
    {
    char*arglist[MAXARGS+1];
    int numargs;
    charargbuf[ARGLEN]; 
    
    numargs = 0;
    while ( numargs < MAXARGS )
    {   
    printf("Arg[%d]? ", numargs);
    if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '
    ' )
    arglist[numargs++] = makestring(argbuf);
    else
    {
    if ( numargs > 0 ){ 
    arglist[numargs]=NULL;  
    execute( arglist ); 
    numargs = 0;
    }
    }
    }
    return 0;
    }
    

    比起1来,多了循环判断,不退出的话就会一直要你输入指令,并且对于子程序存在的状态条件。

    testbuf1

     #include <stdio.h>
     #include <stdlib.h>
    int main()
    {
    printf("hello");
    fflush(stdout);
    while(1);
    }
    

    效果是输出hello,然后换行,之后不退出。

    testbuf2

     #include <stdio.h>
    int main()
    {
    printf("hello
    ");
    while(1);
    }
    

    效果同上。

    fflush(stdout)的效果和换行符 是一样的。

    testbuf3

     #include <stdio.h>
    
    int main()
    {
    fprintf(stdout, "1234", 5);
    fprintf(stderr, "abcd", 4);
    }
    

    将内容格式化输出到标准错误、输出流中。

    testpid

     #include <stdio.h>
     #include <unistd.h>
    
     #include <sys/types.h>
    
    int main()
    {
    printf("my pid: %d 
    ", getpid());
    printf("my parent's pid: %d 
    ", getppid());
    return 0;
    }
    

    输出当前进程pid和当前进程的父进程的pid。

    testpp

     #include <stdio.h>
     #include <stdlib.h>
    int main()
    {
    char **pp;
    pp[0] = malloc(20);
    
    return 0;
    }
    

    问题在于没给pp分配空间就调用了pp[0],毕竟声明的时候只是一个指针,而指针必须要初始化。

    应该改成:

    include <stdio.h>
    include <stdlib.h>
    int main()
    {
    char pp;
    pp = (char)malloc(20);
    pp[0] = (char*)malloc(20);
    return 0;
    }
    

    testsystem

     #include<stdlib.h>
    
    int main ( int argc, char *argv[] )
    {
    
    system(argv[1]);
    system(argv[2]);
    return EXIT_SUCCESS;
    }   /* ----------  end of function main  ---------- */
    

    system()——执行shell命令,也就是向dos发送一条指令。这里是后面可以跟两个参数,然后向dos发送这两个命令,分别执行。如下图,输入ls和dir两个指令后,可以看到分别执行了。

    waitdemo1

     #include<stdio.h>
     #include<stdlib.h>
     #include<sys/types.h>
     #include<sys/wait.h>
     #include<unistd.h>
    
     #define DELAY   4
    
    void child_code(int delay)
    {
    printf("child %d here. will sleep for %d seconds
    ", getpid(), delay);
    sleep(delay);
    printf("child done. about to exit
    ");
    exit(17);
    }
    
    void parent_code(int childpid)
    {
    int wait_rv=0;  /* return value from wait() */
    wait_rv = wait(NULL);
    printf("done waiting for %d. Wait returned: %d
    ", 
    childpid, wait_rv);
    }
    int main()
    {
    int  newpid;
    printf("before: mypid is %d
    ", getpid());
    if ( (newpid = fork()) == -1 )
    perror("fork");
    else if ( newpid == 0 )
    child_code(DELAY);
    else
    parent_code(newpid);
    
    return 0;
    }
    

    如果有子进程,则终止子进程,成功返回子进程pid。结果如下图:

    waitdemo2

     #include<stdio.h>
     #include<stdlib.h>
     #include<sys/types.h>
     #include<sys/wait.h>
     #include<unistd.h>
    
     #define DELAY   10
    
    void child_code(int delay)
    {
    printf("child %d here. will sleep for %d seconds
    ", getpid(), delay);
    sleep(delay);
    printf("child done. about to exit
    ");
    exit(27);
    }
    
    void parent_code(int childpid)
    {
    int wait_rv;
    int child_status;
    int high_8, low_7, bit_7;
    
    wait_rv = wait(&child_status);
    printf("done waiting for %d. Wait returned: %d
    ", childpid, wait_rv);
    
    high_8 = child_status >> 8; /* 1111 1111 0000 0000 */
    low_7  = child_status & 0x7F;   /* 0000 0000 0111 1111 */
    bit_7  = child_status & 0x80;   /* 0000 0000 1000 0000 */
    printf("status: exit=%d, sig=%d, core=%d
    ", high_8, low_7, bit_7);
    }
    
    int main()
    {
    int  newpid;
    
    printf("before: mypid is %d
    ", getpid());
    
    if ( (newpid = fork()) == -1 )
    perror("fork");
    else if ( newpid == 0 )
    child_code(DELAY);
    else
    parent_code(newpid);
    }
    

    这个比起1来就是多了一个子进程的状态区分,把状态拆分成三块,exit,sig和core。具体运行如下:

    本周代码托管截图

    https://git.oschina.net/20145333/CSAPP2E/tree/master/src/11netp?dir=1&filepath=src%2F11netp&oid=380ae507cc882c36d28bd54d3acc447fd64d523b&sha=84c34ef5470d606b830432eeee651091d4efa20a

    学习进度条

    代码行数(新增/累积) 博客量(新增/累积) 学习时间(新增/累积) 重要成长
    目标 5000行 16篇 400小时
    第一周 80/80 1/1 20/20
    第二周 130/210 1/2 18/38
    第三周 300/510 1/3 22/60
    第五周 300/810 1/4 20/80
    第六周 150/960 1/5 20/100
    第七周 120/1080 1/6 20/120
    第八周 0/1080 1/7 20/140
    第九周 300/1380 1/8 20/160
    第十周 428/1808 1/9 20/180
    第十一周 429/2237 1/10 20/200
  • 相关阅读:
    万字长文|Hadoop入门笔记(附资料)
    大数据最后一公里——2021年五大开源数据可视化BI方案对比
    非结构化数据怎么存?——开源对象存储方案介绍
    (三、四)Superset 1.3图表篇——透视表-Pivot Table
    数据湖搭建指南——几个核心问题
    (二)Superset 1.3图表篇——Time-series Table
    DorisDB升级为StarRocks,全面开源!
    (一)Superset 1.3图表篇——Table
    HCNP Routing&Switching之BGP基础
    HCNP Routing&Switching之路由引入导致的问题及解决方案
  • 原文地址:https://www.cnblogs.com/rx719523850/p/6107413.html
Copyright © 2020-2023  润新知