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

Linux——进程控制模拟shell

1.进程创建

        我们在之前的文章中介绍过进程创建的方法,可以通过系统调用接口fork来创建新的进程。

        fork在创建完新的子进程之后,返回值是一个pid,对于父进程返回子进程的pid,对于子进程返回0。fork函数后父子进程共享代码,即二者执行的是同一份代码。不同的是对于数据二者相互独立各有一份,于是对于父进程的数据修改不会影响到子进程(在修改时进行写时拷贝将父子数据独立起来)。在fork函数内,进程已经被创建,进程之间相互独立,各自返回各自的返回值(父进程返回子进程PID,子进程返回0)并向下继续执行代码。因而两个进程进入到了不同的分支语句中。

        因为有了进程和地址空间的基础知识的铺垫,我们来系统的梳理一下进程创建还有写时拷贝问题:

        ①在fork调用后,创建出一个新的进程,即现在存在两个进程,他们的关系是父子进程,通过fork的返回值来区分。

        ②一个进程在内存中的基本内容我们可以暂时简单简化地理解为:进程的PCB,PCB中圈定的一个进程地址空间,以及一个用于映射真实物理地址的页表。对于创建的子进程而言,它为了和父进程独立,所以将这三部分都拷贝了一份给自己。因此此时父进程和子进程的进程地址空间和页表是完全一致的,共用同一份物理内存中的代码和数据。

        ③在子进程拷贝页表时,会将所有内容修改为只读属性,这是为了写时拷贝做准备。

        ④当父子进程的一方想要对数据进行修改,则会通过页表映射修改物理内存内容,但是此时由于只读属性,触发了系统错误。于是发生了缺页中断,经过系统检测判定为要发生写时拷贝。于是就会申请内存、拷贝内容、修改页表,然后再恢复执行。

2.进程终止和等待

2.1 进程正常终止

        进程的正常终止有着多种方式:

        ①在main函数中使用return语句,这样可以结束main函数从而结束进程,并返回状态码。

        ②void exit(int status)

        exit是一个C标准库函数,它可以在代码的任何地方结束进程,并且会完成诸如刷新缓冲区、关闭文件描述符等清理操作,然后返回状态码。

        ③void _exit(int status)

        _exit是一个系统调用接口,它用于直接终止进程,返回状态码,但不会完成清理操作。

        辨析 :

        对于以上三种退出方式,return用于函数返回,当作用于main函数时则会结束主进程,并返回一个值。在执行main函数中的return时,C语言标准库会隐式调用exit()函数来处理程序的退出。

        exit则是可以用在代码的任何位置(和return相比,如果不在main函数中,return只能返回结束当前函数)来直接结束整个进程,与此同时会完成清理操作

        而_exit()和exit一样可以直接结束进程,但是不会完成如刷新缓冲区的清理操作。这是因为_ exit是一个比exit更底层的接口,而缓冲区作为语言缓冲区在其层次之上。如果使用_exit结束一个进程,会造成资源泄露,但是进程结束操作系统会自动完成进程的资源回收工作,所以实际不会出现资源泄露的问题。

2.2 进程等待

        我们在之前介绍过,如果父进程不对结束的子进程进行处理,那么子进程将会成为僵尸进程,其PCB始终占据着内存,导致内存泄漏。这种情况直到父进程主动回收子进程,或者父进程结束后子进程变成孤儿进程,被1号进程领养后处理才结束。所以使用父进程妥善处理已经结束的子进程是很有必要的。

2.2.1 进程等待的方法

①pid_t wait(int *status);

        wait函数帮助父进程获取子进程的退出信息,它会等待任意一个子进程结束,结束的子进程pid作为wait函数的返回值交给父进程,而退出码则会通过输出型参数status带回父进程。如果在调用wait函数时子进程并未退出,那么就会将父进程阻塞在其内部,直到子进程结束。

#include<cstdio>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>int main()
{pid_t id = fork();if(id==0){//子进程int cnn = 5;while(cnn--){printf("我是子进程,我的pid是%d\n",getpid());sleep(1);}exit(87);}else if(id>0){//父进程int status;printf("我是父进程,我的pid是%d\n",getpid());pid_t ret = wait(&status);if(ret > 0){printf("%d 成功退出,退出码为:%d\n",ret,WEXITSTATUS(status));}}return 0;
}

②pid_t waitpid(pid_t pid, int *status, int options);

       另外一个函数接口waitpid相比于wait具有更加丰富的功能。

参数

        pid:可以传递指定要等待进程的pid,或者也可传参为-1来等待任意一个子进程(和wait功能相同)。

        status:同样为一个输出型参数,带出进程的退出信息。

        options:选择等待的可选功能项。如传参为0,表示阻塞等待;传参为WNOHANG时表示非阻塞等待,此时需要自己调用非阻塞接口完成轮询检测。

返回值

        >0 表示等待成功,返回值即为对应子进程的pid;

        ==0 表示等待成功,但是子进程暂未退出;

        <0 则说明等待失败。

#include<cstdio>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>int main()
{pid_t id = fork();if(id==0){//子进程int cnn = 5;while(cnn--){printf("我是子进程,我的pid是%d\n",getpid());sleep(1);}exit(87);}else if(id>0){//父进程int status;printf("我是父进程,我的pid是%d\n",getpid());while(true){pid_t ret = waitpid(id,&status,WNOHANG);if(ret > 0){printf("%d 成功退出,退出码为:%d\n",ret,WEXITSTATUS(status));break;}else if(ret==0){printf("进程未退出\n");//其他工作sleep(2);}else {printf("等待失败");break;}}}return 0;
}

2.2.2 理解退出码

        辨析错误码和退出码

        错误码我们过去经常见到,错误码通常是指errno变量中的值,它表示特定操作(如系统调用或库函数)发生错误的原因。errno是一个全局变量,当出现错误时会自动将错误码存储在errno中,不同的值代表着不同的错误信息。我们可以通过perror和strerror来查看错误信息。

perror:void perror(const char *s); 打印输入的参数字符串+此时errno对应的错误信息

strerror:char *strerror(int errnum); 打印指定错误码(传入参数)的错误信息

         对于退出码,它是在进程结束后返回的一个退出状态信息,表示程序的执行结果,一般约定0为成功,而非0为出现错误,至于退出码和原因的对应关系没有固定的要求。

        错误码是全局变量,是在进程执行出现错误后自动为errno赋值,其本质上还是进程自己内部的事情。而退出码则是子进程向父进程“汇报”的方式,是两个进程间的交互。退出码在子进程结束时通过main函数的return或者exit交付给父进程,父进程用wait的status接收。

        status

        status并非一个简单的int类型的数字,对于不同的退出方式status的内容是不一样的,此处简单讨论status的低16位。

        当程序是执行完成并退出时,视为正常终止,此时16位的高8位是真正的退出码,因此要拿到错误码需要对status右移8位,可以采取WEXITSTATUS(status)宏来优雅完成。因此可以看出退出码的范围是0~255的。

        当程序异常,如被信号终止,那么此时仍然会记录退出信息,放在status中(如下图),具体细节将在信号部分系统解释。

 3.进程程序替换

        我们创建新的进程使用的都是fork,但是我们会发现fork创建的子进程和父进程执行的是同样的代码,区别仅仅是不同分支。为了使得子进程可以去执行新的程序,我们可以通过exec函数,将当前进程的代码和数据由新的程序进行替换,从而启动新的程序。

3.1 exec函数

        exec函数分为如上几种,根据其名字我们就可以推断出其含义和使用方法。

l:表示列表list,即表示该接口的参数是以列表的形式(可变参数)传入的

v:与list相对,表示数组vector,即表示该接口的参数是以数组argv的方式传入的

p:表示可以不使用路径,具体要替换的可执行程序通过PATH环境变量寻找

e:表示可以使用新的环境变量,环境变量在参数最后由数组传入

        对于exec的使用,还有一些要点需要强调:

        (1)execl的基本使用方法
#include<cstdio>
#include<unistd.h>int main()
{execl("/usr/bin/ls","ls","-a","-l","--color");
}

        exec函数本质是从磁盘中找到可执行程序,然后加载到内存中,覆盖调用exec函数的代码和数据,从而执行这个可执行程序。如上所示,path参数应该指定为具体替换的可执行程序,之后的可变参数即为执行这个可执行程序的参数,表示如何执行这个可执行程序。

        如上这段代码就完成了ls程序的替换,后续的可变参数实际上也就是命令行参数了。

        (2)execv的基本使用方法
#include<cstdio>
#include<unistd.h>int main()
{char* const argv[] = {(char*)"ls",(char*)"-a",(char*)"-l",(char*)"--color",nullptr};execv("/usr/bin/ls",argv);
}

        对于vector的传参方式,需要使用一个数组,其中是命令行参数。和之前介绍过的命令行参数列表argv一样,最后一个元素一定是nullptr

        (3)execvpe的基本使用方法
#include<cstdio>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>int main()
{pid_t id = fork();if(id>0){//子进程char* const argv[] = {(char*)"Show",nullptr};char* const env[] = {(char*)"ONE=1",(char*)"TWO=2",(char*)"THREE=3"};execvpe("./show",argv,env);exit(1);}pid_t rid = waitpid(id,nullptr,0);if(rid>0){printf("等待成功\n");}return 0;
}

        我们在上述的代码中使用了execvep,所以需要手动将环境变量表传参,在正常情况下环境变量表是继承自父进程的,但是在这种手动传递的情况下,进程替换后的show程序(打印环境变量表内容)就拿到了我们提供的环境变量表,于是对于替换后的程序而言,环境变量表就是这个env了。

        于是可以总结出,对于一个进程环境变量的来源:

①父进程创建子进程,子进程会拷贝父进程的环境变量表。

        a.可以通过extern char** environ;声明环境变量表后访问环境变量表获取。

        b.通过main函数的参数env也可以拿到环境变量的字符串数组,访问方式和参数列表相同。

        c.可以通过系统调用接口getenv(环境变量名)的方法,获得指定环境变量的内容。

②在进程替换时,使用env参数传递新的环境变量表,这样替换后的进程拿到的环境变量表就是这张env了。

③通过putenv()函数也可以完成对环境变量的新增操作。

        (4) 在(3)的代码中,我们还可以发现,实际上这段代码是一个多进程的方式进行的进程替换。fork出子进程后,exec进行写时拷贝,将子进程的页表指向的物理内存加载入show程序的代码数据,然后从新程序的main函数开始执行。

        (5) 函数名中包括p的函数,表示可以不使用路径,具体要替换的可执行程序通过PATH环境变量寻找,于是有execl("ls","ls","-a","-l","--color");  其中第一个ls是执行的程序,而第二个ls是命令行参数列表。

        (6)我们发现exec函数都有返回值int,实际上当进程成功替换后并没有返回值,因为进程替换成功了代码数据全部被覆盖了。所以虽说返回-1是失败,实际上一旦返回了值就肯定失败了。

        (7)对于如上所示的execl、execlp、execle、execv、execvp、execvpe都是被封装后的C标准库接口,都是真正的系统调用——execve封装过来的。根据传参方式和需求灵活选择即可。

4.shell模拟实现

        shell其实就是一个命令解释器,是用户和计算机与计算机系统交互的一个途径。用户输入的指令被shell获取,然后进行处理解析,调用对应的程序。

        我们一直在强调一件事,就是所有的指令实际都是可执行程序,可以通过which找到指令程序所在的路径。所以shell并不生产程序,它只是个程序的搬运工,是一个调用者。

        考虑shell的工作模式,我们大致可以将其分为四步进行。

        在此之前,需要先对环境变量进行初始化。我们在自己的shell中模拟了一个环境变量列表,在shell进程被父进程创建后,shell实际上有一张真正的环境变量列表。而我们创建的这个环境变量列表则是方便我们理解与传参的一份模拟。

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>using namespace std;const int basesize = 1024; //字符串长度上限
const int argvnum = 64; //命令行列表个数上限
const int envnum = 64; //环境变量个数上限char* gargv[argvnum]; //命令行参数列表
int gargc = 0; //命令行参数个数
char* genv[envnum]; //环境变量列表,以此自己定义的表来模拟,是为系统真正的环境变量表int lastcode = 0; //上个进程的退出码void InitEnv()
{extern char** environ;int i = 0;while(environ[i]){genv[i] = (char*)malloc(strlen(environ[i])+1);strncpy(genv[i], environ[i], strlen(environ[i])+1);i++;}genv[i] = nullptr;
}int main()
{InitEnv(); //初始化环境变量:从父shell中拷贝获取char buffer[basesize];while(true){// 1. 打印命令行提示符PrintCommandLine(); // 2. 获取用户命令if(!GetCommandLine(buffer)){continue;}// 3. 分析命令ParseCommandLine(buffer); if (BuildCommand()){continue;}// 4. 执行命令ExecuteCommand();}return 0;
}

4.1 打印命令行提示符

        命令行提示字符串我们常见,实际上由用户名、主机名、工作路径组合而成。

        注意点:

        ①对于用户名和主机名,我们可以直接从环境变量中获取,调用getenv()函数即可。

        ②对于工作路径,如果直接从环境变量获取,我们会发现在cd命令之后,取到的路径不发生任何改变。这是因为环境变量PWD并不会主动根据我们的工作路径的变化而变化,而是需要靠shell去维护的。当使用了cd命令(调用了chdir()函数),此时修改了工作路径,但这个工作路径的修改是发生在进程(也就是shell进程)的PCB中的。于是我们在获取工作路径时,需要通过getcwd()函数来获得此时真正的工作路径。

        当获得了工作路径后,我们还需要对环境变量中PWD的更新负起责任,通过putenv接口即可修改环境变量表中的内容。但是我们自己实现的shell中,也模拟了一份环境变量表,这个表就需要自己手动修改环境变量了。

string GetUserName()
{string USER = getenv("USER");return USER.empty() ? "None" : USER;
}string GetHostName()
{string HOST = getenv("HOSTNAME");return HOST.empty() ? "None" : HOST;
}string GetPwd()
{//通过getcwd取得工作路径,然后以此为pwd//同时需要对环境变量表更新//putenv更新的是进程真正的环境变量表//自己创建的一个模拟的genv需要手动更新char pwd[basesize];char pwdenv[basesize];if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);putenv(pwdenv); //int putenv(const char *string); 修改或新建环境变量int i = 0;while(strncmp("PWD=",genv[i],4)!=0) i++;strncpy(genv[i],pwdenv,strlen(pwdenv)+1);return pwd;//在这种情况下,是从环境变量中读取PWD,而cd命令使用chdir改变的是进程的工作路径,只修改了PCB中的cwd,而不修改环境变量表//string pwd = getenv("PWD");//return pwd.empty() ? "None" : pwd;
}string GetSimpleDir()
{string pwd = GetPwd();size_t pos = pwd.rfind("/");if(pos == string::npos)return pwd;if(pos == 0) //"/"return pwd;else return pwd.substr(pos+1);
}string MakeCommandLine()
{//[xlz44847@localhost home]$ //[用户名@主机名 工作目录]提示符char command_line[basesize];snprintf(command_line, basesize, "[%s@%s %s]# ",GetUserName().c_str(), GetHostName().c_str(), GetSimpleDir().c_str());return command_line;
}void PrintCommandLine()
{printf("%s", MakeCommandLine().c_str());fflush(stdout); //刷新缓冲区
}

4.2 接收用户输入命令

        对于用户输入命令,使用fgets这个对空格符不敏感的接收方式来接收。

bool GetCommandLine(char* buffer)
{char* ret = fgets(buffer, basesize, stdin);//fgets在遇到换行符和文件结尾时停止,会读入换行符if(!ret)return false;buffer[strlen(buffer)-1] = '\0';if(strlen(buffer) == 0) return false;return true;
}

4.3 解析命令

        对于用户输入命令,实际就是执行程序的命令行参数列表,所以我们要做的就是将这个命令字符串进行分割,组成一个命令行参数列表。

       

void ParseCommandLine(char* buffer)
{memset(gargv, 0, sizeof(gargv));gargc = 0;const char* sep = " ";gargv[gargc++] = strtok(buffer, sep);while((bool)(gargv[gargc++] = strtok(nullptr, sep)));gargc--;
}

4.4 执行命令

4.4.1 外部命令

        执行一个命令,我们一般会通过创建子进程的方式来完成。使用execvpe进行进程替换,然后进行差错处理。

//外部命令由子进程执行
bool ExecuteCommand()
{pid_t id = fork();if(id < 0) return false;if(id == 0){//子进程execvpe(gargv[0], gargv, genv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)) //WIFEXITED:判断子进程是否正常退出//正常退出指通过exit或到达主程序结尾而结束,与之相对的是由信号进行终止{lastcode = WEXITSTATUS(status); //WEXITSTATUS:获得子进程的退出码}else {lastcode = 178;}return true;}return false;
}

4.4.2 内建命令

        有一些命令交给子进程是无法完成任务的。比如cd指令修改shell进程的工作路径,这种指令实际上是对shell这个进程自身的变化操作。当创建子进程后自然无法对父进程进行任何操作了,所以这种命令需要shell自己调用函数来完成。

        其中对于echo而言,将其作为内建命令的原因是因为echo $?打印上一次退出码,这种指令就无法通过子进程完成,因此也将其作为内建命令。

//內建命令shell来执行
bool BuildCommand()
{if(strcmp(gargv[0], "cd") == 0){if(gargc == 2){chdir(gargv[1]);lastcode = 0;}else {lastcode = 1;}return true;}else if(strcmp(gargv[0], "export") == 0){if(gargc == 2){int i = 0;while(genv[i++]);genv[i] = (char*)malloc(strlen(gargv[1])+1);strncpy(genv[i],gargv[1],strlen(gargv[1])+1);genv[++i] = nullptr;lastcode = 0;}else {lastcode = 2;}return true;}else if(strcmp(gargv[0], "env") == 0){for(int i = 0; genv[i]; i++){printf("%s\n", genv[i]);}lastcode = 0;return true;}else if(strcmp(gargv[0], "echo") == 0){if(gargc == 2){if(gargv[1][0]=='$'){//echo $?if(gargv[1][1]=='?'){printf("%d\n",lastcode);lastcode = 0;}//echo $PATHelse {int i = 0;string cmp = gargv[1];cmp = cmp.substr(1);cmp += '=';while(strncmp(cmp.c_str(),genv[i],cmp.length())!=0) i++;printf("%s\n",&genv[i][cmp.length()]);lastcode = 0;}}//echo "xxx"else {printf("%s\n",gargv[1]);lastcode = 0;}}else {lastcode = 3;}return true;}return false;
}

相关文章:

Linux——进程控制模拟shell

1.进程创建 我们在之前的文章中介绍过进程创建的方法&#xff0c;可以通过系统调用接口fork来创建新的进程。 fork在创建完新的子进程之后&#xff0c;返回值是一个pid&#xff0c;对于父进程返回子进程的pid&#xff0c;对于子进程返回0。fork函数后父子进程共享代码&#xff…...

【HarmonyOS】鸿蒙应用实现手机摇一摇功能

【HarmonyOS】鸿蒙应用实现手机摇一摇功能 一、前言 手机摇一摇功能&#xff0c;是通过获取手机设备&#xff0c;加速度传感器接口&#xff0c;获取其中的数值&#xff0c;进行逻辑判断实现的功能。 在鸿蒙中手机设备传感器ohos.sensor (传感器)的系统API监听有以下&#xf…...

Kael‘thas Sunstrider Ashes of Al‘ar

Kaelthas Sunstrider 凯尔萨斯逐日者 <血精灵之王> Kaelthas Sunstrider - NPC - 魔兽世界怀旧服TBC数据库_WOW2.43数据库_70级《燃烧的远征》数据库 Ashes of Alar 奥的灰烬 &#xff08;凤凰 310%速度&#xff09; Ashes of Alar - Item - 魔兽世界怀旧服TBC数据…...

CNCF云原生生态版图

CNCF云原生生态版图 概述什么是云原生生态版图如何使用生态版图 项目和产品&#xff08;Projects and products&#xff09;会员&#xff08;Members&#xff09;认证合作伙伴与提供商&#xff08;Certified partners and providers&#xff09;无服务&#xff08;Serverless&a…...

渐冻症:真的无药可治?

“渐冻症”&#xff0c;这个令人闻之色变的疾病&#xff0c;仿佛是生命的冷酷冰封者。一提到渐冻症&#xff0c;很多人脑海中立刻浮现出绝望的画面&#xff0c;认为它无药可治。但事实真的如此吗&#xff1f; 渐冻症&#xff0c;医学上称为肌萎缩侧索硬化症&#xff0c;是一种渐…...

`pg_wal` 目录

在 PostgreSQL 中&#xff0c;自动清理 pg_wal 目录主要通过配置参数 min_wal_size、max_wal_size 和 wal_keep_size 来实现。以下是如何配置 PostgreSQL 以自动清理 WAL 文件的详细步骤和建议&#xff1a; 配置 min_wal_size 和 max_wal_size&#xff1a; min_wal_size&#x…...

【信息系统项目管理师】论文:论信息系统项目的整合管理

文章目录 正文一、制定项目章程二、指定项目管理计划三、指导与管理项目工作四、管理项目知识五、监控项目工作六、实施整体变更控制七、结束项目或阶段 正文 根据省自然资源厅的总体部署&#xff0c;XX市决定于2023年8月开始全市不动产登记系统建设&#xff0c;要求在2024年8…...

MATLAB深度学习(七)——ResNet残差网络

一、ResNet网络 ResNet是深度残差网络的简称。其核心思想就是在&#xff0c;每两个网络层之间加入一个残差连接&#xff0c;缓解深层网络中的梯度消失问题 二、残差结构 在多层神经网络模型里&#xff0c;设想一个包含诺干层自网络&#xff0c;子网络的函数用H(x)来表示&#x…...

freeswitch(配置event_socket连接)

亲测版本centos 7.9系统–》 freeswitch1.10.9 本人freeswitch安装路径(根据自己的路径进入) /usr/local/freeswitch/etc/freeswitch场景说明: 如果想使用代码进行控制freeswitch添加账号、获取注册信息、强拆等,可以使用ESL控制vim autoload_configs/event_socket.conf.x…...

C++ SQLite轻量化数据库使用总结

官网下载&#xff1a;https://www.sqlite.org/download.html 示例1 #include <iostream> #include <sqlite3.h>int main() {sqlite3* db;char* zErrMsg 0;int rc;// 打开数据库连接&#xff08;如果数据库不存在&#xff0c;则会自动创建&#xff09;rc sqlite…...

docker打包当前使用的某个容器为镜像,导出,导入

容器打包成镜像 要将正在使用的 Docker 容器打包成镜像&#xff0c;你可以使用 docker commit 命令。这个命令会从运行中的容器创建一个新的镜像。以下是详细步骤&#xff1a; 查看正在运行的容器&#xff1a; 使用以下命令查看当前正在运行的容器&#xff1a; docker ps找到目…...

【刷题22】BFS解决最短路问题

目录 一、边权为1的最短路问题二、迷宫中离入口最近的出口三、最小基因变化四、单词接龙五、为高尔夫比赛砍树 一、边权为1的最短路问题 如图&#xff1a;从A到I&#xff0c;怎样走路径最短 一个队列一个哈希表队列&#xff1a;一层一层递进&#xff0c;直到目的地为止哈希表&…...

服务器重启:数字世界的短暂休憩与新生

在互联网的浩瀚海洋中&#xff0c;服务器犹如一座座灯塔&#xff0c;持续稳定地散发着光芒&#xff0c;为无数的网络活动提供着支撑与指引。而服务器重启&#xff0c;便是这数字灯塔周期性进行自我调整与修复的关键环节。 服务器重启是指对服务器进行重新启动的过程&#xff0…...

JavaEE 【知识改变命运】05 多线程(4)

文章目录 单例模式什么是单例模式饿汉模式懒汉模式多线程- 懒汉模式分析多线程问题第一种添加sychronized的方式第二种添加sychronized的方式改进第二种添加sychronized的方式&#xff08;DCL检查锁&#xff09; 阻塞队列什么是阻塞队列什么是消费生产者模型标准库中的阻塞队列…...

【CSS in Depth 2 精译_076】12.4 @font-face 的工作原理

当前内容所在位置&#xff08;可进入专栏查看其他译好的章节内容&#xff09; 第四部分 视觉增强技术 ✔️【第 12 章 CSS 排版与间距】 ✔️ 12.1 间距设置 12.1.1 使用 em 还是 px12.1.2 对行高的深入思考12.1.3 行内元素的间距设置 12.2 Web 字体12.3 谷歌字体12.4 font-fac…...

SQL Having用法

拿个业务场景说这个案例&#xff0c;比如我们有个表里面可能有批改过的数据&#xff0c;批改过得数据不会随着新批改的数据覆盖&#xff0c;而是逐条插入表中&#xff0c;如果想找出包含最早批改的数据和最新批改数据的话&#xff0c;那么我们就需要用到了havinng 用法,假设最开…...

@JsonNaming实现入参接口参数下划线驼峰自动转换

JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) 是用于 Jackson 库中的一个注解&#xff0c;作用是改变 Java 对象的字段命名策略&#xff0c;特别是在序列化和反序列化时。这可以帮助 Java 对象中的字段名从驼峰命名法&#xff08;CamelCase&#xff09;转换为蛇…...

使用PaliGemma2构建多模态目标检测系统:从架构设计到性能优化的技术实践指南

目标检测技术作为计算机视觉领域的核心组件&#xff0c;在自动驾驶系统、智能监控、零售分析以及增强现实等应用中发挥着关键作用。本文将详细介绍PaliGemma2模型的微调流程&#xff0c;该模型通过整合SigLIP-So400m视觉编码器与Gemma 2系列的高级语言模型&#xff0c;专门针对…...

MinerU:PDF文档提取工具

目录 docker一键启动本地配置下载模型权重文件demo.pyGPU使用情况 wget https://github.com/opendatalab/MinerU/raw/master/Dockerfile docker build -t mineru:latest .docker一键启动 有点问题&#xff0c;晚点更新 本地配置 就是在Python环境中配置依赖和安装包 根据需求…...

spark的共享变量

因为RDD在spark中是分布式存储 1、python中定义的变量仅仅在driver中运行&#xff0c;在excutor中是获取不到值的——广播变量 2、若定义了一个变量进行累加&#xff0c;先分别在driver和excutor中进行累加&#xff0c;但是结果是不会主动返回给driver的——累加器 Broadcas…...

Scrapy与MongoDB

Scrapy可以在非常短的时间里获取大量的数据。这些数据无论是直接保存为纯文本文件还是CSV文件&#xff0c;都是不可取的。爬取一个小时就可以让这些文件大到无法打开。这个时候&#xff0c;就需要使用数据库来保存数据了。 MongoDB由于其出色的性能&#xff0c;已经成为爬虫的首…...

爬虫基础与实践

爬虫技术基础与实践 在当今数字化的时代&#xff0c;数据成为了宝贵的资源。爬虫技术作为获取数据的重要手段&#xff0c;受到了广泛的关注和应用。本文将介绍爬虫的基本概念、工作原理以及一些常用的技术和工具。 一、爬虫的基本概念 爬虫&#xff0c;也称为网络蜘蛛或网络机器…...

快速上手Serverless架构与FastAPI结合实现自动化移动应用后端

快速上手Serverless架构与FastAPI结合实现自动化移动应用后端 引言 随着云计算技术的发展&#xff0c;Serverless架构已经成为构建现代应用的一种流行选择。它允许开发者将更多精力集中在核心业务逻辑上&#xff0c;而无需管理底层基础设施。本文将以AWS Lambda和API Gateway…...

ansible自动化运维(二)playbook模式详解

一.Ansible中的playbook模式 Playbook不同于使用单个模块操作远程服务器&#xff0c;Playbook的功能更加强大。如果说单个模块执行类似于Linux系统中的命令&#xff0c;那么Playbook就类似于shell脚本&#xff0c;将多个模块组合起来实现一组的操作。 Playbook还是会用到ad-h…...

基于Springboot社团管理系统【附源码】

基于Springboot社团管理系统 效果如下&#xff1a; 系统登录页面 用户管理页面 社团信息管理页面 社团活动管理页面 经费信息管理页面 新闻信息管理页面 系统主页面 社团信息页面 研究背景 在当今高校与社区环境中&#xff0c;学生社团蓬勃发展&#xff0c;成为学生课余生活…...

CSS:html中,.png的动态图,怎么只让它显示部分,比如只显示右上部分的,或右边中间部分

目录 背景 方法 1: 使用 background-image 和 background-position 示例代码 解释 方法 2: 使用 clip-path 裁剪图像 示例代码 解释 方法 3: 使用 object-fit 和 overflow 示例代码 解释 示例 总结 背景 在HTML中,如果你有一个 .png 的动态图(例如一个 GIF 动画或…...

解读CVPR2024-论文分享|RepViT: Revisiting Mobile CNN From ViT Perspective

论文标题 RepViT: Revisiting Mobile CNN From ViT Perspective 论文链接&#xff1a; https://arxiv.org/abs/2307.09283 论文作者 Ao Wang, Hui Chen, Zijia Lin, Jungong Han, Guiguang Ding 内容简介 这篇论文探讨了在资源受限的移动设备上&#xff0c;轻量级视觉变…...

linux部署安装wordpress

一、环境准备 首先我们先介绍下环境和实验中所需要的包 环境&#xff1a; 我使用的是centos7.6的系统 建议关掉selinux和影响到80端口的防火墙策略 selinux永久有效 修改 /etc/selinux/config 文件中的 SELINUX"" 为 disabled &#xff0c;然后重启。 selinux即…...

[Java] 配置Powershell 的 Maven 环境变量

目录 前言单独为 Powershell 设置 Maven 环境变量 前言 安装使用 maven 的时候发现&#xff0c;明明已经配置好了环境变量。但是在 powershell 中还是无法识别 mvn 命令。原来这货需要另外配置。 单独为 Powershell 设置 Maven 环境变量 要在 PowerShell 中永久配置 Maven 环…...

Android -- [SelfView] 自定义弹窗式颜色选择器

Android – [SelfView] 自定义弹窗式颜色选择器 PS: 1. 弹框式显示&#xff1b; 2. 支持透明度设置&#xff1b; 3. 支持拖动控件选择颜色&#xff1b; 4. 支持 ARGB | HEX 数值填写预览颜色并返回&#xff1b; 5. 输出支持Hex 和 Int 两种格式&#xff1b;效果 使用方法&…...

宁波梅山建设局网站/搜索引擎优化的常用方法

CWinThread::m_pMainWnd该成员变量去存储你的线程主窗口对象。当和m_pMainWnd 相关的窗口被关闭后&#xff0c;MFC会自动终止你的线程。如果该线程是应用程序主线程&#xff0c;程序也将会被终止。如果该数据成员为NULL&#xff0c;应用程序CWinApp对象的主窗口将用来决定什么时…...

重庆网站设计建设/网络营销图片

今天安装Ubuntu的时候&#xff0c;碰到一个问题&#xff0c;装好系统之后&#xff0c;无线网卡是能用的&#xff0c;但是升级完系统之后&#xff0c;或者使用了Ubuntu推荐的无线网卡驱动之后&#xff0c;就不能用了。在网上查阅了相关资料之后终于找到了解决之道&#xff0c;那…...

上海城隍庙必吃美食/seo营销培训

阻力前提 &#xff1a;当有张表同一个字段管理2张表是&#xff0c;今天用one-to-moeny试了一下&#xff0c;开始配置one-to-many时&#xff0c;是不能同时写入数据&#xff1b;后来《Batch update returned unexpected row count from update: 0 actual row count: 0 expected:…...

成都网站建设 工资/石家庄新闻

中文词频统计&#xff1a; 作业连接&#xff1a;https://edu.cnblogs.com/campus/gzcc/GZCC-16SE1/homework/2822 1. 下载一长篇中文小说。 2. 从文件读取待分析文本。 3. 安装并使用jieba进行中文分词。 pip install jieba import jieba jieba.lcut(text) 4. 更新词库&…...

网站内页做排名/自动点击器软件

摘要自簡單來說就是可以將一個 or 多個應用包裝成一個服務&#xff0c;並透過 chart 的形式發佈&#xff0c;讓大家可以方便在 k8s 上安裝特定的服務。幾個元件的名詞Tiller server: 用來與API server溝通&#xff0c;使用chart在k8s cluster上建立服務Helm client: 則是用來操…...

哪些编程语言适合网站开发/市场营销考试题目及答案2022

一、要命的端口 计算机要与外界进行通信&#xff0c;必须通过一些端口。别人要想入侵和控制我们的电脑&#xff0c;也要从某些端口连接进来。某日笔者查看了一位朋友的系统&#xff0c;吃惊地发现开放了139、445、3389、4899等重要端口&#xff0c;要知道这些端口都可以为黑客…...