[Linux]进程控制精讲,简单实现一个shell

news2025/2/25 21:16:16

目录

前言

进程创建

fork函数初识

写时拷贝

fork常见用法

fork调用失败的原因

进程终止

进程退出场景

进程退出码

查看进程退出码

退出码的含义

进程常见退出方法

exit VS _exit

exit函数

_exit函数

二者的区别

return退出

进程等待

进程等待必要性

进程等待的方法

wait方法

waitpid方法

获取子进程status

进程的非阻塞等待

进程程序替换

替换原理

替换函数

execl

execlp

execle

execv

execvp

execve

函数解释

命名理解(建议先看)

做一个自己的shell


前言

        本片博客主要介绍Linux进程控制相关的内容,主要从进程创建、进程终止、进程等待、进程程序替换这四个方面介绍,在我们学习了上述相关的进程控制操作后,我们最后会尝试运用上述内容实现一个我们自己的shell,感觉对自己有帮助的话记得给个三连哦。

进程创建

该部分主要介绍fork函数相关的内容

fork函数初识

        在Linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

pid_t fork(void);

返回值介绍:

  • 子进程中返回0,父进程返回子进程pid,出错返回-1。

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程

  • 将父进程部分数据结构内容拷贝至子进程

  • 添加子进程到系统进程列表当中

  • fork返回,开始调度器调度

 

fork之后,父子进程的代码共享,从fork后的位置开始一起运行。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <assert.h>
int main()
{
    printf("Before: pid:%d\n",getpid());

    pid_t id = fork();
    assert(id != -1);    //检测是否创建成功
    printf("Now:pid:%d , id:%d\n",getpid(),id);
    return 0;
}

打印结果显示,子进程只执行了fork之后的语句。

注意点:

  • fork之前父进程独立执行,fork之后,父子两个执行流分别执行。fork之后,谁先执行完全由调度器决定

写时拷贝

        通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式,各自一份副本

为什么要采用写时拷贝的方案?

  • ​首先进程间具有独立性,两个进程之间不能够相互影响,因而进程的代码和数据理应是独立的,但如果我们每创建一个子进程就另起炉灶,会造成内存空间的大量使用,且效率不高。
  • 因而当我们刚开始创建子进程时,我们不急于直接拷贝,当子进程或者父进程需要对共享的数据进行修改时,子进程再进行拷贝,这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝。
  • ​代码段的数据大部分情况下不会进行写实拷贝,但在进程替换时会。

fork常见用法

简单用法模板:

pid_t id = fork();    //通过对id的判断让父子进程做不同的事情
if(id<0)             //出错返回-1
{
    //打印错误信息或者终止当前进程
}
else if(id == 0)  //子进程fork返回值为0
{
    //子进程做什么
}
else      //父进程fork返回值是子进程id > 0
{
	//父进程干啥    
}

fork常用场景:

  1. 创建子进程后,在子进程内通过进程程序替换执行另一个程序。

  2. 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

fork调用失败的原因

fork函数创建子进程也可能会失败,有以下两种情况:

  1. 系统中有太多的进程,内存空间不足,子进程创建失败。

  2. 实际用户的进程数超过了限制,子进程创建失败。

进程终止

该部分主要介绍退出码,exit和_exit

进程退出场景

进程退出时,无非以下三种场景:

  1. 代码运行完毕,结果正确

  2. 代码运行完毕,结果不正确

  3. 代码异常终止

进程退出码

查看进程退出码

当程序正常终止时,我们可以通过 echo $? 查看进程退出码。

如运行以下程序后:

#include <stdio.h>
int main()
{
	return 0;
}

显示出的就是return的值。

我们平时使用的指令也有退出码

很明显,我们平时使用的指令也是一个个写好了的c程序。

退出码的含义

通常,我们以0表示正常退出,非0表示异常退出。

        C语言当中,我们可以调用strerror函数,其可以通过错误码,获取该错误码在C语言当中对应的错误信息。

#include <stdio.h>
#include <string.h>
int main()
{
    for(int i = 1;i<128;++i)
    {
        printf("%d: %s\n",i,strerror(i));
    }
    return 0;
}

        退出码的含义是人为规定的,C语言的错误信息只是一种参考,不同环境下相同的退出码的对应含义可能不同。

进程常见退出方法

正常终止:

  1. 从main返回

  2. 调用exit

  3. _exit

异常终止:

  • ctrl+c ,通过信号来终止

exit VS _exit

exit函数

 

使用exit函数时,我们需要带stdlib.h的头文件,很明显,这是库中的函数。

_exit函数

而_exit函数是一个系统调用,显然,exit函数是对 _exit系统调用进行了封装。

二者的区别

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。

  2. 关闭所有打开的流,所有的缓存数据均被写入

  3. 调用_exit

 演示:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    printf("hello test "); //没加\n ,不让缓冲区刷新
    
    //exit(1);
    _exit(1);
}

exit时

结果在程序结束后打印出来了。

_exit时

程序运行结束后也没打印。

return退出

        return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

进程等待

该部分主要介绍wait和waitpid

进程等待必要性

  • 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏

  • 进程一旦变成僵尸状态,就难以杀掉,即使发送9号信号也无能为力,因为谁也没有办法杀死一个已经死去的进程。

  • 父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。

  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

wait方法

pid_t wait(int*status);

返回值:

  • 成功返回被等待进程pid,失败返回-1。

参数:

  • 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL。

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id<0) //创建子进程失败
    {
        exit(1);
    } 
    else if(id == 0) //子进程
    {   
        printf("子进程运行,我的pid:%d\n",getpid());
        exit(0);
    }
    //父进程等待子进程
    sleep(5);
    int status;
    wait(&status);
    sleep(5);
    return 0;
}

        我们创建了一个子进程,打印完后子进程退出,父进程5秒后才等待子进程,此时子进程应该处于僵尸状态,5秒后回收子进程,子进程消失,父进程再运行5秒后退出。

通过以下监控脚本观察情况是否如我们预料的一样:

//监控脚本
while :; do ps axj | head -1 && ps axj | grep myprocess | grep -v grep ; sleep 1 ; done;

waitpid方法

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  • 当正常返回的时候waitpid返回收集到的子进程的进程ID;

  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;

  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数讲解:

  • pid:

    • Pid=-1,等待任一个子进程。与wait等效。

    • Pid>0.等待其进程ID与pid相等的子进程。

  • status(输出型参数,获取子进程退出状态,不关心则可以设置成为NULL):

    • WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

    • WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

  • options:

    • WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

注意点:

  1. status中 WIFEXITED和WEXITSTATUS是定义好的宏函数

  2. options用于区分我们是进行阻塞等待还是非阻塞等待,阻塞等待直接填0即可,非阻塞等待就填WNOHANG。(后面会用代码进行演示非阻塞等待

获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。

  • 如果传递NULL,表示不关心子进程的退出状态信息。

  • 操作系统会根据该参数,将子进程的退出信息反馈给父进程。

  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只用了低16比特位):

 通过位运算取出退出状态和终止信号:

ExitCode = ((status>>8) & 0xFF); //退出状态
Signal = (status & 0x7F);   //信号
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id<0) //创建子进程失败
    {
        exit(1);
    } 
    else if(id == 0) //子进程
    {   
        while(1)
        {
           printf("子进程pid: %d\n",getpid());
           sleep(1);
        }
    }
    //父进程等待子进程
    int status;
    waitpid(id,&status,0);
    int ExitCode = ((status>>8) & 0xFF); //退出状态
    int Signal = (status & 0x7F);   //信号
    printf("子进程退出状况:ExitCode:%d , Signal:%d\n",ExitCode,Signal);
    sleep(5);
    return 0;
}

进程的非阻塞等待

什么是非阻塞等待?

        首先,父进程阻塞等待子进程时,父进程什么都干不了,只能等子进程结束后才能干其他事情。而非阻塞等待,就是我们通过轮询的方式,执行等待函数时,如果子进程还没有结束,父进程就先继续执行自己的事情,等下次再来等待。

如何实现非阻塞等待?

pid_ t waitpid(pid_t pid, int *status, int options);

第三个参数options设置为WNOHANG即可.

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id<0) //创建子进程失败
    {
        exit(1);
    } 
    else if(id == 0) //子进程
    {   
        int count = 5;
        while(count--)
        {       
           sleep(1);
        }
        exit(1);
    }
    //父进程等待子进程
    int status;
    pid_t ret = 0;
    do
    {
        ret = waitpid(id,&status,WNOHANG);
        //父进程做自己的事情
        printf("子进程正在运行中\n");
        sleep(1);
    } while (ret == 0);
    // int ExitCode = ((status>>8) & 0xFF); //退出状态
    // int Signal = (status & 0x7F);   //信号
    //使用一下宏函数
    if(WIFEXITED(status))
    {
        printf("子进程退出码为%d\n",WEXITSTATUS(status));
    }
    else
    {
        printf("等待失败\n");
    }
    sleep(5);
    return 0;
}

可以看到,在等待的同时,父进程也能打印,干自己的事情。

两种等待方式并没有优劣之分,看情况使用即可。

进程程序替换

该部分主要讲解替换的原理和六个exec函数

替换原理

        用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变(还是用原来的PCB、页表等)。

 

替换函数

execl

int execl(const char *path, const char *arg, ...);

参数:

  • path表示要执行的程序所在的路径

  • 可变参数列表,最后以NULL进行结尾

返回值:

  • 失败返回-1,并设置相应的错误码

使用:以ls命令为例

execl("/usr/bin/ls","ls","-a","-l".NULL);

execlp

int execlp(const char *file, const char *arg, ...);

参数:

  • file表示要运行的程序名,系统会自动在环境变量PATH对应的路径下寻找

  • 可变参数列表,最后以NULL进行结尾

返回值:

  • 失败返回-1,并设置相应的错误码

使用:以ls命令为例

execlp("ls","ls","-a","-l".NULL);

execle

int execle(const char *path, const char *arg, ...,char *const envp[]);

参数:

  • path表示要执行的程序所在的路径

  • 可变参数列表,最后以NULL进行结尾

  • envp数组中,每个位置都指向一个自己设定的环境变量

与execl的使用相比,进程程序替换后使用的环境变量需要我们自己来进行设置

//myprocess
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
    pid_t id = fork();
    if(id<0) //创建子进程失败
    {
        exit(1);
    } 
    else if(id == 0) //子进程
    {   
        char* env[]= { "MYVALUE=1234"};

        execle("./test",NULL,env);
        exit(1);
    }
    //父进程等待子进程
    int status;
    waitpid(id,&status,0);  
    int ExitCode = ((status>>8) & 0xFF); //退出状态
    int Signal = (status & 0x7F);   //信号
    printf("ExitCode:%d , Signal:%d\n",ExitCode,Signal);
    sleep(5);
    return 0;
}
//test
#include <stdio.h>
#include <stdlib.h>
int main()
{
    char*s = getenv("PATH");
    char*p = getenv("MYVALUE");
    printf("S:%s\n",s);
    printf("P:%s\n",p);

    return 0;
}

可以看到,我们通过进程程序替换函数execle执行的程序只认识我们自己设置的环境变量。

execv

int execv(const char *path, char *const argv[]);

与execl的区别就在于将后面的可变参数列表换成了指针数组。

以ls指令为例:

char*const argv[] = {"ls","-a","-l",NULL};
execv("/usr/bin/ls",argv);

execvp

int execvp(const char *file, char *const argv[]);

        与execvl的区别就在于将路径改为程序名,就不多赘述了。

execve

int execve(const char *path, char *const argv[], char *const envp[]);

        参考上面execle的用法即可

 事实上,只有execve是真正的系统调用,其它五个函数最终都调用 execve,所以execve在man手册 第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示:

函数解释

  • 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。

  • 如果调用出错则返回-1。

  • 所以exec函数只有出错的返回值而没有成功的返回值。

命名理解(建议先看)

这些函数原型看起来很容易混,但只要掌握了规律就很好记。

  • l(list) : 表示参数采用列表

  • v(vector) : 参数用数组

  • p(path) : 有p自动搜索环境变量PATH

  • e(env) : 表示自己维护环境变量

做一个自己的shell

        用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

        然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。所以要写一个shell,需要循环以下过程:

  1. 获取命令行

  2. 解析命令行

  3. 建立一个子进程(fork)

  4. 替换子进程(execvp)

  5. 父进程等待子进程退出(wait)

根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。

实现如下:

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

#define SIZE 1024
char CommandLine[SIZE];  //存放输入的指令

#define OPT_NUM 64
char* Myargv[OPT_NUM];  //存放分割后的程序指令

//保存上次运行时的退出码和退出信号
int lastCode;
int lastSignal;

int main( )
{
  while(true)
  {
    //1.打印提示符
    printf("[用户名@主机名 当前路径]#");
    fflush(stdout);        //刷新缓冲区

    //获取用户输入
    char* s = fgets(CommandLine,sizeof(CommandLine)-1,stdin);
    assert(s != NULL);  //检查释放获取成功
    (void)s;      
    CommandLine[strlen(CommandLine)-1] = 0;  //消除掉输入时带的换行符
    //字符串分割,拿出指令

    Myargv[0] = strtok(CommandLine," ");
    int i = 1;
    //给ls命令增加配色方案
    if(Myargv[0]!=NULL && strcmp(Myargv[0],"ls")==0)
    {
      Myargv[i++] = (char*)"--color=auto";
    }
    while( Myargv[i++] = strtok(NULL," "));  //无法分割时返回空指针。 命令行参数最后刚好需要以NULL结尾

    //内建命令,内置命令不需要创建子进程来执行
    //cd 命令需要改变当前进程的工作目录
    if(Myargv[0]!=NULL && strcmp(Myargv[0],"cd")==0)
    {
        if(Myargv[1]!=NULL)
        chdir(Myargv[1]);
        continue;
    }

    //echo命令获取上次程序的退出码
    if(Myargv[0]!=NULL && Myargv[1]!=NULL && strcmp(Myargv[0],"echo")==0)
    {
        if(strcmp(Myargv[1],"$?")==0)
        {
          printf("lastcode:%d , lastSignal:%d\n",lastCode,lastSignal);
        }
        else
        {
          printf("%s\n",Myargv[1]);
        }
        continue;
    }

//条件编译来测试  编译时带上 -DDEBUG即可运行测试
#ifdef DEBUG
for(int i=0; Myargv[i] ;++i)
printf("%s\n",Myargv[i]);
#endif

 //创建子进程执行相关指令
pid_t id = fork();
assert(id != -1); //检测子进程是否创建失败

if(id == 0) //子进程进程切换 执行对应的指令
{
  execvp(Myargv[0],Myargv);

  exit(1); //异常时才从这退出
}
int status;  //拿到子程序的退出码
waitpid(id,&status,0);

lastCode = ((status>>8) & 0xFF);
lastSignal = (status & 0x7F);
    
 }
  return 0;
}

演示:

注意点:

  • 内建/内置命令不需要创建子进程来执行,如cd命令等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/427584.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Java文件操作】手把手教你拿捏IO 流

哈喽&#xff0c;大家好~我是保护小周ღ&#xff0c;本期为大家带来的是 Java 文件操作&#xff0c;理解文件的概念以及&#xff0c;常用的操作文件的类和方法&#xff0c;FileInputStream 类 和 FileOutputStream , PrintWriter and Scnner, Reader and Wirter 确定不来看看…

Python机器学习:集成学习

前两天看了SVM、逻辑回归、KNN、决策树、贝叶斯分类这几个很成熟的机器学习方法&#xff0c;但是&#xff0c;今天不看方法了&#xff0c;来看一种思想&#xff1a;集成学习&#xff1a; 先来看一下集成学习的基本原理&#xff1a;通过融合多个模型&#xff0c;从不同的角度降…

3.4 随机变量的相互独立性

学习目标&#xff1a; 要学习二维随机变量的相互独立性&#xff0c;我会按照以下步骤进行&#xff1a; 学习独立性的概念&#xff1a;在概率论中&#xff0c;两个事件A和B是相互独立的&#xff0c;当且仅当它们的概率乘积等于它们的联合概率&#xff0c;即P(A∩B)P(A)P(B)。将…

【Java EE】-网络编程(二) Socket(套接字) + Udp版本客户端服务器 +Tcp版本客户端服务器

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【JavaEE】 主要内容&#xff1a;传输层协议对应Socket编程&#xff0c;DatagramSocket&#xff0c;DatagramPacket&#xff0c;Udp版本的客户端和服务器&#xff0c;UdpEchoSeve…

大力出奇迹——GPT系列论文学习(GPT,GPT2,GPT3,InstructGPT)

目录说在前面1.GPT1.1 引言1.2 训练范式1.2.1 无监督预训练1.2.2 有监督微调1.3 实验2. GPT22.1 引言2.2 模型结构2.3 训练范式2.4 实验3.GPT33.1引言3.2 模型结构3.3 训练范式3.4 实验3.4.1数据集3.5 局限性4. InstructGPT4.1 引言4.2 方法4.2.1 数据收集4.2.2 各部分模型4.3 …

【轻NAS】Windows搭建可道云私有云盘,并内网穿透公网访问

文章目录1.前言2. Kodcloud网站搭建2.1. Kodcloud下载和安装2.2 Kodcloud网页测试3. cpolar内网穿透的安装和注册4. 本地网页发布4.1 Cpolar云端设置4.2 Cpolar本地设置5. 公网访问测试6.结语1.前言 云存储作为近些年兴起的概念&#xff0c;成功吸引了各大互联网厂商下场&…

thingsboard ARM网关

G5501边缘计算网关 G5501是采用中高端的通用型 SOC&#xff0c;一款4 核 arm 架构 A55 处理器的 网关设备。标配处理器为 Cortex-A55 四核&#xff0c;最高主频 2GHz 的处理器&#xff0c; 内置 4GB DDR4 内存&#xff0c;32GB eMMC 存储。 集成Mali G52 2EE 图形处理器GPU&am…

matplotlib设置中文字体为微软雅黑

matplotlib无法设置任何中文字体怎么办&#xff1f; 如何在linux系统下让matplotlib显示中文&#xff1f; 下载微软雅黑字体&#xff0c;把它放在某个目录下。 链接&#xff1a; https://pan.baidu.com/s/1SCLYpH_MzY7vn0HA0wxxAw?pwdft2j 提取码&#xff1a;ft2j 在代码中加…

Learning C++ No.18【STL No.8】

引言&#xff1a; 北京时间&#xff1a;2023/3/18/21:47&#xff0c;周末&#xff0c;不摆烂&#xff0c;但是欠钱终于还是遭报应了&#xff0c;导致坐牢7小时&#xff08;上午3.5&#xff0c;下午3.5&#xff09;&#xff0c;难受&#xff0c;充分意识到行哥是那么的和蔼可亲…

DLRover: 云上自动扩缩容 DeepRec 分布式训练作业

背景 如今&#xff0c;深度学习已广泛应用在搜索、广告、推荐等业务中&#xff0c;这类业务场景普遍有两个特点&#xff1a; 1&#xff09;训练样本量大&#xff0c;需要分布式训练提升训练速度&#xff1b; 2&#xff09;模型稀疏&#xff0c;即模型结构中离散特征计算逻辑占…

强训之【走方格的方案数和另类加法】

目录1.走方格的方案数1.1题目1.2思路讲解1.3代码展示2.另类加法2.1题目2.2思路讲解2.3代码展示3.选择题1.走方格的方案数 1.1题目 链接: link 描述 请计算n*m的棋盘格子&#xff08;n为横向的格子数&#xff0c;m为竖向的格子数&#xff09;从棋盘左上角出发沿着边缘线从左上…

第⑦讲:Ceph集群RGW对象存储核心概念及部署使用

文章目录1.RadosGW对象存储核心概念1.1.什么是RadosGW对象存储1.2.RGW对象存储架构1.3.RGW对象存储的特点1.4.对象存储中Bucket的特性1.4.不同接口类型的对象存储访问对比2.在集群中部署RadosGW对象存储组件2.1.部署RGW组件2.2.集群中部署完RGW组件后观察集群的信息状态2.3.修改…

【2023】Kubernetes之Pod与容器状态关系

目录简单创建一个podPod运行阶段&#xff1a;容器运行阶段简单创建一个pod apiVersion: v1 kind: pod metadata: name: nginx-pod spec:containers:- name: nginximages: nginx:1.20以上代码表示创建一个名为nginx-pod的pod资源对象。 Pod运行阶段&#xff1a; Pod创建后&am…

搜索引擎测试报告

文章目录一、项目背景二、项目功能三、测试目的四、测试环境五、测试计划1、功能测试2、自动化测试六、测试结果一、项目背景 java官方文档是我们在学习java语言中不可或缺的权威资料。相比于各种网站的Java资料&#xff0c;官方文档无论是语言表达还是组织方式都要更加全面和…

ThingsBoard开源物联网平台智慧农业实例快速部署教程(Ubuntu、CentOS适用)

ThingsBoard部署教程文档 文章目录ThingsBoard部署教程文档1. JDK环境安装2. 安装thingsBoard2.1 ThingsBoard软件包安装2.2 PostgreSQL安装2.3 PostgreSQL初始化配置3. 修改ThingsBord的配置4. 运行安装脚本测试5. 访问测试6. 导入一个仪表盘库6.1 导出仪表盘并导入自己的项目…

Spring —— Spring Boot 配置文件

JavaEE传送门JavaEE Spring —— Bean 作用域和生命周期 Spring —— Spring Boot 创建和使用 目录Spring Boot 配置文件Spring Boot 配置文件格式properties配置文件properties 基本语法properties 缺点yml 配置文件yml 基本语法yml 配置不同类型数据及 nullyml 配置对象yml…

【SQL Server】数据库开发指南(一)数据库设计

文章目录一、数据库设计的必要性二、什么是数据库设计三、数据库设计的重要性五、数据模型5.1 实体-关系&#xff08;E-R&#xff09;数据模型5.2 实体&#xff08;Entity&#xff09;5.3 属性&#xff08;Attribute&#xff09;5.5 关系&#xff08;Relationship&#xff09;六…

windows搭建ftp及原理(小白向)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录环境一、实验步骤1.1安装ftp二、ftp实验引发的思考1.简单阐述ftp的原理2.ftp建立的流程总结环境 windwos任意环境不需要server windows10 提示&#xff1a;以下是本…

〖Python网络爬虫实战⑤〗- Session和Cookie介绍

订阅&#xff1a;新手可以订阅我的其他专栏。免费阶段订阅量1000python项目实战 Python编程基础教程系列&#xff08;零基础小白搬砖逆袭) 说明&#xff1a;本专栏持续更新中&#xff0c;目前专栏免费订阅&#xff0c;在转为付费专栏前订阅本专栏的&#xff0c;可以免费订阅付费…

Linux的诞生过程

个人简介&#xff1a;云计算网络运维专业人员&#xff0c;了解运维知识&#xff0c;掌握TCP/IP协议&#xff0c;每天分享网络运维知识与技能。座右铭&#xff1a;海不辞水&#xff0c;故能成其大&#xff1b;山不辞石&#xff0c;故能成其高。个人主页&#xff1a;小李会科技的…