CSAPP Lab06——Shell Lab通关思路

news2024/11/16 1:40:33

远距离的欣赏

近距离的迷惘

谁说太阳会找到月亮

——修炼爱情

 

完整代码见:CSAPP/shlab-handout at main · SnowLegend-star/CSAPP (github.com)

 

上来就遇到了些小问题:①本来想看看“tshref”支持的命令,结果命令居然被拒绝执行了,显示“-bash: ./tshref: Permission denied。然后又试了下“./sdriver.pl -h”这个命令,也是报错“-bash: ./sdriver.pl: Permission denied”。我还以为是用户权限的问题,试了下切到root,结果还是不能执行。最后查看了下文件,发现除了自己编译的文件,其他文件都是“rw-r--r--”。下面是GPT对文件默认权限问题的回答。

书上P239,关于子进程和父进程对各自内存里的内容进行操作后的结果。 

在正式进行实验之前,还得正式了解一番waitpid函数

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  1. 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

    pid 

        Pid=-1, 等待任一个子进程。与 wait 等效。

        Pid>0. 等待其进程 ID 与 pid 相等的子进程。

   status:

        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

        WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)

   options:

        WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。

使用的注意事项: 

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。
  1. //pid_t ret=wait(NULL);                                                          
  2. //pid_t ret=waitpid(id,NULL,0);//等待指定为id的子进程。                          
  3. //pid_t ret=waitpid(-1,NULL,0);//设置成-1代表等待任意一个子进程,可能我们有10个子进程,这里只等待任意一个,等价于wait

④在运行“make testxx”时,注意是在linux的shell中运行,而不是在自己编写的tsh中运行。

eval(char *cmdline)

首先解析命令行,调用builtin_cmd判断是否是内置命令。如果是内置命令,执行对应的内置命令。否则,fork一个子进程,execv 对应的代码,将其添加到 job 列表中。

2023/12/15 23:08

书上的P525那份样例代码可以直接拿来用,以便于我们把代码的框架先搭建起来。

有一点我没有理解,为什么在调用fork()之前要先屏蔽SIGCHLD信号,而在fork()结束之后可以立即对SIGCHLD信号解除屏蔽?

        sigprocmask(SIG_BLOCK,&mask_one,&prev_one); 
        if((pid=fork())==0){    
            sigprocmask(SIG_SETMASK,&prev_one,NULL);    
} 

 我忽略了最重要的一点,解除对SIGCHLD信号的屏蔽只是作用在子进程,而父进程依然保持对SIGCHLD信号的屏蔽。只有当

sigprocmask(SIG_SETMASK,&prev_one,NULL);

父进程才会解除对SIGCHLD信号以及其他信号的屏蔽,进而可以着手处理待处理信号队列中的SIGCHLD,从而避免了竞争的问题。

而之所以子进程要解除对SIGCHLD信号的屏蔽,原因可以分为两部分来讲:①子进程 会继承父进程 的被阻塞集合②子进程 可能自己也要创建自己的子进程 ,但是没有屏蔽SIGCHLD信号的需求,所以 清空从 继承过来的被阻塞集合是比较合理的。

void eval(char *cmdline) 
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;
    sigset_t mask_all,mask_one,prev_one;

    sigfillset(&mask_all);  //这一步的作用是什么?
    sigemptyset(&mask_one); //把一会儿要阻塞的信号集清空
    sigaddset(&mask_one,SIGCHLD);   //把子进程发送的SIGCHLD阻塞,避免父子进程的竞争现象
    // signal(SIGCHLD,sigchld_handler); 
    // signal(SIGTSTP,sigtstp_handler);
    // signal(SIGINT,sigint_handler);
    strcpy(buf,cmdline);
    bg=parseline(buf,argv);
    if(argv[0]==NULL)
        return ;    //ignore empty lines

    if(!builtin_cmd(argv)){
        sigprocmask(SIG_BLOCK,&mask_one,&prev_one); //把SIGCHLD阻塞之
        if((pid=fork())==0){    //子进程运行user job
            sigprocmask(SIG_SETMASK,&prev_one,NULL);    //取消对SIGCHLD的阻塞,确保子进程结束后父进程可以收到
            setpgid(0,0);                               //做到一个进程一组,不然父进程一会儿调用kill函数的时候也会给自己发送消息
            if(execve(argv[0],argv,environ)<0){         //execve函数成功不返回,失败才返回-1
                printf("%s: Command not found.\n",argv[0]);
                exit(0);
            }
        }

        //父进程添加jobs
        sigprocmask(SIG_BLOCK,&mask_all,NULL);
        int job_status;
        if(bg==0)
            job_status=FG;
        else
            job_status=BG;
        addjob(jobs,pid,job_status,cmdline);
        sigprocmask(SIG_SETMASK,&prev_one,NULL);

        //父进程等待前台job终止
        if(!bg){    //是前台进程
            waitfg(pid);
        }
        else{       //是后台进程
            printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);
        }        
            
    }

    return;
}

builtin_cmd(char **argv)

按实验要求,我们的tsh需要支持4个内置命令

-quit 终止命令

-jobs 列出所有的后台作业

-bg <job> 重新唤醒<job>,并把该作业运行在后台

-fg   <job> 重新唤醒<job>,并把该作业运行在前台

没什么技术含量,就是把命令行的第一个参数取出来与几个内置命令进行对比,每个内置命令的处理函数已经提供了,直接调用即可。

实现代码如下:
 

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
    if(!strcmp(argv[0],"quit"))     //如果是quit命令
        exit(0);
    if(!strcmp(argv[0],"&"))        //如果只有一个“&”,不用理会
        return 1;
    if(!strcmp(argv[0],"bg")){      //处理bg命令
        do_bgfg(argv);
        return 1;
    }              
    if(!strcmp(argv[0],"fg")){      //处理fg命令
        do_bgfg(argv);
        return 1;
    }           
    if(!strcmp(argv[0],"jobs")){    //处理jobs命令
        listjobs(jobs);
        return 1;
    }     
    return 0;     /* not a builtin command */
}

void waitfg(pid_t pid);

在这个函数体内调用sleep()函数就行,很粗糙的一个函数。

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    while(fgpid(jobs))   //如果检测到这个前台进程还没结束,系统就保持休眠1s,轮询执行直到该前台进程结束
        sleep(1);
    return;
}

void do_bgfg(char **argv)

       这个函数要求我们做好“fg”与“bg”命令的区分,同时“%num”表示用job号来进行查找,“num”则表示用process号来查找。这里有一个比较麻烦的东西,就是如果第二个命令行参数是“%123”,我们现在只需要提取出“123”,一般想法是创建一个新的数组tmp,再把“123”赋值过去。但是,由于我想用atoi()函数,所以又要把tmp数组装换为字符串形式,总之很是麻烦。

       看到CSDN一个博主的妙招之后,我直接被这记妙手所折服。

job_tmp=getjobjid(jobs,atoi(&argv[1][1]));  //很妙的操作

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    //做好bg、fg、pid、jid的区分就行
    struct job_t *job_tmp;
    
    if(argv[1]==NULL){
        printf("%s command requires PID or %%jobid argument\n",argv[0]);
        return ;
    }
    if(argv[1][0]!='%'&&!isdigit(argv[1][0])){   //如果命令行第二个参数的首字符不是数字也不是%
        printf("%s: argument must be a PID or %%jobid\n",argv[0]);
        return ;
    }
    if(argv[1][0]!='%'){    //如果是用pid找
        job_tmp=getjobpid(jobs,atoi(argv[1]));
        if(job_tmp==NULL){
            printf("(%d): NO such job\n",atoi(argv[1]));
            return ;
        }
    }
    else{       //用jid找
        job_tmp=getjobjid(jobs,atoi(&argv[1][1]));  //很妙的操作
        if(job_tmp==NULL){
            printf("%%%d: NO such job\n",atoi(&argv[1][1]));
            return ;
        }
    }

    kill(-job_tmp->pid,SIGCONT);
    if(!strcmp(argv[0],"fg")){   //如果是前台进程
        job_tmp->state=FG;    
        waitfg(job_tmp->pid);
    } 
    else{
        job_tmp->state=BG;
        printf("[%d] (%d) %s",pid2jid(job_tmp->pid),job_tmp->pid,job_tmp->cmdline);
    }
    return;
}

void sigchld_handler(int sig);

为了消除竞争问题,势必要对SIGCHLD信号做一定的处理。我们可以参照书上的P542,其中的关键一招就是在进行deletejob()的时候屏蔽所有信号。

while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)

在这个 while 循环中,waitpid 以非阻塞方式检查所有子进程的状态。如果有子进程的状态发生变化,waitpid 将返回该子进程的进程ID。如果没有子进程的状态发生变化,waitpid 将返回0,这时循环结束。

这种方式的使用通常用于轮询(polling)子进程的状态,而不是阻塞等待它们。这允许主程序在等待子进程的同时继续执行其他任务。如果没有设置 WNOHANG 标志,waitpid 将阻塞,直到有子进程的状态发生变化。

void sigchld_handler(int sig) 
{
    int olderrno=errno;
    sigset_t mask_all,prev_all;
    pid_t pid;
    int status;

    sigfillset(&mask_all);
    while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0){              //回收僵尸进程  waitpid(-1,NULL,0)有问题
        if(WIFEXITED(status)){    //如果这个子进程是正常终止的  WEXITSTATUS(status)
            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
            deletejob(jobs,pid);                                        //从job list里面删除该结束的子进程
            sigprocmask(SIG_SETMASK,&prev_all,NULL);
        }
        else if(WIFSIGNALED(status)){    //如果这个子进程是因为收到某个信号而终止的(SIGINT)
            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
            printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));            
            deletejob(jobs,pid);                                        //从job list里面删除该结束的子进程
            sigprocmask(SIG_SETMASK,&prev_all,NULL);
        }
        else{   //如果是SIGSTP信号 
            struct job_t *job_running=getjobpid(jobs,pid);
            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
            printf("Job [%d] (%d) stopped by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
            job_running->state=ST;
            sigprocmask(SIG_SETMASK,&prev_all,NULL);
        }
    }

    // if(errno!=ECHILD)
    //     printf("waitpid error");
    errno=olderrno;        

    return;
}

void sigtstp_handler(int sig);

由于SIGSTP信号是针对于前台进程的,所以在这个函数内部只需要用kill()发送一个信号给前台进程就好,其他的操作可以放到SIGCHLD信号的处理函数里面去。

这里可能会有一个令人费解的地方,明明SIGSTP的处理函数并没有和SIGCHLD的处理函数进行交互,怎么后者还可以处理前者应该处理的部分呢?

其中原因就在于sigchld_handler ()内部的waitpid()函数,一旦子进程接收到了SIGSTP信号,则该子进程的状态会立即发生改变从而导致waitpid()收到对应的pid,从而代码的实际执行部分就跳转到了sigchld_handler()函数内部。

void sigtstp_handler(int sig) 
{
    int olderrno = errno;
    pid_t fg_pid = fgpid(jobs);
    if(fg_pid){
        kill(-fg_pid,sig);
    }
    errno = olderrno;
    return;
}

void sigint_handler(int sig);

同上。

void sigint_handler(int sig) 
{
    int olderrno=errno;
    pid_t fg_pid=fgpid(jobs); ;//获取当前正在运行的前台进程的pid,如果没有就返回0
    if(fg_pid)
        kill(-fg_pid,sig);
    errno=olderrno;
    return;
}

Test01~04

完成eval()和bulitin_cmd()后,尝试执行了上面前三个test,发现输出都和参考输出一致。只有test04出现了输出格式的问题,修改下输出格式即可。

Test05

执行这个测试的时候,发现jobs输出为空。

猜测是出现了父子竞争的问题,开始着手修改eval()、waitfg()、sigchld_handler()。eval()和sigchld_handler()可以按照书上P543来修改,但是得注意sigchld_handler()内部有一处需要修改的地方。

while((pid=waitpid(-1,NULL,0))>0)

这条命令是等待任意进程结束,但如果进程结束的话我们就会从job list中删除这个进程,此时就不存在这个进程的相关信息了。按提示的意思,是一旦发现后台进程存在,waitpid()就立即返回,顺便打印出该后台进程的相关信息。所以waitpid(pid_t pid,int *status,int options)的options部分应改为“WNOHANG|WUNTRACED”才是正确的。

Test06~08

为什么父进程中有signal(SIGCHLD,sigchld_handler);这种处理SIGCHLD的函数,而并没有处理SIGSTPSIGINT的函数呢?

    signal(SIGCHLD,sigchld_handler); 
    // signal(SIGTSTP,sigtstp_handler);
    // signal(SIGINT,sigint_handler);

 我的理解是处理下面两个信号的函数在main()中已经调用了,所以不用在eval()函数中进行重复调用

    /* These are the ones you will need to implement */
    Signal(SIGINT,  sigint_handler);   /* ctrl-c */
    Signal(SIGTSTP, sigtstp_handler);  /* ctrl-z */
    Signal(SIGCHLD, sigchld_handler);  /* Terminated or stopped child */

之所以要在eval()中重新调用signal(SIGCHLD,sigchld_handler),是因为我们在eval()函数中阻塞了SIGCHLD信号,在对这个信号解阻塞后要重新对SIGCHLD信号进行处理,所以得再次调用signal(SIGCHLD,sigchld_handler)

Md,刚才我signal(SIGCHLD,sigchld_handler)删掉了再对样例进行测试,结果发现输出并没有丝毫变化。事实证明signal(SIGCHLD,sigchld_handler)只需要调用一次就好,在eval()函数中并不需要重新调用这个函数。怪不得连我自己都觉得解释显得很牵强附会,原来我压根儿就在胡说八道啊。

②很奇怪的问题

熄灯之后借着电脑屏幕的光瞄了眼书,结果看差了。应该调用的是WIFEXITED()函数而不是WEXITSTATUS()函数。调用了WEXITSTATUS()后之所以会出现上图结果,是因为子进程因为接收到了SIGINT信号而异常终止。对于非正常终止的进程,WEXITSTATUS()返回的值是未定义的,这可能就导致了进程出现后续一连串的问题。

Test09~10

这里开始要求处理“fg”“bg”命令,完成do_bgfg()即可。

Test11~13

输出的东西有点难看懂,粗略对比下,如果前10个都没问题的话这个应该也没问题。

Test14

测试各种输入错误的处理,简单更加错误处理部分即可。

Test15

将前面所有的测试内容都测试一遍,基本不用再修改代码。

Test16

是测试tsh能否处理不是来自终端而是来自其他进程的SIGSTP和SIGINT信号,顺利通过。

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

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

相关文章

现代密码学-认证协议

A.B两个用户想通过网络先建立安全的共享密钥再进行保密通信&#xff1f;A(B)如何确信自己正在和B(A)通信而不是C&#xff1f;这种通信方式为双向通信&#xff0c;此时的认证为相互认证。 相互认证 A/B两个用户在建立共享密钥时需要考虑的核心问题&#xff1a;保密性和实时性&…

刷代码随想录有感(93):贪心算法——无重叠区间(区间重叠问题:求区间重叠次数)

题干: 代码&#xff1a; class Solution { public:static bool cmp(vector<int>& a, vector<int>& b){return a[0] < b[0];}int eraseOverlapIntervals(vector<vector<int>>& intervals) {sort(intervals.begin(), intervals.end(), c…

2024年湖北职称评审面试答辩技巧有哪些?看完你就懂了

2024年度湖北省部分工程专业水平能力测试面试答辩开始了&#xff0c;答辩时间是&#xff1a;2024年6月15、16日。 测试地点&#xff1a;武汉市武昌区洪山侧路63号茶港军转小区1号楼(武汉大学西门旁) 水平能力测试注意事项&#xff1a; &#xff08;一&#xff09;报名参加202…

企业级数据保护:华企盾DSC敏感内容识别与加密技术

在当今数字化时代&#xff0c;企业面临的数据安全挑战日益严峻。敏感数据的泄露不仅会导致经济损失&#xff0c;还可能损害企业的声誉和客户信任。因此&#xff0c;采用先进的敏感内容识别和加密技术&#xff0c;例如华企盾DSC敏感内容识别&#xff0c;对企业数据进行有效保护至…

Ubuntu server 24 (Linux) IPtables 双网卡 共享上网NAT 安装配置DHCP

一 开启路由转发功能 sudo vim /etc/sysctl.conf net.ipv4.ip_forward1 sudo sysctl -p 二 安装DHCP #更新软件包列表&#xff1a; sudo apt update #安装DHCP服务器 sudo apt install isc-dhcp-server #修改监听网卡,根据实际修改 sudo vi /etc/default/isc-dhcp-server …

【动手学深度学习】使用块的网络(VGG)的研究详情

目录 &#x1f30a;1. 研究目的 &#x1f30a;2. 研究准备 &#x1f30a;3. 研究内容 &#x1f30d;3.1 多层感知机模型选择、欠拟合和过拟合 &#x1f30d;3.2 练习 &#x1f30a;4. 研究体会 &#x1f30a;1. 研究目的 理解块的网络结构&#xff1b;比较块的网络与传统…

AI 入门指南二 :AI提示词(Prompt)

一&#xff0c;提示词的定义 提示词在中文中意为“触发”&#xff0c;在自然语言处理&#xff08;NLP&#xff09;的领域&#xff0c;它更接近于一个“心领神会”的概念&#xff0c;而非具有明确定义的术语。 简而言之&#xff0c;提示词是用户对大型语言模型的输入&#xff0…

Qt | Qt 资源简介(rcc、qmake)

1、资源系统是一种独立于平台的机制,用于在应用程序的可执行文件中存储二进制文件(前面所讨论的数据都存储在外部设备中)。若应用程序始终需要一组特定的文件(比如图标),则非常有用。 2、资源系统基于 qmake,rcc(Qt 的资源编译器,用于把资源转换为 C++代码)和 QFile …

EE trade:通货膨胀对老百姓意味着什么

通货膨胀&#xff0c;是经济领域中的一个常见现象&#xff0c;对社会各阶层尤其是普通老百姓的生活产生了深远影响。理解通货膨胀对老百姓的真实含义&#xff0c;可以帮助我们更好地应对日常生活的变化与挑战。 1. 生活成本的上升 最直接的影响体现在生活成本上。通货膨胀会导致…

音频pop音的数学与物理解释

音频数据跳变太大的时候通常会有pop音&#xff0c;此时频谱上看pop音位置能量较高 音频中的“pop”音通常是由于信号的不连续性或瞬态变化造成的。这种不连续性的数学和物理原因可以从以下几个方面解释&#xff1a; 数学解释 信号不连续性 当音频信号发生突变时&#xff0c;…

Android完整备份:备份Android手机数据的4种最佳方法

如今&#xff0c;人们每天都依赖手机&#xff0c;丢失数据对我们所有人来说都是一个大麻烦。由于生活是不可预测的&#xff0c;没有人知道什么时候他的数据可能会被意外删除或丢失。因此&#xff0c;仔细备份手机数据非常重要。大多数主要智能手机平台都具有将数据备份到计算机…

C/C++学习路线

学习分享 C语言学习路线 视频&#xff1a;浙大翁恺C语言程序设计&#xff0c;当时学习C语言的时候感觉老师讲的很有意思&#xff0c;现在依然很受学生欢迎&#xff0c;播放量也很高&#xff0c;C语言主要需要学习数组&#xff0c;指针&#xff0c;结构体和函数等&#xff0c;…

爆肝三天,制作属于自己的地图——DAY1(地图数据整理)

爆肝第一天&#xff0c;地图数据整理 作者&#xff1a;御剑飞行 引言 本系列我将用三篇文章详细的讲述开源地图数据集、Mapmost Studio介绍、如何通过Mapmost Studio转为生成地图数据。 以下是常用的地图数据集。 Esri 开放数据中心 Esri开放数据中心包含来自全球5,000 多…

【AI基础】第四步:保姆喂饭级-langchain+chatglm2-6b+m3e-base

在第三步手动安装chatglm2-6b时&#xff0c;已经可以通过web进行交互。langchain重新封装了一下AI框架&#xff0c;提供更加友好的开发功能&#xff0c;类似于AI届的spring框架。langchain的安装过程也类似于上一步说的&#xff1a;【AI基础】第三步&#xff1a;纯天然手动安装…

SwiftUI知识点(一)

前言&#xff1a; Swift知识点&#xff0c;大至看完了&#xff0c;公司项目是Swift语言写的&#xff0c;后续苹果新出的SwiftUI&#xff0c;也需要学习一下 不知觉间&#xff0c;SwiftUI是19年出的&#xff0c;现在24年&#xff0c;5年前的东西了 学习的几个原因&#xff1a; …

概要设计说明书实际项目示例(原件直接套用Word)

1引言 1.1编写目的 1.2项目背景 1.3参考资料 2系统总体设计 2.1整体架构 2.2整体功能架构 2.3整体技术架构 2.4运行环境设计 2.5设计目标 3系统功能模块设计 3.1个人办公 3.2系统管理 4性能设计 4.1响应时间 4.2并发用户数 5接口设计 5.1接口设计原则 5.2接口实现方式 6运行设计…

【香橙派 AIpro 开发板】AI 应用部署测评:视频目标检测+Linux镜像+vscode远程连接+全细节试用

香橙派 AIpro 开发板 AI 应用部署测评 写在最前面一、开发板概述官方资料试用印象适用场景 二、详细开发前准备步骤1. 环境准备2. 环境搭建3. vscode安装ssh插件4. 香橙派 AIpro 添加连接配置5. 连接香橙派 AIpro6. SSH配置 二、详细开发步骤1. 登录 juypter lab2. 样例运行3. …

网络原理——http/https ---http(1)

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 今天你敲代码了吗 网络原理 HTTP/HTTPS HTTP,全称为"超文本传输协议" HTTP 诞⽣与1991年. ⽬前已经发展为最主流使⽤的⼀种应⽤层协议. 实际上,HTTP最新已经发展到 3.0 但是当前行业中主要使用的HT…

Java桥接模式

桥接模式 最重要的是 将 抽象 与 实现 解耦 , 通过组合 在 抽象 与 实现 之间搭建桥梁 ; 【设计模式】桥接模式 ( 简介 | 适用场景 | 优缺点 | 代码示例 )-CSDN博客 桥接模式&#xff08;Bridge Pattern&#xff09;-&#xff08;最通俗易懂的案例&#xff09;_桥接模式 例子-…

【Linux】(三)—— 文件管理和软件安装

文件管理 Linux的文件管理是系统管理中的核心部分&#xff0c;它涉及到如何组织、访问、修改和保护文件及目录结构。 目录 文件管理基本概念常用命令查看和切换目录创建文件和目录删除文件和目录文件拷贝移动和重命名文件文件查看cat文件查看more查找文件查找文本 数据流和管道…