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

Linux基础IO

 

目录

 

重点内容

共识原理 

回顾C文件接口 

hello.c写文件 

hello.c 读文件

 输出信息到显示器

 stdin & stdout & stderr

总结 

打开文件的方式 

 过度

系统文件I/O 

hello.c写文件: 

 hello.c读文件

 接口介绍

open 

 小知识:比特位级别的传参方式

open函数返回值 

文件描述符fd 

0 & 1 & 2 

文件描述符的分配规则 

文件的删除与应用计数 

重定向 

使用 dup2 系统调用

例子1. 在minishell添加重定向功能: 

重定向2

Linux下一切皆文件

缓冲区

 为什么要有c语言的这个缓冲区

那么这个缓冲区究竟在哪呢? 

FILE 

makefile

Mystdio.h

Mystdio.c

main.c


重点内容

        1.复习C文件IO相关操作
        2.认识文件相关系统调用接口
        3.认识文件描述符,理解重定向
        4.对比fd和FILE,理解系统调用和库函数的关系
        5.理解文件系统中inode的概念
        6.认识软硬链接,对比区别
        7.认识动态静态库,学会结合gcc选项,制作动静态库

共识原理 

        1.文件 = 内容 + 属性

        2.文件分为打开的文件和未打开的文件

        3.打开的文件:是进程打开------本质是研究进程和文件的关系

我们先重点来看打开的文件:

                a>文件被打开,必须先被加载到内存(冯诺依曼体系结构决定)

                b>进程:进程打开的文件 = 1 : n

                操作系统内部一定存在大量被打开的文件,而操作系统管理他们采用的也是,先描述,再组织的形式。在内核中,每个文件都必须有自己的文件打开对象,包含文件的很多属性。

struct file (文件属性;struct file * next)

        4.未打开的文件:放在磁盘上,因为未打开的文件非常多,因此我们要关注如何分门别类地把文件放置好(即如何存储),我们要能够快速进行增删查改,快速找到文件 

回顾C文件接口 

hello.c写文件 

#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "w");if(!fp){printf("fopen error!\n");}const char *msg = "hello bit!\n";int count = 5;while(count--){fwrite(msg, strlen(msg), 1, fp);}fclose(fp);return 0;
}

hello.c 读文件

#include <stdio.h>
#include <string.h>
int main()
{FILE *fp = fopen("myfile", "r");if(!fp){printf("fopen error!\n");}char buf[1024];const char *msg = "hello bit!\n";while(1){//注意返回值和参数,此处有坑,strlen后不需要+1,仔细查看man手册关于该函数的说明ssize_t s = fread(buf, 1, strlen(msg), fp);if(s > 0){buf[s] = 0;printf("%s", buf);}if(feof(fp)){break;}}fclose(fp);return 0;
}

 输出信息到显示器

#include <stdio.h>
#include <string.h>
int main()
{const char *msg = "hello fwrite\n";fwrite(msg, strlen(msg), 1, stdout);printf("hello printf\n");fprintf(stdout, "hello fprintf\n");return 0;
}

        在操作系统看来,向文件写入和向显示器写入,并没有区别, Linux下一切皆文件

 stdin & stdout & stderr

C默认会打开三个输入输出流,分别是stdin, stdout, stderr

(C++也有相应的cin ,cout,cerr)

仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针 

但是这本质上并不是C的特性,而是操作系统的特性进程会默认打开键盘,显示器,显示器,不同的语言都只是拿来用了而已。

总结 

打开文件的方式 

        r Open text file for reading.
                        The stream is positioned at the beginning of the file.
        r+ Open for reading and writing.
                        The stream is positioned at the beginning of the file.
        w Truncate(缩短) file to zero length or create text file for writing.
                        The stream is positioned at the beginning of the file.
        w+ Open for reading and writing. 

                The file is created if it does not exist, otherwise it is truncated.
                The stream is positioned at the beginning of the file.
        a Open for appending (writing at end of file).
                The file is created if it does not exist.
                The stream is positioned at the end of the file.
        a+ Open for reading and appending (writing at end of file).
                The file is created if it does not exist. The initial file position
                for reading is at the beginning of the file,
                but output is always appended to the end of the file.

        1.进程也有进程的当前路径cwd,该进程建立的文件会建在该进程的当前路径,当我们更改了当前进程从cwd,就可以把文件新建到其它目录下了。

         2.w,写入之前都会对文件进行清空处理

        3.w/a都是写入,而和w不同的是,a是追加写

 过度

        文件其实是在 磁盘上的,磁盘是外部设备,访问磁盘文件其实就是访问硬件

        用户

        使用 C/C++等标准库写的程序 几乎所有的库,只要是要访问硬件设备,必定要封装系统调用

        系统调用。  (printf,fprintf,fscanf,fwrite,fread,fgets等等这些库函数都封装了系统调用接口)

        操作系统

        硬件驱动

        硬件  (我们知道硬件一定是要被操作系统管理的,用户是无法直接访问硬件的)

         

系统文件I/O 

        操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问,先来直接以代码的形式,实现和上面一模一样的代码: 

hello.c写文件: 

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{umask(0);int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}int count = 5;const char *msg = "hello bit!\n";int len = strlen(msg);while(count--){write(fd, msg, len);//fd: 后面讲, msg:缓冲区首地址, len: 本次读取,期望写入多少个字节的        数据。 返回值:实际写了多少字节数据}close(fd);return 0;
}

 hello.c读文件

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}const char *msg = "hello bit!\n";char buf[1024];while(1){ssize_t s = read(fd, buf, strlen(msg));//类比writeif(s > 0){printf("%s", buf);}else{break;}}close(fd);return 0;
}

 接口介绍

open 

#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。
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写返回值:
成功:新打开的文件描述符
失败:-1

mode_t理解:直接 man 手册,比什么都清楚。
        open 函数具体使用哪个,和具体应用场景相关,如目标文件不存在,需要三个参数的open创建,则第三个参数表示创建文件的默认权限,否则,使用两个参数的open。 

        由下图我们可以看出,第三个参数就是我们之前谈的文件权限,它也会受到系统默认umask的影响,我们可以在进程内自己再设定一个

我们可以借此稍微窥探一下库函数与系统调用的关系

 小知识:比特位级别的传参方式

        传参传一个两个三个以及更多都是可以的,用|连接即可 

open函数返回值 

在认识返回值之前,先来回顾一下两个概念: 系统调用 和 库函数
上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口 

再加上之前已经展示过的图片

 系统调用接口和库函数的关系,就一目了然。
所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

文件描述符fd 

通过对open函数的学习,我们知道了文件描述符就是一个小整数

0 & 1 & 2 

        Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
        0,1,2对应的物理设备一般是:键盘,显示器,显示器
        所以输入输出还可以采用如下方式: 

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{char buf[1024];ssize_t s = read(0, buf, sizeof(buf));if(s > 0){buf[s] = 0;write(1, buf, strlen(buf));write(2, buf, strlen(buf));}return 0;
}

 

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

文件描述符的分配规则 

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

 输出发现是 fd: 3

关闭0或者2,再看

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{close(0);//close(2);int fd = open("myfile", O_RDONLY);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);close(fd);return 0;
}

 发现是结果是: fd: 0 或者 fd 2 可见,文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符

         对于上面提到的FILE*指针,它其实指向的是一个结构体

         

                我们可以借此验证封装在其内对应的fd 

文件的删除与应用计数 

        struct file中有一个引用计数count,有几个文件描述符表中的指针指向它,它的值就是几。

        我们要关闭一个 文件,比如close(1),系统把它所指向的文件的count都-1,然后把数组相应位置置空,然后再去查看struct file中的count,如果是0,那么系统直接回收该struct对象,否则它继续存在

重定向 

那如果关闭1呢?看代码: 

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{close(1);int fd = open("myfile", O_WRONLY|O_CREAT, 00644);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fflush(stdout);close(fd);exit(0);
}

         此时,我们发现,本来应该输出到显示器上的内容,输出到了文件 myfile 当中,其中,fd=1。这种现象叫做输出重定向。常见的重定向有:>, >>, <
        那重定向的本质是什么呢?

         stdout里面封装了一个fd = 1的值,但是close(1),然后再打开一个新文件,文件描述符1对应指向的就变成了新打开的那个文件了

使用 dup2 系统调用

函数原型如下:

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

例如我们现在有一个fd = 3,我们要用它替换另一个fd = 1

           那么fd = 3就是oldfd,fd = 1就是newfd 

例子1. 在minishell添加重定向功能: 

#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 2int 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.txtchar *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.txtpos++;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.txtcheck_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;
}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;}// 特殊处理一下lsif(strcmp(_argv[0], "ls") == 0){_argv[_argc++] = "--color";_argv[_argc] = NULL;}return 0;
}int main()
{while(!quit){// 1.rdirfilename = NULL;rdir = NONE;// 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txtinteract(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;
}

        我们看到,我们仅仅只是在创建子进程执行命令之前判断了一下

但是,你重定向和我程序替换又有什么关系呢?他们的数据结构是解耦合的,互相并不影响。 

重定向2

        我们日常的输出重定向,其实省略了1,把本来输出到fd = 1显示器的语句输出到文件normal.log,把本来输出到fd = 2,也是显示器的语句输出到文件err.log。 

        如果我们要把他们都重定向到一个文件里面

 

指令从左往右执行,1(显示器)指向all.log,然后把1的内容向2中拷贝一份,让2也指向all.log

Linux下一切皆文件

         struct  operation func里面的结构都是一样的,比如硬件都要要有读和写的功能,只不过有些硬件不能读或不能写,置空即可,对应的特定硬件的功能实现可以传函数指针

        整体架构类似于继承和多态

缓冲区

        经过一系列实验,观察我们使用c语言接口时发生的现象,我们得出c语言有自己一个独立的缓冲区  (仅仅是举例,其它语言也有各自对应的缓冲区,属于软件层面的缓冲区)

        并且有自己的缓冲区刷新规则,例如显示器显示我们一般采用行缓冲,文件写入我们使用全缓冲 ,而进程退出的时候也会刷新缓冲区

        同时如果我们没有把c语言缓冲区的内容刷新到内核缓冲区,并且关闭显示器文件(假设我们c语言接口把内容输出到显示器),那么相应内容就会丢失

(下面两行字对应c语言缓冲区)

        而用户刷新的本质其实就是通过fd1(显示器) + write把内容写到内核里 (这里也是假设输出到显示器)

目前我们暂时认为,只要把数据刷新到了内核,数据就可以到硬件了)

        同时我们可以再回顾一下我们之前学习的c语言库函数exit和系统调用_exit

 

        系统调用是管不到上层c语言部分的刷新的,而c语言的库函数封装了刷新(fflush)和_exit 

 为什么要有c语言的这个缓冲区

        1.解决用户效率问题,我们只需要把数据交给c语言缓冲区就行了,传完了直接返回,不用低效地直接传到内核

        2.格式化输入输出,我们printf,scanf等等库函数都是为了格式化输入输出,例如我们想输入一个整数123,其实我们输入的是字符1和2和3,这个时候我们写入字符的时候要用%d来将它格式化 

那么这个缓冲区究竟在哪呢? 

        文件操作绕不开这个FILE*的结构体,每个结构体内都有一个自己的缓冲区的维护信息,也就是说我们打开了10个c语言文件,那么就有十个这样的缓冲区 ,这个缓冲区要把自己的内容刷新到内核缓冲区,只需要write即可,这个结构体内本身就有fd的值

FILE 

 一些共识:

        因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的
        所以C库当中的FILE结构体内部,必定封装了fd

来段代码研究一下:

 

#include <stdio.h>
#include <string.h>
int main()
{const char *msg0="hello printf\n";const char *msg1="hello fwrite\n";const char *msg2="hello write\n";printf("%s", msg0);fwrite(msg1, strlen(msg0), 1, stdout);write(1, msg2, strlen(msg2));fork();return 0;
}

运行出结果:

hello printf
hello fwrite
hello write

但如果对进程实现输出重定向呢? ./hello > file , 我们发现结果变成了:

hello write
hello printf
hello fwrite
hello printf
hello fwrite

        我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关。

        1.一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲
        2.printf fwrite 库函数会自带缓冲区(进度条例子就可以说明),当发生重定向到普通文件时,数据的缓冲方式由行缓冲变成了全缓冲
        3.而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
        4.但是进程退出之后,会统一刷新,写入文件当中。
        5.但是fork的时候父子数据会发生写时拷贝,所以当你父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据。
        6.write 没有变化,说明没有所谓的缓冲。

综上:

        1.printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。
        2.那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供

我们可以自己简易模拟实现一下c库的操作

makefile

myfile:main.c Mystdio.cgcc -o $@ $^ -std=c99
.PHONY:clean
clean:rm -f myfile

Mystdio.h

//#pragma once
#ifndef __MYSTDIO_H__
#define __MYSTDIO_H__#include <string.h>#define SIZE 1024#define FLUSH_NOW 1//c库是要对显示文件进行判断的,这里我们手动规定了一下
#define FLUSH_LINE 2
#define FLUSH_ALL 4typedef struct IO_FILE{int fileno;int flag; //char inbuffer[SIZE];输入缓冲区也有,但是这里我们只考虑输出缓冲区//int in_pos;char outbuffer[SIZE]; // 用一下这个int out_pos;
}_FILE;_FILE * _fopen(const char*filename, const char *flag);
int _fwrite(_FILE *fp, const char *s, int len);
void _fclose(_FILE *fp);#endif

 

Mystdio.c

#include "Mystdio.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>#define FILE_MODE 0666// "w", "a", "r"
_FILE * _fopen(const char*filename, const char *flag)
{assert(filename);assert(flag);int f = 0;int fd = -1;if(strcmp(flag, "w") == 0) {f = (O_CREAT|O_WRONLY|O_TRUNC);fd = open(filename, f, FILE_MODE);}else if(strcmp(flag, "a") == 0) {f = (O_CREAT|O_WRONLY|O_APPEND);fd = open(filename, f, FILE_MODE);}else if(strcmp(flag, "r") == 0) {f = O_RDONLY;fd = open(filename, f);}else return NULL;if(fd == -1) return NULL;_FILE *fp = (_FILE*)malloc(sizeof(_FILE));if(fp == NULL) return NULL;fp->fileno = fd;//fp->flag = FLUSH_LINE;fp->flag = FLUSH_ALL;fp->out_pos = 0;return fp;
}// FILE中的缓冲区的意义是什么????
int _fwrite(_FILE *fp, const char *s, int len)
{// "abcd\n"memcpy(&fp->outbuffer[fp->out_pos], s, len); // 没有做异常处理, 也不考虑局部问题fp->out_pos += len;if(fp->flag&FLUSH_NOW){write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}else if(fp->flag&FLUSH_LINE){if(fp->outbuffer[fp->out_pos-1] == '\n'){ // 不考虑其他情况write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}}else if(fp->flag & FLUSH_ALL){if(fp->out_pos == SIZE){write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}}return len;
}void _fflush(_FILE *fp)
{if(fp->out_pos > 0){write(fp->fileno, fp->outbuffer, fp->out_pos);fp->out_pos = 0;}
}void _fclose(_FILE *fp)
{if(fp == NULL) return;_fflush(fp);close(fp->fileno);free(fp);
}

main.c

#include "Mystdio.h"
#include <unistd.h>#define myfile "test.txt"int main()
{_FILE *fp = _fopen(myfile, "a");if(fp == NULL) return 1;const char *msg = "hello world\n";int cnt = 10;while(cnt){_fwrite(fp, msg, strlen(msg));// fflush(fp);sleep(1);cnt--;}_fclose(fp);return 0;
}


http://www.mrgr.cn/news/57306.html

相关文章:

  • x86 Docker镜像转换为 ARM 架构镜像
  • 网安加·百家讲坛 | 徐一丁:金融机构网络安全合规浅析
  • Vue组件开发详解
  • yjs机器学习数据操作01——数据的获取、可视化
  • IMU 助力 BBLeap 的“植物级农业”LeapBox 技术
  • 汽车3d动画效果怎么样?云渲染提升汽车营销体验
  • Android Handler(Looper.getMainLooper()),Kotlin
  • priority_queue (优先级队列的使用和模拟实现)
  • K折交叉验证代码实现——详细注释版
  • IPC 信号-Signal Linux环境
  • 栈的顺序存储总览
  • 关于风险系统解读最全最专业文章:一篇文章讲透风险,跨学科搞懂风险游戏规则,风险信任风险主观性客观性风险本质人格特质与风险态度技术风险系统风险社会新产品风险
  • 栈和队列代码
  • ARM/Linux嵌入式面经(五二):华为
  • Spring 设计模式之单例模式
  • C++新基础类型(C++11~C++20)
  • ECharts图表图例11
  • 解决cad找不到vcruntime140_1.dll,无法继续执行代码的6种方法
  • 《YOLO 目标检测》—— YOLO v3 详细介绍
  • 拟态UI3.0个人页
  • django模板相关配置
  • 一个将.Geojson文件转成shapefile和kml文件的在线页面工具(续)
  • hive数据库,表操作
  • 前缀和 有图文 超详细整理通俗易懂
  • OpenEular + KVM + virt-manager 笔记
  • Python小程序 - 替换文件内容