手写简易操作系统(十三)--编写简单C库

前情提要

因为马上要涉及到一个非常重要的部分,内存管理,所以这里我们编写一个简单的C库,用于支持我们后续的C语言开发

一、Assert断言

assert其实如果大家对C语言比较熟悉的话并不陌生,这个函数被称为断言,也就是程序员断定这个函数内的等式成立,如果不成立的话就停止在这里。

我们使用assert是为了看程序运行到某一句是否会出错,我们看一下他的实现代码

// os/src/lib/kernel/assert.h#ifndef __LIB_KERNEL_ASSERT_H
#define __LIB_KERNEL_ASSERT_H
void panic_spin(char* filename, int line, const char* func, const char* condition);/***************************  __VA_ARGS__  ******************************************************************************************************/
#define PANIC(...) panic_spin (__FILE__, __LINE__, __func__, __VA_ARGS__)#ifdef NDEBUG#define ASSERT(CONDITION) ((void)0)
#else#define ASSERT(CONDITION) if (CONDITION) {} else {PANIC(#CONDITION);}
#endif /*__NDEBUG */#endif /*__KERNEL_ASSERT_H*/
// os/src/lib/kernel/assert.c/* 打印文件名,行号,函数名,条件并使程序悬停 */
void panic_spin(char* filename, int line, const char* func, const char* condition) {intr_disable();put_str("###################assert error########################\n");put_str("filename:");put_str(filename);put_str("\n");put_str("line:");put_int(line);put_str("\n");put_str("function:");put_str((char*)func);put_str("\n");put_str("condition:");put_str((char*)condition);put_str("\n");put_str("#######################################################\n");while(1);
}

其中,PANIC 这个宏定义需要关注一下,里面有四个参数

__FILE__:这是一个预定义的宏,在编译时会被当前源文件的文件名所替代。

__LINE__:同样是一个预定义的宏,在编译时会被当前源文件的行号所替代。

__func__:也是一个预定义的宏,会被当前函数的名称所替代。

__VA_ARGS__:表示可变参数列表,允许宏在调用时接受不定数量的参数在这里,它用于接收传递给 PANIC 宏的额外参数。

由于只是调试时使用,所以可以使用一个宏定义,当不需要调试时,定义宏 NDEBUG 就可以取消所有 ASSERT 的作用。

在调用PANIC 时,#CONDITION 是将 ASSERT 中的判断条件作为字符串传递。

二、string字符串

很明显,这个库是用来处理字符串的。这个库相对而言就比较简单,这里我们只列出我们实现了那些功能,不再一一实现。想要看实现细节的小伙伴可以看github源码,

// os/src/lib/string.h
#ifndef __LIB_STRING_H
#define __LIB_STRING_H#include "stdin.h"/* 将dst_起始的size个字节置为value */
void memset(void* dst_, uint8_t value, uint32_t size);
/* 将src_起始的size个字节复制到dst_ */
void memcpy(void* dst_, const void* src_, uint32_t size);
/* 连续比较以地址a_和地址b_开头的size个字节,若相等则返回0,若a_大于b_返回+1,否则返回-1 */
int memcmp(const void* a_, const void* b_, uint32_t size);
/* 将字符串从src_复制到dst_ */
char* strcpy(char* dst_, const char* src_);
/* 返回字符串长度 */
uint32_t strlen(const char* str);
/* 比较两个字符串,若a_中的字符大于b_中的字符返回1,相等时返回0,否则返回-1. */
int8_t strcmp (const char *a, const char *b); 
/* 从前往后查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strchr(const char* string, const uint8_t ch);
/* 从后往前查找字符串str中首次出现字符ch的地址(不是下标,是地址) */
char* strrchr(const char* string, const uint8_t ch);
/* 将字符串src_拼接到dst_后,将回拼接的串地址 */
char* strcat(char* dst_, const char* src_);
/* 在字符串str中查找指定字符ch出现的次数 */
uint32_t strchrs(const char* filename, uint8_t ch);
#endif

三、list链表

链表是一种常见的数据结构,它由一系列节点组成,每个节点包含两部分:数据和指向下一个节点的指针。链表中的节点按顺序排列,通过指针将它们连接起来,形成一个链式结构。

链表可以分为单向链表和双向链表:

  1. 单向链表:每个节点包含一个数据元素和一个指向下一个节点的指针。
  2. 双向链表:每个节点包含一个数据元素,一个指向下一个节点的指针,以及一个指向前一个节点的指针。

链表的基本操作包括:

  • 插入:在链表中插入一个新节点。
  • 删除:从链表中删除指定节点。
  • 搜索:在链表中查找特定数据元素。
  • 遍历:遍历整个链表,访问每个节点的数据。

链表的优点包括:

  • 相对于数组,链表的长度可以动态变化,不需要提前指定大小。
  • 插入和删除节点的时间复杂度为 O(1),只需要重新连接指针即可。

链表的缺点包括:

  • 无法像数组那样通过索引直接访问元素,需要从头开始遍历到目标位置。
  • 链表需要额外的空间来存储指针信息。

3.1、实现的接口

可以先看一下我们实现的接口

// os/src/lib/list.h/* 链表节点结构 */
struct list_elem {struct list_elem* prev; // 前躯结点struct list_elem* next; // 后继结点
};/* 链表结构,用来实现队列 */
struct list {struct list_elem head; // 定义头节点struct list_elem tail; // 定义尾节点,这两个是哨兵节点
};/* 自定义函数类型function,用于在list_traversal中做回调函数 */
typedef bool (function)(struct list_elem*, int arg);/* 初始化双向链表list */
void list_init (struct list*);
/* 把链表元素elem插入在元素before之前 */
void list_insert_before(struct list_elem* before, struct list_elem* elem);
/* 添加元素到列表队首,类似栈push操作 */
void list_push(struct list* plist, struct list_elem* elem);
/* 追加元素到链表队尾,类似队列的先进先出操作 */
void list_append(struct list* plist, struct list_elem* elem); 
/* 链表中删除元素pelem */
void list_remove(struct list_elem* pelem);
/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct list_elem* list_pop(struct list* plist);
/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty(struct list* plist);
/* 返回链表长度 */
uint32_t list_len(struct list* plist);
/* 判断是否有符合函数func(list_elem,arg)的节点,有则返回地址,没有则返回空 */
struct list_elem* list_traversal(struct list* plist, function func, int arg);
/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find(struct list* plist, struct list_elem* obj_elem);

再看一下具体的实现,具体的实现其实也比较简单。

void list_init(struct list *list) {list->head.prev = NULL;list->head.next = &list->tail;list->tail.prev = &list->head;list->tail.next = NULL;
}void list_insert_before(struct list_elem *before, struct list_elem *elem) {/* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/before->prev->next = elem;/* 更新elem自己的前驱结点为before的前驱,* 更新elem自己的后继结点为before, 于是before又回到链表 */elem->prev = before->prev;elem->next = before;/* 更新before的前驱结点为elem */before->prev = elem;
}void list_push(struct list *plist, struct list_elem *elem) {list_insert_before(plist->head.next, elem); // 在队头插入elem
}void list_append(struct list *plist, struct list_elem *elem) {list_insert_before(&plist->tail, elem); // 在队尾的前面插入
}void list_remove(struct list_elem *pelem) {pelem->prev->next = pelem->next;pelem->next->prev = pelem->prev;
}struct list_elem *list_pop(struct list *plist) {struct list_elem *elem = plist->head.next;list_remove(elem);return elem;
}struct list_elem *list_traversal(struct list *plist, function func, int arg) {struct list_elem *elem = plist->head.next;if (list_empty(plist)) return NULL; // 队列为空则直接返回while (elem != &plist->tail) {if (func(elem, arg)) { // func返回ture则认为符合条件return elem;} // 若回调函数func返回true,则继续遍历elem = elem->next;}return NULL;
}uint32_t list_len(struct list *plist) {struct list_elem *elem = plist->head.next;uint32_t length = 0;while (elem != &plist->tail) {length++;elem = elem->next;}return length;
}bool list_empty(struct list *plist) {return (plist->head.next == &plist->tail ? true : false);
}bool elem_find(struct list *plist, struct list_elem *obj_elem) {struct list_elem *elem = plist->head.next;while (elem != &plist->tail) {if (elem == obj_elem) {return true;}elem = elem->next;}return false;
}

具体的实现比较简单,而且由于我们实现了哨兵结点,所以其实编程上更为简单一些。

3.2、具体使用

可以具体怎么用这个链表呢?这就涉及到比较难的两个宏定义了

#define offset(struct_type,member) (int)(&((struct_type*)0)->member)
#define elem2entry(struct_type, struct_member_name, elem_ptr) \(struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))

首先看第一个,这个宏定义计算了结构体中某个成员相对于结构体起始位置的偏移量。

  • offset 宏接受两个参数:struct_type 表示结构体类型,member 表示结构体中的成员名称。
  • (struct_type*)0 创建了一个指向地址为 0 的结构体类型的指针。
  • &((struct_type*)0)->member 取得了结构体中 member 成员的地址,并通过取地址运算符 & 返回该地址。
  • (int) 将地址转换为整数类型,得到了该成员相对于结构体起始位置的偏移量。

再看第二个,这个宏定义根据给定的结构体成员指针,求出整个结构体的起始地址。

  • elem2entry 宏接受三个参数:struct_type 表示结构体类型,struct_member_name 表示结构体中的成员名称,elem_ptr 表示指向成员的指针。
  • offset(struct_type, struct_member_name) 调用前面定义的 offset 宏,计算出结构体中 struct_member_name 成员相对于结构体起始位置的偏移量。
  • (int)elem_ptr 将指向成员的指针转换为整数类型,表示该成员的地址。
  • (int)elem_ptr - offset(struct_type, struct_member_name) 计算出整个结构体的起始地址,即减去成员偏移量。即这个结构体的起始地址。
  • (struct_type*) 将计算出的整个结构体的起始地址转换为指向该结构体的指针,最终返回该指针。

这两个宏的作用我们举个例子

由于我们是在64位环境下的例子,所以地址是64位的。

// 创建一个教师结构体,其中有链表的结构
struct teacher {int age;int height;struct list_elem teacher_list_elem;
};int main() {struct list* teacher_list = (struct list*)malloc(sizeof(struct list));list_init(teacher_list);struct teacher* teacher1 = (struct teacher*)malloc(sizeof(struct teacher));teacher1->age = 10;teacher1->height = 100;list_append(teacher_list, &(teacher1->teacher_list_elem));struct teacher* teacher2 = (struct teacher*)malloc(sizeof(struct teacher));teacher2->age = 50;teacher2->height = 500;list_append(teacher_list, &(teacher2->teacher_list_elem));struct list_elem* pos = teacher_list->head.next;// 可以看到各个元素在结构体起始地址的偏移量printf("%lld\n",offset(struct teacher, age));printf("%lld\n",offset(struct teacher, height));printf("%lld\n",offset(struct teacher, teacher_list_elem));// 打印头结点,尾节点,两个教师结点的地址printf("head addriss is %lld\n",(long long int)&teacher_list->head);printf("tail addriss is %lld\n",(long long int)&teacher_list->tail);printf("teacher1 addriss is %lld\n",(long long int)teacher1);printf("teacher2 addriss is %lld\n",(long long int)teacher2);// 打印链表长度printf("list len is %d\n",list_len(teacher_list));while(pos != &teacher_list->tail) {// 循环遍历链表printf("address=%lld   ",(long long int)elem2entry(struct teacher, teacher_list_elem, pos));// 获得带有链表节点的结构体的地址printf("age=%d  height=%d\n",(elem2entry(struct teacher, teacher_list_elem, pos))->age,\(elem2entry(struct teacher, teacher_list_elem, pos))->height);// 遍历下一个pos = pos->next;}return 0;
}

看一下结果

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

完全正确,在给一个结构图加深印象

image-20240319193025807

可以看到,链表的前向结点和后向结点都指向了上一个结点中链表的部分,所以我们想要拿到结构体的地址需要一点小手段,也就是 elem2entry 宏。

四、bitmap位图

位图(Bitmap)是一种数据结构,用于表示一组二进制位的集合,每个位通常对应某种状态或者标记。位图常用于解决空间高效利用和快速查询的问题。

在计算机中,位图通常表示为一个由连续的比特位组成的数组或者其他数据结构。例如,一个简单的位图可以用一个整数数组来表示,其中每个整数可以存储多个位的信息。位图可以被用来表示一组开关的状态(开/关)、一组标记的存在与否、一组IP地址的分配情况等等。它的使用场景非常广泛。

位图数据结构通常支持以下操作:

  1. 设置某一位的值(置位)。
  2. 清除某一位的值(清零)。
  3. 查询某一位的值。

位图的优点包括:

  • 空间效率高:位图通常比使用其他数据结构来表示相同信息所需的空间更少,因为它们可以紧凑地存储大量的布尔值信息。
  • 快速查询:可以通过位运算来快速查询某个位置的值,不需要进行复杂的遍历操作。

位图的缺点有:

  • 位图的大小通常是固定的,因此当需要存储的元素数量不确定时,可能会浪费一些空间。
  • 插入、删除等操作相对复杂,可能需要进行大量的位移和逻辑运算。

看一下其具体的结构

image-20240319194619082

4.1、实现的接口

先看一下我们实现的接口

#define BITMAP_MASK 1/* 位图结构体 */
struct bitmap {uint32_t btmp_bytes_len;uint8_t* bits;
};/* 将位图btmp初始化 */
void bitmap_init(struct bitmap* btmp);
/* 判断bit_idx位是否为1,若为1则返回true,否则返回false */
bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx);
/* 在位图中申请连续cnt个位,返回其起始位下标 */
int bitmap_scan(struct bitmap* btmp, uint32_t cnt);
/* 将位图btmp的bit_idx位设置为value */
void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value);

然后看一下具体的实现

void bitmap_init(struct bitmap* btmp) {memset(btmp->bits, 0, btmp->btmp_bytes_len);   
}bool bitmap_scan_test(struct bitmap* btmp, uint32_t bit_idx) {uint32_t byte_idx = bit_idx / 8;    // 向下取整用于索引数组下标uint32_t bit_odd  = bit_idx % 8;    // 取余用于索引数组内的位return (btmp->bits[byte_idx] & (BITMAP_MASK << bit_odd));
}int bitmap_scan(struct bitmap* btmp, uint32_t cnt) {uint32_t idx_byte = 0;	 // 用于记录空闲位所在的字节/* 先逐字节比较*/while (( 0xff == btmp->bits[idx_byte]) && (idx_byte < btmp->btmp_bytes_len)) {/* 1表示该位已分配,所以若为0xff,则表示该字节内已无空闲位,向下一字节继续找 */idx_byte++;}ASSERT(idx_byte < btmp->btmp_bytes_len);if (idx_byte == btmp->btmp_bytes_len) {  // 若该内存池找不到可用空间		return -1;}/* 若在位图数组范围内的某字节内找到了空闲位,在该字节内逐位比对,返回空闲位的索引。*/int idx_bit = 0;while ((uint8_t)(BITMAP_MASK << idx_bit) & btmp->bits[idx_byte]) { idx_bit++;}int bit_idx_start = idx_byte * 8 + idx_bit;    // 空闲位在位图内的下标if (cnt == 1) {return bit_idx_start;}uint32_t bit_left = (btmp->btmp_bytes_len * 8 - bit_idx_start);   // 记录还有多少位可以判断uint32_t next_bit = bit_idx_start + 1;uint32_t count = 1;	      // 用于记录找到的空闲位的个数bit_idx_start = -1;	      // 先将其置为-1,若找不到连续的位就直接返回while (bit_left-- > 0) {if (!(bitmap_scan_test(btmp, next_bit))) {	 // 若next_bit为0count++;} else {count = 0;}if (count == cnt) {	    // 若找到连续的cnt个空位bit_idx_start = next_bit - cnt + 1;break;}next_bit++;          }return bit_idx_start;
}void bitmap_set(struct bitmap* btmp, uint32_t bit_idx, int8_t value) {ASSERT((value == 0) || (value == 1));uint32_t byte_idx = bit_idx / 8;    // 向下取整用于索引数组下标uint32_t bit_odd  = bit_idx % 8;    // 取余用于索引数组内的位/* 一般都会用个0x1这样的数对字节中的位操作,* 将1任意移动后再取反,或者先取反再移位,可用来对位置0操作。*/if (value) {btmp->bits[byte_idx] |= (BITMAP_MASK << bit_odd);} else {btmp->bits[byte_idx] &= ~(BITMAP_MASK << bit_odd);}
}

bitmap我们这里实现的较为简单。索引直接采用的从左到右的暴力索引。

结束语

这节实现了我们后面编程需要用到的一些数据结构。下一节我们将实现一个内存池,以后我们想要内存就需要从内存池申请了。不在需要手动管理。

老规矩,代码地址 https://github.com/lyajpunov/os.git

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HTTP 工作流程请求响应 - 面试常问

文章目录 HTTP 工作流程请求和响应格式HTTP请求格式请求行&#xff1a;请求头部字段&#xff1a;空行&#xff1a;消息正文&#xff08;请求正文&#xff09;&#xff1a; HTTP响应格式状态行&#xff1a;响应头部字段&#xff1a;空行&#xff1a; HTTP方法HTTP状态码常用HTTP…

消息队列—RabbitMQ如何保证消息可靠性?

1. 如何保证消息的可靠性&#xff1f; 先来看看我们的万年老图&#xff0c;从图上我们大概可以看出来一个消息会经历四个节点&#xff0c;只有保证这四个节点的可靠性才能保证整个系统的可靠性。 生产者发出后保证到达了MQ。MQ收到消息保证分发到了消息对应的Exchange。Exchan…

工厂投产、电池装车,广汽能上动力电池行业的“餐桌”吗?

文 | 智能相对论 作者 | 沈浪 “如果你不在餐桌上&#xff0c;你就会出现在菜单上。”在某种程度上&#xff0c;追逐效益的动力电池行业正在上演着布林肯的“餐桌菜单论”。 于是&#xff0c;我们可以看到&#xff0c;尽管整体的动力电池市场被宁德时代、比亚迪、LG新能源、…

AI基础知识(4)--贝叶斯分类器

1.什么是贝叶斯判定准则&#xff08;Bayes decision rule&#xff09;&#xff1f;什么是贝叶斯最优分类器&#xff08;Bayes optimal classifier&#xff09;&#xff1f; 贝叶斯判定准则&#xff1a;为最小化总体风险&#xff0c;只需在每个样本上选择那个能使条件风险最小的…

【Java常用API】正则表达式练习

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【Java】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏 …

c/c++整数和浮点数在内存中存储

了解变量的储存原理是我们灵活运用和防止数据截断改变带来的危害的有效途径。 那么我们从int char和float double两类来阐述内存的储存。 首先我们讲内存单位&#xff1a; 内存单位从小到大分别是bit byte KB MB GB TB PB。 bit是最小的内存单位&#xff0c;它可以存储一…

【嵌入式——QT】QWT应用

【嵌入式——QT】QWT应用 概述步骤一步骤二步骤三代码声明代码实现图示 概述 QWT&#xff0c;全称Qt Widgets for Technical Applications&#xff0c;是一个基于Qt开发的第三方库。它主要为具有技术背景的程序提供GUI组件和一组实用类&#xff0c;用于生成各种统计图。QWT的主…

科技助力高质量发展:新质生产力的崛起与企业数字化转型

引言 随着科技的飞速发展&#xff0c;我们正逐渐步入数字化智能时代&#xff0c;这个时代不仅为企业带来了无限的机遇&#xff0c;也让其面对前所未有的挑战。在这个快速变革的时代&#xff0c;企业必须不断调整自己的经营策略&#xff0c;适应数字化转型的浪潮&#xff0c;以…

时间减少90%以上!分布式系统的性能优化实战

1背景 分布式批量系统指的是采用分布式数据库架构&#xff0c;主体功能由批量程序实现的系统。分布式系统批量程序的性能测试&#xff0c;除了和联机交易性能测试一样关注服务器资源使用率是否合理、是否存在性能异常外&#xff0c;在测试执行阶段需要关注是否因数据分布不均衡…

初识二叉树

文章目录 一.什么是树二.什么是二叉树三.二叉树的访问次序四.特殊的二叉树五.求结点个数六.平衡二叉树总结 一.什么是树 树是由一个集合以及在该集合上定义的一种关系构成的。 集合中的元素称为树的节点&#xff0c;所定义的关系称为父子关系。 父子关系在树的节点之间建立了一…

RuoYi-Vue-Plus(基础知识点jackson、mybatisplus、redis)

一、JacksonConfig 全局序列化反序列化配置 1.1yml中配置 #时区 spring.jackson.time-zoneGMT8 #日期格式 spring.jackson.date-formatyyyy-MM-dd HH:mm:ss #默认转json的属性&#xff0c;这里设置为非空才转json spring.jackson.default-property-inclusionnon_null #设置属性…

JetPack之DataBinding基础使用

目录 一、简介二、使用2.1 使用环境2.2 xml文件绑定数据2.3 数据绑定的对象2.3.1 object2.3.2 ObseravbleField2.3.3 ObseravbleCollection 2.4 绑定数据 三、应用场景 一、简介 DataBinding是谷歌15年推出的library,DataBinding支持双向绑定&#xff0c;能大大减少绑定app逻辑…

python灾害应急救援平台flask-django-php-nodejs

灾害应急救援平台的目的是让使用者可以更方便的将人、设备和场景更立体的连接在一起。能让用户以更科幻的方式使用产品&#xff0c;体验高科技时代带给人们的方便&#xff0c;同时也能让用户体会到与以往常规产品不同的体验风格。 与安卓&#xff0c;iOS相比较起来&#xff0c;…

【Selenium(五)】

一、鼠标事件 from selenium import webdriver # 导入ActionChains类进行鼠标悬停操作 from selenium.webdriver.common.action_chains import ActionChains import time# 打开一个浏览器 # 法一、添加环境变量重启电脑 # 法二、填写浏览器驱动的绝对路径 driver webdriver.E…

功率半导体IGBT模块封装工艺

功率器件最近非常火热&#xff0c;作为新型功率半导体器件的主流器件&#xff0c;IGBT应用非常广泛&#xff0c;如家用电器、电动汽车、铁路、充电基础设施、充电桩&#xff0c;光伏、风能&#xff0c;工业制造、电机驱动&#xff0c;以及储能等领域。IGBT模块是新一代的功率半…

undo log

从这篇「执行一条 SQL 查询语句&#xff0c;期间发生了什么&#xff1f; (opens new window)」中&#xff0c;我们知道了一条查询语句经历的过程&#xff0c;这属于「读」一条记录的过程&#xff0c;如下图&#xff1a; 那么&#xff0c;执行一条 update 语句&#xff0c;期间发…

桌面显示器PD芯片:引领桌面显示技术的新篇章

随着科技的飞速发展&#xff0c;桌面显示器作为人们日常工作与生活中不可或缺的重要设备&#xff0c;其性能与品质也在不断提升。其中&#xff0c;PD芯片作为桌面显示器中的核心组件&#xff0c;发挥着至关重要的作用。本文将对桌面显示器PD芯片进行详细介绍&#xff0c;探讨其…

MD5源码(C语言描述)

本文介绍MD5源码&#xff08;C语言描述&#xff09;。 MD5(Message-Digest Algorithm 5)&#xff0c;即消息摘要算法5&#xff0c;是一种被广泛使用的消息散列算法。散列算法的基础原理是&#xff1a;将数据&#xff08;如一段文字&#xff09;经过运算转换为一段固定长度&…

#Linux(VMwareTOOL安装)

&#xff08;一&#xff09;发行版&#xff1a;Ubuntu16.04.7 &#xff08;二&#xff09;记录&#xff1a; &#xff08;1&#xff09; &#xff08;2&#xff09;打开虚拟机然后安装&#xff0c;出现灰色可能是已经安装过但是自己没有找到 &#xff08;3&#xff09;删除VM…

流畅切换Linux的应用程序

流畅切换Linux的应用程序 流畅切换Linux的应用程序一.Linux启动一个程序在后台执行1. 使用nohup和&&#xff1a;2. 使用ctrlZ&#xff1a;3.使用screen&#xff1a;3.1 创建会话3.2 要重新连接到此会话&#xff1a;3.3 中途退出会话&#xff0c;但程序继续运行:3.4 结束一个…