操作系统入门系列-MIT6.828(操作系统工程)学习笔记(三)---- xv6初探与实验一(Lab: Xv6 and Unix utilities)

news2024/11/24 2:59:04

系列文章目录

操作系统入门系列-MIT6.S081(操作系统)学习笔记(一)---- 操作系统介绍与接口示例
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(二)----课程实验环境搭建(wsl2+ubuntu+quem+xv6)
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(三)---- xv6初探与实验一(Lab: Xv6 and Unix utilities)


文章目录

  • 系列文章目录
  • 前言
  • 一、xv6文档第一章
    • 1. 引论
  • 二、实验一
    • 1.启动xv6
    • 2.实现休眠函数
    • 3.实现父子进程pingpong打印
    • 4.使用pipeline实现质数检测
    • 5.实现find函数(文件查找)
    • 6.实现xargs函数
  • 总结


前言

MIT 6.828课程资料与进度计划表
完成第一节课的学习后,按照课程的计划进度表,应当阅读xv6文档的第一章节,以及完成实验1。
本文主要内容就是对xv6文档(2023年版)和实验(2023年版)的讲解
xv6英文文档—2023
xv6中文文档—2013
xv6实验1原文


一、xv6文档第一章

1. 引论

xv6操作系统提供 Unix 操作系统中的基本接口(由 Ken Thompson 和 Dennis Ritchie 引入),同时模仿 Unix 的内部设计,包括 BSD,Linux,Mac OS X,Solaris (甚至 Microsoft Windows 在某种程度上)都有类似 Unix 的接口,理解 xv6 是理解这些操作系统的一个良好起点。
首先需要对操作系统有一个系统结构的大致概念,如下图:
在这里插入图片描述
如图所示,操作系统的核心就是kernel。kernel起到了桥梁的作用,连接各种应用程序和底层硬件资源。从软件编程者的角度来看,程序员就是使用各种kernel提供的系统调用函数,来实现各种各样的功能。xv6的系统调用函数如下图所示:
在这里插入图片描述
后面的实验就是需要调用这些系统调用函数来实现相应的功能,比如在Linux里面常用的find、xargs等命令的简易版。
在之前的文章中已经将第一章的部分进行了讲解,详情见:
操作系统入门系列-MIT6.S081(操作系统)学习笔记(一)---- 操作系统介绍与接口示例

二、实验一

1.启动xv6

详见xv6环境的配置和启动
操作系统入门系列-MIT6.828(操作系统工程)学习笔记(二)----课程实验环境搭建(wsl2+ubuntu+quem+xv6)
(1)实验系统目录:
整个实验代码的文件目录如下,我们需要关注:

1.绿色的”grade-lab-util“可执行文件,它是测试机,用来测试你的实验代码正确性。
2.kernel文件夹,是xv6的kernel代码
3.Makefile是用来编译内核的,需要在这里添加参数,我们写的实验代码才能编译进去
4.user文件夹是我们添加自己实验代码的地方,也就是用户空间	

在这里插入图片描述
(2)Makefile添加文件
在这里插入图片描述
(3)退出xv6
使用”ctrl+a“,后按x退出
(4)测试机打分
需要在lab目录中添加time.txt文件,里面写一个整数,代表你的实验花了多少小时。
在这里插入图片描述

2.实现休眠函数

(1)原题如下:
在这里插入图片描述
(2)大致内容为:
调用“系统调用函数——sleep()”(一次sleep()系统暂停一个tick),实现输入数字,系统暂停该数字次数的ticks。
参数错误时,要有错误打印
(3)代码如下:

// 参考echo.c、grep.c、rm.c的头文件引用
#include "kernel/types.h"//声明数据类型
#include "kernel/stat.h"//声明文件数据结构
#include "kernel/fcntl.h"//open函数的模式参数申明
#include "user/user.h"//声明各种系统调用函数、以及有用的库函数


int main(int argc, char *argv[])
{
    int number;

    //参数数量不对,打印错误信息
    //参数还有其他错误情况,此处仅考虑数量问题
    if(argc <= 1)
    {
        fprintf(2, "usage: sleep number\n");
        exit(1);
    }

    number = atoi(argv[1]);//字符串转整数
    sleep(number);//系统调用

    exit(0);
}

(4)结果如下:
使用官方提供的测试机程序,在lab目录下面运行"./grade-lab-util + 文件名"

./grade-lab-util sleep

测试机自主进行测试,如下图:
在这里插入图片描述
(5)总结:
在做实验之前我们需要知道有哪些函数我们是可以调用的,可以查看文件:“user/user.h”

struct stat;

// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(const char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);

// ulib.c
int stat(const char*, struct stat*);
char* strcpy(char*, const char*);
void *memmove(void*, const void*, int);
char* strchr(const char*, char c);
int strcmp(const char*, const char*);
void fprintf(int, const char*, ...);
void printf(const char*, ...);
char* gets(char*, int max);
uint strlen(const char*);
void* memset(void*, int, uint);
void* malloc(uint);
void free(void*);
int atoi(const char*);
int memcmp(const void *, const void *, uint);
void *memcpy(void *, const void *, uint);

3.实现父子进程pingpong打印

(1)原题如下:
在这里插入图片描述
(2)大致内容为:
实现过程:父进程向子进程发送ping,子进程打印该信息,后子进程向父进程发送pong,父进程再打印该信息
实现结果为:
在这里插入图片描述

(3)代码如下:
分为单管道实现和双管道实现
单管道实现:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/fcntl.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
    int p[2];
    char buf[512];//用来接收信息的buffer

    pipe(p);//父 <=======> 子

    //子进程执行if,父进程执行else
    if(fork() == 0)
    {
        read(p[0], buf, sizeof(buf));//阻塞,直到父进程写入管道p内容,后读取父进程的信息
        fprintf(1,"%d: received %s\n", getpid(), buf);//打印父进程的信息
        write(p[1], "pong", 5);//使用p的写通道向父进程发送信息
        close(p[0]);//关闭读通道
        close(p[1]);//关闭写通道
        exit(0);//子进程退出
    }
    else
    {
        write(p[1], "ping", 5);//父进程通过p的写通道向子进程发送信息
        wait((int *)0);//等待子进程结束
        read(p[0], buf, sizeof(buf));//读取p管道中子进程的信息
        fprintf(1,"%d: received %s\n", getpid(), buf);//打印子进程的信息
        close(p[0]);//关闭读通道
        close(p[1]);//关闭写通道
    }

    exit(0);
}

双管道实现:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/fcntl.h"
#include "user/user.h"

int main(int argc, char *argv[])
{
    int p[2];
    int p2[2];
    char buf[512];//用来接收信息的buffer

    //向内核申请两个管道
    pipe(p);//父进程写,子进程读:父 ——————> 子
    pipe(p2);//子进程写,父进程读:子 ——————> 父

    //子进程执行if,父进程执行else
    if(fork() == 0)
    {
        close(p[1]);//关闭p的写通道,以便于父进程写完可以直接读取
        read(p[0], buf, sizeof(buf));//阻塞,直到父进程写入管道p内容,后读取父进程的信息
        fprintf(1,"%d: received %s\n", getpid(), buf);//打印父进程的信息
        close(p[0]);//关闭p的读通道
        write(p2[1], "pong", 5);//使用p2的写通道向父进程发送信息
        close(p2[1]);//写完关闭写通道
        close(p2[0]);//关闭p2的读通道
        exit(0);//子进程退出
    }
    else
    {
        write(p[1], "ping", 5);//父进程通过p的写通道向子进程发送信息
        close(p[1]);//写完关闭写通道
        close(p2[1]);//关闭p2的写通道,以便于子进程写完可以直接读取
        read(p2[0], buf, sizeof(buf));//阻塞,直到子进程写入管道p2内容,读取子进程的信息
        fprintf(1,"%d: received %s\n", getpid(), buf);//打印子进程的信息
        close(p[0]);//关闭p的读通道
        close(p2[0]);//关闭p2的读通道
    }

    exit(0);//结束程序
}

(4)结果如下:
在这里插入图片描述
(5)总结:

4.使用pipeline实现质数检测

(1)原题如下:
在这里插入图片描述
(2)大致内容为:
使用pipeline+多进程来检测35以内的质数检测
这个过程比较难想,但是在纸上推演一下流程就懂了,参考文献如下:Bell Labs and CSP Threads

参考伪代码:
在这里插入图片描述
参考图:
在这里插入图片描述
(3)代码如下:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/fcntl.h"
#include "user/user.h"


void child(int * p)
{
    int p1[2];
    char num[35];
    int prime;
    int i;
    int ret;
    int number;

    pipe(p1);

    close(p[1]);
    number = 0;
    for(i=0;i<35;i++)
    {
        ret = read(p[0], num+i, 1);
        if(ret == 0)
        {
            break;
        }
        number++;
    }

    if(number == 0)
    {
        exit(0);
    }

    prime = num[0];
    printf("prime %d\n", prime);
    close(p[0]);

    if(fork() == 0)
    {
        child(p1);
    }
    else
    {
        close(p1[0]);
        for(i=1;i<number;i++)
        {
            if((num[i]%prime)!=0)
            {
                write(p1[1], num+i, 1);
            }
        }
        close(p1[1]);
        wait((int *)0);
    }

    exit(0);
}


int main()
{
    int prime = 2;
    int p[2];
    int i;

    pipe(p);

    if(fork() == 0)
    {
        child(p);
    }
    else
    {
        close(p[0]);
        printf("prime %d\n", prime);
        for(i=3;i<=35;i++)
        {
            if((i%prime)!=0)
            {
                write(p[1], &i, 1);
            }
        }
        close(p[1]);
        wait((int *)0);
    }


    exit(0);
}

(4)结果如下:
在这里插入图片描述
在这里插入图片描述
(5)总结:
核心是算法题,载体是进程树之间使用pipeline进行读写控制

5.实现find函数(文件查找)

(1)原题如下:
在这里插入图片描述
(2)大致内容为:
实现目录下搜索文件的功能,要求能够找到该文件夹目录下以及其子目录下的指定文件名,输出所有满足文件的路径。(不要求实现正则表达式)(!!!记得规避".“与”…"子文件)(如何访问文件夹可以参考文件ls.c的实现)

  "  .  "  表示当前目录
  "  ..  "  表示当前目录的上一级目录。
  "  ./  "  表示当前目录下的某个文件或文件夹,视后面跟着的名字而定
  "  ../  "   表示当前目录上一级目录的文件或文件夹,视后面跟着的名字而定。

在这里插入图片描述
(3)代码如下:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/fcntl.h"
#include "kernel/fs.h"
#include "user/user.h"

char* fmtname(char *path)
{
  char *p;

  // Find first character after last slash.
  for(p=path+strlen(path); p >= path && *p != '/'; p--)
    ;
  p++;

  return p;
}

int find(char *name, char *p)
{
    int fd;
    int path=0;
    char buf[512];
    struct dirent de;
    struct stat st;

    fd = open(p, O_RDONLY);
    if(fd < 0)
    {
        fprintf(2, "fd: can not open %s\n");
        exit(1);
    }
    strcpy(buf, p);
    p = buf+strlen(buf);
    *p++ = '/';
    // printf("%d  %s\n", fd, buf);

    while(read(fd, &de, sizeof(de)) == sizeof(de))
    {
        if(de.inum == 0)
        {
            continue;
        }

        memmove(p, de.name, DIRSIZ);
        p[DIRSIZ] = 0;

        // printf("%s\n", p);

        if(stat(buf, &st) < 0)
        {
            printf("ls: cannot stat %s\n", buf);
            continue;
        }

        // printf("%s %s %d\n", buf, fmtname(buf), st.type);
        // printf("%d %d %d\n", strcmp(fmtname(buf), name), strcmp(fmtname(buf), "."), strcmp(".", "."));

        if (st.type == T_FILE && (strcmp(fmtname(buf), name) == 0))
        {
            printf("%s\n", buf);
            path = 1;
        } 

        if (st.type == T_DIR && (strcmp(fmtname(buf), ".") != 0) && (strcmp(fmtname(buf), "..") != 0))
        {
            path = find(name, buf);
        }   
    }

    return path;
}


int main(int argc, char *argv[])
{
    int path;

    if(argc < 3)
    {
        fprintf(2, "fd: find [dir] [filename]\n");
        exit(1);
    }

    path = find(argv[2], argv[1]);
    if(path == 0)
    {
        fprintf(1, "can not find such file\n");
    }

    exit(0);
}

(4)结果如下:
在这里插入图片描述
(5)总结:
核心知识点是xv6系统下的文件结构体与文件夹结构体,使用递归方法进行实现。

6.实现xargs函数

(1)原题如下:
在这里插入图片描述
(2)大致内容为:
实现xargs命令,xargs命令很难简单理解,建议先查一下命令的用法。
xargs 命令教程
(3)代码如下:

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/fcntl.h"
#include "user/user.h"
#include "kernel/param.h"


int getcmd(char *buf, char**arg, int arg_num)
{
    int i;
    int j;
    j = 0;
    arg[arg_num] = (char *)malloc(30 * sizeof(char));


    for(i=0; i<512; i++)
    {
        if((buf[i] == ' ') || (buf[i] == 10) || (buf[i] == '\0') || (buf[i] == '\n'))
        {
            // printf("%c %d\n", buf[i], buf[i]);
            arg_num++;
            if(buf[i] == 10)
            {
                // printf("argmun: %d\n", arg_num);
                return arg_num;
            }
            if(buf[i] == '\0')
            {
                // printf("argmun: %d\n", arg_num);
                return arg_num;
            }
            arg[arg_num] = (char *)malloc(30 * sizeof(char));
            // printf("111\n");
            // printf("%s %p kkk\n", arg[arg_num], arg[arg_num]);
            j = 0;
        }
        else
        {
            // printf("%d %d %c %d\n", j, i, buf[i], arg[arg_num][j]);
            arg[arg_num][j] = buf[i];
            // printf("%d %c \n", j, arg[arg_num][j]);
            j++;
        }
    }

    return arg_num;
}


int main(int argc, char *argv[])
{
    char buf[512];
    int num = 0;
    int ret;
    int cmd = 0;
    char *arg[MAXARG];
    int arg_num;
    arg_num = argc - 1;
    
    int i = 0;
    for(i=0; i<arg_num; i++)
    {
        arg[i] = argv[i+1];
    }

    if(argc < 2)
    {
        fprintf(2, "xarg: xarg [process] [args...]\n");
        exit(1);
    }

    int j = 0;
    while(1)
    {
        ret = read(0, buf+j, 1);
        if(ret != 1)
        {
            break;
        }
        else if(buf[j] == '\n')
        {
            // printf("read number is : %d\n%s\n", num, buf);
            j = 0;
            num = 0;
            cmd = getcmd(buf, arg, arg_num);
            // printf("\n\n%d %d\n", cmd, arg_num);
            // printf("%s\n%s\n%s\n%s\n", arg[0], arg[1], arg[2], arg[3]);
            if(cmd == arg_num)
            {
                printf("%d %d\n", cmd, arg_num);
                printf("no content of stdin\n");
            }
            else
            {
                if(fork() == 0)
                {
                    exec(argv[1], arg);
                    fprintf(2, "exec failed\n");
                    exit(1);
                }
                else
                {
                    wait((int *)0);
                }
            }
        }
        else
        {
            j++;
            num++;
        }
    }
    
    for(i=0; i<cmd; i++)
    {
        free(arg[i]);
    }
    exit(0);
}

(4)结果如下:
在这里插入图片描述
(5)总结:
在实现字符串的单词分割的时候,对于指针的操作出现问题。
char **arg的arg[]元素应该动态分配空间,而不是讲temp数组的指针头直接赋值。


总结

课程每年可能有变化,本文按照最新的2023计划表进行学习

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

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

相关文章

【文末附gpt升级秘笈】埃隆·马斯克芯片调配策略对特斯拉股价的影响分析

埃隆马斯克芯片调配策略对特斯拉股价的影响分析 一、引言 在现代商业环境中&#xff0c;企业间的资源调配与策略布局往往对其股价产生深远影响。据外媒CNBC报道&#xff0c;埃隆马斯克在芯片资源分配上的决策引起了业界的广泛关注。他秘密要求英伟达将原本预留给特斯拉的高端…

苹果Vision Pro 界面中英翻译

目录 菜单 &#x1f537;General&#x1f504;一般 AirDrop 隔空投送 Background App Refresh 后台应用刷新 Keyboards 键盘 VPN & Device Management VPN与设备管理​编辑Legal & Regulatory 法律法规 &#x1f537;Apps&#x1f504;应用程序 &#x1f537;Pe…

飞睿智能工业无线通信模块图传WiFi,地对空图传超5km,4堵实墙穿透稳定传输

在当今高科技飞速发展的时代&#xff0c;远距离无线通信技术的应用越来越广泛&#xff0c;尤其是在地对空通信领域。今天&#xff0c;我们将深入探讨一种远距离无线通信模块WiFi图传&#xff0c;它不仅能实现地对空远距离图传超过5公里&#xff0c;还具备穿透4堵实墙的穿墙效果…

Qt Creator常用的快捷键和常用功能

常用快捷键 新建项目&#xff0c;ctrl n 运行项目&#xff0c;ctrl r 构建项目&#xff0c;ctrl b 改变编辑器界面字体显示比例大小&#xff0c;ctrl 鼠标滚轮 对齐代码&#xff0c;ctrl a; ctrl i 跳转到上一行&#xff0c;ctrl shift enter 跳转到下一行&#xff0c;…

PHP实现一个简单的接口签名方法以及思路分析

文章目录 签名生成说明签名生成示例代码签名校验示例代码 签名生成说明 B项目需要调用A项目的接口&#xff0c;由A项目为B项目分配 AccessKey 和 SecretKey&#xff0c;用于接口加密&#xff0c;确保不易被穷举&#xff0c;生成算法不易被猜测。 最终需要确保包含签名的参数只…

vscode 编程工具配置Java开发环境

vs code 开发环境配置。 环境准备&#xff1a; 1. 安装JDK/安装maven/安装vs code 首先安装好vs code 之后&#xff0c;需要安装 Extension Pack for Java 这么个插件 配置maven&#xff0c;进入setting&#xff0c; 3&#xff1a;配置 maven安装目录&#xff0c;4&#xff1a…

超详细!新手入门PMP®考试指南,收藏起来备考更高效​!

回复数字“6”&#xff0c;查看PMP考试过关口诀 无论你是刚刚踏入项目管理领域的新手&#xff0c;对于PMP考试充满好奇与期待&#xff1b; 还是已经在职场中摸爬滚打多年&#xff0c;希望通过PMP认证来进一步提升自己的项目管理能力和职业竞争力。 相信这份指南都会为你提供…

react 中使用 swiper

最近项目中需要用到轮播图&#xff0c;我立马想起了 swiper &#xff0c;那么本文就来带大家体验一下如何在 React 中使用这个插件&#xff0c;使用的是 函数组 hooks 的形式。 需求非常简单&#xff0c;就是一个可以自动播放、点击切换的轮播图&#xff08;跑马灯&#xff0…

UDSonCAN刷写之StayInBOOT和FlashDiver

目录 0 前言 1 StayInBOOT 2 Flash Driver 0 前言 最近在做刷写相关的工作&#xff0c;顺便搞懂了StayInBOOT和FlashDiver&#xff0c;写出来作为分享&#xff0c;如果有哪里不对也请多多指正。 1 StayInBOOT StayInBOOT在整个流程中的位置如下图所示&#xff0c;从图中可…

青否数字人直播源码超级管理后台操作步骤!

青否数字人直播源码超级管理后台&#xff0c;我们将详细介绍一下数字人的管理后台的详细操作步骤&#xff01; 1.管理端入口 2.管理后台预览 账号管理&#xff0c;模特管理&#xff0c;声音管理&#xff0c;任务管理&#xff0c;卡类管理&#xff0c;代理商&#xff0c;克隆端 …

python绘制热力图

python绘制热力图 热力图效果代码 热力图 热力图&#xff08;Heatmap&#xff09;是一种数据可视化技术&#xff0c;用于显示数据的密度或强度。通过颜色的变化来反映数值的大小或分布情况&#xff0c;热力图能够直观地展示数据的聚集区域、模式和趋势。它广泛应用于各个领域&…

基于51单片机的简易温控水杯恒温杯仿真设计( proteus仿真+程序+设计报告+讲解视频)

基于51单片机的简易温控水杯恒温杯仿真设计( proteus仿真程序设计报告讲解视频&#xff09; 仿真图proteus7.8及以上 程序编译器&#xff1a;keil 4/keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;S0099 1. 主要功能&#xff1a; 基于51单片机的简易温控水杯恒温…

百元级蓝牙耳机推荐,五大最新真香品牌机型盘点!

蓝牙耳机已成为我们不可或缺的伴侣无论是通勤路上的音乐陪伴&#xff0c;还是健身房里的动感节奏&#xff0c;一副高品质的蓝牙耳机都能为我们带来无与伦比的听觉享受&#xff0c;今天我将为大家盘点五大最新真香品牌机型&#xff0c;这些百元级的蓝牙耳机不仅价格亲民&#xf…

Qt如何让按钮的菜单出现在按钮的右侧

直接上代码&#xff0c;我们用到了一个eventfilter的函数功能。这个函数比较厉害和重要&#xff0c;大家务必经常拿出来看看。 void MainWindow::initMenu() { QMenu* menuLiXiang new QMenu; QAction* actXiangMuZhangCheng new QAction("项目章程"); …

cocos入门4:项目目录结构

Cocos Creator 项目结构教程 Cocos Creator 是一个功能强大的游戏开发工具&#xff0c;它为开发者提供了直观易用的界面和强大的功能来快速创建游戏。在使用 Cocos Creator 开发游戏时&#xff0c;合理地组织项目结构对于项目的可维护性和扩展性至关重要。以下是一个关于如何设…

新一代企业共享服务中心,开启企业智慧管理决策新纪元

随着数字化浪潮加速来袭&#xff0c;企业面临着与以往全然不同的挑战与机遇。 业务与人员的增长致使服务请求与日俱增&#xff0c;业务类型愈加复杂&#xff0c;大量来自不同业务线的服务请求使内部服务压力增加。业务激增后只得依赖更多资源投入&#xff0c;势必掣肘服务效率。…

Nvidia/算能 +FPGA+AI大算力边缘计算盒子:医疗健康智能服务

北京天星医疗股份有限公司(简称“天星医疗”)作为国产运动医学的领导者&#xff0c;致力于提供运动医学的整体临床解决方案&#xff0c;公司坐落于北京经济技术开发区。应用于肩关节、膝关节、足/踝关节、髋关节、肘关节、手/腕关节的运动医学设备、植入物和手术器械共计300多个…

Postgresql源码(135)生成执行计划——Var的调整set_plan_references

1 总结 set_plan_references主要有两个功能&#xff1a; 拉平&#xff1a;生成拉平后的RTE列表&#xff08;add_rtes_to_flat_rtable&#xff09;。调整&#xff1a;调整前每一层计划中varno的引用都是相对于本层RTE的偏移量。放在一个整体计划后&#xff0c;需要指向一个统一…

Cochrane Library循证医学数据库的介绍及文献下载

今天要讲的数据库是Cochrane Library循证医学数据库&#xff0c;我们先来了解一下该数据库&#xff1a; Cochrane Library是国际Cochrane Collaboration的主要产品&#xff0c;由英国Wiley InterScience公司出版发行。是一个提供高质量证据的数据库&#xff0c;是循证医学的证…

如何把试卷上的字去掉再打印?分享三种方法

如何把试卷上的字去掉再打印&#xff1f;随着科技的不断发展&#xff0c;现代教育和学习方式也在逐渐变革。在学习过程中&#xff0c;我们经常需要对试卷进行整理和分析&#xff0c;以便更好地掌握知识点和复习。然而&#xff0c;传统的试卷整理方法往往效率低下且容易出错。幸…