【Linux】基础IO——文件操作|文件描述符|重定向|缓冲区

news2025/1/12 12:06:40

文章目录

  • 一、文件操作
    • 1. 文件预备知识
    • 2. 回顾C文件操作
    • 3. 文件操作的系统调用
      • 标志位的传递
      • open
      • write
      • read
  • 二、文件描述符
    • 1. 文件描述符的理解
    • 2. 文件描述符的分配规则
  • 三、重定向
    • 1. 重定向的本质
    • 2. dup2系统调用
  • 四、缓冲区
    • 1. 缓冲区的刷新策略
    • 2. 缓冲区的位置
    • 3. 简单模拟实现缓冲区

一、文件操作

1. 文件预备知识

我们在学习下面文件的内容之前,先预备一些基础的文件知识:

  • 文件 = 内容 + 属性,对文件的操作就是对文件内容和文件属性的操作。
  • 当文件没有被操作的时候,文件一般都是在磁盘上存放。
  • 当我们对文件操作时,文件都会被提前加载到内存中,加载的内容至少得有属性。
  • 当文件被加载到内存中时,在Linux下并不一定只有你一个人在打开文件,内存中一定存在大量的不同文件的属性。
  • 因此,打开文件的本质就是将文件属性加载到内存中,OS中一定存在大量被打开的文件,操作系统对这些被打开的文件进行管理需要先描述、再组织,所以需要先构建在内存中的文件结构体(struct file)。
  • 文件可以被分为两部分:磁盘文件被打开的文件(内存文件)
  • 文件是被OS所打开的,是被用户所创建的进程让OS打开的。
  • 我们之前所有的文件操作,都是进程(struct task_struct) 和被 打开文件(struct file) 的关系。

2. 回顾C文件操作

我们曾经学过C语言的文件操作,那么是不是只有C语言有文件操作呢?答案显然是否定的,因为无论哪一门语言(Python、Java、php、go…)他们都有对应的文件操作。无论上层语言如何变化,该语言对应的库函数底层都必须调用文件级别的系统调用来完成对文件的操作。

下面我们来回顾一下C语言的文件操作:

在这里插入图片描述

这里我们需要强调一点:如果没有指明路径,则默认在当前路径下进行文件操作。

💕 w的方式打开,向文件中写入数据以写的方式打开文件,如果文件不存在则创建文件

在这里插入图片描述
在这里插入图片描述

💕 r的方式从文件中读取数据以读的方式打开文件,如果文件不存在则报错

在这里插入图片描述
在这里插入图片描述

💕 a的方式打开,向文件中追加数据以追加的方式打开文件,如果文件不存在则打开失败

在这里插入图片描述

在这里插入图片描述


3. 文件操作的系统调用

操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问:

上面的fopen fclose fread fwrite都是C标准库当中的函数,我们称之为库函数(libc)。而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags。
返回值:
成功:新打开的文件描述符
失败:-1

参数:

  • O_RDONLY: 只读打开
  • O_WRONLY: 只写打开
  • O_RDWR : 读,写打开
    这三个常量,必须指定一个且只能指定一个
  • O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
  • O_APPEND: 追加写

在这里插入图片描述

标志位的传递

在C语言中我们经常用一个整形来传递选项,但是如果如果选项较多时,就会造成空间的浪费,这里我们可以通过使用一个比特位表示一个标志位,这样一个int就可以同时传递至少32个标志位,此时的flag就可以看待成位图的数据类型。

#include <stdio.h>
//每个宏只占用一个比特位,该比特位为1说明选项成立。
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
#define FOUR 0x8
#define FIVE 0x10

// 0000 0000 0000 0000 0000 0000 0000 0000
//按位与的结果为1,说明flags对应的比特位为1
void Print(int flags)
{
    if(flags & ONE) printf("hello 1\n"); //充当不同的行为
    if(flags & TWO) printf("hello 2\n");
    if(flags & THREE) printf("hello 3\n");
    if(flags & FOUR) printf("hello 4\n");
    if(flags & FIVE) printf("hello 5\n");
}


int main()
{
    printf("--------------------------\n");
    Print(ONE);
    printf("--------------------------\n");
    Print(TWO);
    printf("--------------------------\n");
    Print(FOUR);
    printf("--------------------------\n");

    Print(ONE|TWO);
    printf("--------------------------\n");

    Print(ONE|TWO|THREE);
    printf("--------------------------\n");

    Print(ONE|TWO|THREE|FOUR|FIVE);
    printf("--------------------------\n");

    return 0;
}

在这里插入图片描述

open

open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>

#define LOG "log.txt"

int main()
{
    int fd = open(LOG, O_WRONLY|O_CREAT);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);  // close关闭文件,参数是对应的文件描述符

    return 0;
}

在这里插入图片描述

这里我们发现创建出来的文件权限是乱的,这里我们就需要传入open的第三个参数了,我们可以先使用umask系统调用设置为0,通过手动设置当前进程的权限掩码,从而不使用父进程继承过来的umask。

在这里插入图片描述
在这里插入图片描述


write

write表示向文件中写入数据,下面我们来举例看一下:

在这里插入图片描述

ssize_t write(int fd, const void* buf, size_t count);
# 头文件:<unistd.h>
# fd:目标文件的文件描述符
# buf:要写入数据的来源
# count:要写入数据的字节数
# ssize_t:函数返回值,写入成功返回成功写入的字节数,写入失败返回-1

在这里插入图片描述
在这里插入图片描述

这里我们需要注意几点:

  • 向文件中写入数据时,如果不指定O_TRUNC选项,新的数据就会覆盖原来的数据
  • C语言中字符串是以’\0‘结尾的,但是文件中的字符串并不以’\0’结尾,所以当我们向文件中写字符串时,strlen(str)后面不需要+1,如果要是加上的话就会出现乱码。

如果我们想要每次都向文件中追加内容怎么办呢?

在这里插入图片描述
在这里插入图片描述


read

read表示从文件中读取数据

在这里插入图片描述

ssize_t read(int fd, void* buf, size_t count);
# 头文件:<unistd.h>
# fd:目标文件的文件描述符
# buf:读取数据存放的位置
# count:要读取数据的字节数
# ssize_t:函数返回值,读取成功返回读取写入的字节数,读到文件末尾返回0,读取失败返回-1

在这里插入图片描述
在这里插入图片描述

由于C语言字符串以'\0'结尾,但是文件字符串数据并不包含’\0’,所以这里我们需要预留一个位置,这样的话无论如何buf数组的末尾都能够存放’\0’。


二、文件描述符

1. 文件描述符的理解

进程可以打开多个文件,这也就意味着系统中一定会存在大量的被打开的文件,然而被打开的文件则需要被操作系统管理,我们知道,管理的本质就是先描述在组织,所以操作系统为了管理对应的打开文件,操作系统必定要为文件创建对应的内核数据结构来标识文件,这个内核数据结构就是struct file{}结构体(与C语言的FILE没有关系哦);包含了文件的大部分属性。

而进程和被打开的文件如何关联,也就是说进程和被打开文件的关系是如何维护的?

通过文件打开(open)的返回值和文件描述符进行联系。下面我们通过代码来看一看返回值究竟是多少。

在这里插入图片描述
在这里插入图片描述

这里我们可以看到文件描述符是从3开始的,C 语言程序会默认打开三个流:stdin(标准输入流:键盘)、stdout(标准输出流:显示器)和 stderr(标准错误流:显示器)。这三个流的类型都是FILE*,而FILE是结构体。C 语言进行文件操作是使用的是FILE*,而操作系统使用的是文件描述符fd,那么结构体FILE中肯定包含文件描述符fd。所以 0、1、2 就被这三个流使用了。

在这里插入图片描述

进程的tast_struct里面有一个struct files_struct *files 指针变量,它指向一个该进程的数据结构对象struct files_struct,该对象中包含了一个指针数组 struct file * fd_array[],即进程的文件描述符,数组里面的每一个元素都是一个指针,指向一个struct file对象,这个数组的下标就是我们所得到的用户描述符fd。所以,文件描述符是从0开始的小整数,本质上是文件描述符表中的数组下标。

进程会通过进程控制块(task_struct)中的files变量找到 files_struct结构体,再通过files_struct 中的文件描述符找到具体文件的内核数据结构file,从而实现数据的读取和写入。

当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件。

💕 如何理解Linux下一切皆文件?

在这里插入图片描述

当我们打开一个文件时,操作系统会将该文件加载到内存,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。file结构体中不仅有文件的各种属性(文件的权限、文件的大小...),还有自己的缓冲区,当然了,最后面还有两个函数指针,那么这两个函数指针又是做什么的呢?

其实由于Linux操作系统出现的比C++要早,所以当时没有面向对象的语言程序设计,所以这里使用函数指针来保存函数的地址从而实现面向对象的功能。每一个外设都有对应的IO读写方法,在我们的操作系统的硬件之上有一个该外设对应的驱动程序,该驱动程序层中保存了对应了各个IO设备的读写方法。file结构体中的函数指针就保存了该方法的地址。如果我们需要写入或者读取文件中的信息,只需要通过struct file结构体中的函数指针找到方法的地址调用对应的方法即可。


2. 文件描述符的分配规则

上面我们已经了解了文件描述符,那么他又是如何给我们的文件进行分配的呢?

在这里插入图片描述
在这里插入图片描述

为什么文件描述符是从数组下标为3的地方开始的呢?数组下标0,1,2又去哪儿了呢?其实,这三个下标是被默认打开的三个标准流所占用了——标准输入流stdin、标准输出流stdout 和 标准错误流stderr。他们分别对应键盘文件、显示器文件和显示器文件。因此我们默认打开其他文件时候是从3号开始分配的。

int main()                                                                                                                                             
{                                                
  printf("stdin->fd:%d\n",stdin->_fileno);       
  printf("stdtout->fd:%d\n",stdout->_fileno);    
  printf("stderr->fd:%d\n",stderr->_fileno);    
  return 0;    
}

在这里插入图片描述

我们知道,C语言中的fopen接口的返回值是FILE* ,其中FILE是一个结构体类型,因为fopen底层是调用了open接口的,所以FILE中一定封装了一个变量来表示fd,这个变量是_fileno

在这里插入图片描述

这里,当我们将0和2号文件描述符关闭后,系统将会自动将他们分配给我们新打开的文件。所以,文件描述符的分配规则是,从小到大依次搜寻,寻找未被使用的最小的fd作为新打开文件的fd。


三、重定向

1. 重定向的本质

int main()    
{    
  close(1);    
  int fd = open(LOG,O_WRONLY|O_CREAT,O_TRUNC,0666);                                                                                                      
  assert(fd != -1);    
  printf("hello world\n");    
  fprintf(stdout,"hello chenjiale\n");    
    
  fflush(stdout);    
  close(fd);    
    
  return 0;    
}

在这里插入图片描述

1号文件描述符对应的是标准输出(显示器)文件。printf默认是向显示器打印的,当我们将1号文件描述符关闭后,1号文件描述符将会分配给我们新打开的文件,所以数据就会被打印到log.txt文件里。

这种现象就是 重定向,常见的重定向有输出重定向>,输入重定向<和追加重定向>>。重定向的本质就是:上层使用的fd不变,在内核中更改fd对于struct file*的地址(同一个fd指向不同的file对象)。


2. dup2系统调用

Linux操作系统中为我们提供了一个系统调用接口dup2来让我们直接进行冲定向。

在这里插入图片描述

int dup2(int oldfd, int newfd);
# 头文件:<unistd.h>
# oldfd:旧的文件描述符
# newfd:新的文件描述符
# int:函数返回值,成功返回 newfd,失败返回-1

💕 输出重定向

int main()    
{    
  int fd = open(LOG,O_WRONLY|O_CREAT,O_TRUNC,0666);    
  assert(fd != -1);    
    
  int ret = dup2(fd,1);    
  if(ret == -1){    
    return -1;    
  }    
                                                                                                                                                         
  printf("hello,fd:%d\n",fd);    
  fprintf(stdout,"hello,fd:%d\n",fd);    
    
  fflush(stdout);    
  close(fd);    
    
  return 0;    
}

在这里插入图片描述

在这里插入图片描述

这里我们需要注意的是,dup2的系统调用让newfd成为old的一份拷贝,本质就是将oldfd下标里面存放的file对象的地址拷贝到newfd下标的空间中,拷贝的是fd对应空间中的数据,而并不是两个fd数字之间进行拷贝。

💕 追加重定向

追加重定向和输出重定向的不同点在于在打开文件的时候将O_TRUNC选项去掉,然后换成O_APPEND选项。

int main()    
{    
  int fd = open(LOG,O_WRONLY|O_CREAT,O_APPEND,0666);    
  assert(fd != -1);    
    
  int ret = dup2(fd,1);    
  if(ret == -1){    
    return -1;    
  }    
    
  const char* msg = "hello I want to IDM\n";    
  write(1,msg,strlen(msg));    
                                                                                                                                                         
  fflush(stdout);    
  close(fd);    
    
  return 0;    
}

在这里插入图片描述

💕 输入重定向

int main()    
{    
  int fd = open(LOG,O_RDONLY);    
  assert(fd != -1);    
    
  int ret = dup2(fd,0);    
  if(ret == -1){    
    return -1;    
  }    
    
  char buf[64];    
  while(fgets(buf,sizeof(buf) - 1,stdin)!=NULL)    
  {    
    buf[strlen(buf)] = '\0';    
    printf("%s",buf);                                                                                                                                    
  }    
    
  close(fd);    
    
  return 0;    
}

在这里插入图片描述


四、缓冲区

进程向磁盘文件中写数据时,由于磁盘属于外设,进程直接向磁盘文件中写数据的效率非常低,所以有了缓冲区,进程可以将自己的数据拷贝到缓冲区中,再由缓冲区将数据写入到磁盘文件中去。在计算机中,缓冲区的意义是节省进程进行数据 IO 的时间。虽然我们认为 fwrite 是将数据写入到文件的函数,但 fwrite 本质上是进行数据拷贝的函数,因为 fwrite 函数只是将数据从进程拷贝到缓冲区中,并没有真正将数据写入到磁盘文件中。

1. 缓冲区的刷新策略

💕 三种刷新策略:

  • 立即刷新 (无缓冲): 缓冲区中一出现数据就立马刷新,这种很少出现;
  • 行刷新 (行缓冲): 每拷贝一行数据就刷新一次,显示器采用的就是这种刷新策略,因为显示器是给人看了,而按行刷新符合人的阅读习惯,同时刷新效率也不会太低;
  • 缓冲区满 (全缓冲): 待数据把缓冲区填满后再刷新,这种刷新方式效率最高,一般应用于磁盘文件。

💕 两种特殊情况:

  • 用户使用 fflush 等函数强制进行缓冲区刷新
  • 进程退出时一般都要进行缓冲区刷新
int main() {    
                                                                                                                                                         
    printf("hello printf\n");    
    fprintf(stdout, "hello fprintf\n");    
    const char* fputsString = "hello fputs\n";    
    fputs(fputsString, stdout);    
    
    const char* msg = "hello write\n";    
    write(1, msg, strlen(msg));    
    
    return 0;    
}

在这里插入图片描述
在这里插入图片描述

当我们在程序结尾使用fork函数创建一个子进程后:

int main() {    
    
    printf("hello printf\n");    
    fprintf(stdout, "hello fprintf\n");    
    const char* fputsString = "hello fputs\n";    
    fputs(fputsString, stdout);    
    
    const char* msg = "hello write\n";    
    write(1, msg, strlen(msg));    
                                                                                                                                                         
    fork();    
    
    return 0;    
}

在这里插入图片描述

这是什么原因呢?为什么 write 函数重定向了一次,但是其他三个函数却重定向了两次呢?下面我们会细细解释一下。


2. 缓冲区的位置

我们谈论的所有缓冲区都不在操作系统内核中,而是位于用户级语言层面;实际上,对于C语言来说,缓冲区位于 FILE 结构体中。

对于C语言的printf、fwrite、fputs 等库函数会自带缓冲区,而 write 系统调用没有带缓冲区;同时,我们这里所说的缓冲区,都是用户级缓冲区。那这个缓冲区谁提供呢? printf、fwrite、fputs 是库函数, write 是系统调用,库函数在系统调用的 “上层”, 是对系统调用的 “封装”,但是 write 没有缓冲区,而 printf、fwrite、fputs 有,足以说明该缓冲区是二次加上的,又因为是C库函数,所以是由C标准库提供的。

显示器采用行缓冲,所以在 fork 之前 printf、fprintf、fputs 三条语句的数据均已刷新到显示器上了,而对于进程数据来说,如果数据位于缓冲区内,那么该数据属于进程,此时 fork 子进程也会指向该数据;但如果该数据已经写入到磁盘文件了,那么数据就不属于进程了,此时 fork 子进程也不在指向该数据了;所以,这里 fork 子进程不会做任何事情。

我们使用重定向指令将本该写入显示器文件的数据写入到磁盘文件中,而磁盘文件采用全缓冲,所以 fork 子进程时 printf、fprintf、fputs 的数据还存在于缓冲区中 (缓冲区没满,同时父进程还没有退出,所以缓冲区没有刷新),也就是说,此时数据还属于父进程,那么 fork 之后子进程也会指向该数据;而 fork 之后紧接着就是进程退出,父子进程某一方先退出时会刷新缓冲区,由于刷新缓冲区会清空缓冲区中的数据,为了保持进程独立性,先退出的一方会发生 写时拷贝,然后向磁盘文件中写入 printf、fprintf、fputs 三条数据;然后,后退出的一方也会进行缓冲区的刷新;所以,最终 printf、fprintf、fputs 的数据会写入两份 (父子进程各写入一份),且 write 由于属于系统调用没有缓冲区,所以只写入一份数据且最先写入。


3. 简单模拟实现缓冲区

mystdio.h

#pragma once

#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SIZE 1024   // 缓冲区大小
#define SYNC_NOW   1    // 无缓冲
#define SYNC_LINE  2    // 行缓冲
#define SYNC_FULL  4    // 全缓冲

typedef struct FILE_
{
    int flags;  // 缓冲区刷新策略
    int fileno; // 文件描述符
    int size;   // buffer当前的使用量
    int capacity;   // buffer的总容量
    char buffer[SIZE];  //缓冲区
}FILE_;


FILE_* fopen_(const char* pathname, const char* mode);
void fwrite_(const void* ptr, int num, FILE_* fp);
void fflush_(FILE_* fp);
void fclose_(FILE_* fp);

mystdio.c

#include "myStdio.h"

FILE_* fopen_(const char* pathname, const char* mode)
{
    int flags = 0;
    int defaultMode = 0666; // 默认创建权限
    
    if(strcmp(mode, "r") == 0)
    {
        flags |= O_RDONLY;
    }
    else if(strcmp(mode, "w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_TRUNC);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flags |= (O_WRONLY | O_CREAT | O_APPEND);
    }
    else
    {
        // TODO:r+, w+...
    }

    int fd = 0;
    if(flags & O_RDONLY)  fd = open(pathname, flags);
    else  fd = open(pathname, flags, defaultMode);

    if(fd < 0)
    {
        const char* err = strerror(errno);
        write(2, err, strlen(err));
        return NULL;    // 打开文件失败返回NULL的原因
    }

    FILE_* fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp != NULL);

    fp->flags = SYNC_LINE; // 默认设置成行刷新
    fp->fileno = fd;
    fp->size = 0;
    fp->capacity = SIZE;
    memset(fp->buffer, 0, SIZE);

    return fp;  // 打开文件成功返回FILE*的原因
}

void fwrite_(const void* ptr, int num, FILE_* fp)
{
    // 数据写入到缓冲区
    memcpy(fp->buffer + fp->size, ptr, num); // 这里不考虑缓冲区溢出的问题
    fp->size += num;

    // 是否刷新缓冲区
    if(fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; // 清空缓冲区
    }
    else if(fp->flags & SYNC_LINE)
    {
        // 不考虑abcd\nef的情况
        if(fp->buffer[fp->size - 1] == '\n')
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0; // 清空缓冲区
        }
    }
    else if(fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->capacity)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0; // 清空缓冲区
        }
    }
    else
    {
        return;
    }
}

void fflush_(FILE_* fp)
{
    if(fp->size > 0)  write(fp->fileno, fp->buffer, fp->size);
    fsync(fp->fileno);	// 强制刷新内核缓冲区,将数据刷新到外设中
    fp->size = 0;	// 清空缓冲区
}

void fclose_(FILE_* fp)
{
    fflush_(fp);
    close(fp->fileno);
    free(fp);
}

main.c

#include "myStdio.h"
#include <stdio.h>

int main()
{
    FILE_* fp = fopen_("log.txt", "w");
    if(fp == NULL)
    {
        return 1;
    }

    int cnt = 10;
    const char* msg = "hello world\n";
    while(1)
    {
        --cnt;
        fwrite_(msg, strlen(msg), fp);
        sleep(1);
        printf("count:%d\n", cnt);
        if(cnt == 0)   break;
    }
    fclose_(fp);

    return 0;
}
监控脚本
while :; do cat log.txt ; sleep 1; echo "------------------"; done

我们向外设中写入数据其实一共分为三个步骤 – 先通过 fwrite 等语言层面的文件操作接口将进程数据拷贝到语言层面的缓冲区中,然后再根据缓冲区的刷新策略 (无、行、全) 通过 write 系统调用将数据拷贝到 file 结构体中的内核缓冲区中,最后再由操作系统自主将数据真正的写入到外设中。(所以 fwrite 和 write 其实叫做拷贝函数更合适)。这里操作系统的刷新策略比我们应用层 FILE 中的缓冲区的刷新策略要复杂的多,因为操作系统要根据不同的整体内存使用情况来选择不同的刷新策略,而不仅仅是死板的分为分行缓冲、全缓冲、无缓冲这么简单。

操作系统提供了一个系统调用函数 fsync,其作用就是将内核缓冲区中的数据立刻直接同步到外设中,而不再采用操作系统的刷新策略。

在这里插入图片描述


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

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

相关文章

当程序员的好处和坏处,我用七年经历来和大家聊一聊

我想和大家分享一下我做程序员这七年来的一些感受和经验&#xff0c;同时也想和大家聊一聊做程序员的好处和坏处&#xff0c;让大家真正深入了解程序员的工作&#xff0c;是不是和大家想象中的一样。 首先&#xff0c;我毕业于四川某不知名的二本院校&#xff0c;于2016年进入…

【软考备战·希赛网每日一练】2023年4月19日

文章目录 一、今日成绩二、错题总结第一题第二题第三题 三、知识查缺 题目及解析来源&#xff1a;2023年04月19日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 第二题 解析&#xff1a; server-side n.服务器端 enterprise n.企业 client n.客户 d…

matplotlib的配色(随机颜色函数,各种渐变色,彩虹色)

也是画图的时候经常会遇到的问题&#xff0c;什么颜色好看&#xff1f; 先直接上一个配色表&#xff1a; plt官网&#xff1a;List of named colors — Matplotlib 3.8.0.dev898g4f5b5741ce documentation 需要什么颜色传入就行了。 例如我下面画一个柱状图&#xff0c;自己选…

ctfhub技能树 web sql注入

1.整型注入 页面正常时 判断注入字段数 ?id1 order by 2判断注入回显位 ?id-1 union select 1,2查数据库 ?id-1 union select 1,database()库名&#xff1a;sqli 查数据表 ?id-1 union select 1,group_concat(table_name) from information_schema.tables where tabl…

kotlin协程、线程切换,函数方法委托

kotlin协程、线程切换&#xff0c;函数方法委托 一般编程的技法&#xff0c;比如&#xff0c;在Android中&#xff0c;假设在主线程中实现了一个函数&#xff0c;但该函数是耗时操作&#xff0c;毫无疑问&#xff0c;需要将这个函数的实现切入非主线程中操作&#xff0c;那么可…

Springcloud核心组件

在这里总结一下所有组件&#xff1a; springcloud是分布式微服务的一站式解决方案&#xff0c;可以说微服务是一个概念&#xff0c;而springcloud就是这个的实现 springcloud有五大核心组件&#xff1a; 注册中心 引言 由于微服务处于不同的进程&#xff0c;也就是说&…

【软考备战·希赛网每日一练】2023年4月13日

文章目录 一、今日成绩二、错题总结第一题第二题第三题第四题第五题 三、知识查缺 题目及解析来源&#xff1a;2023年04月13日软件设计师每日一练 一、今日成绩 二、错题总结 第一题 解析&#xff1a; 本题有争议&#xff0c;了解即可。 第二题 解析&#xff1a; 上图UML图为…

[计算机图形学]几何:网格处理(前瞻预习/复习回顾)

一、前言 网格的三种处理&#xff1a;网格细分&#xff0c;网格简化&#xff0c;网格正则化&#xff0c;细分会产生更多的三角面片来让模型更加光滑&#xff0c;简化则相反会减少网格的三角面片数量&#xff0c;正则化则会让三角形面更加规则。如上图中最右边两幅图&#xff0…

SpringBoot监听器源码解析

1.1 创建SpringApplication对象 public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return new SpringApplication(primarySources).run(args); }SpringApplication(。。){//获取到所有配置的ApplicationListener类型的监…

Android中的多线程编程与异步处理

Android中的多线程编程与异步处理 引言 在移动应用开发中&#xff0c;用户体验是至关重要的。一个流畅、高效的应用能够吸引用户并提升用户满意度。然而&#xff0c;移动应用面临着处理复杂业务逻辑、响应用户输入、处理网络请求等多个任务的挑战。为了确保应用的性能和用户体验…

《springboot实战》第六章 实现自定义全局异常处理

前言 springboot实现自定义全局异常处理&#xff0c;以及统一返回数据。 1、分析 首先&#xff0c;实现全局异常的流程 从图中可以看到&#xff0c;实现全局异常会需要这样几个类&#xff1a; 自定义异常接口类自定义异常枚举类自定义异常类自定义异常处理类自定义全局响应…

藏在GPT背后的治理分歧:那些赞同和反对的人们|AI百态(下篇)

AGI的火种正在燎原。 一面是无可否认的AI生产力&#xff0c;正在赋能千行百业&#xff1b;而另一面&#xff0c;这团火似乎烧向了我们不可控的隐秘角落。 在《AI百态&#xff08;上篇&#xff09;——ChatGPT的“N宗罪”》中&#xff0c;我们提到监管重锤在落下&#xff0c;意大…

安装 Docker和基本操作实验文档

一、安装 Docker 目前 Docker 只能支持 64 位系统。 systemctl stop firewalld.service setenforce 0 #安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2 yum-utils&#xff1a;提供了 yum-config-manager 工具。device mapper&#xff1a; 是Li…

分布式系统概念和设计-操作系统中的支持和设计

分布式系统概念和设计 操作系统支持 中间件和底层操作系统的关系&#xff0c;操作系统如何满足中间件需求。 中间件需求:访问物理资源的效率和健壮性&#xff0c;多种资源管理策略的灵活性。 任何一个操作系统的目标都是提供一个在物理层&#xff08;处理器&#xff0c;内存&a…

【网络安全】Xss漏洞

xss漏洞 xss漏洞介绍危害防御方法xss测试语句xss攻击语句1. 反射性xss2.存储型xss3.DOM型xssdvwa靶场各等级渗透方法xss反射型&#xff08;存储型方法一致&#xff09;LowMediumHightimpossible Dom型LowMediumHight xss漏洞介绍 定义&#xff1a;XSS 攻击全称跨站脚本攻击&am…

Twitter|GraphJet:推特的实时内容推荐(论文+源码解读)

以下内容具有主观性&#xff0c;有些问题的理解和回答不一定准确&#xff0c;仅供参考。翻译不确定的后面都有原文。 1.论文 1.1论文的动机是什么&#xff1f; 作者在追溯基于图推荐的系统的进化过程&#xff0c;发现了两大趋势&#xff08;更快更广&#xff09;。 趋势一是…

MySQL ,MyBatis 1.参数占位符 2. ParameterType 3. SQL 语句中特殊字符处理

1.参数占位符&#xff1a; 1&#xff09;#{}&#xff1a;执行sql时&#xff0c;会将#仔占位符替换为&#xff1f;&#xff0c;将来自动设置参数值 2&#xff09;${}&#xff1a;拼SQL。会存在SQL注入问题 3.使用时机&#xff1a; * 参数传递&#xff0c;都使用#{} *如果要对表名…

Elasticsearch:保留字段名称

作为 Elasticsearch 用户&#xff0c;我们从许多不同的位置收集数据。 我们使用 Logstash、Beats 和其他工具来抓取数据并将它们发送到 Elasticsearch。 有时&#xff0c;我们无法控制数据本身&#xff0c;我们需要管理数据的结构&#xff0c;甚至需要在摄取数据时处理字段名称…

腾讯云轻量服务器测评:4核8G12M带宽流量CPU限制说明

腾讯云轻量4核8G12M服务器446元一年&#xff0c;之前是4核8G10M配置&#xff0c;现在公网带宽和月流量包整体升级&#xff0c;系统盘为180GB SSD盘&#xff0c;每月2000GB免费流量&#xff0c;如果是选择买赠专区的4核8G12M配置是518元15个月&#xff0c;腾讯云百科来详细说下4…

MyBatis插入时获取自增id

关于MyBatis在插入时获取自增id 1.1 注释方法 Insert("insert into book(bid, bname, price, number, cratedate) values(null,#{bname},#{price},#{number},#{cratedate}) ")int add(Book book); //注意&#xff1a;在mapper中是不能重载的这里我特意写为add01Ins…