学习系统编程No.8【bash实现】
引言:
北京时间:2023/3/22/6:59,一晃3月都要过去了,时间真快,我都不知道自己这个月是怎么过的呢?怎么就要结束了,难受,恍惚自己还在2022年,刚刚晨跑回来,洗完澡,一个字形容,困,昏昏欲睡,可能是昨天没怎么睡好,也可能是睡的时间少了一点,也可能是正常情况,待会就不会了,并且我只知道,早上一睁眼就看见全宿舍都起床了,都在卷,一人独睡,所以咱们起的比别人迟,现在就更不能睡,乘热打铁,算了,铁还没热,乘虚而入,不对,咱不是那种人,乘风而去,算了,这个咱也不行,还是老老实实的乘胜追击、乘其不备的学习一下哈哈哈哈哈!所以今天我们就接着上篇博客的内容,继续谈谈什么是进程替换,然后把进程替换玩明白之后,自己实现一个命令行解释器bash

理解加载过程和进程程序替换原理
在上篇博客中,我们了解了什么是进程替换,知道进程替换的本质就是:原进程的内核数据结构不变,把原进程在物理内存中的代码和数据替换为新进程的代码和数据(从磁盘中加载),所以从原进程的角度来看,可以看出进程替换的本质就是代码和数据被替换了,那么此时我们站在替换代码和数据的角度看一看,就又可以理解一个现象,如下:
替换原进程的代码和数据原本是在磁盘之中存储着的,它是被系统调用接口(execl)识别,然后被动的被操作系统加载到内存,然后被动执行,所以由于这个程序是被动的加载到了内存之中,所以我们将这种现象中被动被加载到内存中的程序称之为 加载器
程序如何加载到内存
我相信我们一直都知道一个问题,当然也是一个客观实际,就是程序为什么要加载到内存?这个问题的本质非常好解释,无论是冯诺依曼体系规定的,还是CPU和内存之间规定,我们都知道,一个文件想要被执行,它就一定要加载到内存之中,只有将程序加载到了内存,CPU才可以从内存和该文件对应的进程pcb交互,进而通过进程pcb,来执行该文件中,或者说是该进程中的代码和数据。明白了这个点,此时通过该点,我们接着来谈谈,一个程序如何被加载到内存?
一个程序需要被加载到内存和一个程序如何被加载到内存显然是两个不一样的问题?并且后者比前者更加深入计算机系统,所以此时我们就来聊聊程序是如何被加载到内存之中的,这个问题,就涉及到了我们这篇博客的主要内容,进程替换 ,所以我们明白一个程序被加载到内存是利用了进程替换的方式,并且我们上述也强调了,这种使用进程替换的方式将一个程序加载到内存,此时该程序也叫 加载器 , 并且程序加载到内存的过程就是程序替换的过程,所以我们就有了很强烈的关系,有了很强的扳手,就是程序想要加载到内存之中,就需要程序替换,想要程序替换就需要有一个进程,想要有一个进程首先就需要有一个bash命令行解释器(所有指令和进程的父进程 公式:指令 = 可执行文件 = 进程),并且要知道,此时的bash命令行解释器受操作系统控制, 明白了这一串的联系之后,此时就可以知道,想要进行程序替换,首先要有一个进程,并且该进程中需要使用execl这样的系统调用接口,所以程序加载到内存的本质就是使用 加载器,通过加载器的形式,将一个程序加载到内存。
明白了上述程序是如何别加载到内存之中的之后,此时我们来看看程序被加载到内存的过程中,操作系统做了什么?想要回答这个问题,此时就先要解决,当我们创建进程的时候,是先有进程数据结构,还是先加载代码和数据到内存之中,答案是显然的,我们在学习虚拟地址空间的时候,就有谈到,操作系统是不允许任何资源的浪费,所以它不允许先加载代码和数据,而是先创建该进程对应的pcb,通过pcb来管理该进程,并且在该进程需要被执行的时候,才会将该进程对应的代码和数据加载到内存, 明白了这点之后,此时就可以回答,程序加载到内存,操作系统做了什么工作,或者间接就可以把这个问题改成操作系统如何把程序加载到内存,或者是操作系统同应该如何执行可执行文件?
从程序加载到内存,我们应该先创建进程pcb为落脚点,进而回答上述问题,例如:我们在test.c代码文件中,写了一份C代码,并且利用gcc生成了一个可执行文件(mytest),发现如果想要执行该可执行文件,一定需要在该文件前面添加 ./ 的符号,才能使该可执行文件运行起来,所以此时通过这个最普通的现象,我们可以有一个解释,就是 ./ 就是用来将我们的可执行程序,从磁盘中加载到内存,因为上述说了,创建一个进程的时候,是先创建出该进程的数据结构,所以明白该进程的代码和数据此时还并没有被加载到内存,是需要通过 ./让 操作系统去调用相应的接口, 加载器,来将可执行文件在磁盘中的代码和数据加载到内存,并且此时又通过加载器和程序替换之间的关系,可以知道,./ 的本质就是去调用了像execl这样的程序替换接口;所以当我们的操作系统使用 ./ 加载我们的程序时,此时操作系统就相当于,把当前对应的指令,bash指令(就是命令行指令)加载到了内存,所以操作系统执行进程的方法就是,先在内核中创建一个结构体,在这个结构体上创建子进程,然后直接让这些子进程去调用系统调用接口(execl等函数),就相当于是让我们自己的数据和代码加载到内存(加载器和execl等函数挂钩)所以创建进程时,操作系统一定是先帮我们把进程的数据结构,进程pcb给创建好,然后在需要的时候再通过execl这个接口去把该进程对应的代码和数据加载到内存,然后通过进程pcb来控制或者使用,自然而然这个也就是一个进程的创建过程!
总的来说: 就是操作系统在内核中帮我们创建一个该进程数据结构,此时CPU开始调度,然后操作系统首先就把execl这个指令给给CPU执行,然后把用户想要运行的指令,传递给execl,然后将该指令对应的在磁盘上的代码和数据加载到内存中,这样就变成了一个用户想要执行的进程,完成的就是一个地地道道的进程替换过程(加载器加载过程)
深入进程替换
因为进程替换,把原程序的代码和数据都给替换了,后续的代码是直接被替换,是没有机会执行的,从而证明程序替换是整体替换,不是局部替换
如下图:

可以发现,程序替换,只在子进程中进行是不会影响父进程的,只会影响调用的进程,本质是因为进程具有独立性,但是此时是为什么呢?子进程和父进程它们在物理内存中的代码和数据不是相同的吗,如何理解,代码数据相同,但是又具备进程独立性呢? 所以此时具体的原理,如下:
可以这么理解,由于父进程和子进程在没有进行数据修改之前,也就是没有进行写时拷贝之时,两个进程的代码和数据是相同的,此时如果将子进程进行程序替换,此时就会导致一个问题,就是子进程的代码和数据被替换,是否会导致父进程的代码和数据被替换,从上述的结论可以看出,答案是不会的 ,所以想要搞定这个问题,此时就又涉及到了在进程控制中了解的写时拷贝问题,当我们的子进程或者父进程其中一个进程进行了程序替换或者说是被程序替换,那么此时操作系统检测到之后,就会对子进程或者父进程进行写时拷贝,将子进程或者父进程的代码和数据拷贝两份,其中一份供给程序替换,并且回忆写时拷贝的目的就是:防止资源浪费,不必要是不开辟空间(操作系统的特性);所以,当子进程进行程序替换时,操作系统第一步先是进成写时拷贝,然后才是加载新程序的代码和数据,然后子进程再重新利用页表建立新的映射关系,所以进程在程序替换之后,还能保持进程独立性的本质原因,就是操作系统会进行写时拷贝,并且回忆之前的知识可以发现,写实拷贝不仅可以在父子进程修改数据的时候进行,也可以在代码区发生!
程序替换失败问题
深入理解明白,execl是一个函数接口,它是有可能会调用失败的,也就是无法进行程序替换(进程太多等问题)例如下图:

当我们在execl中给了一个不存在的路径,或者说路径中没有相应的可执行文件之时,此时也就必然会导致execl程序替换失败,所以此时的现象就是程序没有被替换,而是继续执行原程序,所以执行原程序,就只执行了一次父进程中的程序,因为子进程已经被exit退出了,并且父进程也获取了子进程的退出码(-1);
所以得到一个小白白点:就是程序替换成功执行新程序,程序替换失败,继续执行原程序
并且明白程序替换不需要对返回值进行判断(因为只要程序替换函数有返回值,就表示替换失败)
如果execl成功执行新程序,返回值是数据吗 ?这个数据是干嘛的呢?所以此时可以明白,使用了execl,进程替换成功之后,不会有返回值返回,因为代码和数据已经被替换了(去执行新的代码和数据了),但是如果替换失败,那么此时就一定要一个返回值,也就是有返回值,那么程序替换就一定失败,所以该execl函数不需要对该函数进行返回值判断,只要有返回值就是失败,所以只要程序替换失败,此时就可以无脑的exit,直接进行程序终止就行(不需要检查和判断)
所以无论是让子进程执行新的程序还是旧的程序,此时我们在父进程中使用 waitpid 接口,父进程都是可以接收到相应的返回值的,也就是检测到子进程的运行状态的,是正常退出,还是异常退出,退出码正确,还是退出码错误
类似execl的进程替换接口
明白了上述的知识,此时我们就来看看有关程序替换的所有接口,也就是开始熟悉熟悉execl等7个系统调用接口,如下:
(1)int execl(const char *path, const char *arg, ...);(2)int execlp(const char *file, const char *arg, ...);(3)int execle(const char *path, const char *arg, ..., char * const envp[]);(4)int execv(const char *path, char *const argv[]);(5)int execvp(const char *file, char *const argv[]);(6)int execve(const char *filename, char *const argv[], char *const envp[]);(7)int execvpe(const char *file, char *const argv[],char *const envp[]);
注意:execve是真正意思上的系统调用接口,别的都是通过封装它实现
所以此时我们根据执行一个程序的基本步骤来讲讲这些函数的使用
第一步,找到它
第二步,加载它
第三步,执行它
-
execl第一个参数表示的就是你想执行谁,一个字符指针,所以第一个参数,完成的步骤就是找到它,第二个参数,执行它(想怎么执行它)例:ls -a 、ls -l 等,此时就涉及我想怎么执行它,就怎么传参,(原理:在命令下怎么执行它,我们的参数就怎么一个一个的传给它),最后确定好了我要执行的程序和传递好了相应的指令参数,此时最后一定还要跟上一个NULL结尾,所以具体的使用方法就是:execl("/bin/ls","ls","-a","-l",NULL)
注意: execl,此时最后一个l的意思表示的就是list,表示该接口是一个list实现的接口,支持的是一个数据一个数据的传参 -
execv第一个参数表示的也是你想要执行的指令,但是区别就在于第二个参数,此时它的第二个参数使用的是一个字符指针数组,这个参数最大的好处就是,传第二个参数的时候,不需要一个一个字符的传,而是可以直接传一个数组,具体使用方法:先建立一个字符指针数组,char* myargv[]={"ls","-a","-l","-n",NULL}; 然后直接使用该字符指针数组进行传参execv("/bin/ls",myargv);并且从名字上出发,execv,最后的v代表的就是vector -
execlp,参数(const char* file,const char* arg,……)首先从参数出发,以p结尾的此时的第一个参数是file,不以p结尾的第一个参数就是path(如果第一个参数是path,那么在找到它这个问题上,就需要用户,也就是我们自己,去给给它一个路径),如果带了p,那么此时就不需要给给相对或者绝对路径,只需要把程序名给给它就行,系统会自动在环境变量path中查找(自动查找和手动查找的区别),具体使用方法:execlp("ls","ls","-a","-l",NULL),这个也就是为什么execlp,它的最后是以p结尾的原因,以p结尾就支持自动查找,不带p就不支持
总结: 第一个参数是const char* file就支持自动查找,第一个参数是const char* path就不支持自动查找,需要手动查找
-
execvp(const char* file,char* const argv[]),这个还是按照名字出发,发现它不仅带v而且带p,所以此时它的使用就是不仅可以直接传一个数组,而且可以自动去环境变量中找
-
execle(const char* path,const char* arg,……,char* const envp[])可以发现多了一个参数
char* const envp[],并且该参数此时就是涉及到了环境变量的相关知识,就是我们可以通过该参数,传一个环境变量给给这个接口,然后让这个接口将我们的环境变量传递给那个被调用的程序,此时这个程序就拥有了一个新的环境变量,所以在进行程序替换的时候,如果使用了该接口,那么就是就可以传递一个我们想要传递的环境变量,所以这个接口最重要的一个点,就是理解,自定义的环境变量可以替代系统环境变量的(覆盖式传参),原因就是我们可以手动传递环境变量给被调用的进程,这个点也就涉及到了环境变量的继承问题,下面单独讲解,具体使用方法:extern char** environ;或者char* const myenv[]={"MYENV=you can see me",NULL};定义两个环境变量(一个是自己实现的,一个是系统自带的),execle("./test/a.out","a.out",NULL,myenv);或者execle("./test/a.out","a.out",NULL,environ);进行进程替换了 -
execvpe 这个接口跟execle大致相同,都是一个提供了传递环境变量的接口,大致的区别就是这个接口是file接口,支持自动查找相应的路径,而execle不支持自动找路径,一定要手动给路径。
-
execve 这个接口是真正的系统调用接口,也就是操作系统的门户,别的都是通过封装它实现,所以这个接口是最重要的,一切进程替换的源头
搞定了上述的这几个程序替换接口,此时我们就可以从名字上发现一定的规律,如下:
l : 使用链表方式,实现一个一个参数的传递
v:通过构造指针数组的方式,实现数组传参
p:用来区分,你是手动查找还是自动查找
e:多了一个环境变量数组envp[],让我们可以使用新的环境变量代替调用进程的环境变量(覆盖式,但可以使用putenv先保存后覆盖解决)
从execle深入理解环境变量:
一个话题:环境变量,环境变量具有全局属性,可以被子进程继承下去,这是为什么呢?
答案跟我们的execle接口密不可分,因为所有的指令都是bash的子进程,而bash执行所有的指令,都可以调用execle去执行,所以我们想要把bash的环境变量交给子进程,只需要调用execle,然后把我们的环境变量以最后一个参数的形式,传给子进程,子进程就可以拿到环境变量了(environ),所以这个就是环境变量具有全局性的原因。
使用程序替换接口,自己实现简易bash

代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>#define MAXLEN 1024
#define LEN 32int main()
{char shell[MAXLEN]={0};char* DOS[LEN]={0};while(1){printf("[dodamce@My_centos dir] ");fgets(shell,MAXLEN,stdin);shell[strlen(shell)-1]='\0';DOS[0]=strtok(shell," ");int i=1;while(DOS[i]=strtok(NULL," ")){i++;}pid_t id=fork();if(id==0){//childexecvp(DOS[0],DOS);exit(1);}int status=0;pid_t wait=waitpid(id,&status,0);if(wait>0){printf("Exit Code=%d\n",WEXITSTATUS(status));}}return 0;
}

总结:今天该博客非常的不尽人意,主要还是因为笔试强训的原因,困恼!哎!
相关文章:
学习系统编程No.8【bash实现】
引言: 北京时间:2023/3/22/6:59,一晃3月都要过去了,时间真快,我都不知道自己这个月是怎么过的呢?怎么就要结束了,难受,恍惚自己还在2022年,刚刚晨跑回来,洗完…...
2023年顶级编程语言趋势
对于开发人员和软件工程师来说,选择更优秀的编程语言使编写可以在任何地方运行的软件变得更加容易,工作效率更高。从 Java 的缓慢衰落到 MATLAB 的惊人流行,对当今最流行的编程语言的分析,可以帮助你了解最新趋势并响应最新趋势。…...
网络安全之认识勒索病毒
一、什么是勒索病毒 勒索病毒,是一种新型电脑病毒,伴随数字货币兴起,主要以邮件、程序木马、网页挂马、服务器入侵、捆绑软件等多种形式进行传播,一旦感染将给用户带来无法估量的损失。如果遭受勒索病毒攻击,将会使绝…...
C语言手撕一个Hash表(HashTable)
什么是Hash Table 散列表用的是数组支持按照下标随机访问数据的特性,所以散列表其实就是数组的一种扩展,由数组演化而来。可以说,如果没有数组,就没有散列表。 散列函数 散列函数是将我们想插入的节点散列成一个数值的函数。它…...
代码随想录第二十七天(669、108、538、回溯算法介绍)
669. 修剪二叉搜索树 不能简单地通过递归实现代码,比如: class Solution { public:TreeNode* trimBST(TreeNode* root, int low, int high) {if (root nullptr || root->val < low || root->val > high) return nullptr;root->left t…...
【Leetcode】设计循环队列
目录 【Leetcode622】设计循环队列 A.链接 B.题目再现 C.解法 【Leetcode622】设计循环队列 A.链接 设计循环队列 B.题目再现 C.解法 其实这题用数组或是链表都能解决,但是如果是用链表的话,那么队列为空的条件和队列满了的条件是一样的࿰…...
【Linux】浅谈shell命令以及运行原理
前言:上篇博文把linux下的基本指令讲解完了。本期我们聊聊Linux下【shell】命令及其运行原理。 目录 Shell的基本概念与作用 原理图展示 shell命令执行原理 Shell的基本概念与作用 Linux严格意义上说的是一个操作系统,我们称之为“核心(ker…...
【shell脚本】nginx服务管理及存活检测脚本实战
前言 今天终于敢说自己是csdn万粉博主了,感谢大家的厚爱,我会继续输出更多优质的好文章,一起学习。 座右铭: 先努力让自己发光,再帮助更多的人。 🏠 个人主页:我是沐风晓月 🧑 个人…...
web服务器—nginx
一、nginx介绍Nginx(“engine x”)是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。和apache一样,都是web服务器软件,因为其性能优异,所以被广大运维喜欢。又因…...
网络安全工具大合集
还是一句话,功夫再高,也怕菜刀首先,恭喜你发现了宝藏。本文章集成了全网优秀的开源攻防武器项目,包含:信息收集工具(自动化利用工具、资产发现工具、目录扫描工具、子域名收集工具、指纹识别工具、端口扫描…...
什么是SHA256?比特币是如何应用SHA256算法的?
SHA 256算法是一种具有确定性的单向哈希函数 算法是执行操作的一系列步骤或过程 哈希函数是种数学函数,输入的长度任意,但是输出长度固定,可以理解为文件的数字指纹,同一个输入值,总是得相同的输出 SHA256࿰…...
JDK20正式发布了GA版本,短期维护支持,以及JDK21预览
最近,Oracle发布了JDK20,相比对于Java开发者来说,JDK的发版是比较收关注的事情了,小简也来和大家一起了解了解JDK20发生了什么变化呢? 首先,JDK20是一个短周期版本,有6个月的维护时间࿰…...
.NET/C#/GC与内存管理(含深度解析)
详情请看参考文章:.NET面试题解析(06)-GC与内存管理 - 不灬赖 - 博客园 (cnblogs.com)一、对象创建及生命周期一个对象的生命周期简单概括就是:创建>使用>释放,在.NET中一个对象的生命周期:new创建对象并分配内存对象初始化…...
Java开发 | 内部类 | 静态内部类 | 非静态内部类 | 匿名内部类
目录 1.内部类 1.1内部类的简单创建 1.2内部类的分类 1.2.1普通内部类 1.2.2静态内部类 1.3匿名内部类 1.4局部内部类 1.内部类 内部类就是一是一个类里面装着另外一个类,就像俄罗斯套娃一样。最外层的类我们叫外部类,内层的类我们叫内部类。 1…...
Portal认证
Portal认证Portal认证简介Portal认证协议Portal认证方式Portal认证流程Portal认证用户下线Portal认证简介 定义: Portal认证通常也称作Web认证,一般将Portal认证网站成为门户网站。用户上网时,必须在门户网站进行认证,如果没有认…...
论文解读:ChangeFormer | A TRANSFORMER-BASED SIAMESE NETWORK FOR CHANGE DETECTION
论文地址:https://arxiv.org/pdf/2201.01293.pdf 项目代码:https://github.com/wgcban/ChangeFormer 发表时间:2022 本文提出了一种基于transformer的siamese网络架构(ChangeFormer),用于一对共配准遥感图…...
Redis 内存优化技巧
这次跟大家分享一些优化神技如何用更少的内存保存更多的数据?我们应该从 Redis 是如何保存数据的原理展开,分析键值对的存储结构和原理。从而继续延展出每种数据类型底层的数据结构,针对不同场景使用更恰当的数据结构和编码实现更少的内存占用…...
【java】笔试强训Day2【倒置字符串与排序子序列】
目录 ⛳选择题 1.A 派生出子类 B , B 派生出子类 C ,并且在 java 源代码有如下声明: 2.下面代码将输出什么内容:( ) 3.阅读如下代码。 请问,对语句行 test.hello(). 描述正确的有&…...
【Linux】基础IO(一) :文件描述符,文件流指针,重定向
🍎作者:阿润菜菜 📖专栏:Linux系统编程 码字不易,请多多支持😘😘 这是目录重新认识文件系统内部的文件操作我们C语言的文件操作系统内部的文件操作OS一般会如何让用户给自己传递标志位的&#x…...
【C语言】通讯录的实现(静态版)
【C语言】通讯录的实现(静态版一.前言1.前期准备a.菜单实现b.联系人结构体的构建c.菜单选项的功能d.#define 的定义2.功能的实现a.初始化通讯录b.增加联系人c.显示通讯录d.查找联系人e.修改联系人d.删除联系人3. 总代码test.ccontact.ccontact.h一.前言 本文将会用c语言实现一…...
RevokeMsgPatcher 2.1终极指南:一键实现微信QQ防撤回的完整教程
RevokeMsgPatcher 2.1终极指南:一键实现微信QQ防撤回的完整教程 【免费下载链接】RevokeMsgPatcher :trollface: A hex editor for WeChat/QQ/TIM - PC版微信/QQ/TIM防撤回补丁(我已经看到了,撤回也没用了) 项目地址: https://g…...
实战应用:为团队部署即装即用的中文版mobaxterm统一环境
在团队协作开发中,统一开发环境配置是个常见痛点。最近我们团队就遇到了这个问题:新成员加入时,每个人都要手动配置MobaXterm的中文界面、服务器连接、工具集等,既费时又容易出错。经过实践摸索,我总结出一套用脚本自动…...
Phi-4-mini-reasoning与IDEA集成开发:提升Java代码推理与注释生成效率
Phi-4-mini-reasoning与IDEA集成开发:提升Java代码推理与注释生成效率 1. 引言:当AI遇见Java开发 作为一名Java开发者,你是否经常遇到这样的困扰:接手一个复杂项目时,面对层层嵌套的代码逻辑感到无从下手;…...
单片机存储系统:哈佛架构与ROM/RAM技术解析
1. 单片机存储系统概述单片机作为微型计算机系统的核心,其存储架构直接决定了系统的性能和功能实现方式。与通用计算机不同,单片机的存储系统通常采用哈佛结构,将程序存储器和数据存储器物理分离。这种设计源于早期计算机科学家对处理器效率的…...
抖音下载器技术深度解析:构建高效无水印视频批量采集系统
抖音下载器技术深度解析:构建高效无水印视频批量采集系统 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback su…...
Alerter终极声音设置指南:为Android通知添加音频反馈的完整教程
Alerter终极声音设置指南:为Android通知添加音频反馈的完整教程 【免费下载链接】Alerter Tapadoo/Alerter: 是一个简单易用的 Android 通知和进度条控件库。适合对 Android 开发、用户界面以及想要在 Android 应用中显示通知和进度条的开发者。 项目地址: https:…...
HelixDB安全特性解析:类型安全查询如何确保生产环境可靠性
HelixDB安全特性解析:类型安全查询如何确保生产环境可靠性 【免费下载链接】helix-db HelixDB is a powerful, graph-vector database built entirely in Rust for millisecond query latency and ease of use. 项目地址: https://gitcode.com/gh_mirrors/he/heli…...
戴尔DELL笔记本Ubuntu24.04与Windows11双系统共存:从分区到引导的完整避坑指南
1. 准备工作:磁盘分区与系统盘制作 第一次在戴尔笔记本上装双系统时,我对着磁盘管理界面发呆了半小时——既怕误删Windows分区,又担心空间分配不合理。后来发现,只要掌握几个关键点,整个过程比想象中简单得多。 先说说…...
只要一行代码,瞬间搭建 Web 服务器 python -m http.server 8000
只要一行代码,瞬间搭建 Web 服务器 python -m http.server 8000 目录 只要一行代码,瞬间搭建 Web 服务器 python -m http.server 8000 1. 核心机制:内置的 `http.server` 模块 2. 为什么它能“求生”,但不能“生产”? 🚀 并发处理能力 (Concurrency) 🛡️ 安全性 (Se…...
赋能音乐自由:Unlock Music技术解密与全场景应用指南
赋能音乐自由:Unlock Music技术解密与全场景应用指南 【免费下载链接】unlock-music 在浏览器中解锁加密的音乐文件。原仓库: 1. https://github.com/unlock-music/unlock-music ;2. https://git.unlock-music.dev/um/web 项目地址: https:…...
