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));
    // 更新pwd
    if(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 内建命令的特点

  1. 直接由 Shell 解释器处理:内建命令的代码是 Shell 程序的一部分,执行时不会创建新的进程(例如 cd、echo、export)。

  2. 与 Shell 环境紧密相关:内建命令通常用于修改 Shell 自身的状态(例如修改当前目录 cd、设置环境变量 export)。

  3. 执行速度快:因为不需要启动外部进程或搜索磁盘上的可执行文件。

  4. 无法通过 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 为什么需要内建命令

  1. 修改 Shell 自身状态:例如 cd 必须内建,因为外部命令无法修改父 Shell 的当前目录。
  2. 提高效率:高频操作(如 echo)内建可减少进程创建开销。
  3. 提供特殊功能:例如 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)
    {
        // Todo
        return true;
    }
    else if(strcmp(my_argv[0], "alias") == 0)
    {
        // Todo
        return 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));
    // 更新pwd
    if(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)
    {
        // Todo
        return true;
    }
    else if(strcmp(my_argv[0], "alias") == 0)
    {
        // Todo
        return 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 还有一定的区别:

  1. 输入时会将方向键,删除键等识别为字符,而不会发挥其原本的文本编辑功能。
  2. ls 不会根据文件类型显示对应的颜色。

尝试过解决,但是放弃了。先这样吧,说不定以后学的知识多了就知道怎么解决了。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DeepSeek专题:DeepSeek-V2核心知识点速览

AIGCmagic社区知识星球是国内首个以AIGC全栈技术与商业变现为主线的学习交流平台&#xff0c;涉及AI绘画、AI视频、大模型、AI多模态、数字人以及全行业AIGC赋能等100应用方向。星球内部包含海量学习资源、专业问答、前沿资讯、内推招聘、AI课程、AIGC模型、AIGC数据集和源码等…

游戏引擎学习第138天

仓库:https://gitee.com/mrxiao_com/2d_game_3 资产&#xff1a;game_hero_test_assets_003.zip 发布 我们的目标是展示游戏运行时的完整过程&#xff0c;从像素渲染到不使用GPU的方式&#xff0c;我们自己编写了渲染器并完成了所有的工作。今天我们开始了一些新的内容&#…

【Hadoop】详解HDFS

Hadoop 分布式文件系统(HDFS)被设计成适合运行在通用硬件上的分布式文件系统&#xff0c;它是一个高度容错性的系统&#xff0c;适合部署在廉价的机器上&#xff0c;能够提供高吞吐量的数据访问&#xff0c;非常适合大规模数据集上的应用。为了做到可靠性&#xff0c;HDFS创建了…

计算机网络:计算机网络的概念

1.计算机网络&#xff1a;由若干个结点和链接这些的链路组成。 2.集线器&#xff08;Hub&#xff09;&#xff1a;可以把多个结点连接起来&#xff0c;组成一个计算机网络。 不能避免数据冲突的情况 3.交换机&#xff08;Switch&#xff09;:可以把多个结点连接起来&#x…

C/C++类型转换

目录 C语言中的类型转换 隐式类型转换&#xff1a; 显示类型转换&#xff1a; 总结&#xff1a; C中的类型转换 static_cast: reinterpret_cast: const_cast: dynamic_cast: RTTI C语言中的类型转换 在C语言中&#xff0c;如果赋值运算符左右两侧类型不同&#xff0c;或者形参…

发行基础:宣传片

转载自官方文件 --------------- 宣传片 概览 作为 Steam 发行流程的一部分&#xff0c;您需要上传自己产品的宣传片。 宣传片将会显示在您的产品商店页的顶端&#xff0c;通常是您的潜在客户第一眼会看见的内容。 最佳实践 在 Steam 平台上&#xff0c;宣传片是产品营销中…

MyBatis-Plus (超详细 Spring版)

1 MyBatis-Plus 简介 1.1 简介 MyBatis-Plus&#xff08;简称MP&#xff09;是一个 MyBatis的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为 简化开发、提高效率而生。 1.2 特性 无侵入&#xff1a;只做增强不做改变&#xff0c;引入它不会对现有…

地下井室可燃气体监测装置:守护地下安全,防患于未“燃”!

在城市的地下&#xff0c;隐藏着无数的燃气管道和井室&#xff0c;它们是城市基础设施建设的重要部分&#xff0c;燃气的使用&#xff0c;给大家的生活提供了极大的便利。在便利生活的背后&#xff0c;也存在潜在的城市安全隐患。 近年来&#xff0c;地下井室可燃气体泄漏事故…

BGP 基本配置实验

实验拓扑 实验需求 按照图示配置 IP 地址&#xff0c;R1 和 R5 上使用环回口模拟业务网段&#xff0c;R2&#xff0c;R3&#xff0c;R4 的环回口用于配置 Router-id 和建立 IBGP 邻居AS 200 运行 OSPF 实现内部网络互通R1&#xff0c;R2&#xff0c;R4&#xff0c;R5 运行 BGP…

解锁AIGC新时代:通义万相2.1与蓝耘智算平台的完美结合引领AI内容生成革命

前言 通义万相2.1作为一个开源的视频生成AI模型&#xff0c;在发布当天便荣登了VBench排行榜的榜首&#xff0c;超越了Sora和Runway等业内巨头&#xff0c;展现出惊人的潜力。模型不仅能够生成1080P分辨率的视频&#xff0c;而且没有时长限制&#xff0c;能够模拟自然动作&…

梦三国2游戏下载

梦三国2是由杭州电魂网络科技股份有限公司自主研发的一款将rts元素融合到rpg网游中的即时战略网络游戏。该作保留了前作的三国文化背景&#xff0c;继承了其特色玩法&#xff0c;优化了画面和操作体验&#xff0c;还基于玩家们的需求提供了有趣新颖的地图和系统。游戏主要分为p…

模型微调-基于LLaMA-Factory进行微调的一个简单案例

模型微调-基于LLaMA-Factory进行微调的一个简单案例 1. 租用云计算资源2. 拉取 LLaMa-Factory3. 安装依赖环境4. 启动 LLaMa-Factory 界面5. 从 Huggingface 下载模型6. 模型验证7. 模型微调 1. 租用云计算资源 以下示例基于 AutoDL 云计算资源。 在云计算平台选择可用的云计…

【十三】Golang 通道

&#x1f4a2;欢迎来到张胤尘的开源技术站 &#x1f4a5;开源如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 通道通道声明初始化缓冲机制无缓冲通道代码示例 带…

聊天服务器分布式改造

目前的聊天室是单节点的&#xff0c;无论是http接口还是socket接口都在同一个进程&#xff0c;无法承受太多人同时在线&#xff0c;容灾性也非常差。因此&#xff0c;一个成熟的IM产品一定是做成分布式的&#xff0c;根据功能分模块&#xff0c;每个模块也使用多个节点并行部署…

DeepSeek 医疗大模型微调实战讨论版(第一部分)

DeepSeek医疗大模型微调实战指南第一部分 DeepSeek 作为一款具有独特优势的大模型,在医疗领域展现出了巨大的应用潜力。它采用了先进的混合专家架构(MoE),能够根据输入数据的特性选择性激活部分专家,避免了不必要的计算,极大地提高了计算效率和模型精度 。这种架构使得 …

深入解析 BitBake 日志机制:任务调度、日志记录与调试方法

1. 引言&#xff1a;为什么 BitBake 的日志机制至关重要&#xff1f; BitBake 是 Yocto 项目的核心构建工具&#xff0c;用于解析配方、管理任务依赖&#xff0c;并执行编译和打包任务。在 BitBake 构建过程中&#xff0c;日志记录机制不仅用于跟踪任务执行情况&#xff0c;还…

OpenCV计算摄影学(16)调整图像光照效果函数illuminationChange()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 对选定区域内的梯度场应用适当的非线性变换&#xff0c;然后通过泊松求解器重新积分&#xff0c;可以局部修改图像的表观照明。 cv::illuminati…

【DuodooTEKr 】多度科技 以开源之力,驱动企业数字化转型

多度科技 背景 / Background 在全球产业链重构与国内经济双循环的浪潮下&#xff0c;中国制造业与贸易企业正面临数字化升级的迫切需求。开源技术作为数字化转型的基石&#xff0c;不仅能打破技术壁垒、降低企业成本&#xff0c;更能通过协作创新加速产业智能化进程。 多度科技…

VBA经典应用69例应用7:从字符串中删除数字

《VBA经典应用69例》&#xff08;版权10178981&#xff09;&#xff0c;是我推出的第九套教程&#xff0c;教程是专门针对初级、中级学员在学习VBA过程中可能遇到的案例展开&#xff0c;这套教程案例众多&#xff0c;紧贴“实战”&#xff0c;并做“战术总结”&#xff0c;以便…

Vue 系列之:插槽

前言 插槽是定义在子组件中的&#xff0c;相当于一个占位符&#xff0c;父组件可以在这个占位符中填充HTML代码、组件等内容。 插槽显不显示、怎样显示是由父组件来控制的&#xff0c;而插槽在哪里显示就由子组件来进行控制。 基本使用 子组件&#xff1a; <template&g…