Linux笔记---自定义shell
目录
前言
1. 程序框架
2. 打印命令行提示符
2.1 获取用户名(GetUserName)
2.2 获取主机名(GetHostName)
2.3 获取工作目录(GetPwd)
3. 获取命令行输入
4. 判断是否有重定向
5. 解析命令行
6. 内建命令
6.1 内建命令的特点
6.2 常见内建命令
6.3 内建命令 vs 外部命令
6.4 为什么需要内建命令
6.5 处理内建命令
6.5.1 cd 命令
6.5.2 echo 命令
7. 执行命令
8. 完整代码
9. 缺陷
前言
该自定义shell并没有任何实用价值,一是因为其功能并不完善,二是因为其是建立在bash的基础之上进行的编写的成果(初始环境变量取自bash),仅具有学习价值。
为了贴近真实的 shell 程序开发,我们的代码完全用C语言完成。
本文通过完成一个自定义shell小项目的方式,对前面学习的知识做一个简单的总结,帮助读者深入理解有关概念(子进程,进程调度,环境变量……)。
前置参考文章:https://blog.csdn.net/2302_80372340/category_12833896.html
1. 程序框架
在开始之前,我们要先明确我们要做什么,我们的目标是:(1)不断显示当前用户名、主机和工作目录,并要求用户输入指令;(2)解析用户输入的指令,执行对应的操作或启动对应的进程。
当然,在正式开始之前,我们需要初始化我们自己的 shell 程序(后文统称myshell)的环境变量。
// myshell的环境变量
#define MAX_ENV_SIZE 100
char *my_env[MAX_ENV_SIZE];
int env_count = 0;
据此,我们可以大致拟出 myshell 的框架:
int main()
{// 初始化环境变量InitEnv();// PrintEnv();while(true){// 打印命令行提示符PrintCommandPrompt();// 获取命令行输入char commandline[MAX_COMMAND_LEN];if(!GetCommandLine(commandline, sizeof(commandline)))continue;// 判断是否有重定向RedirCheck(commandline);// 命令行解析if(!ParseCommandLine(commandline))continue;// PrintArgv();// 判断是否有为内建命令,如果是就执行// 后文中再解释何为内建命令if(CheckAndExecBuiltin())continue;// 执行命令Execute();}return 0;
}
bash 会在启动时,通过解析配置文件,将环境变量加载到内存中并维护起来。
我们并不了解其配置文件的解析方式,为了简单起见,我们直接抄袭 bash 的环境变量以初始化的环境变量。
在拷贝完成之后,需要让environ指向我们自己的环境变量表,这样一来,各种与环境变量有关的系统调用就会对我们自己的环境变量表进行操作。
实际上,就算直接使用 environ 指向的环境变量表也是完全一样的效果,因为其本来就是从 bash 继承下来的一个拷贝。但就像我们之前说的,我们只是用拷贝 bash 环境变量的方式代替了从配置文件中读取的过程,真正的 shell 程序肯定是要自己维护一张环境变量表的。虽然将环境变量拷贝到自己的环境变量表中略显多余,但是为了帮助我们更好地理解 shell 程序的运作方式,还是自己维护一张环境变量表的方式更好。
// 初始化环境变量
void InitEnv()
{memset(my_env, 0, sizeof(my_env));extern char** environ;// 本来应该从配置文件中读取,但为了方便就直接拷贝bash的环境变量for(env_count = 0; environ[env_count]; env_count++){my_env[env_count] = (char*)malloc(strlen(environ[env_count]) + 1);strcpy(my_env[env_count], environ[env_count]);}my_env[env_count++] = NULL;// 让environ指向我们自己的环境变量表environ = my_env;
}
2. 打印命令行提示符
所谓命令行提示符,就是:用户名 + 主机 + 工作目录。
上图是Ubuntu环境下的命令行提示符,它将工作目录完整显示出来了,当工作目录层次较深时看起来很冗余。
相比之下,我更喜欢CentOS的格式,下图是myshell运行起来的提示符:
只显示了最后一级工作目录,看起来要简洁一些。
void PrintCommandPrompt()
{// CentOs的格式,感觉更简洁清晰一些printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetBaseName());
}
2.1 获取用户名(GetUserName)
直接到环境变量中找到 "USER" 环境变量即可。
// 默认字符串
const char* default_str = "None";// 获取用户名
const char* GetUserName()
{const char* username = getenv("USER");return username == NULL ? default_str : username;
}
2.2 获取主机名(GetHostName)
在Unbuntu中没有 "HOSTNAME" 环境变量,我们可以通过系统调用 gethostname 来获取。
// 主机名hostname
#define MAX_HOST_SIZE 256
char hostname[MAX_HOST_SIZE];// 获取主机名
const char* GetHostName()
{// Ubuntu中没有HOSTNAME环境变量// const char* hostname = getenv("HOSTNAME");return gethostname(hostname, sizeof(hostname)) != 0 ? default_str : hostname;
}
2.3 获取工作目录(GetPwd)
同样,在找到环境变量 "PWD" 即可,但是我们只需要最后一级目录,所以对GetPwd进行了一层封装。
这里要解释一下 cwd 是什么。cwd 是进程的一个属性而不是环境变量,用于表示进程的当前工作目录。使用 getcwd 系统调用可以获得 cwd。至于为什么要用 cwd 来更新 PWD ,我们在后文中讲到 cd 命令的时候在详细解释。
// cwd---当前用户工作目录
#define MAX_CWD_SIZE 1024
char cwd[MAX_CWD_SIZE];
char pwdOfcwd[MAX_CWD_SIZE + 6];// 获取当前工作目录
const char* GetPwd()
{const char* pwd = getcwd(cwd, sizeof(cwd));// 更新pwdif(pwd != NULL){snprintf(pwdOfcwd, sizeof(pwdOfcwd), "PWD=%s", cwd);putenv(pwdOfcwd);}return pwd == NULL ? default_str : pwd;
}// 最后一级路径,也叫"基本名称"
const char* GetBaseName()
{const char* pwd = GetPwd();int pos = strlen(pwd) - 1;while(pos > 0 && pwd[pos] != '/')pos--;if(pwd[pos] == '/') pos++;return (pwd + pos);
}
3. 获取命令行输入
第一个参数是存放命令行输入的字符数组,第二个参数是字符数组的大小。
这个函数只负责将用户的输入照搬下来,放到一个一维数组,包括空格、重定向符号等。
为了读取一整行的字符,这里用 fgets 函数来进行读取。
// 命令行输入的最大值
#define MAX_COMMAND_LEN 1024
// 在主循环中定义的数组
char commandline[MAX_COMMAND_LEN];bool GetCommandLine(char* commandline, int n)
{char* str = fgets(commandline, n, stdin);if(str == NULL) return false;// 清理'\n',顺便在结尾加上'\0'commandline[strlen(commandline) - 1] = '\0';if(strlen(commandline) == 0) return false;return true;
}
4. 判断是否有重定向
在解析命令行之前,需要先判断是否存在重定向,并将重定向类型与相关文件记录下来。接着,将命令行输入中的重定向符号与文件名删除。
这样一来,解析命令行部分要做的就只有将命令行参数按空格分开,而不需要再检查某个参数中是否含有重定向符号了。
// 重定向状态
enum redir_status
{NONE_REDIR,INPUT_REDIR,OUTPUT_REDIR,APPEND_REDIR
};
enum redir_status redir = NONE_REDIR;
#define MAX_FILENAME 1024
char filename[MAX_FILENAME];void RedirCheck(char* commandline)
{redir = NONE_REDIR;int len = strlen(commandline);int pos = 0;for(pos = 0; pos < len; pos++){// 输入重定向if(commandline[pos] == '<'){commandline[pos] = '\0';redir = INPUT_REDIR;break;}else if(commandline[pos] == '>'){commandline[pos] = '\0';// 追加重定向if(pos < len - 1 && commandline[pos + 1] == '>'){redir = APPEND_REDIR;pos++;}// 输出重定向else{redir = OUTPUT_REDIR;}break;}}pos++;// 发生了重定向,提取文件名if(pos != len){while(commandline[pos] == ' '){pos++;}int i = 0;for(i = 0; pos + i < len; i++){filename[i] = commandline[pos + i];}filename[i] = '\0';}
}
5. 解析命令行
利用 strtok 函数将命令行的输入按空格拆分成一个个的命令行参数,并存放到命令行参数向量 my_argv 中。
// 命令行参数
#define MAX_ARGC 128
char* my_argv[MAX_ARGC];
int my_argc = 0;bool ParseCommandLine(char* commandline)
{my_argc = 0;static const char sep[] = {" "};for(my_argv[my_argc] = strtok(commandline, sep); my_argv[my_argc++]; my_argv[my_argc] = strtok(NULL, sep)){;}my_argc--;return my_argc > 0 ? true : false;
}
6. 内建命令
在最后一个部分中,我们可以通过启动子程序的方式来执行大多数命令(就像 bash 做的那样),但是某些命令却不能用这样的方式来实现,因为它们是 shell 本身的一种功能,而非外部命令。
在 Linux 和类 Unix 系统的 Shell 中,内建命令(Built-in Command) 是直接集成在 Shell 解释器内部的命令,不需要调用外部的可执行文件。它们由 Shell 自身直接执行,因此具有更高的执行效率和特殊的功能特性。
6.1 内建命令的特点
直接由 Shell 解释器处理:内建命令的代码是 Shell 程序的一部分,执行时不会创建新的进程(例如 cd、echo、export)。
与 Shell 环境紧密相关:内建命令通常用于修改 Shell 自身的状态(例如修改当前目录 cd、设置环境变量 export)。
执行速度快:因为不需要启动外部进程或搜索磁盘上的可执行文件。
无法通过 PATH 环境变量查找:内建命令的名称是固定的,不能通过路径调用(例如 /bin/cd 并不存在,但 cd 是 Shell 的内建命令)。
6.2 常见内建命令
- cd:切换当前工作目录。
- echo:输出文本。
- export:设置环境变量。
- source(或 .):加载并执行脚本(不创建子 Shell)。
- exit:退出 Shell 或脚本。
- alias/unalias:设置或取消命令别名。
- type:查看命令类型(内建、外部或别名)。
6.3 内建命令 vs 外部命令
特性 | 内建命令 | 外部命令 |
存储位置 | 集成在 Shell 解释器中 | 存储在磁盘上的可执行文件(如 /bin/ls) |
执行方式 | 由 Shell 直接执行 | 需要启动新进程(通过 fork + exec) |
依赖环境 | 直接影响当前 Shell 环境 | 在子进程中运行,不影响父 Shell 环境 |
执行速度 | 快(无进程创建开销) | 慢(需启动进程和加载可执行文件) |
6.4 为什么需要内建命令
- 修改 Shell 自身状态:例如 cd 必须内建,因为外部命令无法修改父 Shell 的当前目录。
- 提高效率:高频操作(如 echo)内建可减少进程创建开销。
- 提供特殊功能:例如 source 命令用于在当前 Shell 中执行脚本,而非新建子进程。
某些命令可能同时有内建和外部版本(例如 echo、printf),默认优先使用内建版本。可以通过路径强制调用外部版本(例如 /bin/echo)。
不同 Shell(如 Bash、Zsh)的内建命令可能略有差异。
6.5 处理内建命令
下面的代码仅实现了内建命令 cd 和 echo,对于其他的内建命令,各位可以自行尝试实现。
bool CheckAndExecBuiltin()
{if(strcmp(my_argv[0], "cd") == 0){Cd();return true;}else if(strcmp(my_argv[0], "echo") == 0){Echo();return true;}else if(strcmp(my_argv[0], "export") == 0){// Todoreturn true;}else if(strcmp(my_argv[0], "alias") == 0){// Todoreturn true;}else{return false;}
}
6.5.1 cd 命令
cd 命令的作用是修改 shell 的工作目录,即修改 shell 进程的 cwd 属性,在代码层面,我们可以使用 chdir 系统调用来实现这一点。
// 内建命令cd
void Cd()
{if(my_argc == 1 || strcmp(my_argv[1], "~") == 0 || strcmp(my_argv[1], "-") == 0){const char* home = GetHome();if(strcmp(home, default_str) != 0)chdir(home);if(strcmp(my_argv[1], "-") == 0)printf("%s\n", home);}else{chdir(my_argv[1]);}
}
cwd(Current Working Directory)是进程的一个属性,表示进程当前的工作目录。
PWD(Print Working Directory)是进程环境变量表中的一个环境变量,用于打印当前工作目录。
在 bash 中,我们使用 cd 修改的是 cwd,程序关注的也是 cwd (cwd + 相对路径 = 绝对路径),但是 bash 会自动更新 PWD 。
当我们自己管理环境变量表时,一定要记得更新 PWD ,这一点我们放到 GetPwd 中完成。
6.5.2 echo 命令
// 最近的一次指令执行的退出码
int last_exit_code = 0;// 内建命令echo
void Echo()
{FILE* file = stdout;if(redir == OUTPUT_REDIR){file = fopen(filename, "w");}else if(redir == APPEND_REDIR){file = fopen(filename, "a");}if(strcmp(my_argv[1], "$?") == 0){fprintf(file, "%d\n", last_exit_code);}else if(my_argv[1][0] == '$'){const char* env = getenv(my_argv[1] + 1);if(env)fprintf(file, "%s\n", env);}else{fprintf(file, "%s\n", my_argv[1]);}if(file != stdout)fclose(file);
}
7. 执行命令
就像我们在Linux笔记---进程:进程替换-CSDN博客和Linux笔记---系统文件I/O-CSDN博客中讲到的,启动子程序,重定向,程序替换,三步即可完成。
看上去是核心,实际上却是全篇最简单的部分(因为巨人主要站在这部分)。
// 执行
void Execute()
{pid_t id = fork();if(id == 0){int fd = 0;if(redir == INPUT_REDIR){fd = open(filename, O_RDONLY);if(fd < 0) exit(1);dup2(fd, 0);}else if(redir == OUTPUT_REDIR){fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd, 1);}else if(redir == APPEND_REDIR){fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(1);dup2(fd, 1);}execvp(my_argv[0], my_argv);exit(1);}redir = NONE_REDIR;int status;pid_t rid = waitpid(id, &status, 0);if(rid > 0)last_exit_code = WEXITSTATUS(status);
}
8. 完整代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <sys/stat.h>// myshell的环境变量
#define MAX_ENV_SIZE 100
char *my_env[MAX_ENV_SIZE];
int env_count = 0;// cwd---当前用户工作目录
#define MAX_CWD_SIZE 1024
char cwd[MAX_CWD_SIZE];
char pwdOfcwd[MAX_CWD_SIZE + 6];// 主机名hostname
#define MAX_HOST_SIZE 256
char hostname[MAX_HOST_SIZE];// 命令行参数
#define MAX_ARGC 128
char* my_argv[MAX_ARGC];
int my_argc = 0;// 命令行输入的最大值
#define MAX_COMMAND_LEN 1024// 最近的一次指令执行的退出码
int last_exit_code = 0;// 默认字符串
const char* default_str = "None";// 重定向状态
enum redir_status
{NONE_REDIR,INPUT_REDIR,OUTPUT_REDIR,APPEND_REDIR
};
enum redir_status redir = NONE_REDIR;
#define MAX_FILENAME 1024
char filename[MAX_FILENAME];// 初始化环境变量
void InitEnv()
{memset(my_env, 0, sizeof(my_env));extern char** environ;// 本来应该从配置文件中读取,但为了方便就直接拷贝bash的环境变量for(env_count = 0; environ[env_count]; env_count++){my_env[env_count] = (char*)malloc(strlen(environ[env_count]) + 1);strcpy(my_env[env_count], environ[env_count]);}my_env[env_count++] = NULL;// 让environ指向环境变量表environ = my_env;
}// 打印环境变量,用于调试
void PrintEnv()
{extern char** environ;for(int i = 0; environ[i]; i++){printf("%s\n", environ[i]);}
}// 获取用户名
const char* GetUserName()
{const char* username = getenv("USER");return username == NULL ? default_str : username;
}// 获取主机名
const char* GetHostName()
{// Ubuntu中没有HOSTNAME环境变量// const char* hostname = getenv("HOSTNAME");return gethostname(hostname, sizeof(hostname)) != 0 ? default_str : hostname;
}// 获取当前工作目录
const char* GetPwd()
{const char* pwd = getcwd(cwd, sizeof(cwd));// 更新pwdif(pwd != NULL){snprintf(pwdOfcwd, sizeof(pwdOfcwd), "PWD=%s", cwd);putenv(pwdOfcwd);}return pwd == NULL ? default_str : pwd;
}// 最后一级路径,也叫"基本名称"
const char* GetBaseName()
{const char* pwd = GetPwd();int pos = strlen(pwd) - 1;while(pos > 0 && pwd[pos] != '/')pos--;if(pwd[pos] == '/') pos++;return (pwd + pos);
}// 获取家目录
const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? default_str : home;
}void PrintCommandPrompt()
{// CentOs的格式,感觉更简洁清晰一些printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetBaseName());
}bool GetCommandLine(char* commandline, int n)
{char* str = fgets(commandline, n, stdin);if(str == NULL) return false;// 清理'\n',顺便在结尾加上'\0'commandline[strlen(commandline) - 1] = '\0';if(strlen(commandline) == 0) return false;return true;
}bool ParseCommandLine(char* commandline)
{my_argc = 0;static const char sep[] = {" "};for(my_argv[my_argc] = strtok(commandline, sep); my_argv[my_argc++]; my_argv[my_argc] = strtok(NULL, sep)){;}my_argc--;return my_argc > 0 ? true : false;
}// 打印命令行参数,用于调试
void PrintArgv()
{for(int i = 0; i < my_argc; i++){printf("%s ", my_argv[i]);}printf("\n");
}// 内建命令cd
void Cd()
{if(my_argc == 1 || strcmp(my_argv[1], "~") == 0 || strcmp(my_argv[1], "-") == 0){const char* home = GetHome();if(strcmp(home, default_str) != 0)chdir(home);if(strcmp(my_argv[1], "-") == 0)printf("%s\n", home);}else{chdir(my_argv[1]);}
}// 内建命令echo
void Echo()
{FILE* file = stdout;if(redir == OUTPUT_REDIR){file = fopen(filename, "w");}else if(redir == APPEND_REDIR){file = fopen(filename, "a");}if(strcmp(my_argv[1], "$?") == 0){fprintf(file, "%d\n", last_exit_code);}else if(my_argv[1][0] == '$'){const char* env = getenv(my_argv[1] + 1);if(env)fprintf(file, "%s\n", env);}else{fprintf(file, "%s\n", my_argv[1]);}if(file != stdout)fclose(file);
}bool CheckAndExecBuiltin()
{if(strcmp(my_argv[0], "cd") == 0){Cd();return true;}else if(strcmp(my_argv[0], "echo") == 0){Echo();return true;}else if(strcmp(my_argv[0], "export") == 0){// Todoreturn true;}else if(strcmp(my_argv[0], "alias") == 0){// Todoreturn true;}else{return false;}
}// 执行
void Execute()
{pid_t id = fork();if(id == 0){int fd = 0;if(redir == INPUT_REDIR){fd = open(filename, O_RDONLY);if(fd < 0) exit(1);dup2(fd, 0);}else if(redir == OUTPUT_REDIR){fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);if(fd < 0) exit(1);dup2(fd, 1);}else if(redir == APPEND_REDIR){fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);if(fd < 0) exit(1);dup2(fd, 1);}execvp(my_argv[0], my_argv);exit(1);}redir = NONE_REDIR;int status;pid_t rid = waitpid(id, &status, 0);if(rid > 0)last_exit_code = WEXITSTATUS(status);
}void RedirCheck(char* commandline)
{redir = NONE_REDIR;int len = strlen(commandline);int pos = 0;for(pos = 0; pos < len; pos++){// 输入重定向if(commandline[pos] == '<'){commandline[pos] = '\0';redir = INPUT_REDIR;break;}else if(commandline[pos] == '>'){commandline[pos] = '\0';// 追加重定向if(pos < len - 1 && commandline[pos + 1] == '>'){redir = APPEND_REDIR;pos++;}// 输出重定向else{redir = OUTPUT_REDIR;}break;}}pos++;// 发生了重定向,提取文件名if(pos != len){while(commandline[pos] == ' '){pos++;}int i = 0;for(i = 0; pos + i < len; i++){filename[i] = commandline[pos + i];}filename[i] = '\0';}
}int main()
{// 初始化环境变量InitEnv();// PrintEnv();while(true){// 打印命令行提示符PrintCommandPrompt();// 获取命令行输入char commandline[MAX_COMMAND_LEN];if(!GetCommandLine(commandline, sizeof(commandline)))continue;// 判断是否有重定向RedirCheck(commandline);// 命令行解析if(!ParseCommandLine(commandline))continue;// PrintArgv();// 判断是否有为内建命令,如果是就执行if(CheckAndExecBuiltin())continue;// 执行命令Execute();}return 0;
}
9. 缺陷
除了之前提到的内建命令不完整的问题以外,myshell 与真正的 bash 还有一定的区别:
- 输入时会将方向键,删除键等识别为字符,而不会发挥其原本的文本编辑功能。
- ls 不会根据文件类型显示对应的颜色。
尝试过解决,但是放弃了。先这样吧,说不定以后学的知识多了就知道怎么解决了。
相关文章:

Linux笔记---自定义shell
目录 前言 1. 程序框架 2. 打印命令行提示符 2.1 获取用户名(GetUserName) 2.2 获取主机名(GetHostName) 2.3 获取工作目录(GetPwd) 3. 获取命令行输入 4. 判断是否有重定向 5. 解析命令行 6. 内建命令 6.1 内建命令的特点 6.2 常见内建命令 6.3 内建命令 vs 外部命…...

大语言模型从理论到实践(第二版)-学习笔记(绪论)
大语言模型的基本概念 1.理解语言是人工智能算法获取知识的前提 2.语言模型的目标就是对自然语言的概率分布建模 3.词汇表 V 上的语言模型,由函数 P(w1w2 wm) 表示,可以形式化地构建为词序列 w1w2 wm 的概率分布,表示词序列 w1w2 wm…...

2025-03-08 学习记录--C/C++-C 语言 判断一个数是否是完全平方数
C 语言 判断一个数是否是完全平方数 使用 sqrt 函数计算平方根,然后判断平方根的整数部分是否与原数相等。 #include <stdio.h> #include <math.h>int isPerfectSquare(int num) {if (num < 0) {return 0; // 负数不是完全平方数}int sqrtNum (int)…...
八、排序算法
一些简单的排序算法 8.1 冒泡排序 void Bubble_sort(int a[] , int len){int i,j,flag,tmp;for(i=0 ; i < len-1 ; i++){flag = 1;for(j=0 ; j < len-1-i ; j++){if(a[j] > a[j+1]){tmp = a[j];a[j] = a[j+1];a[j+1] = tmp;flag = 0;}}if(flag == 1){break;}}…...
计算机网络篇:基础知识总结与基于长期主义的内容更新
基础知识总结 和 MySQL 类似,我同样花了一周左右的时间根据 csview 对计算机网络部分的八股文进行了整理,主要的内容包括:概述、TCP 与 UDP、IP、HTTP,其中我个人认为最重要的是 TCP 这部分的内容。 在此做一篇目录索引…...

nodejs学习——nodejs和npm安装与系统环境变量配置及国内加速
nodejs和npm安装与系统环境变量配置及国内加速 下载node-v22.14.0-x64.msi 建议修改为非C盘文件夹 其它步骤,下一步,下一步,完成。 打开CMD窗口查看安装详情 $ node -v v22.14.0 $ npm -v 10.9.2$ npm config list创建node_global和node_c…...
《打造视频同步字幕播放网页:从0到1的技术指南》
《打造视频同步字幕播放网页:从0到1的技术指南》 为什么要制作视频同步字幕播放网页 在数字化信息飞速传播的当下,视频已然成为内容输出与获取的核心载体,其在教育、娱乐、宣传推广等诸多领域发挥着举足轻重的作用 。制作一个视频同步字幕播…...

清华大学第八弹:《DeepSeek赋能家庭教育》
大家好,我是吾鳴。 之前吾鳴给大家分享过清华大学出版的七份报告,它们分别是: 《DeepSeek从入门到精通》 《DeepSeek如何赋能职场应用》 《普通人如何抓住DeepSeek红利》 《DeepSeekDeepResearch:让科研像聊天一样简单》 《D…...
自我训练模型:通往未来的必经之路?
摘要 在探讨是否唯有通过自我训练模型才能掌握未来的问题时,文章强调了底层技术的重要性。当前,许多人倾向于关注应用层的便捷性,却忽视了支撑这一切的根本——底层技术。将模型简单视为产品是一种短视行为,长远来看,理…...

C++ Primer 交换操作
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...

深度学习模型组件之优化器--自适应学习率优化方法(Adadelta、Adam、AdamW)
深度学习模型组件之优化器–自适应学习率优化方法(Adadelta、Adam、AdamW) 文章目录 深度学习模型组件之优化器--自适应学习率优化方法(Adadelta、Adam、AdamW)1. Adadelta1.1 公式1.2 优点1.3 缺点1.4 应用场景 2. Adam (Adaptiv…...
使用jcodec库,访问网络视频提取封面图片上传至oss
注释部分为FFmpeg(确实方便但依赖太大,不想用) package com.zuodou.upload;import com.aliyun.oss.OSS; import com.aliyun.oss.model.ObjectMetadata; import com.aliyun.oss.model.PutObjectRequest; import com.zuodou.oss.OssProperties;…...

新品速递 | 多通道可编程衰减器+矩阵系统,如何破解复杂通信测试难题?
在无线通信技术快速迭代的今天,多通道可编程数字射频衰减器和衰减矩阵已成为测试领域不可或缺的核心工具。它们凭借高精度、灵活配置和强大的多通道协同能力,为5G、物联网、卫星通信等前沿技术的研发与验证提供了关键支持。从基站性能测试到终端设备校准…...

扩展------项目中集成阿里云短信服务
引言 在当今数字化时代,短信服务在各种项目中扮演着重要角色,如用户注册验证、订单通知、营销推广等。阿里云短信服务凭借其稳定、高效和丰富的功能,成为众多开发者和企业的首选。本文将详细介绍如何在项目中集成阿里云短信服务,帮…...

MySQL面试篇——性能优化
MySQL性能优化 在MySQL中,如何定位慢查询 慢查询表象:页面加载过慢、接口压测响应时间过长(超过1s)。造成慢查询的原因通常有:聚合查询、多表查询、表数据量过大查询、深度分页查询 方案一:开源工具 调试工…...

Java EE 进阶:Spring MVC(2)
cookie和session的关系 两者都是在客户端和服务器中进行存储数据和传递信息的工具 cookie和session的区别 Cookie是客⼾端保存⽤⼾信息的⼀种机制. Session是服务器端保存⽤⼾信息的⼀种机制. Cookie和Session之间主要是通过SessionId关联起来的,SessionId是Co…...
ShardingSphere 和 Spring 的动态数据源切换机制的对比以及原理
ShardingSphere 与 Spring 动态数据源切换机制的对比及原理 一、核心定位对比 维度ShardingSphereSpring动态数据源(如 AbstractRoutingDataSource)定位分布式数据库中间件轻量级多数据源路由工具核心目标分库分表、读写分离、分布式事务多数据源动态切…...

基于Django的协同过滤算法养老新闻推荐系统的设计与实现
基于Django的协同过滤算法养老新闻推荐系统(可改成普通新闻推荐系统使用) 开发工具和实现技术 Pycharm,Python,Django框架,mysql8,navicat数据库管理工具,vue,spider爬虫࿰…...
AI视频生成工具清单(附网址与免费说明)
以下是一份详细的AI视频制作网站总结清单,包含免费/付费信息及核心功能说明: AI视频生成工具清单(附网址与免费说明) 1. Synthesia 网址:https://www.synthesia.io是否免费:免费试用(生成视频…...

JavaWeb学习——HTTP协议
HTTP 协议 什么是 HTTP 协议 HTTP(超文本传输协议,HyperText Transfer Protocol)是用于在客户端(如浏览器)和服务器之间传输超文本(如网页、图片、视频等)的应用层协议。它是现代互联网数据通…...

【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...

关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...
在四层代理中还原真实客户端ngx_stream_realip_module
一、模块原理与价值 PROXY Protocol 回溯 第三方负载均衡(如 HAProxy、AWS NLB、阿里 SLB)发起上游连接时,将真实客户端 IP/Port 写入 PROXY Protocol v1/v2 头。Stream 层接收到头部后,ngx_stream_realip_module 从中提取原始信息…...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...
【JavaSE】绘图与事件入门学习笔记
-Java绘图坐标体系 坐标体系-介绍 坐标原点位于左上角,以像素为单位。 在Java坐标系中,第一个是x坐标,表示当前位置为水平方向,距离坐标原点x个像素;第二个是y坐标,表示当前位置为垂直方向,距离坐标原点y个像素。 坐标体系-像素 …...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

倒装芯片凸点成型工艺
UBM(Under Bump Metallization)与Bump(焊球)形成工艺流程。我们可以将整张流程图分为三大阶段来理解: 🔧 一、UBM(Under Bump Metallization)工艺流程(黄色区域ÿ…...
CppCon 2015 学习:Reactive Stream Processing in Industrial IoT using DDS and Rx
“Reactive Stream Processing in Industrial IoT using DDS and Rx” 是指在工业物联网(IIoT)场景中,结合 DDS(Data Distribution Service) 和 Rx(Reactive Extensions) 技术,实现 …...

【PX4飞控】mavros gps相关话题分析,经纬度海拔获取方法,卫星数锁定状态获取方法
使用 ROS1-Noetic 和 mavros v1.20.1, 携带经纬度海拔的话题主要有三个: /mavros/global_position/raw/fix/mavros/gpsstatus/gps1/raw/mavros/global_position/global 查看 mavros 源码,来分析他们的发布过程。发现前两个话题都对应了同一…...

Java中HashMap底层原理深度解析:从数据结构到红黑树优化
一、HashMap概述与核心特性 HashMap作为Java集合框架中最常用的数据结构之一,是基于哈希表的Map接口非同步实现。它允许使用null键和null值(但只能有一个null键),并且不保证映射顺序的恒久不变。与Hashtable相比,Hash…...