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

【Linux】基础IO-文件描述符

【Linux】基础IO

  • C语言的文件接口
  • 文件的初步理解
  • 文件IO的系统接口
    • 打开文件
    • write
    • read
  • 文件描述符fd
  • 语言层的fd
  • 文件描述符的分配规则
  • 重定向和缓冲区的理解
    • 重定向
    • 缓冲区
      • 作用
      • 刷新策略
      • C语言的缓冲区
  • 模拟实现重定向
    • 检查是否是重定向
    • 执行命令
  • 0、1、2的作用

C语言的文件接口

这里我们简单回顾一下C语言的文件操作

使用 “w” 模式打开文件 log.txt,如果文件不存在就创建文件;文件存在,打开文件后清空内容

使用 fprintf 向文件中写入字符串

int main()    
{    
  const char* message = "hello Linux!";    
  FILE* fp = fopen("log.txt", "w");    
    
  fprintf(fp, "%s\n", message);    
  fclose(fp);                                                                                                                                                
  return 0;    
}

在这里插入图片描述

每次使用 w 模式打开文件都会清空文件内容,如果先要追加内容就要使用 a 模式打开文件

FILE* fp = fopen("log.txt", "a");

在这里插入图片描述

C语言文件操作:

  • 使用 w 模式打开文件时,文件不存在就创建;文件存在就清空内容
  • 使用 a 模式打开文件时,会在文件原有内容基础上追加内容

Linux 的重定向操作符与此有异曲同工之妙:

  • >:将数据重定向到指定文件,如果文件不存在就创建,存在就清空内容

在这里插入图片描述

  • >>:将数据追加到指定文件

在这里插入图片描述

由此我们可知,重定向也是在打开文件,将数据写入到文件

文件的初步理解

虽然我们可以使用接口来进行文件相关的操作,但是我们真的理解文件吗?

我们通过上面的程序打开了文件,其而程序启动后就会被加载为进程,所以打开文件的本质就是进程打开了文件,是 CPU 执行我们写的代码

而一个进程可以打开多个文件吗?当然可以,就是多调用几次接口嘛。也就是说一个进程可以打开多个文件,而系统中的进程肯定不止一个,那么系统内就会存在大量的文件。所以就需要操作系统将这些文件统一管理起来,学习了进程后,我们猜想可能会有类似于 PCB 的数据结构,管理这些数据结构就是管理文件。关于这一点我们后面就知道了,这里先放一放

当我们创建一个空文件时,它会占用空间吗?会占用,虽然它的内容是空的,但是还是需要空间来储存文件属性的,例如文件创建时间,文件类型等。也就是说文件=属性+内容

文件IO的系统接口

除了C语言的文件接口,我们还可以使用系统接口进行文件访问

打开文件

#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:标记位,打开文件的方式,为什么是一个 int 呢?——这个 int 是位图,打开方式可以是多个,传递时进行或运算就可以将多种打开方式一并传递给 flags

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

mode:定义新创建文件的权限,例如要求所有人可以读写:0666

返回值:

失败返回 -1;成功返回文件描述符号,这里先不管,作用类似于C语言的FILE*,用来找到打开的文件

write

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

参数:

fd:文件描述符,可以找到打开的文件

buf:缓冲区首地址,可以理解为要写入的数据的首地址

count:要写入多少字节的数据

返回值:实际写了多少字节数据

现在将我们上面代码中的C语言接口替换为系统调用接口

#include <stdio.h>    
#include <sys/types.h>    
#include <sys/stat.h>    
#include <fcntl.h>    
#include <string.h>    
#include <unistd.h>    
    
int main()    
{    
  const char* message = "hello Linux!\n";
  // 打开                                                                                                         
  int fd = open("log.txt", O_WRONLY | O_CREAT, 0666); 
  // 写入数据
  write(fd, message, strlen(message));    
  close(fd); // 关闭文件描述符指向的文件    
  return 0;    
}

read

与 write 的使用类似

int main()
{
  const char* message = "hello Linux!\n";
  // 只读打开
  int fd = open("log.txt", O_RDONLY);
  char buf[1024];
  // 将数据读入到 buf
  read(fd, buf, strlen(message));
  printf("%s",buf);
  return 0;
}

在这里插入图片描述

以上就是关于文件的系统接口的简单使用,我们可以感觉到和C语言文件接口的使用很相似。

而实际上语言是给我们用户层面使用的,而用户是不被允许直接修改操作系统的数据的,所以才会有系统调用接口供我们使用,而C语言的文件接口就是封装的系统调用接口

在这里插入图片描述

文件描述符fd

可以多打开几个文件,看看这 fd 的值到底是怎么样的:

int main()    
{    
  int fd1 = open("fd1.txt", O_WRONLY|O_CREAT);    
  printf("%d\n", fd1);    
    
  int fd2 = open("fd2.txt", O_WRONLY|O_CREAT);    
  printf("%d\n", fd2);                                                                                                                                       
  int fd3 = open("fd3.txt", O_WRONLY|O_CREAT);    
  printf("%d\n", fd3);    
  
  close(fd1);
  close(fd2);
  close(fd3);
  return 0;    
} 

在这里插入图片描述

可以看到我们打开的三个文件的描述符分别是 3、4、5,难道文件描述符是从 3 开始的吗?——当然不是,文件描述符是从 0 开始的,而 0、1、2 这三个描述符已经被其他文件占用了,它们分别是:

  • 0:标准输入,键盘
  • 1:标准输出,显示器
  • 2:标准错误,显示器

在C语言中,我们知道,当程序启动时,会自动打开三个文件:stdin、stdout、stderr,这就是对标的 0、1、2 三个文件描述符

在这里插入图片描述

那么说了这么多,文件描述符它到底是什么呢?这里我们就要回归到操作系统是怎么组织管理文件这个问题了

实际上,打开一个文件后,就会在系统内核中创建一个名为 strcut file 的数据结构,file 中储存着文件的属性

而文件=属性+内容,打开文件的同时,会开辟一块空间来当作文件的缓冲区,而打开文件后,无论是读还是写,都会将文件的内容从硬盘加载到缓冲区,file 会指向这一块空间

在这里插入图片描述

系统中不可能只存在一个打开的文件,不同的文件的 file 会相互链接在一起,形成一个链表

在这里插入图片描述

这么多的文件,一个进程可能打开很多文件,那么进程如何知道自己打开的是哪一个文件呢?

进程 PCB 中,存在一个指针 struct files_struct* files,指向了一张表 files_struct。这张表最重要的部分就是包含了一个指针数组 struct flie* fd_array[],数组中每个元素都是一个指针,指向了打开文件,这个数组就叫文件描述符表

在这里插入图片描述

数组的下标就是文件描述符,凭借文件描述符就可以找到打开的文件

现在我们理解了什么是文件描述符,那么为什么 0、1、2 这三个东西是默认打开的?它们又不是文件,而是硬件啊?

这些硬件在冯诺依曼体系中属于IO外设,虽然是硬件,但是在 Linux 看来:一切皆文件

一切皆文件,这些外设也可以抽象为一个个数据结构 struct device,其中存储着设备的名称、类型、状态等等。也就是说 device 中存储的是设备的属性,那么就可以被 struct file 指向,因为 file 中存储的就是文件的属性

在这里插入图片描述

而想使用这些设备,例如读取键盘的数据,向屏幕写入数据等等,需要特定的方法,这些方法由驱动层提供。而我们想从缓冲区写出数据到设备或者将设备的数据读入到缓冲区,势必离不开这些方法。所以这些方法也应该放到 file 中,这些方法是以函数指针的形式存入到 file 中的,且是一个集合,称作方法集

这样加一层后,虽然每个设备的方法、属性不同,但是都被封装到了 file 中,在 file 层面来看是没有这些不同的

在这里插入图片描述

所以现在我们理解了外设可以被加载为文件,我们可以通过一些手段查看一下 0、1、2 指向的设备

先启动一个进程并获取它的 pid

int main()
{
  pid_t pid = getpid();    
  while(1)    
  {    
    sleep(1);                                                                                                                                                
    printf("pid:%d\n", pid);    
  }    
  return 0;    
} 

然后到 etc/proc/进程id 目录下,可以看到有一个 fd 文件夹,里面就存放着此进程打开的文件的 fd

在这里插入图片描述

可以看到,确实有0、1、2 三个文件描述符,它们都指向了一个设备,这是因为我这里的机器是云服务器,所以指向的设备是同一个终端。此时我们可以编写代码,向这个设备写入数据,就会看到终端有数据输出

int main()
{
  const char* msg = "hello!\n";
  int fd = open("/dev/pts/1", O_WRONLY);
  if (fd < 0) return -1;  
  else{                   
                          
    while(1)              
    {                     
      sleep(1);           
      write(fd,msg, strlen(msg));                                            
    }                                         
  }                
  return 0;        
}

在这里插入图片描述

语言层的fd

了解了什么是 fd 之后,我们知道,文件的操作离不开 fd。而其他语言的文件操作是对系统调用接口的封装,所以肯定离不开对 fd 进行封装

例如C语言,打开文件时会返回一个指针 FILE*,后面对文件的操作离不开这个指针。FILE 就是C语言封装的一个结构体,里面包含了文件描述符 fd

我们可以把 FILE* 中的数据打印出来看看

int main()
{ 
  FILE* pf1 = fopen("fd1.txt", "w");    
  printf("pf1:%d\n", pf1->_fileno);    
    
  FILE* pf2 = fopen("fd2.txt", "w");    
  printf("pf2:%d\n", pf2->_fileno);    
    
  FILE* pf3 = fopen("fd3.txt", "w");    
  printf("pf3:%d\n", pf3->_fileno);    
    
  fclose(pf1);    
  fclose(pf2);    
  fclose(pf3);                                                                                                                                               
  return 0;    
}

在这里插入图片描述

可以看到,和之前使用系统调用打印出来的文件描述符一样

文件描述符的分配规则

我们已经知道,进程启动时会默认打开三个文件,0、1、2就会被占用,后面打开的文件的描述符都是从 3 开始

如果一开始就把 0、1、2这三个文件关掉,再再打开我们自己的文件,那么文件描述符又会怎么分配呢?

尝试关闭0、2,然后打开文件并打印文件描述符

int main()                               
{                                        
  close(0);                              
  close(2);                              
                                         
  int fd1 = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);      
  if (fd1 < 0) return 1;      
                                                                      
  int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);      
  if (fd2< 0) return 1;      
                                                                       
  printf("this is fd1:%d\n", fd1);      
  fprintf(stdout, "this is fd2:%d\n", fd2);                                                                                                                  
      
  close(fd1);    
  close(fd2);                
  return 0;      
}

在这里插入图片描述

从代码运行结果来看,文件描述符的分配规则是这样的:当一个新文件打开时,会从文件描述符表 fd_array 寻找最小的没有被占用的下标,作为新文件的文件描述符

那为什么只关闭0、2,不关闭1呢?因为 1 是标准输出,关闭了就看不到打印结果了。但是我们可以通过别的方式查看:关闭 1 以后,新打开一个文件,理论上这个文件的文件描述符就会分配 1,然后我们向这个文件中写入数据,不就可以查看结果了么

int main()                                                                                                                                                   {   
  close(1);
               
  int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if (fd < 0) return 1;                                            
                           
  printf("this is fd:%d\n", fd);
  fprintf(stdout, "this is fd:%d\n", fd);
  fflush(stdout); // 刷新缓冲区               
                                                                                                                                                             
  close(fd);    
  return 0;     
}

在这里插入图片描述

但是很奇怪的一点是:printf 是向标准输出写入数据,fprintf 我们也规定向标准输出写入数据,但是怎么都写到了 log.txt 中了?

上面已经说过,C语言的文件接口都是封装的系统调用,其中 stdout 是一个结构体FILE对象的指针,FILE结构体封装了文件描述符 fd,且 stdout 的 fd 为1。这样 stdout 就可以通过 fd 找到文件描述符为 1 的文件,但是此时文件描述符 1 指向的文件已经被我们更改了,不再指向标准输出,而是指向文件 log.txt

所以我们向 stdout 中写入数据,stdout 又拿着 fd 找到了文件 log.txt,将数据写入到文件中

在这里插入图片描述

那么代码中的 fflush(stdout) 又是什么作用呢?可不可以去掉呢?如果去掉,数据就会丢失

在这里插入图片描述

这是为什么呢?——文件在内核中有缓冲区,而C语言也有语言层的缓冲区,通常叫做用户级缓冲区,存在于结构体FILE中。我们使用语言层的文件接口写数据时,默认是向用户级缓冲区写数据,然后达成某些条件时,就会被刷新到内核层的缓冲区,而系统调用的 close 关闭文件时,会将内核缓冲区的数据刷入到硬盘,然后关闭文件

在这里插入图片描述

如果使用 close 关闭文件时数据还留在用户级缓冲区没有刷新,然后内核级缓冲区是空的,系统调用将内核缓冲区数据刷新到文件,导致了数据的丢失

重定向和缓冲区的理解

重定向

上面我们把本来要写入到标准输出的数据写到了文件中,这不就是重定向吗?但是这个重定向的实现方式有点挫啊,有没有优雅一点的方法呢?有的,系统中有个接口,可以更改文件描述符指向的内容

#include <unistd.h>
int dup2(int oldfd, int newfd);

描述:

在这里插入图片描述

简单来说,就是将 oldfd 的内容拷贝到 newfd,那么 oldfd 和 newfd 都会指向同一个文件。所以说,重定向的本质就是文件描述符下标内容的拷贝

例如,进程中默认打开 0、1、2三个文件,我们又打开自己的文件 log.txt,文件描述符为 fd。如果我们想把写入到标准输出的内容重定向到文件,就可以把文件描述符表中,fd 对应的内容拷贝到 1 号下标中,这样 1 号文件描述符指向的文件就是 log.txt 了,向标准输出写数据就是向 log.txt 写内容

实践一下:

int main()                                                                                                                                                   {   
  
  // 打开 log.txt
  int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
  if (fd < 0) return 1;                                            
                           
  // 重定向
  dup2(fd, 1); 
  // 写入数据到 std
  printf("this is fd:%d\n", fd);    
  fprintf(stdout, "this is fd:%d\n", fd);    
  fflush(stdout);

  close(fd);                                                                                                                                                 
  return 0;     
}

在这里插入图片描述

缓冲区

作用

为什么语言层也要设置一个用户级缓冲区呢?我不可以直接将数据写到内核缓冲区吗?这不是多此一举吗?其实缓冲区也是有妙用的:

  1. 解耦:把用户和操作系统解耦,用户只需要将数据写到缓冲区即可,不需要操心怎么把数据搞到内核缓冲区,再刷新到硬盘
  2. 提高效率:多了一层不是还要拷贝吗?为什么还提高了效率了?——提高的是用户使用的效率
    1. 用户只需要向缓冲区读写数据即可,效率更高
    2. 系统调用是有消耗的,越少调用,效率越高。如果先把数据丢到用户级缓冲区,达成某个条件时,直接使用一次系统调用即可将数据刷新到内核缓冲区;内核缓冲区同理,先缓存数据,然后一次刷新到硬盘文件即可,极大减少了频繁使用系统调用的消耗

刷新策略

  1. 立即刷新:例如C语言的 fflush,系统调用的 fsync,都可以刷新缓冲区
  2. 行刷新:这个比较特殊,显示器采用的策略就是这样的,因为这样更符合用户的使用习惯
  3. 满了才刷新:普通文件

特殊情况:

  1. 程序结束,会自动刷新缓冲区。之前进程说过的 exit 会刷新缓冲区,_exit 则不会
  2. 强制刷新,类似于立即刷新

此时我们写一个代码,先使用语言层接口向标准输出打印数据,再使用系统调用打印数据。标准输出也就是屏幕,这里采用的刷新策略是行刷新

int main()
{                          
  // C                     
  printf("hello,printf!\n");
  fprintf(stdout, "hello, fprintf!\n");
                                               
  // sysytem call
  const char* msg = "hello write!\n";                                                                                                                        
  write(1, msg, strlen(msg));
  return 0;                      
}

在这里插入图片描述

可以看到,确实是按照我们输出的顺序一行一行地刷新的。那么我们再将这些数据进行重定向输出到 log.txt 文件看看效果

在这里插入图片描述

输出的顺序发生了变化,这是为什么呢?这是因为我们进行了重定向输出,本质还是打开了 log.txt 文件,因为是文件,所以缓冲区刷新策略变为了满了才刷新

printf 和 fprintf 都是语言层的接口,所以默认将数据写入了用户级缓冲区,而 write 是系统接口,直接将数据写入到了内核缓冲区

在这里插入图片描述

代码中还有一个细节,没有关闭文件。根据刷新策略,程序结束时,用户级缓冲区的数据会被刷新到内核缓冲区,接着内核缓冲区的数据被刷新到硬盘的文件中。所以打印的顺序发生了变化

在这里插入图片描述

这时候我们再来添加一条语句,分别将数据输出到屏幕和文件

int main()
{                          
  // C                     
  printf("hello,printf!\n");
  fprintf(stdout, "hello, fprintf!\n");
                                               
  // sysytem call
  const char* msg = "hello write!\n";                                                                                                                        
  write(1, msg, strlen(msg));
  
  fork(); // 创建子进程
  return 0;                      
}

输出到屏幕:

在这里插入图片描述

没什么变化,再来看看重定向输出到文件

在这里插入图片描述

为什么 printf 和 fprintf 多打印了一遍呢?问题肯定出在 fork() 上,创建了子进程

上面我们说过,在程序结束之前,printf 和 fprintf 一开始是在用户级缓冲区中的。然后父进程创建子进程,此时程序也要结束了,那么父子进程都会刷新用户级的缓冲区,也就是刷新了两次数据到内核缓冲区,所以才会有两对 printf 和 fprintf 的输出

C语言的缓冲区

说了这么多,我们可以看一下C语言的缓冲区长什么样子

在 /usr/include/stdio.h

在这里插入图片描述

而 struct _IO_FILE 在 /usr/include/libio.h

在这里插入图片描述

模拟实现重定向

把我们之前写的 shell-链接,模拟实现一下重定向的实现

检查是否是重定向

如果输入的命令是这样 ls -a -l > log.txt在获取到用户输入命令后,需要进行检查是否具有重定向符号

// 获取用户输入命令
// ...

CheckRedir(usercommand);

// 分割命令行字符串
// ...

我们需要获取命令的重定向相关信息,例如重定向的类型,重定向的文件

// 重定向相关                                                                   
  #define No_Redir 0                                                              
  #define In_Redir 1 // <                                           
  #define Out_Redir 2 // >                                                         
  #define Add_Redir 3 // >>
  int redir_type = No_Redir; // 类型 
  char* filename = NULL; // 重定向文件名

接下来编写 CheckRedir

  • 遍历字符串,寻找重定向符号 > >> <
  • 找到之后,修改相应的重定向类型,然后把重定向符号改为 0,断开命令
  • 跳过重定向后面的空格,寻找文件名
void CheckRedir(char cmd[])
{
  // 寻找重定向符号
  int pos = 0;
  int end = strlen(cmd);
  while(pos < end)
  {
    // ls -a -l >   log.txt
    if (cmd[pos] == '>')
    {
      if (cmd[pos + 1] == '>')
      {
        // 追加重定向
        redir_type = Add_Redir;
        cmd[pos++] = 0;
        pos++; // 跳过两个>
        // 跳过空格
        SkipSpace(cmd, pos);
        filename = cmd + pos;
      }
      else{
        // 输出重定向
        redir_type = Out_Redir;
        cmd[pos++] = 0;
        SkipSpace(cmd, pos);
        filename = cmd + pos;
      }
    }
    else if (cmd[pos] == '<')
    {
        // 输入重定向
        redir_type = In_Redir;
        cmd[pos++] = 0;
        // 跳过空格,寻找文件名
        SkipSpace(cmd, pos);
        filename = cmd + pos;
    }
    else{
      pos++;
    }
  }                                                                                                                                                          
}                                                                                                                                                  

这里写一个跳过空格的宏函数

#define SkipSpace(cmd, pos) do{\
                               while(1)\
                               {\
                                if(cmd[pos] == ' ') pos++;\
                                else break;\
                              }\
                            }while(0)

先把下面的代码屏蔽,测试一下:

在这里插入图片描述

在这里插入图片描述

看上去是没什么问题,那么接下来写执行重定向的命令

执行命令

在子进程执行命令之前,打开相关文件,然后调用 dup2 即可

void ExecuteCmd()
{
  pid_t id = fork();
  if (id < 0) exit(1);
  else if (id == 0)
  {
    // 重定向,打开文件
    if (filename != NULL)
    {
      if (redir_type == In_Redir)
      {
        // 输入重定向
        int fd = open(filename, O_RDONLY);
        dup2(fd, 0);
      }
      else if (redir_type == Out_Redir)
      {
        // 输出重定向
        int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
        dup2(fd, 1);                                                                                                                                         
      }
      else if (redir_type == Add_Redir)
      {
                // 追加重定向
        int fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
        dup2(fd, 1);
      }
    }
    // child
    execvp(gArgv[0], gArgv);
    exit(errno); // 执行失败
  }
  // father
  int status = 0;
  pid_t rid = waitpid(id, &status, 0);
  if (rid > 0)
  {
    // wait sucess
    lastcode = WEXITSTATUS(status);
    if (lastcode)
      printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
  }
}                                                                                                                                                            

测试:

输出重定向

在这里插入图片描述

追加重定向

在这里插入图片描述

0、1、2的作用

这里我们总结一下 0、1、2 的作用。我们写的程序不就是对数据进行操作吗,例如计算、存储等。那么这些数据从哪里来,又到哪里去呢?

  • 数据从标准输入来,也就是0
  • 数据要输出给用户看,也就是输出到标准输出1

那么 2 又有什么作用呢?1 和 2 不都是指向的标准输出吗?

我们写的程序,输出信息一般有两种:正确的和错误的。一般进行打印时,都会打印在屏幕上,如下:

int main()                        
{ 
  fprintf(stdout, "hello fprintf, stdout\n");    
  fprintf(stderr, "hello fprintf, stderrt\n");                                                                                                                
  return 0;                                                                                                                                
}

在这里插入图片描述

如果我们使用>,将输出信息重定向到 log.txt 文件中,会发生奇怪的现象:

在这里插入图片描述

向 stderr 写入的数据并没有输出到文件中,依然输出在了屏幕。这是为什么呢?——因为>符号默认情况下是标准输出重定向,也就是只把文件描述符表中 1 号下标的内容修改,指向了 log.txt 文件;而 2 号下标的内容依然指向屏幕

在这里插入图片描述

那么如何把 2 号下标的内容也重定向呢?正确写法是这样的:

在这里插入图片描述

其中 1>log.txt 就不说了,而 2>&1 的意思就是把 1 号下标的内容拷贝给 2 号下标。那么 1、2都会指向 log.txt 文件

在这里插入图片描述

还可以这样,把正确信息和错误信息输出到两个不同文件中

在这里插入图片描述

C语言的 perror 接口,默认就是向 stderr,也就是系统的 2 中写入数据

在这里插入图片描述

到这里我们也可以体会到 2 的作用了,就是可以将程序输出的正确信息和错误信息输入到不同文件,使用重定向即可分离输出


http://www.kler.cn/a/412313.html

相关文章:

  • Kubernetes 还是 SpringCloud?
  • 【大数据学习 | Spark-Core】RDD的缓存(cache and checkpoint)
  • Burp入门(2)-代理功能介绍
  • ubuntu搭建k8s环境详细教程
  • redmi 12c 刷机
  • logminer挖掘日志归档查找问题
  • 【Linux学习】【Ubuntu入门】2-5 shell脚本入门
  • CentOS 环境使用代理下载数据失败-EOF occurred in violation of protocol (_ssl.c:1002)
  • 自主研发,基于PHP+ vue2+element+ laravel8+ mysql5.7+ vscode开发的不良事件管理系统源码,不良事件管理系统源码
  • 一篇文章了解Linux
  • react项目初始化配置步骤
  • 关于 Android LocalSocket、LocalServerSocket
  • C++中虚继承为什么可以解决菱形继承的数据冗余问题
  • EasyAnimate:基于Transformer架构的高性能长视频生成方法
  • LeetCode 2924. Find Champion II
  • CRTP mixins EBO
  • 代理模式 (Proxy Pattern)
  • C#基础36-40
  • 【大数据测试 Elasticsearch 的 四大 常见问题及处理方案】
  • 【模糊查询Redis的Key,过滤出其中ZSET类型中包含自定义字符串的元素并删除】
  • 老旧前端项目如何升级工程化的项目
  • 鸿蒙Native使用Demo
  • ubuntu使用Docker,安装,删除,改源等记录
  • 类的加载机制
  • 自制Windows系统(十)
  • Unity 设计模式-单例模式(Singleton)详解