Linux - 基础IO(重定向 - 重定向模拟实现 - shell 当中的 重定向)- 下篇

news2024/11/16 13:32:46

前言

上一篇博客当中,我们对 文件 在操作系统当中是 如何就管理的,这个问题做了 详细描述,本篇博客将基于上篇 博客当中的内容进行 阐述,如有疑问,请参考上篇博客:

Linux - 基础IO(Linux 当中的文件,文件系统调用接口,文件描述符)- 上篇-CSDN博客

重定向

文件描述符的 分配规则

我们先来看一个例子:

此时我们先 关闭 0 号文件,也就是 stdin 这个文件,然后在使用 open()系统调用接口来 创建一个新的文件,打印这个新文件的 fd 。输出:
 

发现新文件的 fd 文件描述符是 0,也就是原本的 stdin 这个文件描述符。


 同样,我们把 1 号文件 和 2 号文件都试一试,先是 1 号文件:

输出:

发现并没有输出,因为 printf()函数其中使用的系统调用接口就是 1 号文件的 stdout 这个流。

但其实,输出就是1 。

此时,其实是几输入到文件当中了的,只是当前的进程的 stdout 已经被关闭了,所以进程当中是无法使用 printf()函数打印出信息的:

 


 关闭 2 号文件:

输出:
 

 发现,此时新打开的文件的文件描述符 fd 就是 开始关闭的 2 号文件。

所以,按照以上的 输出,其实我们已经可以得出结论了:

在 Linux 当中的文件描述符的分配规则是:


如果有新的文件要打开,那么就要为这个文件分配一个 新的 没有被其他文件使用过的 文件描述符 fd。

分配是按照 : 从 文件描述符表当中的 0 号下标位置处 开始,寻找最小的 还没有使用过的 数组位置,这个位置的下标就是新文件的 fd 文件描述符

 重定向理解 - 重定向的原理

像上述的,把 1 号文件 也就是 stdout 这个显示器文件 给关闭了, 所以我们使用printf()函数就是不能在显示器当中输出数据了。

但是,上述也说过,虽然不会在 显示器当中输入, 但是会在 log.txt 这个新打开的文件当中,把原本应该输出在显示器当中的数据,写入到 log.txt 这个文件当中。

这是为什么呢?
其实看上述的代码你应该也知道了,因为 write()函数是固定像 1 号文件当中写入 数据,但是 1 号文件在 log.txt 这个新文件打开之前就被 关闭了。

也就是说,在 log.txt 这个新文件打开之前, 1 号 fd 文件描述符已经空闲下来了,所以, 新文件就会直接使用 1 号这个 fd 文件描述符。

所以,在后序循环当中 使用 write()函数在 1  号文件当中写入数据,实际是在 log.txt 当中写入数据。

其实,像上述的过程,就是一种重定向

如上图所示,原本 1 号 文件描述符指向的是 显示器文件,但是现在指向的是 log.txt 这个文件了。

而这个变化的过程, 编译器在执行之时 ,也就是 while()循环当中的代码在执行之时,其实是不知道的,他只知道,现在要向 1 号文件当中写入 数据,但是它不知道 1 号文件,此时是 显示器文件 还是 log.txt 文件,还是其他什么文件,他是不知道的。

 而,上述的过程,就是重定向的 原理

 所以,在上图当中的  fd_array [] 数组当中的 每一个元素存储的是 文件对象的起始地址,修改 几号fd ,其实就是修改  fd_array [] 数组当中的 fd 下标位置的 文件对象的地址地址指针。

而,在代码层面,他不管他现在的任务是要在哪一个文件当中写入数据,这个文件在哪,这个文件的地址是多少,这些他都不关系;它只关心,这是几号文件,要在哪一个 fd 的文件的文件当中写入数据。

 重定向的系统调用接口

有三个调用接口。我们主要来谈谈 dup2()这个函数:

 使用 dup2()函数,就可以不向上述一样显示的使用 close()函数来关闭文件,来实现重定向的 功能,直接使用这个 dup2()函数即可

使用 dup2()函数,就不用在关闭 1 号文件了当我们在 程序当中已经打开了 某一个文件,创建了这个文件的 文件描述符,那么在这个 文件描述符指向的 文件描述符表的 下标位置,就存储了这个文件的 文件对象的地址。

使用 dup2()函数,直接 把 某一个文件 在 文件描述符表当中存储的 文件对象的地址,直接拷贝到 需要重定向的 在 文件描述符表当中存储的 文件对象的地址 当中。如下图所示:

  


而 dup2()函数的两个参数:
 

在上述说过的拷贝的过程当中,谁是 "oldfd" , 谁是 "newfd" 呢?

在上述例子当中, 1 号文件是要被 fd 也就是 log,txt 文件对象地址所拷贝的,1 号 是 被拷贝;fd 是拷贝。

所以,在上述例子当中 1 号是  "newfd"; fd 是 "oldfd" 

dup2(fd , 1);

dup2() 接口例子测试

输出重定向 

 还是上述的例子,只不过,此时不使用 close()关闭文件来实现了,而是使用 dup2()函数来实现:
 

输出:
 

在原本是空的 log.txt 文件当中,在运行 text 可执行程序之后,log.txt 当中已经被写入了数据。

 现在的输出结果和上述 使用 close()关闭文件实现的效果是一样的 。

像上述我使用的是 O_TRUNC ,是先清空 文件当中的内容,然后在从头开始写入数据,对应的就是 ">" 这个输出重定向。

如果我们把 O_TRUNC  换成 O_APPEND,就是追加的方式来写入数据,对应的就是 ">>"这个输出重定向。


( 输入重定向) - 使用 read()系统调用接口 和 dup2()

其中的 count 是我们期望 read()函数读取多少个 字节的内容,返回值 ssize_t(这个是有符号整数) 是实际read()函数读取到多少字节的内容。写入到 buf 当中。

比如,count 期望大小我们填入 1024个字节,fd 文件我们选择 0 号 键盘文件,那么,他就会一直阻塞等待我们在键盘当中输入数据到缓冲区当中读取。

 在 open()函数当中,我们可以使用 O_RDONLT 这个参数,代表的意思就是 只读

 输出:
 


 此时,我们使用read()函数在 log.txt 文件当中读取内容,在读取之前,使用 dup2()函数,把 0 号文件(也就是键盘文件 的 fd 值) ,直接替换为 log.txt 文件对象的地址:

此时输出:
 

此时,运行程序,直接把文件当中的内容给输出出来了。 

 此时我们使用 O_RDONLT 这个参数,就实现了 "<" 输入重定向的操作。

这不就是cat命令吗?

直接使用 cat 命令,就可以等待 我们在键盘当中的输入,然后把输入内容打印出来:

或者是 使用 "<" 来向文件当中,读取文件数据并打印:
 

 使用 printf()向文件当中写入数据

在这个例子当中,我们把 1 号文件,利用 dup2()函数 把 1 号文件的文件指针修改为 log.txt 文件,此时,printf()函数 fprintf()就在  log.txt 文件当中写入数据了
 

在上述输出当中, 在显示器当中没有输出,但是在 log.txt 文件当中已经 有数据了。 

 所以,在上述,就算我们 close(fd),这个程序同样是会在 log.txt 文件当中的输入数据的,因为,此时 log.txt 文件对象的地址不只是 fd 保存的,还有 1 号文件描述符也是保存了 log.txt 的文件对象的地址。

而且,我们是在 1 号文件当中写入的 ,所以,是不会影响在log.txt 文件当中写入数据的。

 shell 当中的 输入/输出重定向 的实现 概述

 上述我们已经介绍了 输入/输出重定向 的简单实现,所以,在shell 当中,其实这些 输入/输出重定向 命令是属于 内建命令,直接在内建命令当中 判断 类似 ">>" ">" "<" 这样的字符子串,就可以判断,当前是不是 输入/输出重定向 的操作,就可以执行上述所实现的逻辑。

ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt

 向上述的操作,我们可以发现,  ">>" ">" "<" 这样的字符子串 都是被空格 分隔开的,我们可以利用 之前在shell 模拟实现当中 分割字符串的操作,来提取出  ">>" ">" "<" 这样的字符子串。从而判断当前是不是  输入/输出重定向 操作。

(对于 shell 的模拟实现,参考这篇博客:Linux - 实现一个简单的 shell-CSDN博客)

其实做的工作非常简单:

在知道了上述的 重定向实现原理之后,其实我们只需要做判断即可:
 

比如上述判断 ">" 和 ">>" ,如果 当前的字符是 ">" 的话,如果下一个字符还是   ">" 的话,说明是 ">>" ,反之亦然;找到  ">" 或者 "<" 先把这两个字符修改为 '\0' 作为分割,因为 在 ">" 或者 "<" 之前的是 要输入或者要输出的数据,而在 ">" 或者 "<" 后面的是 文件名。

我们使用 isspace()函数来判断空格:

 在空格之后就是我们想要的文件名。

找到之后,就要保存 两段的数据 命令 和 文件名


在 shell 父进程 创建子进程的之后 马上  ,就要判断当前是否是 需要执行 重定向操作的(如果是,还有判断 是 哪一个重定向操作):

如上所示,在判断 是哪一个重定向,然后就 按照对应的要求,修改 1 或者0 的其中一个 文件描述符值。

同时,文件打开方式也是区分 不同重定向操作的 步骤。0666 是 八进制 的 666 ,代表 用 open()函数创建的文件,是什么访问权限。(注意要减去 umack)

完整代码:

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

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

#define NONE -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2

int lastcode = 0;
int quit = 0;
extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGC_SIZE];
char pwd[LINE_SIZE];
char* rdirfilename = NULL;
int rdir = NONE;

// 自定义环境变量表
char myenv[LINE_SIZE];
// 自定义本地变量表


const char* getusername()
{
    return getenv("USER");
}

const char* gethostname1()
{
    return getenv("HOSTNAME");
}

void getpwd()
{
    getcwd(pwd, sizeof(pwd));
}

void check_redir(char* cmd)
{

    // ls -al -n
    // ls -al -n >/</>> filename.txt
    char* pos = cmd;
    while (*pos)
    {
        if (*pos == '>') // 判断当前是 ">" ">>" 还是 "<"
        {
            if (*(pos + 1) == '>') {   // 判断当前是 ">" 还是 ">>" 
                *pos++ = '\0';
                *pos++ = '\0';
                while (isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir = APPEND_RDIR;
                break;
            }
            else {      // 是 ">"          
                *pos = '\0';
                pos++;
                while (isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir = OUT_RDIR;
                break;
            }
        }
        else if (*pos == '<')  // 是 "<"
        {
            *pos = '\0'; // ls -a -l -n < filename.txt
            pos++;
            while (isspace(*pos)) pos++;
            rdirfilename = pos;
            rdir = IN_RDIR;
            break;
        }
        else {
            //do nothing
        }
        pos++;
    }
}

void interact(char* cline, int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd);
    char* s = fgets(cline, size, stdin);
    assert(s);
    (void)s;
    // "abcd\n\0"
    cline[strlen(cline) - 1] = '\0';

    //ls -a -l > myfile.txt
    check_redir(cline);  // 在上述打印完 命令行,保存命令之后,用这个函数判断
                         // 命令当中是否有 重定向操作
}

int splitstring(char cline[], char* _argv[])
{
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while (_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
    return i - 1;
}

// 这个函数主要是实现 有 shell 父进程创建 子进程的过程
void NormalExcute(char* _argv[])
{
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return;
    }
    else if (id == 0) {
        int fd = 0;

        // 判断当前子进程是否需要执行 重定向的工作
        if (rdir == IN_RDIR) // 执行输入重定向
        {
            fd = open(rdirfilename, O_RDONLY);
            dup2(fd, 0);
        }
        else if (rdir == OUT_RDIR) // 执行 ">" 
        {
            fd = open(rdirfilename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if (rdir == APPEND_RDIR)// 执行 ">>" 
        {
            fd = open(rdirfilename, O_CREAT | O_WRONLY | O_APPEND, 0666);
            dup2(fd, 1);
        }
        //让子进程执行命令
        //execvpe(_argv[0], _argv, environ);
        execvp(_argv[0], _argv);
        exit(EXIT_CODE);
    }
    else {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid == id)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}


// 这个函数当中是判断 和 实现一些内建命令的
int buildCommand(char* _argv[], int _argc)
{
    if (_argc == 2 && strcmp(_argv[0], "cd") == 0) {
        chdir(argv[1]);
        getpwd();
        sprintf(getenv("PWD"), "%s", pwd);
        return 1;
    }
    else if (_argc == 2 && strcmp(_argv[0], "export") == 0) {
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        return 1;
    }
    else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) {
        if (strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode = 0;
        }
        else if (*_argv[1] == '$') {
            char* val = getenv(_argv[1] + 1);
            if (val) printf("%s\n", val);
        }
        else {
            printf("%s\n", _argv[1]);
        }

        return 1;
    }

    // 特殊处理一下ls
    if (strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while (!quit) {
        // 1.rdirfilename 用于保存文件名, rdir 保存要输入/输出的数据方式(命令)
        rdirfilename = NULL;
        rdir = NONE;
        // 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
        interact(commandline, sizeof(commandline));

        // commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
        // 3. 子串分割的问题,解析命令行
        int argc = splitstring(commandline, argv);
        if (argc == 0) continue;

        // 4. 指令的判断 
        // debug
        //for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
        //内键命令,本质就是一个shell内部的一个函数
        int n = buildCommand(argv, argc);

        // 5. 普通命令的执行
        if (!n) NormalExcute(argv);
    }
    return 0;
}

shell 当中的 输入/输出重定向  和 进程替换之间的关系

 不知道你有没有发现,我们在 check_redir()函数当中判断,是否需要重定向操作,然后修改对应存储 重定向操作符前后的 命令 和 文件名。

用这两个 存储 重定向操作符前后的 命令 和 文件名 的两个全局变量的值,在 shell 父进程创建子进程 之后 马上,判断当前是否需要执行 重定向的操作。

 但是,如果需要执行重定向,那么 在 修改 文件描述符值 之后,说明此时是在 子进程 当中修改的 文件描述符值。

然后我们要进行程序替换,让子进程执行我们在命令行当中输入的命令。

 那么,在执行 程序替换之时不会替换掉 这些文件描述符表 当中存储的文件对象的地址么吗?

 答案是不会的

首先你要搞清楚的是,一个进程的 各个文件描述符值 是存储在那里的?

  是存储在 struct file_struct 这个结构体 当中的,在这个结构体当中有一个数组,这个数组 就被称作 -- 文件描述符表。在这个数组当中就存储了 这个进程当前所 打开的 各个文件的  文件对象的地址

所以,这是一个机构体,是被 操作系统做管理的结构体对象,所以这个 struct file_struct 这个结构体  和 进程 的 PCB对象一样,都属于 操作系统当中的 内核结构体

而 , 我们进行程序替换,替换的是 代码 数据,把 原本子进程 从 父进程那里拷贝或者共用的代码,进行直接拷贝 或者 写时拷贝的方式,替换为 我们在命令行当中输入 的命令的 代码,而数据也是跟着一起刷新的。

那么 这些 代码 和 数据是存储在哪里的?是存储在 内存的物理空间当中的,要注意区分。

 所以,进程历史打开的所有文件,都在 struct file_struct 这个结构体 当中的 特定数组当中存储了 文件描述符,程序替换不会修改到这个 结构体,所以,程序替换 和 fd 文件描述符 无关

 所以,我们上述在实现之时,才是先把重定向的工作做了,再去程序替换;这样,重定向当中要修改的 文件描述符,和 进程替换无关,那么就不会被修改到。

而且,我们判断 是否需要重定向 的 两个全局变量是在父进程当中存储的,是在创建子进程之前就 已经 完成判断了的,这些数据,在进行程序替换之时都会被替换掉。

 stdout 和 stderr 是区别

 其实两个都是 显示器文件,都是输出重定向来使用的。

两者我们发现,使用 fprintf()函数都可以在 屏幕上打印 数据。

但是,之所以要 使用 两个显示器文件是因为,一个是 normal 正常的 数据输出;另一个 是 error 错误码输出;他想做到一种分流的 作用,把 正常的数据 和 错误的数据分成种方式输出。

为什么呢?

别忘了,输出不仅仅是在 显示器上输出,还可以在 文件当中输出。

所以,可以用两个文件,一个存储 正常的输出数据,一个存储错误输出数据;而我们在使用两种显示器文件输出数据之时,就可以在两个文件当中进程输出不同的数据了。


比如,此时有些 正常数据 和 一些错误的数据要输出:

那么,我们可以使用不同的 显示器文件来 输出,达到分流的目的:
 

 上述这个命令,就是把 mytext 执行文件的输出结果,把 1 号文件的内容输出重定向到 normal.log 当中;把 2 号文件当中的内容 输出重定向到 arr.log 当中。(其中 1 可以不写(这些简写)

这样在两个文件当中就是不同 输出内容了。

 像这样是 先把 1 号文件文件描述符 对应的 文件 当中的内容输入到 all.log 当中,然后把 1 号文件描述符当中的内容写到 2 号文件描述符值当中(2>&1)

 注意,是直接把 1 号文件描述符当中的内容,拷贝到 2 号文件描述符当中,1 号文件描述符当中的内容就是 1 号文件描述符 对应 的文件的 地址。所以,是直接把 1 号文件地址 直接 拷贝到 了 2 号文件描述符当中,相当于是 dup2()函数一样。

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

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

相关文章

matlab 多自由度的车辆垂向振动模型 车辆平稳性研究

1、内容简介 略 17-可以交流、咨询、答疑 多自由度的车辆垂向振动模型 多自由度的车辆垂向振动模型&#xff0c;包含四分之一车体模型、半车模型和整车模型 垂向振动模型、四分之一车体模型、半车模型和整车模型 2、内容说明 略 3、仿真分析 略 4、参考论文 略 链接&…

第七章 块为结构建模 P3|系统建模语言SysML实用指南学习

仅供个人学习记录 块行为建模 块提供了行为情境&#xff0c;行为这个 SysML 词条覆盖了块如何处理输如/输出和其内部状态改变的所有描述。 块可以指定某个行为作为其主行为或者分类器行为&#xff0c;该行为在块实例化后启动执行。其他行为可以指定为方法&#xff0c;提供了处…

人机交互——自然语言生成

自然语言生成是让计算机自动或半自动地生成自然语言的文本。这个领域涉及到自然语言处理、语言学、计算机科学等多个领域的知识。 1.简介 自然语言生成系统可以分为基于规则的方法和基于统计的方法两大类。基于规则的方法主要依靠专家知识库和语言学规则来生成文本&#xff0…

【Redis】list列表

上一篇&#xff1a; String 类型 https://blog.csdn.net/m0_67930426/article/details/134362606?spm1001.2014.3001.5501 目录 Lpush LRange Rpush Lpop Rpop Lindex Ltrim Lset 列表不存在的情况 如果列表存在 Linsert ​编辑 在………之前插入 在……后面插入…

Windows系统安装Redis、配置环境变量

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 第五章 Spring Cloud Netflix 之 Ribbon 第六章 Spring Cloud 之 OpenFeign 第七章 Spring Cloud 之 GateWay 第八章 Sprin…

【 第十一章】软件设计师 之 面向对象设计与结构化分析设计

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 备考资料导航 软考好处&#xff1a;软考的…

域名无法访问了,如何找回浏览器的缓存

背景需求 双十一即将来临&#xff0c;这意味着我购买了三年低配的阿里服务器&#xff0c;而它的服务期限也即将到期。为了提前做好准备&#xff0c;我在一周前对静态网站进行了备份&#xff0c;并成功地使用了Vercel进行部署&#xff08;已经有了域名&#xff09;。相比于付费…

腾讯云3年轻量应用服务器2核2G4M带宽540元,它来了

腾讯云轻量应用服务器特价是有新用户限制的&#xff0c;所以阿腾云建议大家选择3年期轻量应用服务器&#xff0c;一劳永逸&#xff0c;免去续费困扰。腾讯云轻量应用服务器3年可以选择2核2G4M和2核4G5M带宽&#xff0c;3年轻量2核2G4M服务器540元&#xff0c;2核4G5M轻量应用服…

jdk21 虚拟线程原理及使用分享

虚拟线程概述 jdk21已于北京时间9月19日21点正式发布, 其中引人注目的就是虚拟线程(Virtual Thread)随之正式发布, 不再是此前jdk19、jdk20中的预览版本。 平台线程&#xff1a;java传统的线程是对系统线程的包装&#xff0c;为了区别于虚拟线程&#xff0c;因此将通过传统方式…

C#源代码生成器深入讲解一

C#源代码生成器 01 源代码生成器初体验 新建一个类库&#xff0c;一定是standard2.0版本&#xff0c;否则会出问题。引用Nuget包Microsoft.CodeAnalysis.Common新建一个类&#xff0c;继承自ISourceGenerator接口 //一定要写&#xff0c;制定语言 [Generator(LanguageNames.…

Django 基于ORM的CURD、外键关联,请求的生命周期

文章目录 基于ORM进行的CURDORM外键关联Django请求的生命周期流程图 基于ORM进行的CURD 本质上就是通过面向对象的方式&#xff0c;对数据库的数据进行增、删、改、查。 这里将会将我们之前所有内容结合到一起&#xff0c;首先确保基于上序操作已经建立好了UserInfo表&#xff…

Three.js——基于原生WebGL封装运行的三维引擎

文章目录 前言一、什么是WebGL&#xff1f;二、Three.js 特性 前言 Three.js中文官网 Three.js是基于原生WebGL封装运行的三维引擎&#xff0c;在所有WebGL引擎中&#xff0c;Three.js是国内文资料最多、使用最广泛的三维引擎。既然Threejs是一款WebGL三维引擎&#xff0c;那么…

Python 使用tkinter的Scrollbar方法创建Text水平和垂直滚动条

在Python的Tkinter中&#xff0c;可以使用Scrollbar来实现Text组件的上下或左右滑动。首先&#xff0c;需要创建一个Scrollbar对象并将其与Text组件绑定&#xff0c;然后将Scrollbar放置在Text组件的右侧或底侧&#xff0c;使其能够控制Text组件的上下或左右滑动。 运行结果&am…

隔离在高可用架构中的使用

写作目的 最近看到了河北王校长隔离的视频&#xff0c;结合自己在工作中的应用&#xff0c;分享常见的隔离落地方案。 隔离落地方案 服务环境隔离 因为我们的项目服务于整个国内的多条产品线&#xff0c;也服务于国外。为了低成本所以使用一套代码。在产品线之间隔离&#…

mysql讲解2 之事务 索引 以及权限等

系列文章目录 mysql 讲解一 博客链接 点击此处即可 文章目录 系列文章目录一、事务1.1 事务的四个原则1.2 脏读 不可重复读 幻读 二、索引三,数据库用户管理四、mysql备份 一、事务 1.1 事务的四个原则 什么是事务 事务就是将一组SQL语句放在同一批次内去执行 如果一个SQ…

深入研究SVN代码检查的关键工具:svnchecker vs. SonarQube

目录 一、SVN代码检查(整合svnchecker)1、创建SVN代码库2、下载安装包3、修改SVN配置4、新建代码检查配置文件(名称自定义)5、hooks目录添加配置文件6、设置只对Java文件进行检查7、测试 二、SonarQube代码检测1、什么是SonarQube2、MySQL数据库的安装3、SonarQube服务端软件安…

Linux系统编程——修改配置文件(应用)

该应用主要调用到strstr函数&#xff0c;我们只需调用该函数并传入相关文件和修改数值即可&#xff0c;下面就是对strstr函数的定义解读以及实现案例 1.调用strstr函数需要包含以下头文件 #include<string.h>2.函数定义格式 char *strstr(char *str1, const char *str…

springboot苍穹外卖实战:十、缓存菜品(手动用redisTemplate实现缓存逻辑)+缓存套餐(Spring cache实现)

缓存菜品 缺点 缓存和数据库的数据一致性通常解决方案&#xff1a;延时双删、异步更新缓存、分布式锁。 该项目对于缓存菜品的处理较为简单&#xff0c;实际可以用管道技术提高redis的操作效率、同时cache自身有注解提供使用。 功能设计与缓存设计 建议这部分去看下原视频&…

吃透 Spring 系列—MVC部分

目录 ◆ SpringMVC简介 - SpringMVC概述 - SpringMVC快速入门 - Controller中访问容器中的Bean - SpringMVC关键组件浅析 ◆ SpringMVC的请求处理 - 请求映射路径的配置 - 请求数据的接收 - Javaweb常用对象获取 - 请求静态资源 - 注解驱动 标签 ◆ SpringMV…

【JUC】二、线程间的通信(虚假唤醒)

文章目录 0、多线程编程的步骤1、wait和notify2、synchronized下实现线程的通信&#xff08;唤醒&#xff09;3、虚假唤醒4、Lock下实现线程的通信&#xff08;唤醒&#xff09;5、线程间的定制化通信 0、多线程编程的步骤 步骤一&#xff1a;创建&#xff08;将来被共享的&am…